Module:Coordinates

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua
CodeDiscussionEditHistoryLinksLink countNo test API Subpages:DocumentationTestsResultsSandboxLive code All modules

Careful, this function is live, and called by {{Location}} template. The documentation might not be complete

This module provides most of the logic behind {{location}} and related geolocation templates. It provides several methods visible to the templates:

  • {{#Invoke:Coordinates | parseAttribute| attribute string | attribute name }} : parse {{location}} attribute parameter attribute string and return value of the attribute name parameter
  • {{#Invoke:Coordinates | getHeader| attribute string }} : parse {{location}} attribute parameter attribute string and return the numeric value of the header attribute.
  • {{#Invoke:Coordinates | GeoHack_link| lat=... | lon=... |lang=xx | site=... | globe=... }}: creates link to GeoHack tool and display location coordinates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth.
  • {{#Invoke:Coordinates | lat_lon| lat=... | lon=... |lang=xx }}: create coordinate location string based on decimal degrees latitude and longitude number. Language is used to localize the presentation of the numbers.
  • {{#Invoke:Coordinates | deg2dms| degrees|lang=xx }}: create dms (degree/minute/second) string based on decimal degrees number. Language is used to localize the presentation of the numbers.
  • {{#Invoke:Coordinates | externalLink| site=... | globe=... | lat=... | lon=... |lang=xx }}: create URLs for different sites which are used by coordinate location templates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth.

Examples

Functions:

  • deg2dms(degree, degree_precision, language)
  • {{#invoke:Coordinates| deg2dms | 12.3456789}} will display "12° 20′ 44.44″"
  • {{#invoke:Coordinates| deg2dms | 12.3456789 |1}} will display "12°"
  • {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-1}} will display "12° 21′"
  • {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-3}} will display "12° 20′ 44″"
  • {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-4}} will display "12° 20′ 44.4″"
  • {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-5}} will display "12° 20′ 44.44″"
  • lat_lon
  • {{#invoke:Coordinates| lat_lon | lat=51.48 | lon=0}} will display "51° 28′ 48″ N, 0° 00′ 00″ E"
  • GeoHack_link
  • externalLinksSection
    {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "OpenStreetMap - Google Earth"
  • {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=Category}} displays "OpenStreetMap - Google Earth - Proximityrama"
  • {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=ru | namespace=Category}} displays "OpenStreetMap - Google Планете Земля - Proximityrama"
  • {{#invoke:Coordinates| externalLinksSection | globe = Mars| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
  • {{#invoke:Coordinates| externalLinksSection | globe = Moon| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
  • LocationTemplateCore

See testcases to see more examples.

Dependencies

Relies on Module:I18n/coordinates for all of the internationalization translations.

Code

--[[
  __  __           _       _         ____                    _ _             _            
 |  \/  | ___   __| |_   _| | ___ _ / ___|___   ___  _ __ __| (_)_ __   __ _| |_ ___  ___ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) |   / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
 | |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
                                                                                          

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 yesno = require('Module:Yesno')

-- =======================================
-- === 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]]',
	wikidata0     = '[[Category:Pages with coordinates from Wikidata]]',
	wikidata1     = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]',
	wikidata2     = '[[Category:Pages with local coordinates and similar Wikidata coordinates]]',
	wikidata3     = '[[Category:Pages with local coordinates and mismatching Wikidata coordinates]]',
	wikidata4     = '[[Category:Pages with local coordinates and missing Wikidata coordinates]]',
	wikidata5     = '[[Category:Pages with locations and Wikidata ID to wrong type of entry]]',
	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]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n'
}

-- =======================================
-- === Local Functions ===================
-- =======================================

local function getArgs(frame)
	local args = frame.args
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = frame:callParserFunction("int","lang")  -- get user's chosen language
	end
	return args
end

local NoLatLonString = 'latitude, longitude'

local function langSwitch(list,lang)
	local langList = mw.language.getFallbacksFor(lang)
	table.insert(langList,1,lang)
	for i,language in ipairs(langList) do
		if list[language] then
			return list[language]
		end
	end
end

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 mergeWithWikidata(q, lat1, lon1)
	-- we are given wikidata q-code so look up the coordinates
	local dist_str=''
	local entity
	-- Wikiata coordinates
	if q:match( '^[Qq]%d+$' ) then
		entity = mw.wikibase.getEntity(q)
	else
		entity = q
	end
	q = entity.id
	local v, lat2, lon2, precision
	if entity then 
		local P625 = entity:getBestStatements( 'P625' ) --  coordinate location
		local P159 = entity:getBestStatements( 'P159' ) --  headquarters location
		if P625[1] and P625[1].mainsnak.datavalue.value.latitude then 
			v = P625[1].mainsnak.datavalue.value
		elseif P159[1] and P159[1].qualifiers and P159[1].qualifiers.P625 then
			v = P159[1].qualifiers.P625[1].datavalue.value
		end
		if v and v.globe == 'http://www.wikidata.org/entity/Q2' then
			lat2 = v.latitude
			lon2 = v.longitude
			precision = v.precision or 1e-4
			precision = math.floor(precision*111000) -- convert precision from degrees to meters and round
			precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree
		end
	end
			
	-- compare coordinates
	local cat = ''
	if not lat1 or not lon1 then -- wikidata coordinates only
		lat1 = lat2
		lon1 = lon2
		cat = CoorCat.wikidata0
	elseif lat1 and lon1 and not lat2 and not lon2 then	
		cat = string.format('The above coordinates are missing from linked Wikidata item [[d:%s|%s]].  Click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
		"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span> to copy it",
		q, q, q, lat1, lon1)
		cat = CoorCat.wikidata4 .. info_box(cat)
	elseif lat1 and lon1 and lat2 and lon2 then
		-- 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) -- rind it to even meters
		
		local frame = mw.getCurrentFrame()
		local info = frame:preprocess(add_maplink2(lat1, lon1, lat2, lon2)) -- fancy link to OSM
		info = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at linked Wikidata item [[d:%s|%s]] (%s, precision: %i m). "..
			'Please reconcile them. To copy Commons coordinates to Wikidata, click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
			"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span>",
			d, q, q, info, precision, q, lat1, lon1)

		if d<20 or d<precision then -- will consider location within 20 meters or precisi0on distance as the same
			cat = CoorCat.wikidata1
			dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', d) -- will be displayed when hovering a mouse above wikidata icon
		elseif d>1000 and d>5*precision then -- locations 1 km off and 5 precision distances away are likely wrong
			cat = CoorCat.wikidata3 .. info_box(info) 
		else
			cat = CoorCat.wikidata2 .. info_box(info) 
		end
	end
	
	-- verify proper P31 (instance of). List is based on https://www.wikidata.org/wiki/Property_talk:P625
	local QCodes = {  
		Q5        = 1, -- human
		Q11879590 = 1, -- female given name
		Q202444   = 1, -- given name
		Q12308941 = 1, -- male given name 
		Q4167836  = 1, -- Wikimedia category
		Q4167410  = 1, -- Wikimedia disambiguation page
		Q783794   = 2, -- company
		Q4830453  = 2, -- business enterprise
	}
	local s = entity:getBestStatements( 'P31' )
	if s[1] and s[1].mainsnak.datavalue.value['id'] then
		local instanceOf = s[1].mainsnak.datavalue.value['id']
		if QCodes[instanceOf] then
			cat = '' -- wipe out categories
			if QCodes[instanceOf]==1 then -- add problem category
				cat = CoorCat.wikidata5 
			end
		end
	end
	
	return lat1, lon1, q, cat, dist_str
end

-- =======================================
-- === External Functions ================
-- =======================================
local p = {}

-- 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 ~= nil 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 = 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

--[[============================================================================
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 = 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 = 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 = 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)
	-- indicate which template called it
	if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
		if string.find(attributes, 'type:camera')==nil then
			attributes = 'type:camera_' .. attributes
		end
	elseif mode=='object'or mode =='globe' then              -- Used by {{Object location}}
		if mode=='object' and string.find(attributes, 'type:')==nil then
			attributes = 'type:object_' .. attributes
		end
		if string.find(attributes, 'class:object')==nil 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
	return 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(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().namespace
	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, langSwitch(i18n.OpenStreetMaps, lang),
			link2, 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, langSwitch(i18n.OpenStreetMaps, lang),
			--link2, langSwitch(i18n.GoogleMaps, lang),
			link3, langSwitch(i18n.GoogleEarth, lang),
			link4, 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, langSwitch(i18n.GoogleMaps, lang))
	end
	
	return str
end

function p.externalLinksSection(frame)
	return p._externalLinksSection(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().namespace
	end
	if args.namespace=='' then -- if empty than it is a gallery
		args.namespace = 'Gallery'
	end
	local bare   = yesno(args.bare,false)
	local Status = 'primary' -- used by {{#coordinates:}}
	if yesno(args.secondary,false) then
		Status = 'secondary'
	end
	local attributes0 = args.attributes
	args.attributes = p.alterAttributes(args.attributes or '', args.mode)
	
	-- Convert coordinates from string to numbers
	local lat = tonumber(args.lat)
	local lon = tonumber(args.lon)
	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, wikidata_link = '', '', '', ''
	if args.wikidata and args.wikidata~='' then
		local dist_str, q
	  -- if lat/lon is not provided but we are given wikidata q-code than look up the coordinates
		lat, lon, q, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon)
		wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, q);
	end

	args.lat = string.format('%010.6f', lat or 0)
	args.lon = string.format('%011.6f', lon or 0)

	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 attributes0 and string.find(attributes0, '=') then
				Categories = Categories .. CoorCat.attribute
			end
			if args.error=='1' or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
				Categories = Categories .. 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 .. CoorCat.erroneous
		end
	end

	-- Call helper functions to render different parts of the template
	local coor,  info_link, inner_table, heading, OSM = '','','','','','',''
	coor = p._GeoHack_link(args)  			-- the p and link to GeoHack
	heading = p._getHeading(attributes0)	-- get heading arrow section
	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 = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link 	-- header of the link section for {{location}} template
	elseif external_link then
		external_link = 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]]', 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 '', info_link, wikidata_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 = langSwitch(i18n.CameraLocation, args.lang)
		elseif args.mode=='object' then 
			field_name = langSwitch(i18n.ObjectLocation, args.lang)
		elseif args.mode=='globe' then
			local field_list = 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 = frame.args
	if not args or not args.lat then -- if no arguments provided than use parent arguments
		args = mw.getCurrentFrame():getParent().args
	end
	if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then 
		args.lang = frame:callParserFunction("int","lang") -- get user's chosen language 
	end
	local templateText, Categories, coorTag = p._LocationTemplateCore(args)
	return templateText .. Categories .. coorTag
end

return p