Module:Coordinates: Difference between revisions

From WWII Archives

m (1 revision imported)
wc>Multichill
("Category:Files with coordinates missing SDC location of creation..." space missing)
Line 1: Line 1:
--[[
--[[
This module is intended to replace the functionality of {{Coord}} and related
  __  __          _      _        ____                    _ _            _           
templates. It provides several methods, including
|  \/  | ___  __| |_  _| | ___ _ / ___|___  ___  _ __ __| (_)_ __  __ _| |_ ___  ___
| |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
| |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
  |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
                                                                                         


{{#invoke:Coordinates | coord }} : General function formatting and displaying
This module is intended to provide functionality of {{location}} and related
coordinate values.
templates. It was developed on Wikimedia Commons, so if you find this code on
other sites, check there for updates and discussions.


{{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing
degree values to DMS format.
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.


{{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
Authors and maintainers:
to decimal degree format.
* User:Jarekt
* User:Ebraminio


{{#invoke:Coordinates | link }} : Export the link used to reach the tools
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)


]]
]]


require('strict')
-- =======================================
-- === Dependencies ======================
-- =======================================
require('strict') -- 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')


local math_mod = require("Module:Math")
-- =======================================
local coordinates = {};
-- === Hardwired parameters ==============
-- =======================================


local current_page = mw.title.getCurrentTitle()
-- ===========================================================
local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
-- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
local coord_link = '//geohack.toolforge.org/geohack.php?pagename=' .. page_name .. '&params='
local compass_points = {
local templatestyles = 'Module:Coordinates/styles.css'
  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,
}


--[[ Helper function, replacement for {{coord/display/title}} ]]
-- ===========================================================
local function displaytitle(s, notes)
-- files to use for different headings
local l = "[[Geographic coordinate system|Coordinates]]: " .. s
local heading_icon = {
local co = '<span id="coordinates">' .. l .. notes .. '</span>';
[ 1] = 'File:Compass-icon bb N.svg',
return '<span style="font-size: small;">' .. co .. '</span>';
[ 2] = 'File:Compass-icon bb NbE.svg',
end
[ 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        = 'https://geohack.toolforge.org/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
--GoogleEarth    = '//geocommons.toolforge.org/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
Proximityrama  = 'https://tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
WikimediaMap  = 'https://maps.wikimedia.org/#16/$lat/$lon',
--OpenStreetMap1 = '//wiwosm.toolforge.org/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
OpenStreetMap1 = 'https://wikimap.toolforge.org/?wp=false&cluster=false&zoom=16&lat=$lat&lon=$lon',
OpenStreetMap2 = 'https://tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
GoogleMaps = {
Mars  = 'https://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
Moon  = 'https://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
Earth = 'https://wp-world.toolforge.org/googlmaps-proxy.php?page=http://kmlexport.toolforge.org/%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]]',
sHeading5  = '[[Category:Pages with local %s heading:0 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 ===================
-- =======================================


--[[ Helper function, Replacement for {{coord/display/inline}} ]]
-------------------------------------------------------------------------------
local function displayinline(s, notes)
local function getProperty(entity, prop)
return s .. notes
return (core.parseStatements(entity:getBestStatements( prop ), nil) or {nil})[1]
end
end


--[[ Helper function, used in detecting DMS formatting ]]
-- ===========================================================
local function dmsTest(first, second)
local function add_maplink(lat, lon, marker, text)
if type(first) ~= 'string' or type(second) ~= 'string' then
local tstr = ''
return nil
if text then
tstr = string.format('text="%s" ', text)
end
end
local s = (first .. second):upper()
return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'..
return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
'  "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
end


 
-- ===========================================================
--[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
local function add_maplink2(lat1, lon1, lat2, lon2)
local function makeInvokeFunc(funcName)
return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
return function (frame)
' "type": "Feature",'..
local args = require('Module:Arguments').getArgs(frame, {
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
wrappers = 'Template:Coord'
' "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons"  }'..
})
'},{'..
return coordinates[funcName](args, frame)
'  "type": "Feature",'..
end
'  "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
end


--[[ Helper function, handle optional args. ]]
-- ===========================================================
local function optionalArg(arg, supplement)
local function info_box(text)
return arg and arg .. supplement or ''
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
end


--[[
-- ===========================================================
Formats any error messages generated for display
local function distance(lat1, lon1, lat2, lon2)
]]
-- calculate distance
local function errorPrinter(errors)
local dLat = math.rad(lat1-lat2)
local result = ""
local dLon = math.rad(lon1-lon2)
for i,v in ipairs(errors) do
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))
local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>'
d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d))  -- angular distance in radians
result = result .. errorHTML .. "<br />"
d = 6371000 * d      -- radians to meters conversion
end
d = math.floor(d+0.5) -- round it to even meters
return result
return d
end
end


--[[
-- ===========================================================
Determine the required CSS class to display coordinates
local function getSDCoords(entity, prop)
 
    -- get coordinates from structured data (either wikidata or SDC)
Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
local coords = {id=entity.id, source=prop}
default is the mode as specificied by the user when calling the {{coord}} template
if not entity or not entity.claims or not entity.claims[prop]then  
mode is the display mode (dec or dms) that we will need to determine the css class for
return coords
]]
local function displayDefault(default, mode)
if default == "" then
default = "dec"
end
end
 
for _, statement in pairs( entity:getBestStatements( prop )) do
if default == mode then
local v = statement.mainsnak.datavalue.value -- get coordinates
return "geo-default"
if v.latitude then
else
coords.lat  = v.latitude
return "geo-nondefault"
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
end
return coords
end
end


--[[
-- ===========================================================
specPrinter
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


Output formatter. Takes the structure generated by either parseDec
if not loc.lat or not loc.lon then -- structured data/wikidata coordinates only
or parseDMS and formats it for inclusion on Wikipedia.
coord = sd
]]
cat = string.format(CoorCat.strucData0, mode, source)
local function specPrinter(args, coordinateSpec)
case = 0
local uriComponents = coordinateSpec["param"]
elseif loc.lat and loc.lon and not sd.lat and not sd.lon then
if uriComponents == "" then
cat = string.format(CoorCat.strucData4, mode, source)
-- RETURN error, should never be empty or nil
case = 4 -- local coordinates only
return "ERROR param was empty"
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 [[Commons:Structured data/Reconciliation|reconcile them]]. ",
dist, source, mapLink, sd.prec)
cat = string.format(CoorCat.strucData3, mode, source) .. info_box(message)
case = 3
end
end
end
if args["name"] then
if not loc.heading and sd.heading then -- structured data/wikidata heading only
uriComponents = uriComponents .. "&title=" .. mw.uri.encode(coordinateSpec["name"])
coord.heading = sd.heading
elseif loc.heading==0 and not sd.heading and sd.lat and sd.lon then -- local heading only
cat = cat .. string.format(CoorCat.sHeading5, mode, source)
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(math.fmod(loc.heading,360) - math.fmod(sd.heading,360))
if dh>1 and dh<359 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 [[Commons:Structured data/Reconciliation|reconcile them]]. ", dh, loc.heading, source, sd.heading)
cat = cat .. string.format(CoorCat.sHeading3, mode, source)  .. info_box(message)
end
end
end
 
if source=='Wikidata' and case>=3 then
local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
local url = mw.title.getCurrentTitle():canonicalUrl()
.. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
local today = '+' .. os.date('!%F') .. 'T00:00:00Z/11' -- today's date in QS format
.. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
qs = string.format('%s|P625|@%09.5f/%09.5f|S143|Q565|S813|%s|S4656|"%s"', sd.wID, loc.lat, loc.lon, today, url)
.. '</span>'
qs = string.gsub (mw.uri.encode(qs),'%%2520','%%20')
 
qs = 'https://quickstatements.toolforge.org/#/v1=' .. qs    -- create full URL link
local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
qs = string.format("[[File:Commons_to_Wikidata_QuickStatements.svg|15px|link=%s|Copy geo coordinates to Wikidata]]", qs)
local geodeclat
if lat < 0 then
-- FIXME this breaks the pre-existing precision
geodeclat = tostring(coordinateSpec["dec-lat"]):sub(2) .. "°S"
else
geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
end
end
local ret = {dist_str=dist_str, case=case, qs=qs }
return coord, cat, ret
end


local long = tonumber( coordinateSpec["dec-long"] ) or 0
-- Check if location of creation (P1071) is set
local geodeclong
local function checkLocationOfCreation(entity, lat, lon)
if long < 0 then
local cat = ''
-- FIXME does not handle unicode minus
local latFloor, lonFloor, latAbs, lonAbs
geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
if entity and entity.statements and entity.statements['P1071'] then
else
return cat
geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
end
end
 
latFloor = math.floor(lat)
local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
lonFloor = math.floor(lon)
.. geodeclat .. ' '
latAbs = math.abs(latFloor)
.. geodeclong
lonAbs = math.abs(lonFloor)
.. '</span>'
-- This is a rough bounding box of the Netherlands and part of neighbor countries as a pilot
 
if (48 <= latFloor) and (latFloor < 54) and (0 <= lonFloor) and (lonFloor < 12) then
local geonumhtml = '<span class="geo">'
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° E)]]", latAbs, lonAbs)
.. coordinateSpec["dec-lat"] .. '; '
return cat
.. coordinateSpec["dec-long"]
-- Part of the United Kingdom
.. '</span>'
elseif (50 <= latFloor) and (latFloor < 56) and (-5 <= lonFloor) and (lonFloor < 0) then
 
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° W)]]", latAbs, lonAbs)
local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
return cat
.. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
-- Canberra and Sydney
.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';
elseif (-36 <= latFloor) and (latFloor < -33) and (149 <= lonFloor) and (lonFloor < 152 ) then
 
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° E)]]", latAbs, lonAbs)
if not args["name"] then
return cat
inner = inner .. geodechtml
-- Buenos Aires and Montevideo
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span></span>'
elseif (-36 <= latFloor) and (latFloor < -33) and (-58 <= lonFloor) and (lonFloor < -55) then
else
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° W)]]", latAbs, lonAbs)
inner = inner .. '<span class="vcard">' .. geodechtml
return cat
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span>'
.. '<span style="display:none">&#xfeff; (<span class="fn org">'
.. args["name"] .. '</span>)</span></span></span>'
end
end
 
