Module:Coordinates

    From Commons

    Documentation for this module may be created at Module:Coordinates/doc

    --[[
      __  __           _       _         ____                    _ _             _            
     |  \/  | ___   __| |_   _| | ___ _ / ___|___   ___  _ __ __| (_)_ __   __ _| |_ ___  ___ 
     | |\/| |/ _ \ / _` | | | | |/ _ (_) |   / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
     | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
     |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
                                                                                              
    
    This module is intended to provide functionality of {{location}} and related
    templates. It was developed on Wikimedia Commons, so if you find this code on
    other sites, check there for updates and discussions.
    
    Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing 
    at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
    
    Authors and maintainers:
    * User:Jarekt
    * User:Ebraminio
    
    Functions:
    *function p.LocationTemplateCore(frame)
    **function p.GeoHack_link(frame)
    ***function p.lat_lon(frame)
    ****function p._deg2dms(deg,lang)
    ***function p.externalLink(frame)
    ****function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
    **function p._getHeading(attributes)
    **function p.externalLinksSection(frame)
    ***function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
    *function p.getHeading(frame)  
    *function p.deg2dms(frame)
    
    ]]
    
    -- =======================================
    -- === Dependencies ======================
    -- =======================================
    require('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables
    local i18n = require('Module:I18n/coordinates')    -- get localized translations of site names
    local core = require('Module:Core')
    
    -- =======================================
    -- === Hardwired parameters ==============
    -- =======================================
    
    -- ===========================================================
    -- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
    local compass_points = {
      N    = 0,
      NBE  = 11.25,
      NNE  = 22.5,
      NEBN = 33.75,
      NE   = 45,
      NEBE = 56.25,
      ENE  = 67.5,
      EBN  = 78.75,
      E    = 90,
      EBS  = 101.25,
      ESE  = 112.5,
      SEBE = 123.75,
      SE   = 135,
      SEBS = 146.25,
      SSE  = 157.5,
      SBE  = 168.75,
      S    = 180,
      SBW  = 191.25,
      SSW  = 202.5,
      SWBS = 213.75,
      SW   = 225,
      SWBW = 236.25,
      WSW  = 247.5,
      WBS  = 258.75,
      W    = 270,
      WBN  = 281.25,
      WNW  = 292.5,
      NWBW = 303.75,
      NW   = 315,
      NWBN = 326.25,
      NNW  = 337.5,
      NBW  = 348.75,
    }
    
    -- ===========================================================
    -- files to use for different headings
    local heading_icon = {
    	[ 1] = 'File:Compass-icon bb N.svg',
    	[ 2] = 'File:Compass-icon bb NbE.svg',
    	[ 3] = 'File:Compass-icon bb NNE.svg',
    	[ 4] = 'File:Compass-icon bb NEbN.svg',
    	[ 5] = 'File:Compass-icon bb NE.svg',
    	[ 6] = 'File:Compass-icon bb NEbE.svg',
    	[ 7] = 'File:Compass-icon bb ENE.svg',
    	[ 8] = 'File:Compass-icon bb EbN.svg',
    	[ 9] = 'File:Compass-icon bb E.svg',
    	[10] = 'File:Compass-icon bb EbS.svg',
    	[11] = 'File:Compass-icon bb ESE.svg',
    	[12] = 'File:Compass-icon bb SEbE.svg',
    	[13] = 'File:Compass-icon bb SE.svg',
    	[14] = 'File:Compass-icon bb SEbS.svg',
    	[15] = 'File:Compass-icon bb SSE.svg',
    	[16] = 'File:Compass-icon bb SbE.svg',
    	[17] = 'File:Compass-icon bb S.svg',
    	[18] = 'File:Compass-icon bb SbW.svg',
    	[19] = 'File:Compass-icon bb SSW.svg',
    	[20] = 'File:Compass-icon bb SWbS.svg',
    	[21] = 'File:Compass-icon bb SW.svg',
    	[22] = 'File:Compass-icon bb SWbW.svg',
    	[23] = 'File:Compass-icon bb WSW.svg',
    	[24] = 'File:Compass-icon bb WbS.svg',
    	[25] = 'File:Compass-icon bb W.svg',
    	[26] = 'File:Compass-icon bb WbN.svg',
    	[27] = 'File:Compass-icon bb WNW.svg',
    	[28] = 'File:Compass-icon bb NWbW.svg',
    	[29] = 'File:Compass-icon bb NW.svg',
    	[30] = 'File:Compass-icon bb NWbN.svg',
    	[31] = 'File:Compass-icon bb NNW.svg',
    	[32] = 'File:Compass-icon bb NbW.svg'
    }
    
    -- ===========================================================
    -- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be 
    -- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings.
    local SiteURL = {
    	GeoHack        = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
    	GoogleEarth    = '//tools.wmflabs.org/geocommons/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
    	Proximityrama  = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
    	WikimediaMap   = '//maps.wikimedia.org/#16/$lat/$lon',
    	OpenStreetMap1 = '//tools.wmflabs.org/wiwosm/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
    	OpenStreetMap2 = '//tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
    	GoogleMaps = { 
    		Mars  = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
    		Moon  = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
    		Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic'
    	}
    }
    
    -- ===========================================================
    -- Categories
    local CoorCat = {
    	-- File       = '[[Category:Media with locations]]',
    	-- Gallery    = '[[Category:Galleries with coordinates]]',
    	-- Category   = '[[Category:Categories with coordinates]]',
    	strucData0 = '[[Category:Pages with %s coordinates from %s]]',
    	strucData1 = '[[Category:Pages with local %s coordinates and matching %s coordinates]]',
    	strucData2 = '[[Category:Pages with local %s coordinates and similar %s coordinates]]',
    	strucData3 = '[[Category:Pages with local %s coordinates and mismatching %s coordinates]]',
    	strucData4 = '[[Category:Pages with local %s coordinates and missing %s coordinates]]',
    	sHeading3  = '[[Category:Pages with local %s heading and mismatching %s heading]]',
    	sHeading4  = '[[Category:Pages with local %s heading and missing %s heading]]',
    	globe      = '[[Category:Media with %s locations]]',
    	default    = '[[Category:Media with default locations]]',
    	attribute  = '[[Category:Media with erroneous geolocation attributes]]',
    	erroneous  = '[[Category:Media with erroneous locations]]',
    	dms        = '[[Category:Media with coordinates in DMS format]]'
    }
    
    local globeLUT = { Q2='Earth', Q111='Mars', Q405='Moon'}
    local NoLatLonString = 'latitude, longitude'
    
    -- =======================================
    -- === Local Functions ===================
    -- =======================================
    
    -- ===========================================================
    local function add_maplink(lat, lon, marker, text)
    	local tstr = ''
    	if text then
    		tstr = string.format('text="%s" ', text)
    	end
    	return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'..
    		'  "type": "Feature",'..
    		'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
    		'  "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0"  }'..
    		'}</maplink>', tstr, lat, lon, lon, lat, marker)
    end
    
    -- ===========================================================
    local function add_maplink2(lat1, lon1, lat2, lon2)
    	return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
    		'  "type": "Feature",'..
    		'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
    		'  "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons"  }'..
    		'},{'..
    		'  "type": "Feature",'..
    		'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
    		'  "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata"  }'..
    		'}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2)
    end
    
    -- ===========================================================
    local function info_box(text)
    	return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '..
    		'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'..
    		'<tr>'..
    		'<td class="mbox-image" style="padding-left:.9em;">'..
    		' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
    		'<td class="mbox-text" style="">%s</td>'..
    		'</tr></table>', text)
    end
    
    -- ===========================================================
    local function distance(lat1, lon1, lat2, lon2)
    	-- calculate distance
    	local dLat = math.rad(lat1-lat2)
    	local dLon = math.rad(lon1-lon2)
    	local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
    	d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d))  -- angular distance in radians
    	d = 6371000 * d       -- radians to meters conversion
    	d = math.floor(d+0.5) -- round it to even meters
    	return d
    end
    
    -- ===========================================================
    local function getSDCoords(entity, prop)
        -- get coordinates from structured data (either wikidata or SDC)
    	local coords = {id=entity.id, source=prop}
    	if not entity or not entity.claims or not entity.claims[prop]then 
    		return coords
    	end
    	for _,statement in ipairs(entity.claims[prop]) do
    		local v = statement.mainsnak.datavalue.value	-- get coordinates
    		if v.latitude then
    			coords.lat   = v.latitude
    			coords.lon   = v.longitude
    			coords.prec  = v.precision or 1e-4
    			coords.prec  = math.floor(coords.prec*111000)           -- convert precision from degrees to meters and round
    			coords.prec  = math.max(math.min(coords.prec,111000),5) -- bound precision to a number between 5 meters and 1 degree
    			coords.globe = string.gsub(v.globe, 'http://www.wikidata.org/entity/','')
    			coords.globe = globeLUT[coords.globe]
    			if statement.qualifiers and statement.qualifiers.P7787 then
    				v = statement.qualifiers.P7787[1].datavalue.value
    				if v.unit == "http://www.wikidata.org/entity/Q28390" then     -- in degrees
    					coords.heading = v.amount
    				elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians
    					coords.heading = v.amount*57.2957795131
    				end	
    			end			
    			return coords
    		end
    	end
    	return coords
    end
    
    -- ===========================================================
    local function compareCoords(loc, sd, mode, source)
    -- compare coordinates
    --INPUTS:
    --  * loc - local coordinates
    --  * sd  - structured data coords
    	local coord = loc
    	local cat, dist_str = '', ''
    	local case, dist, qs, mapLink, message
    	dist=0
    
    	if not loc.lat or not loc.lon then -- structured data/wikidata coordinates only
    		coord = sd
    		cat = string.format(CoorCat.strucData0, mode, source)
    		case = 0
    	elseif loc.lat and loc.lon and not sd.lat and not sd.lon then	
    		cat = string.format(CoorCat.strucData4, mode, source)
    		case = 4 -- local coordinates only
    	elseif loc.lat and loc.lon and sd.lat and sd.lon then
    		dist = distance(loc.lat, loc.lon, sd.lat, sd.lon) -- calculate distance
    		dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', dist) -- will be displayed when hovering a mouse above wikidata icon
    
    		if dist<20 or dist<sd.prec then -- will consider location within 20 meters or precision distance as the same
    			if source=='Wikidata' then
    				cat = string.format(CoorCat.strucData1, mode, source)
    			end
    			case = 1
    		elseif (dist<1000 or dist<5*sd.prec) and mode=='object' then 
    			--cat = string.format(CoorCat.strucData2, mode, source)
    			case = 2
    		else -- locations 1 km off and 5 precision distances away are likely wrong. The issue might be with wrong precission
    			mapLink = mw.getCurrentFrame():preprocess(add_maplink2(loc.lat, loc.lon, sd.lat, sd.lon)) -- fancy link to OSM
    			message = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at %s (%s, precision: %i m). Please reconcile them. ",
    			dist, source, mapLink, sd.prec)	
    			cat = string.format(CoorCat.strucData3, mode, source) .. info_box(message)
    			case = 3
    		end
    	end
    	if not loc.heading and sd.heading then -- structured data/wikidata heading only
    		coord.heading = sd.heading
    	elseif loc.heading and not sd.heading and sd.lat and sd.lon then -- local heading only
    		cat = cat .. string.format(CoorCat.sHeading4, mode, source) 
    	elseif loc.heading and sd.heading then
    		local dh = math.abs(loc.heading - sd.heading)
    		if dh>1 then
    			message = string.format("There is a discrepancy of %i degrees between the above camera heading (set to %i) and the ones stored at %s (set to %i). Please reconcile them. ", dh, loc.heading, source, sd.heading)
    			cat = cat .. string.format(CoorCat.sHeading3, mode, source)  .. info_box(message)
    		end
    	end
    	if source=='Wikidata' and case>=3 then
    		local today = '+' .. os.date('!%F') .. 'T00:00:00Z/11' -- today's date in QS format
    		qs = string.format('%s|P625|@%09.5f/%09.5f|S143|Q565|S813|%s', sd.wID, loc.lat, loc.lon, today)
    		qs = mw.ustring.gsub(qs, '|', "%%09") -- replace some characters with ASCII hex values
    		qs = 'https://tools.wmflabs.org/quickstatements/index_old.html#v1=' .. qs    -- create full URL link
    		qs = string.format("[[File:Commons_to_Wikidata_QuickStatements.svg|15px|link=%s|Copy geo coordinates to Wikidata]]", qs)
    	end
    	local ret = {dist_str=dist_str, case=case, qs=qs }
    	return coord, cat, ret
    end
    
    -- ===========================================================
    local function dms2deg_ ( d, m, s, h )
      	d,m,s = tonumber(d), tonumber(m), tonumber(s)
      	if not (d and m and s and h) then
    		return nil
    	end
    	local LUT = {N=1, S=-1, E=1, W=-1} -- look up table
    	h = LUT[mw.ustring.upper( h )]
    	if not h then
    		return nil
    	end
    	return h * (d + m/60.0 + s/3600.0)
    end
    
    -- ===========================================================
    local function dms2deg ( dms )
      	local ltab  = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/")
      	local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4])
    	--return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "")  .. '->' .. (degre or 'nil')
    	return degre or dms
    end
    
    -- =======================================
    -- === External Functions ================
    -- =======================================
    local p = {}
    p.debug = 'nothing'
    
    -- parse attribute variable returning desired field (used for debugging)
    function p.parseAttribute(frame)
      return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
    end
    
    -- ===========================================================
    -- Helper core function for getHeading. 
    function p._getHeading(attributes)
    	if attributes == nil then
    		return nil
    	end
    	local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
    	if hStr == nil then
    		return nil
    	end
    	local hNum = tonumber( hStr )
    	if hNum == nil then
    		hStr = string.upper (hStr)
    		hNum = compass_points[hStr]  
    	end
    	if hNum then
    		hNum = hNum%360
    	end
    	return hNum
    end
    
    --[[============================================================================
    Parse attribute variable returning heading field. If heading is a string than 
    try to convert it to an angle
    ==============================================================================]]
    
    function p.getHeading(frame)  
    	local attributes
    	if frame.args[1] then
    		attributes = frame.args[1]
    	elseif frame.args.attributes then
    		attributes = frame.args.attributes
    	else
    		return ''
    	end
    	local hNum  = p._getHeading(attributes)
    	if hNum == nil then
    		return ''
    	end
    	return tostring(hNum)
    end
    
    
    --[[============================================================================
    Helper core function for deg2dms. deg2dms can be called by templates, while 
    _deg2dms should be called from Lua.
    Inputs:
    * degree - positive coordinate in degrees
    * degPrec - coordinate precision in degrees will result in different angle format
    * lang - language to used when formatting the number
    ==============================================================================]]
    function p._deg2dms(degree, degPrec, lang)
    	local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero
    	local Lang = mw.language.new(lang)
    
    	-- adjust number display based on precision
    	secPrec = degPrec*3600.0                     -- coordinate precision in seconds
    	if secPrec<0.05 then                         -- degPrec<1.3889e-05
    		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
    		c = 360000
    	elseif secPrec<0.5 then                      -- 1.3889e-05<degPrec<1.3889e-04
    		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
    		c = 36000
    	elseif degPrec*60.0<0.5 then                 -- 1.3889e-04<degPrec<0.0083
    		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
    		c = 3600
    	elseif degPrec<0.5 then                      -- 0.0083<degPrec<0.5
    		formatStr = '%s°&nbsp;%s′'               -- use DD° MM′ format
    		c = 60
    	else -- if degPrec>0.5 then                  
    		formatStr = '%s°'                        -- use DD° format
    		c = 1
    	end
    	
    	-- create degree, minute and seconds numbers and string
    	d = c/60
    	k  = math.floor(c*(degree%360)+0.49)  -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding
    	dNum = math.floor(k/c) % 360          -- degree number (integer in 0-360 range)
    	mNum = math.floor(k/d) %  60          -- minute number (integer in 0-60 range)
    	sNum =      3600*(k%d) / c            -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
    	dStr = Lang:formatNum(dNum)           -- degree string 
    	mStr = Lang:formatNum(mNum)           -- minute string 
    	sStr = Lang:formatNum(sNum)           -- second string 
    	zero = Lang:formatNum(0)              -- zero string in local language
    	if mNum<10 then
    		mStr = zero .. mStr                 -- pad with zero if a single digit
    	end
    	if sNum<10 then
    		sStr = zero .. sStr                 -- pad with zero if less than ten
    	end
    	return string.format(formatStr, dStr, mStr, sStr);
    end
    
    --[[============================================================================
    Convert degrees to degrees/minutes/seconds notation commonly used when displaying 
    coordinates.
    Inputs:
    1) latitude or longitude angle in degrees
    2) georeference precision in degrees
    3) language used in formatting of the number
    ==============================================================================]]
    function p.deg2dms(frame)
    	local args = core.getArgs(frame)
    	local degree  = tonumber(args[1])
    	local degPrec = tonumber(args[2]) or 0-- precision in degrees
    
    	if degree==nil then
    		return args[1];
    	else
    		return p._deg2dms(degree, degPrec, args.lang)
    	end
    end
    
    function p.dms2deg(frame)
    	return dms2deg(frame.args[1])
    end
    
    --[[============================================================================
    Format coordinate location string, by creating and joining DMS strings for 
    latitude and longitude. Also convert precision from meters to degrees.
    INPUTS:
     * lat        = latitude in degrees
     * lon        = longitude in degrees
     * lang       = language code
     * prec       = geolocation precision in meters
    ==============================================================================]]
    function p._lat_lon(lat, lon, prec, lang)
    	lat  = tonumber(lat)
    	lon  = tonumber(lon)
    	prec = math.abs(tonumber(prec) or 0)
    	if lon then -- get longitude to be in -180 to 180 range
    		lon=lon%360
    		if lon>180 then
    			lon = lon-360
    		end
    	end
    	if lat==nil or lon==nil then
    		return NoLatLonString
    	else
    		local nsew = core.langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language 
    		local SN, EW, latStr, lonStr, lon2m, lat2m, phi
    		if lat<0 then SN = nsew.S else SN = nsew.N end              -- choose S or N depending on latitude  degree sign
    		if lon<0 then EW = nsew.W else EW = nsew.E end              -- choose W or E depending on longitude degree sign
    		lat2m=1
    		lon2m=1
    		if prec>0 then -- if user specified the precision of the geo location...
    			phi   = math.abs(lat)*math.pi/180   -- latitude in radiants
    			lon2m = 6378137*math.cos(phi)*math.pi/180  -- see https://en.wikipedia.org/wiki/Longitude
    			lat2m = 111000  -- average latitude degree size in meters
    		end
    		latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude  degrees to degrees/minutes/seconds
    		lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds
    		return string.format('%s&nbsp;%s, %s&nbsp;%s', latStr, SN, lonStr, EW)
    		--return string.format('<span class="latitude">%s %s</span>, <span class="longitude">%s %s</span>', latStr, SN, lonStr, EW)
    	end
    end
    
    function p.lat_lon(frame)
    	local args = core.getArgs(frame)
    	return p._lat_lon(args.lat, args.lon, args.prec, args.lang)
    end
    
    --[[============================================================================
    Helper core function for externalLink. Create URL for different sites:
    INPUTS:
     * site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
                    OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
     * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                    Ganymede are also supported but are unused as of 2013.
     * latStr     = latitude string or number
     * lonStr     = longitude string or number
     * lang       = language code
     * attributes = attributes to be passed to GeoHack
    ==============================================================================]]
    function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level)
    	local URLstr = SiteURL[site];
    	level = level or 1
    	local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' )
    	pageName = mw.ustring.gsub( pageName, '%%', '%%%%')
    
    	if site == 'GoogleMaps' then
    		URLstr = SiteURL.GoogleMaps[globe]
    	elseif site == 'GeoHack' then
    		attributes = string.format('globe:%s_%s', globe, attributes)
    		URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
    	end
    	URLstr = mw.ustring.gsub( URLstr, '$lat'  , latStr)
    	URLstr = mw.ustring.gsub( URLstr, '$lon'  , lonStr)
    	URLstr = mw.ustring.gsub( URLstr, '$lang' , lang)
    	URLstr = mw.ustring.gsub( URLstr, '$level', level)
    	URLstr = mw.ustring.gsub( URLstr, '$page' , pageName)
    	URLstr = mw.ustring.gsub( URLstr, '+', '')
    	URLstr = mw.ustring.gsub( URLstr, ' ', '_')
    	return URLstr
    end
    
    --[[============================================================================
    Create URL for different sites.
    INPUTS:
     * site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
                    OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
     * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                    Ganymede are also supported but are unused as of 2013.
     * lat        = latitude string or number
     * lon        = longitude string or number
     * lang       = language code
     * attributes = attributes to be passed to GeoHack
    ==============================================================================]]
    function p.externalLink(frame)
    	local args = core.getArgs(frame)
    	return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
    end
    
    --[[============================================================================
    Adjust GeoHack attributes depending on the template that calls it
    INPUTS:
     * attributes = attributes to be passed to GeoHack
     * mode = set by each calling template
    ==============================================================================]]
    function p.alterAttributes(attributes, mode, heading)
    	-- indicate which template called it
    	if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
    		if not string.find(attributes, 'type:camera') then
    			attributes = 'type:camera_' .. attributes
    		end
    	elseif mode=='object'  then              -- Used by {{Object location}}
    		if mode=='object' and not string.find(attributes, 'type:') then
    			attributes = 'type:object_' .. attributes
    		end
    		if not string.find(attributes, 'class:object') then
    			attributes = 'class:object_' .. attributes
    		end
    	elseif mode=='inline' then                               -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
    	elseif mode=='user' then                                 -- Used by {{User location}}
    		attributes = 'type:user_location'
    	elseif mode=='institution' then                          --Used by {{Institution/coordinates}} (categories only)	
    		attributes = 'type:institution'
    	end
    	local hStr = ''
    	if heading then -- if heading is a  number 
    		hStr = string.format('heading:%6.2f', heading)
    	end
    	if not string.find(attributes, 'heading:') then
    		attributes = attributes .. '_' .. hStr
    	else
    		attributes = string.gsub(attributes,'heading:[^_]*', hStr) -- replace heading in form heading:N with heading=0 
    		attributes = string.gsub(attributes,'__', '_') 
    	end
    
    	return string.gsub(attributes,' ', '')
    end
    	
    --[[============================================================================
     Create link to GeoHack tool which displays latitude and longitude coordinates 
     in DMS format
     INPUTS:
     * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                    Ganymede are also supported but are unused as of 2013.
     * lat        = latitude in degrees
     * lon        = longitude in degrees
     * lang       = language code
     * prec       = geolocation precision in meters
     * attributes = attributes to be passed to GeoHack
    ==============================================================================]]
    function p._GeoHack_link(args)
    	-- create link and coordintate string
    	local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
    	if latlon==NoLatLonString then
    		return latlon
    	else
    		local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
    		return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
    	end
    end
    
    function p.GeoHack_link(frame)
    	return p._GeoHack_link(core.getArgs(frame))
    end
    
    
    --[[============================================================================
     Create full external links section of {{Location}} or {{Object location}} 
     templates, based on:
     * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
     * mode       = Possible options: 
      - camera - call from {{location}}
      - object - call from {{Object location}}
      - globe  - call from {{Globe location}}
     * lat        = latitude in degrees
     * lon        = longitude in degrees
     * lang       = language code
     * namespace  = namespace name: File, Category, (Gallery)
    ==============================================================================]]
    function p._externalLinksSection(args)
    	local lang = args.lang
    	if not args.namespace then
    		args.namespace = mw.title.getCurrentTitle().nsText
    	end
    	
    	local str, link1, link2, link3, link4
    	if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links
    		link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '')
    		link2 = p._externalLink('GoogleEarth'   , 'Earth', args.lat, args.lon, lang, '')
    		str = string.format('[%s %s] - [%s %s]', 
    			link1, core.langSwitch(i18n.OpenStreetMaps, lang),
    			link2, core.langSwitch(i18n.GoogleEarth, lang)) 
    	elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links
    		link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
    		--link2 = p._externalLink('GoogleMaps'    , 'Earth', args.lat, args.lon, lang, '', args.catRecurse) 
    		link3 = p._externalLink('GoogleEarth'   , 'Earth', args.lat, args.lon, lang, '')
    		link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '')
    		str = string.format('[%s %s] - [%s %s] - [%s %s]', 
    			link1, core.langSwitch(i18n.OpenStreetMaps, lang),
    			--link2, core.langSwitch(i18n.GoogleMaps, lang),
    			link3, core.langSwitch(i18n.GoogleEarth, lang),
    			link4, core.langSwitch(i18n.Proximityrama, lang))
    	elseif args.globe=='Mars' or args.globe=='Moon' then
    		link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
    		str = string.format('[%s %s]', link1, core.langSwitch(i18n.GoogleMaps, lang))
    	end
    	
    	return str
    end
    
    function p.externalLinksSection(frame)
    	return p._externalLinksSection(core.getArgs(frame))
    end
    
    --[[============================================================================
    Core section of template:Location, template:Object location and template:Globe location.
    This method requires several arguments to be passed to it or it's parent method/template:
     * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
     * mode       = Possible options: 
      - camera - call from {{location}}
      - object - call from {{Object location}}
      - globe  - call from {{Globe location}}
     * lat        = latitude in degrees
     * lon        = longitude in degrees
     * attributes = attributes
     * lang       = language code
     * namespace  = namespace: File, Category, Gallery
     * prec       = geolocation precision in meters
    ==============================================================================]]
    function p._LocationTemplateCore(args)
    	-- prepare arguments
    	if not (args.namespace) then -- if namespace not provided than look it up
    		args.namespace = mw.title.getCurrentTitle().nsText
    	end
    	if args.namespace=='' then -- if empty than it is a gallery
    		args.namespace = 'Gallery'
    	end
    	local bare   = core.yesno(args.bare,false)
    	local Status = 'primary' -- used by {{#coordinates:}}
    	if core.yesno(args.secondary,false) then
    		Status = 'secondary'
    	end
    	args.globe = mw.language.new('en'):ucfirst(args.globe or 'Earth') 
    	
    	-- Convert coordinates from string to numbers
    	local lat = tonumber(args.lat)
    	local lon = tonumber(args.lon)
    	local heading = p._getHeading(args.attributes)	-- get heading arrow section
    	if lon then -- get longitude to be in -180 to 180 range
    		lon=lon%360
    		if lon>180 then
    			lon = lon-360
    		end
    	end
    	
    	-- If wikidata link provided than compare coordinates
    	local Categories, geoMicroFormat, coorTag, edit_icon, wikidata_link = '', '', '', '', '', '', ''
    	local entity, coord, sd, cmp
    	local loc = {lat=lat, lon=lon, heading=heading, source='loc'}
    	local ID = args.wikidata
    	if (ID==nil) then 
    		entity = mw.wikibase.getEntity()
    	elseif type(ID)=='string' and ID:match( '^[QqMm]%d+$' ) then
    		entity = mw.wikibase.getEntity(ID)
    	elseif type(ID)~='string' and ID.id then
    		entity = ID -- entities can be passed from outside
    	end
    	
    	if entity then
    		if (args.mode=='object' or args.mode=='globe') then
    			sd = getSDCoords(entity,'P625')  -- fetch object coordinates 
    		elseif (args.mode=='camera') then
    			sd = getSDCoords(entity,'P1259') -- fetch camera coordinates or coordinates of the point of view
    		end
    		if (args.namespace=='File') then -- look up lat/lon on SDC
    			coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'SDC')
    			if coord.source~='loc' then
    				 edit_icon = core.editAtSDC(coord.source, args.lang)
    				 lat, lon, heading = coord.lat, coord.lon, coord.heading
    			end
    		elseif (args.namespace == 'Category') then  -- look up lat/lon on wikidata
    			sd.wID = entity.id
    			coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'Wikidata')
    			if coord.source~='loc' then
    				local str = "\n[[File:Wikidata-logo.svg|20px|Field with data from Wikidata's %s property<br/>%s|link=wikidata:%s#%s]]"
    				edit_icon = core.editAtWikidata(entity.id, coord.source, args.lang)
    				lat, lon, heading = coord.lat, coord.lon, coord.heading
    			end
    			if cmp.qs then
    				wikidata_link = cmp.qs 
    			end
    		end
    	elseif (args.namespace=='File') then
    		Categories = string.format(CoorCat.strucData4, args.mode, 'SDC')
    	end
    
    	args.lat = string.format('%010.6f', lat or 0)
    	args.lon = string.format('%011.6f', lon or 0)
    	args.attributes = p.alterAttributes(args.attributes or '', args.mode, heading)
    	local frame = mw.getCurrentFrame()
    
    	-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
    	if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then
    		if lat and lon then -- if lat and lon are numbers...
    			if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources
    				Categories = Categories .. CoorCat.default
    			end
    			if args.attributes and string.find(args.attributes, '=') then
    				Categories = Categories .. CoorCat.attribute
    			end
    			if (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
    				Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are outside allowed range)</span>\n' .. CoorCat.erroneous
    			end
    			-- local cat = CoorCat[args.namespace]
    			-- if cat then -- add category based on namespace
    				-- Categories = Categories .. cat
    			-- end
    			-- if not earth than add a category for each globe
    			if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then
    				Categories = Categories .. string.format(CoorCat[args.mode], args.globe)
    			end
    			-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
    			geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
    			-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
    			if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then 
    				coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } )
    			elseif args.namespace == 'File' and args.mode=='object' then 
    				coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } )
    			end
    		else -- if lat and lon are not numbers then add error category
    			Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are missing or not numeric)</span>\n' .. CoorCat.erroneous
    		end
    	end
    
    	-- Call helper functions to render different parts of the template
    	local coor,  info_link, inner_table, OSM = '','','','','',''
    	coor = p._GeoHack_link(args)  			-- the p and link to GeoHack
    	coor = string.format('<span class=plainlinks>%s</span>%s', coor, edit_icon)
    	if heading then  
    		local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
    		local fname = heading_icon[k]
    		coor = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading)
    	end
    	if args.globe=='Earth' then
    		local icon = 'marker'
    		if args.mode=='camera' then 
    			icon = 'camera'
    		end
    		OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM
    	end
    	local external_link = p._externalLinksSection(args) 					-- external link section
    	if external_link and args.namespace == 'File' then
    		external_link = core.langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link 	-- header of the link section for {{location}} template
    	elseif external_link then
    		external_link = core.langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template
    	end
    	info_link   = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', core.langSwitch(i18n.COM_GEO, args.lang) )
    	inner_table = string.format('<td style="border:none;">%s&nbsp;%s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>', 
    		coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat)
    	
    	-- combine strings into a table
    	local templateText
    	if bare then
    		templateText  = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table)
    	else
    		-- choose name of the field
    		local field_name = 'Location'
    		if args.mode=='camera' then 
    			field_name = core.langSwitch(i18n.CameraLocation, args.lang)
    		elseif args.mode=='object' then 
    			field_name = core.langSwitch(i18n.ObjectLocation, args.lang)
    		elseif args.mode=='globe' then
    			local field_list = core.langSwitch(i18n.GlobeLocation, args.lang)
    			if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
    				field_name = field_list[args.globe]
    			end
    		end
    		--Create HTML text
    		local dir, text_align
    		if mw.language.new( args.lang ):isRTL() then
    			dir = 'rtl'
    			text_align = 'right'
    		else
    			dir = 'ltr'
    			text_align = 'left'
    		end 
    		local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"', 
    		args.lang, dir, text_align, args.lang)
    		templateText  = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', args.lang, style, field_name, inner_table)
    	end
    	return templateText, Categories, coorTag
    end
    
    function p.LocationTemplateCore(frame)
    	local args = core.getArgs(frame)
    	args.namespace = mw.title.getCurrentTitle().nsText
    	if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
    		if args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W"
    			args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] )
    			args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
    			args.attributes = args.attributes or args[9]
    		elseif args[2] and not (type(args[2])=='string' and args[2]:find(":")) then -- decimal format or DMS with one pipe, ex. "34° 05′ 32.36″ N| 116° 09′ 24.55″ W"
    			args.lat = args[1]
    			args.lon = args[2]
    			args.attributes = args.attributes or args[3]
    		elseif args[1] then -- detect a single argument in the form "34° 05′ 32.36″ N, 116° 09′ 24.55″ W" or similar
    			local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter
    			args.lat, args.lon = v[1], v[2]
    			args.attributes = args.attributes or args[2]
    		end
    	end
    	local cat = ''
    	if args.lat and args.lon then
    		local lat = tonumber(args.lat)
    		local lon = tonumber(args.lon)
    		if not lat or not lon then
    			args.lat = dms2deg(args.lat or '')
    			args.lon = dms2deg(args.lon or '')
    			if (args.namespace == 'File' or args.namespace == 'Category') then
    				cat = CoorCat.dms
    			end
    		end
    	end
    	local templateText, Categories, coorTag = p._LocationTemplateCore(args)
    	return templateText .. Categories .. cat .. coorTag
    end
    
    return p