cat = '[[Category:Files with coordinates missing SDC location of creation]]'
return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
return cat
      .. '<span class="plainlinks nourlexpansion">'
      .. '[' .. coord_link .. uriComponents .. ' ' .. inner .. ']'
      .. '</span>'
end
end


--[[ Helper function, convert decimal to degrees ]]
-- ===========================================================
local function convert_dec2dms_d(coordinate)
local function dms2deg_ ( d, m, s, h )
local d = math_mod._round( coordinate, 0 ) .. "°"
  d,m,s = tonumber(d), tonumber(m), tonumber(s)
return d .. ""
  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
end


--[[ Helper function, convert decimal to degrees and minutes ]]
-- ===========================================================
local function convert_dec2dms_dm(coordinate)
local function dms2deg ( dms )
coordinate = math_mod._round( coordinate * 60, 0 );
  local ltab  = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/")
local m = coordinate % 60;
  local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4])
coordinate = math.floor( (coordinate - m) / 60 );
--return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "") .. '->' .. (degre or 'nil')
local d = coordinate % 360 .."°"
return degre or dms
 
return d .. string.format( "%02d′", m )
end
end


--[[ Helper function, convert decimal to degrees, minutes, and seconds ]]
-- =======================================
local function convert_dec2dms_dms(coordinate)
-- === External Functions ================
coordinate = math_mod._round( coordinate * 60 * 60, 0 );
-- =======================================
local s = coordinate % 60
local p = {}
coordinate = math.floor( (coordinate - s) / 60 );
p.debug = 'nothing'
local m = coordinate % 60
coordinate = math.floor( (coordinate - m) / 60 );
local d = coordinate % 360 .."°"


return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
-- 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
end


--[[
-- ===========================================================
Helper function, convert decimal latitude or longitude to
-- Helper core function for getHeading.
degrees, minutes, and seconds format based on the specified precision.
function p._getHeading(attributes)
]]
if attributes == nil then
local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
return nil
local coord = tonumber(coordinate)
end
local postfix
local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
if coord >= 0 then
if hStr == nil then
postfix = firstPostfix
return nil
else
end
postfix = secondPostfix
local hNum = tonumber( hStr )
if hNum == nil then
hStr = string.upper (hStr)
hNum = compass_points[hStr] 
end
end
 
if hNum then
precision = precision:lower();
hNum = hNum%360
if precision == "dms" then
return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
elseif precision == "dm" then
return convert_dec2dms_dm( math.abs( coord ) ) .. postfix;
elseif precision == "d" then
return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
end
end
return hNum
end
end


--[[
--[[============================================================================
Convert DMS format into a N or E decimal coordinate
Parse attribute variable returning heading field. If heading is a string than
]]
try to convert it to an angle
local function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
==============================================================================]]
local degrees = tonumber(degrees_str)
local minutes = tonumber(minutes_str) or 0
local seconds = tonumber(seconds_str) or 0


local factor = 1
function p.getHeading(frame) 
if direction == "S" or direction == "W" then
local attributes
factor = -1
if frame.args[1] then
attributes = frame.args[1]
elseif frame.args.attributes then
attributes = frame.args.attributes
else
return ''
end
end
 
local hNum  = p._getHeading(attributes)
local precision = 0
if hNum == nil then
if seconds_str then
return ''
precision = 5 + math.max( math_mod._precision(seconds_str), 0 );
elseif minutes_str and minutes_str ~= '' then
precision = 3 + math.max( math_mod._precision(minutes_str), 0 );
else
precision = math.max( math_mod._precision(degrees_str), 0 );
end
end
 
return tostring(hNum)
local decimal = factor * (degrees+(minutes+seconds/60)/60)
return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
end
end


--[[
Checks input values to for out of range errors.
]]
local function validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, source, strong )
local errors = {};
lat_d = tonumber( lat_d ) or 0;
lat_m = tonumber( lat_m ) or 0;
lat_s = tonumber( lat_s ) or 0;
long_d = tonumber( long_d ) or 0;
long_m = tonumber( long_m ) or 0;
long_s = tonumber( long_s ) or 0;
if strong then
if lat_d < 0 then
table.insert(errors, {source, "latitude degrees < 0 with hemisphere flag"})
end
if long_d < 0 then
table.insert(errors, {source, "longitude degrees < 0 with hemisphere flag"})
end
--[[
#coordinates is inconsistent about whether this is an error.  If globe: is
specified, it won't error on this condition, but otherwise it will.
For not simply disable this check.


if long_d > 180 then
--[[============================================================================
table.insert(errors, {source, "longitude degrees > 180 with hemisphere flag"})
Helper core function for deg2dms. deg2dms can be called by templates, while
end
_deg2dms should be called from Lua.
]]
Inputs:
end
* 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)


if lat_d > 90 then
-- adjust number display based on precision
table.insert(errors, {source, "latitude degrees > 90"})
secPrec = degPrec*3600.0                    -- coordinate precision in seconds
end
if secPrec<0.05 then                         -- degPrec<1.3889e-05
if lat_d < -90 then
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
table.insert(errors, {source, "latitude degrees < -90"})
c = 360000
end
elseif secPrec<0.5 then                      -- 1.3889e-05<degPrec<1.3889e-04
if lat_m >= 60 then
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
table.insert(errors, {source, "latitude minutes >= 60"})
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
end
if lat_m < 0 then
table.insert(errors, {source, "latitude minutes < 0"})
-- 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
end
if lat_s >= 60 then
if sNum<10 then
table.insert(errors, {source, "latitude seconds >= 60"})
sStr = zero .. sStr                -- pad with zero if less than ten
end
end
if lat_s < 0 then
return string.format(formatStr, dStr, mStr, sStr);
table.insert(errors, {source, "latitude seconds < 0"})
end
if long_d >= 360 then
table.insert(errors, {source, "longitude degrees >= 360"})
end
if long_d <= -360 then
table.insert(errors, {source, "longitude degrees <= -360"})
end
if long_m >= 60 then
table.insert(errors, {source, "longitude minutes >= 60"})
end
if long_m < 0 then
table.insert(errors, {source, "longitude minutes < 0"})
end
if long_s >= 60 then
table.insert(errors, {source, "longitude seconds >= 60"})
end
if long_s < 0 then
table.insert(errors, {source, "longitude seconds < 0"})
end
 
return errors;
end
end


--[[
--[[============================================================================
parseDec
Convert degrees to degrees/minutes/seconds notation commonly used when displaying
 
coordinates.
Transforms decimal format latitude and longitude into the
Inputs:
structure to be used in displaying coordinates
1) latitude or longitude angle in degrees
]]
2) georeference precision in degrees
local function parseDec( lat, long, format )
3) language used in formatting of the number
local coordinateSpec = {}
==============================================================================]]
local errors = {}
function p.deg2dms(frame)
 
local args = core.getArgs(frame)
if not long then
local degree  = tonumber(args[1])
return nil, {{"parseDec", "Missing longitude"}}
local degPrec = tonumber(args[2]) or 0-- precision in degrees
elseif not tonumber(long) then
return nil, {{"parseDec", "Longitude could not be parsed as a number: " .. long}}
end


errors = validate( lat, nil, nil, long, nil, nil, 'parseDec', false );
if degree==nil then
coordinateSpec["dec-lat"] = lat;
return args[1];
coordinateSpec["dec-long"] = long;
 
local mode = coordinates.determineMode( lat, long );
coordinateSpec["dms-lat"]  = convert_dec2dms( lat, "N", "S", mode)  -- {{coord/dec2dms|{{{1}}}|N|S|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
coordinateSpec["dms-long"] = convert_dec2dms( long, "E", "W", mode)  -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
 
if format then
coordinateSpec.default = format
else
else
coordinateSpec.default = "dec"
return p._deg2dms(degree, degPrec, args.lang)
end
end
end


return coordinateSpec, errors
function p.dms2deg(frame)
return dms2deg(frame.args[1])
end
end


--[[
--[[============================================================================
parseDMS
Format coordinate location string, by creating and joining DMS strings for
 
latitude and longitude. Also convert precision from meters to degrees.
Transforms degrees, minutes, seconds format latitude and longitude
INPUTS:
into the a structure to be used in displaying coordinates
* lat        = latitude in degrees
]]
* lon        = longitude in degrees
local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
* lang      = language code
local coordinateSpec, errors, backward = {}, {}
* prec      = geolocation precision in meters
 
==============================================================================]]
lat_f = lat_f:upper();
function p._lat_lon(lat, lon, prec, lang)
long_f = long_f:upper();
lat  = tonumber(lat)
 
lon  = tonumber(lon)
-- Check if specified backward
prec = math.abs(tonumber(prec) or 0)
if lat_f == 'E' or lat_f == 'W' then
if lon then -- get longitude to be in -180 to 180 range
lat_d, long_d, lat_m, long_m, lat_s, long_s, lat_f, long_f, backward = long_d, lat_d, long_m, lat_m, long_s, lat_s, long_f, lat_f, true;
lon=lon%360
end
if lon>180 then
 
lon = lon-360
errors = validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, 'parseDMS', true );
if not long_d then
return nil, {{"parseDMS", "Missing longitude" }}
elseif not tonumber(long_d) then
return nil, {{"parseDMS", "Longitude could not be parsed as a number:" .. long_d }}
end
 
if not lat_m and not lat_s and not long_m and not long_s and #errors == 0 then
if math_mod._precision( lat_d ) > 0 or math_mod._precision( long_d ) > 0 then
if lat_f:upper() == 'S' then
lat_d = '-' .. lat_d;
end
if long_f:upper() == 'W' then
long_d = '-' .. long_d;
end
 
return parseDec( lat_d, long_d, format );
end
end
end
end
 
if lat==nil or lon==nil then
coordinateSpec["dms-lat"]  = lat_d.."°"..optionalArg(lat_m,"′") .. optionalArg(lat_s,"″") .. lat_f
return NoLatLonString
coordinateSpec["dms-long"] = long_d.."°"..optionalArg(long_m,"′") .. optionalArg(long_s,"″") .. long_f
coordinateSpec["dec-lat"]  = convert_dms2dec(lat_f, lat_d, lat_m, lat_s) -- {{coord/dms2dec|{{{4}}}|{{{1}}}|0{{{2}}}|0{{{3}}}}}
coordinateSpec["dec-long"] = convert_dms2dec(long_f, long_d, long_m, long_s) -- {{coord/dms2dec|{{{8}}}|{{{5}}}|0{{{6}}}|0{{{7}}}}}
 
if format then
coordinateSpec.default = format
else
else
coordinateSpec.default = "dms"
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
end


return coordinateSpec, errors, backward
function p.lat_lon(frame)
local args = core.getArgs(frame)
return p._lat_lon(args.lat, args.lon, args.prec, args.lang)
end
end


--[[
--[[============================================================================
Check the input arguments for coord to determine the kind of data being provided
Helper core function for externalLink. Create URL for different sites:
and then make the necessary processing.
INPUTS:
]]
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
local function formatTest(args)
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
local result, errors
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
local backward, primary = false, false
                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, '%%', '%%%%')


local function getParam(args, lim)
if site == 'GoogleMaps' then
local ret = {}
URLstr = SiteURL.GoogleMaps[globe]
for i = 1, lim do
elseif site == 'GeoHack' then
ret[i] = args[i] or ''
attributes = string.format('globe:%s_%s', globe, attributes)
end
URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
return table.concat(ret, '_')
end
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


if not args[1] then
--[[============================================================================
-- no lat logic
Create URL for different sites.
return errorPrinter( {{"formatTest", "Missing latitude"}} )
INPUTS:
elseif not tonumber(args[1]) then
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
-- bad lat logic
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
return errorPrinter( {{"formatTest", "Unable to parse latitude as a number:" .. args[1]}} )
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,  
elseif not args[4] and not args[5] and not args[6] then
                Ganymede are also supported but are unused as of 2013.
-- dec logic
* lat        = latitude string or number
result, errors = parseDec(args[1], args[2], args.format)
* lon        = longitude string or number
if not result then
* lang      = language code
return errorPrinter(errors);
* 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
end
-- formatting for geohack: geohack expects D_N_D_E notation or D;D notation
elseif mode=='object' then              -- Used by {{Object location}}
-- wikiminiatlas doesn't support D;D notation
if mode=='object' and not string.find(attributes, 'type:') then
-- #coordinates parserfunction doesn't support negative decimals with NSWE
attributes = 'type:object_' .. attributes
result.param = table.concat({
math.abs(tonumber(args[1])),
((tonumber(args[1]) or 0) < 0) and 'S' or 'N',
math.abs(tonumber(args[2])),
((tonumber(args[2]) or 0) < 0) and 'W' or 'E',
args[3] or ''}, '_')
elseif dmsTest(args[4], args[8]) then
-- dms logic
result, errors, backward = parseDMS(args[1], args[2], args[3], args[4],
args[5], args[6], args[7], args[8], args.format)
if args[10] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
end
if not result then
if not string.find(attributes, 'class:object') then
return errorPrinter(errors)
attributes = 'class:object_' .. attributes
end
end
result.param = getParam(args, 9)
elseif mode=='inline' then                              -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
elseif dmsTest(args[3], args[6]) then
elseif mode=='user' then                                 -- Used by {{User location}}
-- dm logic
attributes = 'type:user_location'
result, errors, backward = parseDMS(args[1], args[2], nil, args[3],
elseif mode=='institution' then                         --Used by {{Institution/coordinates}} (categories only)
args[4], args[5], nil, args[6], args['format'])
attributes = 'type:institution'
if args[8] then
end
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
local hStr = ''
end
if heading then -- if heading is a  number
if not result then
hStr = string.format('heading:%6.2f', heading)
return errorPrinter(errors)
end
end
if not string.find(attributes, 'heading:') then
result.param = getParam(args, 7)
attributes = attributes .. '_' .. hStr
elseif dmsTest(args[2], args[4]) then
-- d logic
result, errors, backward = parseDMS(args[1], nil, nil, args[2],
args[3], nil, nil, args[4], args.format)
if args[6] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
if not result then
return errorPrinter(errors)
end
result.param = getParam(args, 5)
else
else
-- Error
attributes = string.gsub(attributes,'heading:[^_]*', hStr) -- replace heading in form heading:N with heading=0
return errorPrinter({{"formatTest", "Unknown argument format"}}) .. '[[Category:Pages with malformed coordinate tags]]'
attributes = string.gsub(attributes,'__', '_')  
end
result.name = args.name
 
local extra_param = {'dim', 'globe', 'scale', 'region', 'source', 'type'}
for _, v in ipairs(extra_param) do
if args[v] then
table.insert(errors, {'formatTest', 'Parameter: "' .. v .. '=" should be "' .. v .. ':"' })
end
end
end


local ret = specPrinter(args, result)
return string.gsub(attributes,' ', '')
if #errors > 0 then
ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
end
return ret, backward
end
end
 
--[[
--[[============================================================================
Generate Wikidata tracking categories.
Create link to GeoHack tool which displays latitude and longitude coordinates
]]
in DMS format
local function makeWikidataCategories(qid)
INPUTS:
local ret
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
local qid = qid or mw.wikibase.getEntityIdForCurrentPage()
                Ganymede are also supported but are unused as of 2013.
if mw.wikibase and current_page.namespace == 0 then
* lat        = latitude in degrees
if qid and mw.wikibase.entityExists(qid) and mw.wikibase.getBestStatements(qid, "P625") and mw.wikibase.getBestStatements(qid, "P625")[1] then
* lon        = longitude in degrees
local snaktype = mw.wikibase.getBestStatements(qid, "P625")[1].mainsnak.snaktype
* lang      = language code
if snaktype == 'value' then
* prec      = geolocation precision in meters
-- coordinates exist both here and on Wikidata, and can be compared.
* attributes = attributes to be passed to GeoHack
ret = 'Coordinates on Wikidata'
==============================================================================]]
elseif snaktype == 'somevalue' then
function p._GeoHack_link(args)
ret = 'Coordinates on Wikidata set to unknown value'
-- create link and coordintate string
elseif snaktype == 'novalue' then
local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
ret = 'Coordinates on Wikidata set to no value'
if latlon==NoLatLonString then
end
return latlon
else
-- We have to either import the coordinates to Wikidata or remove them here.
ret = 'Coordinates not on Wikidata'
end
end
if ret then
return string.format('[[Category:%s]]', ret)
else
else
return ''
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
end
end


--[[
function p.GeoHack_link(frame)
link
return p._GeoHack_link(core.getArgs(frame))
 
Simple function to export the coordinates link for other uses.
 
Usage:
{{#invoke:Coordinates | link }}
 
]]
function coordinates.link(frame)
return coord_link;
end
end


--[[
dec2dms


Wrapper to allow templates to call dec2dms directly.
--[[============================================================================
 
Create full external links section of {{Location}} or {{Object location}}
Usage:
templates, based on:
{{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix |
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
negative_suffix | precision }}
* mode      = Possible options:
 
  - camera - call from {{location}}
decimal_coordinate is converted to DMS format. If positive, the positive_suffix
  - object - call from {{Object location}}
is appended (typical N or E), if negative, the negative suffix is appended. The
  - globe  - call from {{Globe location}}
specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
* lat        = latitude in degrees
to use.
* lon        = longitude in degrees
]]
  * lang      = language code
coordinates.dec2dms = makeInvokeFunc('_dec2dms')
* namespace  = namespace name: File, Category, (Gallery)
function coordinates._dec2dms(args)
==============================================================================]]
local coordinate = args[1]
function p._externalLinksSection(args)
local firstPostfix = args[2] or ''
local lang = args.lang
local secondPostfix = args[3] or ''
if not args.namespace then
local precision = args[4] or ''
args.namespace = mw.title.getCurrentTitle().nsText
 
end
return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
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, '')
Helper function to determine whether to use D, DM, or DMS
--link2 = p._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, '')
format depending on the precision of the decimal input.
str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang))
]]
--link2, core.langSwitch(i18n.GoogleEarth, lang))  
function coordinates.determineMode( value1, value2 )
elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links
local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) );
link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
if precision <= 0 then
--link2 = p._externalLink('GoogleMaps'    , 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
return 'd'
--link3 = p._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, '')
elseif precision <= 2 then
--link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '')
return 'dm';
str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang))
else
--link2, core.langSwitch(i18n.GoogleMaps, lang),
return 'dms';
--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
end
return str
end
end


--[[
function p.externalLinksSection(frame)
dms2dec
return p._externalLinksSection(core.getArgs(frame))
 
Wrapper to allow templates to call dms2dec directly.
 
Usage:
{{#invoke:Coordinates | dms2dec | direction_flag | degrees |
minutes | seconds }}
 
Converts DMS values specified as degrees, minutes, seconds too decimal format.
direction_flag is one of N, S, E, W, and determines whether the output is
positive (i.e. N and E) or negative (i.e. S and W).
]]
coordinates.dms2dec = makeInvokeFunc('_dms2dec')
function coordinates._dms2dec(args)
local direction = args[1]
local degrees = args[2]
local minutes = args[3]
local seconds = args[4]
 
return convert_dms2dec(direction, degrees, minutes, seconds)
end
end


--[[
--[[============================================================================
coord
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:
Main entry point for Lua function to replace {{coord}}
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 
* mode      = Possible options:  
Usage:
  - camera - call from {{location}}
{{#invoke:Coordinates | coord }}
  - object - call from {{Object location}}
{{#invoke:Coordinates | coord | lat | long }}
  - globe  - call from {{Globe location}}
{{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
* lat        = latitude in degrees
...
* lon        = longitude in degrees
 
* attributes = attributes
Refer to {{coord}} documentation page for many additional parameters and
* lang      = language code
configuration options.
* namespace  = namespace: File, Category, Gallery
 
* prec      = geolocation precision in meters
Note: This function provides the visual display elements of {{coord}}.  In
==============================================================================]]
order to load coordinates into the database, the {{#coordinates:}} parser
function p._LocationTemplateCore(args)
function must also be called, this is done automatically in the Lua
-- prepare arguments
version of {{coord}}.
if not (args.namespace) then -- if namespace not provided than look it up
]]
args.namespace = mw.title.getCurrentTitle().nsText
coordinates.coord = makeInvokeFunc('_coord')
function coordinates._coord(args)
if not tonumber(args[1]) and not args[2] then
args[3] = args[1]; args[1] = nil
local entity = mw.wikibase.getEntityObject(args.qid)
if entity
and entity.claims
and entity.claims.P625
and entity.claims.P625[1].mainsnak.snaktype == 'value'
then
local precision = entity.claims.P625[1].mainsnak.datavalue.value.precision
args[1] = entity.claims.P625[1].mainsnak.datavalue.value.latitude
args[2] = entity.claims.P625[1].mainsnak.datavalue.value.longitude
if precision then
precision = -math_mod._round(math.log(precision)/math.log(10),0)
args[1] = math_mod._round(args[1],precision)
args[2] = math_mod._round(args[2],precision)
end
end
end
end
 
if args.namespace=='' then -- if empty than it is a gallery
local contents, backward = formatTest(args)
args.namespace = 'Gallery'
local Notes = args.notes or ''
local Display = args.display and args.display:lower() or 'inline'
 
local function isInline(s)
-- Finds whether coordinates are displayed inline.
return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti'
end
end
local function isInTitle(s)
local bare  = core.yesno(args.bare,false)
-- Finds whether coordinates are displayed in the title.
local Status = 'primary' -- used by {{#coordinates:}}
return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti'
if core.yesno(args.secondary,false) then
Status = 'secondary'
end
end
 
args.globe = mw.language.new('en'):ucfirst(args.globe or 'Earth')  
local function coord_wrapper(in_args)
-- Calls the parser function {{#coordinates:}}.
-- Convert coordinates from string to numbers
return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
local lat = tonumber(args.lat)
local lon = tonumber(args.lon)
local precission = tonumber(args.prec or '0')
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
end
 
local text = ''
-- If wikidata link provided than compare coordinates
if isInline(Display) then
local Categories, geoMicroFormat, coorTag, edit_icon, wikidata_link = '', '', '', '', '', '', ''
text = text .. displayinline(contents, Notes)
local entity, coord, sd, cmp, locationCat
local loc = {lat=lat, lon=lon, heading=heading, source='loc'}
local ID = args.wikidata
if (ID==nil) then  
entity = mw.wikibase.getEntity()
if entity and args.namespace == 'Category' then
-- this is category connected to Wikidata through sitelink
ID = getProperty(entity, "P301")
if getProperty(entity, "P31") == 'Q4167836' and ID then
-- wikidata item is a "category item" with "category's main topic (P301)"
-- follow P301 to the actual item for this category
entity = mw.wikibase.getEntity(ID)
end
end
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
end
if isInTitle(Display) then
text = text
if entity then
.. displaytitle(contents, Notes)
if (args.mode=='object' or args.mode=='globe') then
.. makeWikidataCategories(args.qid)
sd = getSDCoords(entity,'P9149') -- fetch coordinates of depicted place
if not sd.lat then
sd = getSDCoords(entity,'P625')  -- fallback to coordinate location
end
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, precission = coord.lat, coord.lon, coord.heading, coord.prec
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, precission = coord.lat, coord.lon, coord.heading, coord.prec
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
end
if not args.nosave then
local page_title, count = mw.title.getCurrentTitle(), 1
-- Check if location of creation (P1071) is set and if not, add tracker
if backward then
if args.namespace == 'File' and lat and lon then
local tmp = {}
locationCat = checkLocationOfCreation(entity, lat, lon)
while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
if locationCat then
tmp.count = count; count = 2*(count-1)
Categories = Categories .. locationCat
while count >= tmp.count do table.insert(tmp, 1, (args[count] or '')); count = count-1 end
for i, v in ipairs(tmp) do args[i] = v end
else
while count <= 9 do args[count] = (args[count] or ''); count = count+1 end
end
end
if isInTitle(Display) and not page_title.isTalkPage and page_title.subpageText ~= 'doc' and page_title.subpageText ~= 'testcases' then args[10] = 'primary' end
args.notes, args.format, args.display = nil
text = text .. coord_wrapper(args)
end
end
return text
end


--[[
args.lat  = string.format('%010.6f', lat or 0)
coord2text
args.lon  = string.format('%011.6f', lon or 0)
args.prec = precission
args.attributes = p.alterAttributes(args.attributes or '', args.mode, heading)
local frame = mw.getCurrentFrame()


Extracts a single value from a transclusion of {{Coord}}.
-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
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


Usage:
-- Call helper functions to render different parts of the template
 
local coor, info_link, inner_table, OSM = '','','','','',''
    {{#invoke:Coordinates | coord2text | {{Coord}} | parameter }}
coor = p._GeoHack_link(args) -- the p and link to GeoHack
 
coor = string.format('<span class=plainlinks>%s</span>%s', coor, edit_icon)
Valid values for the second parameter are: lat (signed integer), long (signed integer), type, scale, dim, region, globe, source
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]
function coordinates.coord2text(frame)
coor = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=Heading=%]]</span>', coor, heading, fname, heading)
if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
end
frame.args[2] = mw.text.trim(frame.args[2])
if args.globe=='Earth' then
if frame.args[2] == 'lat' or frame.args[2] == 'long' then
local icon = 'marker'
local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ')
if args.mode=='camera' then
if frame.args[2] == 'lat' then
icon = 'camera'
result, negative = result[1], 'S'
else
result, negative = result[2], 'W'
end
end
result = mw.text.split(result, '°')
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
if result[2] == negative then result[1] = '-'..result[1] end
end
return result[1]
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
else
return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
-- choose name of the field and create row
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
templateText  = string.format('<tr><th class="type fileinfo-paramfield">%s</th>%s</tr>', field_name, inner_table)
--Create HTML text
local dir  = mw.language.new( args.lang ):getDir()    -- get text direction
local style = 'class="toccolours mw-content-%s layouttemplate commons-file-information-table" style="width: 100%%;" dir="%s" lang="%s"'
style = string.format(style, dir, dir, args.lang)
templateText  = string.format('<table %s>\n%s\n</table>', style, templateText)
end
end
return templateText, Categories, coorTag
end
end


--[[
function p.LocationTemplateCore(frame)
coordinsert
local args = core.getArgs(frame)
 
args.namespace = mw.title.getCurrentTitle().nsText
Injects some text into the Geohack link of a transclusion of {{Coord}} (if that text isn't already in the transclusion). Outputs the modified transclusion of {{Coord}}.
if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
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] )
Usage:
args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
 
args.attributes = args.attributes or args[9]
    {{#invoke:Coordinates | coordinsert | {{Coord}} | parameter:value | parameter:value | … }}
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]
Do not make Geohack unhappy by inserting something which isn't mentioned in the {{Coord}} documentation.
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
function coordinates.coordinsert(frame)
local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter
for i, v in ipairs(frame.args) do
args.lat, args.lon = v[1], v[2]
if i ~= 1 then
args.attributes = args.attributes or args[2]
if not mw.ustring.find(frame.args[1], (mw.ustring.match(frame.args[i], '^(.-:)') or '')) then
frame.args[1] = mw.ustring.gsub(frame.args[1], '(params=.-)_? ', '%1_'..frame.args[i]..' ')
end
end
end
end
end
if frame.args.name then
local cat = ''
if not mw.ustring.find(frame.args[1], '<span class="vcard">') then
if args.lat and args.lon then
local namestr = frame.args.name
local lat = tonumber(args.lat)
frame.args[1] = mw.ustring.gsub(frame.args[1],
local lon = tonumber(args.lon)
'(<span class="geo%-default">)(<span[^<>]*>[^<>]*</span><span[^<>]*>[^<>]*<span[^<>]*>[^<>]*</span></span>)(</span>)',
if not lat or not lon then
'%1<span class="vcard">%2<span style="display:none">&#xfeff; (<span class="fn org">' .. namestr .. '</span>)</span></span>%3')
args.lat = dms2deg(args.lat or '')
frame.args[1] = mw.ustring.gsub(frame.args[1], '(&params=[^&"<>%[%] ]*) ', '%1&title=' .. mw.uri.encode(namestr) .. ' ')
args.lon = dms2deg(args.lon or '')
if (args.namespace == 'File' or args.namespace == 'Category') then
cat = CoorCat.dms
end
end
end
end
end
return frame.args[1]
local templateText, Categories, coorTag = p._LocationTemplateCore(args)
return templateText .. Categories .. cat .. coorTag
end
end


return coordinates
return p

Revision as of 19:11, 8 May 2024

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('strict') -- 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        = 'https://geohack.toolforge.org/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
	--GoogleEarth    = '//geocommons.toolforge.org/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
	Proximityrama  = 'https://tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
	WikimediaMap   = 'https://maps.wikimedia.org/#16/$lat/$lon',
	--OpenStreetMap1 = '//wiwosm.toolforge.org/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
	OpenStreetMap1 = 'https://wikimap.toolforge.org/?wp=false&cluster=false&zoom=16&lat=$lat&lon=$lon',
	OpenStreetMap2 = 'https://tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
	GoogleMaps = { 
		Mars  = 'https://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
		Moon  = 'https://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
		Earth = 'https://wp-world.toolforge.org/googlmaps-proxy.php?page=http://kmlexport.toolforge.org/%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]]',
	sHeading5  = '[[Category:Pages with local %s heading:0 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 getProperty(entity, prop)
	return (core.parseStatements(entity:getBestStatements( prop ), nil) or {nil})[1]
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 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 pairs( entity:getBestStatements( 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 [[Commons:Structured data/Reconciliation|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==0 and not sd.heading and sd.lat and sd.lon then -- local heading only
		cat = cat .. string.format(CoorCat.sHeading5, mode, source) 
	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(math.fmod(loc.heading,360) - math.fmod(sd.heading,360))
		if dh>1 and dh<359 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 [[Commons:Structured data/Reconciliation|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 url = mw.title.getCurrentTitle():canonicalUrl()
		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|S4656|"%s"', sd.wID, loc.lat, loc.lon, today, url)
		qs = string.gsub (mw.uri.encode(qs),'%%2520','%%20')
		qs = 'https://quickstatements.toolforge.org/#/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

-- Check if location of creation (P1071) is set
local function checkLocationOfCreation(entity, lat, lon)
	local cat = ''
	local latFloor, lonFloor, latAbs, lonAbs
	if entity and entity.statements and entity.statements['P1071'] then
		return cat
	end
	latFloor = math.floor(lat)
	lonFloor = math.floor(lon)
	latAbs = math.abs(latFloor)
	lonAbs = math.abs(lonFloor)
	-- This is a rough bounding box of the Netherlands and part of neighbor countries as a pilot
	if (48 <= latFloor) and (latFloor < 54) and (0 <= lonFloor) and (lonFloor < 12) then
		cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° E)]]", latAbs, lonAbs)
		return cat
	-- Part of the United Kingdom
	elseif (50 <= latFloor) and (latFloor < 56) and (-5 <= lonFloor) and (lonFloor < 0) then
		cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° W)]]", latAbs, lonAbs)
		return cat
	-- Canberra and Sydney
	elseif (-36 <= latFloor) and (latFloor < -33) and (149 <= lonFloor) and (lonFloor < 152 ) then
		cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° E)]]", latAbs, lonAbs)
		return cat 
	-- Buenos Aires and Montevideo
	elseif (-36 <= latFloor) and (latFloor < -33) and (-58 <= lonFloor) and (lonFloor < -55) then
		cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° W)]]", latAbs, lonAbs)
		return cat 
	end
	cat = '[[Category:Files with coordinates missing SDC location of creation]]'
	return cat
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]', 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]', 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 precission = tonumber(args.prec or '0')
	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, locationCat
	local loc = {lat=lat, lon=lon, heading=heading, source='loc'}
	local ID = args.wikidata
	if (ID==nil) then 
		entity = mw.wikibase.getEntity()
		if entity and args.namespace == 'Category' then 
			-- this is category connected to Wikidata through sitelink
			ID = getProperty(entity, "P301")
			if getProperty(entity, "P31") == 'Q4167836' and ID then
				-- wikidata item is a "category item" with "category's main topic (P301)" 
				-- follow P301 to the actual item for this category
				entity = mw.wikibase.getEntity(ID)
			end
		end
	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,'P9149')  -- fetch coordinates of depicted place
			if not sd.lat then
				sd = getSDCoords(entity,'P625')  -- fallback to coordinate location
			end
		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, precission = coord.lat, coord.lon, coord.heading, coord.prec
			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, precission = coord.lat, coord.lon, coord.heading, coord.prec
			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
	
	-- Check if location of creation (P1071) is set and if not, add tracker
	if args.namespace == 'File' and lat and lon then
		locationCat = checkLocationOfCreation(entity, lat, lon)
		if locationCat then
			Categories = Categories .. locationCat
		end
	end

	args.lat  = string.format('%010.6f', lat or 0)
	args.lon  = string.format('%011.6f', lon or 0)
	args.prec = precission
	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 and create row
		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
		templateText  = string.format('<tr><th class="type fileinfo-paramfield">%s</th>%s</tr>', field_name, inner_table)
		--Create HTML text
		local dir   = mw.language.new( args.lang ):getDir()    -- get text direction
		local style = 'class="toccolours mw-content-%s layouttemplate commons-file-information-table" style="width: 100%%;" dir="%s" lang="%s"' 
		style = string.format(style, dir, dir, args.lang)
		templateText  = string.format('<table %s>\n%s\n</table>', style, templateText)
	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