Module:Coordinates: Difference between revisions

Safer nicotine wiki Tobacco Harm Reduction
Jump to navigation Jump to search
m (1 revision imported)
imported>WOSlinker
(use require('strict') instead of require('Module:No globals'))
 
Line 1: Line 1:
--[[
--[[
  __  __          _      _        ____                    _ _            _           
This module is intended to replace the functionality of {{Coord}} and related
|  \/  | ___  __| |_  _| | ___ _ / ___|___  ___  _ __ __| (_)_ __  __ _| |_ ___  ___
templates. It provides several methods, including
| |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
| |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
  |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
                                                                                         


This module is intended to provide functionality of {{location}} and related
{{#invoke:Coordinates | coord }} : General function formatting and displaying
templates. It was developed on Wikimedia Commons, so if you find this code on
coordinate values.
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
{{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
degree values to DMS format.


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


Functions:
{{#invoke:Coordinates | link }} : Export the link used to reach the tools
*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('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables
local i18n = require('Module:I18n/coordinates')    -- get localized translations of site names
local core = require('Module:Core')


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


-- ===========================================================
local current_page = mw.title.getCurrentTitle()
-- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
local compass_points = {
local coord_link = '//geohack.toolforge.org/geohack.php?pagename=' .. page_name .. '&params='
  N    = 0,
local templatestyles = 'Module:Coordinates/styles.css'
  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}} ]]
-- files to use for different headings
local function displaytitle(s, notes)
local heading_icon = {
local l = "[[Geographic coordinate system|Coordinates]]: " .. s
[ 1] = 'File:Compass-icon bb N.svg',
local co = '<span id="coordinates">' .. l .. notes .. '</span>';
[ 2] = 'File:Compass-icon bb NbE.svg',
return '<span style="font-size: small;">' .. co .. '</span>';
[ 3] = 'File:Compass-icon bb NNE.svg',
end
[ 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'
}


-- ===========================================================
--[[ Helper function, Replacement for {{coord/display/inline}} ]]
-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be
local function displayinline(s, notes)
-- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings.
return s .. notes
local SiteURL = {
end
GeoHack        = '//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  = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
WikimediaMap  = '//maps.wikimedia.org/#16/$lat/$lon',
--OpenStreetMap1 = '//wiwosm.toolforge.org/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
OpenStreetMap1 = '//wikimap.toolforge.org/?wp=false&basemap=2&cluster=false&zoom=16&lat=$lat&lon=$lon',
OpenStreetMap2 = '//tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
GoogleMaps = {
Mars  = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
Moon  = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic'
}
}


-- ===========================================================
--[[ Helper function, used in detecting DMS formatting ]]
-- Categories
local function dmsTest(first, second)
local CoorCat = {
if type(first) ~= 'string' or type(second) ~= 'string' then
-- File      = '[[Category:Media with locations]]',
return nil
-- Gallery    = '[[Category:Galleries with coordinates]]',
end
-- Category  = '[[Category:Categories with coordinates]]',
local s = (first .. second):upper()
strucData0 = '[[Category:Pages with %s coordinates from %s]]',
return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
strucData1 = '[[Category:Pages with local %s coordinates and matching %s coordinates]]',
end
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'


-- =======================================
--[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
-- === Local Functions ===================
local function makeInvokeFunc(funcName)
-- =======================================
return function (frame)
 
local args = require('Module:Arguments').getArgs(frame, {
-- ===========================================================
wrappers = 'Template:Coord'
local function add_maplink(lat, lon, marker, text)
})
local tstr = ''
return coordinates[funcName](args, frame)
if text then
tstr = string.format('text="%s" ', text)
end
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
end


-- ===========================================================
--[[ Helper function, handle optional args. ]]
local function add_maplink2(lat1, lon1, lat2, lon2)
local function optionalArg(arg, supplement)
return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
return arg and arg .. supplement or ''
' "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
end


-- ===========================================================
--[[
local function info_box(text)
Formats any error messages generated for display
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; ">'..
local function errorPrinter(errors)
'<tr>'..
local result = ""
'<td class="mbox-image" style="padding-left:.9em;">'..
for i,v in ipairs(errors) do
' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>'
'<td class="mbox-text" style="">%s</td>'..
result = result .. errorHTML .. "<br />"
'</tr></table>', text)
end
return result
end
end


-- ===========================================================
--[[
local function distance(lat1, lon1, lat2, lon2)
Determine the required CSS class to display coordinates
-- 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


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


-- ===========================================================
--[[
local function compareCoords(loc, sd, mode, source)
specPrinter
-- compare coordinates
 
--INPUTS:
Output formatter.  Takes the structure generated by either parseDec
-- * loc - local coordinates
or parseDMS and formats it for inclusion on Wikipedia.
--  * sd  - structured data coords
]]
local coord = loc
local function specPrinter(args, coordinateSpec)
local cat, dist_str = '', ''
local uriComponents = coordinateSpec["param"]
local case, dist, qs, mapLink, message
if uriComponents == "" then
dist=0
-- RETURN error, should never be empty or nil
return "ERROR param was empty"
end
if args["name"] then
uriComponents = uriComponents .. "&title=" .. mw.uri.encode(coordinateSpec["name"])
end


if not loc.lat or not loc.lon then -- structured data/wikidata coordinates only
local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
coord = sd
.. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
cat = string.format(CoorCat.strucData0, mode, source)
.. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
case = 0
.. '</span>'
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
local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
if source=='Wikidata' then
local geodeclat
cat = string.format(CoorCat.strucData1, mode, source)
if lat < 0 then
end
-- FIXME this breaks the pre-existing precision
case = 1
geodeclat = tostring(coordinateSpec["dec-lat"]):sub(2) .. "°S"
elseif (dist<1000 or dist<5*sd.prec) and mode=='object' then  
else
--cat = string.format(CoorCat.strucData2, mode, source)
geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
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 not loc.heading and sd.heading then -- structured data/wikidata heading only
 
coord.heading = sd.heading
local long = tonumber( coordinateSpec["dec-long"] ) or 0
elseif loc.heading==0 and not sd.heading and sd.lat and sd.lon then -- local heading only
local geodeclong
cat = cat .. string.format(CoorCat.sHeading5, mode, source)  
if long < 0 then
elseif loc.heading and not sd.heading and sd.lat and sd.lon then -- local heading only
-- FIXME does not handle unicode minus
cat = cat .. string.format(CoorCat.sHeading4, mode, source)
geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
elseif loc.heading and sd.heading then
else
local dh = math.abs(math.fmod(loc.heading,360) - math.fmod(sd.heading,360))
geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
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 url = mw.title.getCurrentTitle():canonicalUrl()
local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
local today = '+' .. os.date('!%F') .. 'T00:00:00Z/11' -- today's date in QS format
.. geodeclat .. ' '
qs = string.format('%s|P625|@%09.5f/%09.5f|S143|Q565|S813|%s|S4656|"%s"', sd.wID, loc.lat, loc.lon, today, url)
.. geodeclong
qs = string.gsub (mw.uri.encode(qs),'%%2520','%%20')
.. '</span>'
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)
local geonumhtml = '<span class="geo">'
.. coordinateSpec["dec-lat"] .. '; '
.. coordinateSpec["dec-long"]
.. '</span>'
 
local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
.. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';
 
if not args["name"] then
inner = inner .. geodechtml
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span></span>'
else
inner = inner .. '<span class="vcard">' .. geodechtml
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span>'
.. '<span style="display:none">&#xfeff; (<span class="fn org">'
.. args["name"] .. '</span>)</span></span></span>'
end
end
local ret = {dist_str=dist_str, case=case, qs=qs }
 
return coord, cat, ret
return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
      .. '<span class="plainlinks nourlexpansion">'
      .. '[' .. coord_link .. uriComponents .. ' ' .. inner .. ']'
      .. '</span>'
end
end


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


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


-- parse attribute variable returning desired field (used for debugging)
return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
function p.parseAttribute(frame)
  return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
end
end


-- ===========================================================
--[[
-- Helper core function for getHeading.  
Helper function, convert decimal latitude or longitude to
function p._getHeading(attributes)
degrees, minutes, and seconds format based on the specified precision.
if attributes == nil then
]]
return nil
local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
local coord = tonumber(coordinate)
local postfix
if coord >= 0 then
postfix = firstPostfix
else
postfix = secondPostfix
end
end
local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
 
if hStr == nil then
precision = precision:lower();
return nil
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
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
end


--[[============================================================================
--[[
Parse attribute variable returning heading field. If heading is a string than
Convert DMS format into a N or E decimal coordinate
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


function p.getHeading(frame) 
local factor = 1
local attributes
if direction == "S" or direction == "W" then
if frame.args[1] then
factor = -1
attributes = frame.args[1]
end
elseif frame.args.attributes then
 
attributes = frame.args.attributes
local precision = 0
if seconds_str then
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
else
return ''
precision = math.max( math_mod._precision(degrees_str), 0 );
end
end
local hNum  = p._getHeading(attributes)
 
if hNum == nil then
local decimal = factor * (degrees+(minutes+seconds/60)/60)
return ''
return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
end
return tostring(hNum)
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
Helper core function for deg2dms. deg2dms can be called by templates, while
if lat_d < 0 then
_deg2dms should be called from Lua.
table.insert(errors, {source, "latitude degrees < 0 with hemisphere flag"})
Inputs:
end
* degree - positive coordinate in degrees
if long_d < 0 then
* degPrec - coordinate precision in degrees will result in different angle format
table.insert(errors, {source, "longitude degrees < 0 with hemisphere flag"})
* lang - language to used when formatting the number
end
==============================================================================]]
--[[
function p._deg2dms(degree, degPrec, lang)
#coordinates is inconsistent about whether this is an error. If globe: is
local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero
specified, it won't error on this condition, but otherwise it will.
local Lang = mw.language.new(lang)


-- adjust number display based on precision
For not simply disable this check.
secPrec = degPrec*3600.0                    -- coordinate precision in seconds
 
if secPrec<0.05 then                        -- degPrec<1.3889e-05
if long_d > 180 then
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
table.insert(errors, {source, "longitude degrees > 180 with hemisphere flag"})
c = 360000
end
elseif secPrec<0.5 then                     -- 1.3889e-05<degPrec<1.3889e-04
]]
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
end
c = 36000
 
elseif degPrec*60.0<0.5 then                 -- 1.3889e-04<degPrec<0.0083
if lat_d > 90 then
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
table.insert(errors, {source, "latitude degrees > 90"})
c = 3600
end
elseif degPrec<0.5 then                     -- 0.0083<degPrec<0.5
if lat_d < -90 then
formatStr = '%s°&nbsp;%s′'              -- use DD° MM′ format
table.insert(errors, {source, "latitude degrees < -90"})
c = 60
end
else -- if degPrec>0.5 then                
if lat_m >= 60 then
formatStr = '%s°'                        -- use DD° format
table.insert(errors, {source, "latitude minutes >= 60"})
c = 1
end
if lat_m < 0 then
table.insert(errors, {source, "latitude minutes < 0"})
end
if lat_s >= 60 then
table.insert(errors, {source, "latitude seconds >= 60"})
end
if lat_s < 0 then
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
end
if long_s >= 60 then
-- create degree, minute and seconds numbers and string
table.insert(errors, {source, "longitude seconds >= 60"})
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 sNum<10 then
if long_s < 0 then
sStr = zero .. sStr                -- pad with zero if less than ten
table.insert(errors, {source, "longitude seconds < 0"})
end
end
return string.format(formatStr, dStr, mStr, sStr);
 
return errors;
end
end


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


if degree==nil then
errors = validate( lat, nil, nil, long, nil, nil, 'parseDec', false );
return args[1];
coordinateSpec["dec-lat"]  = lat;
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
return p._deg2dms(degree, degPrec, args.lang)
coordinateSpec.default = "dec"
end
end
return coordinateSpec, errors
end
end


function p.dms2deg(frame)
--[[
return dms2deg(frame.args[1])
parseDMS
end
 
Transforms degrees, minutes, seconds format latitude and longitude
into the a structure to be used in displaying coordinates
]]
local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
local coordinateSpec, errors, backward = {}, {}
 
lat_f = lat_f:upper();
long_f = long_f:upper();
 
-- Check if specified backward
if lat_f == 'E' or lat_f == 'W' then
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;
end
 
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 );
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
end
end
if lat==nil or lon==nil then
 
return NoLatLonString
coordinateSpec["dms-lat"]  = lat_d.."°"..optionalArg(lat_m,"′") .. optionalArg(lat_s,"″") .. lat_f
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
local nsew = core.langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language
coordinateSpec.default = "dms"
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


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


--[[============================================================================
--[[
Helper core function for externalLink. Create URL for different sites:
Check the input arguments for coord to determine the kind of data being provided
INPUTS:
and then make the necessary processing.
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
]]
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
local function formatTest(args)
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
local result, errors
                Ganymede are also supported but are unused as of 2013.
local backward, primary = false, false
* 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
local function getParam(args, lim)
URLstr = SiteURL.GoogleMaps[globe]
local ret = {}
elseif site == 'GeoHack' then
for i = 1, lim do
attributes = string.format('globe:%s_%s', globe, attributes)
ret[i] = args[i] or ''
URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
end
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
Create URL for different sites.
-- no lat logic
INPUTS:
return errorPrinter( {{"formatTest", "Missing latitude"}} )
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
elseif not tonumber(args[1]) then
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
-- bad lat logic
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
return errorPrinter( {{"formatTest", "Unable to parse latitude as a number:" .. args[1]}} )
                Ganymede are also supported but are unused as of 2013.
elseif not args[4] and not args[5] and not args[6] then
* lat        = latitude string or number
-- dec logic
* lon        = longitude string or number
result, errors = parseDec(args[1], args[2], args.format)
* lang      = language code
if not result then
* attributes = attributes to be passed to GeoHack
return errorPrinter(errors);
==============================================================================]]
end
function p.externalLink(frame)
-- formatting for geohack: geohack expects D_N_D_E notation or D;D notation
local args = core.getArgs(frame)
-- wikiminiatlas doesn't support D;D notation
return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
-- #coordinates parserfunction doesn't support negative decimals with NSWE
end
result.param = table.concat({
 
math.abs(tonumber(args[1])),
--[[============================================================================
((tonumber(args[1]) or 0) < 0) and 'S' or 'N',
Adjust GeoHack attributes depending on the template that calls it
math.abs(tonumber(args[2])),
INPUTS:
((tonumber(args[2]) or 0) < 0) and 'W' or 'E',
* attributes = attributes to be passed to GeoHack
args[3] or ''}, '_')
* mode = set by each calling template
elseif dmsTest(args[4], args[8]) then
==============================================================================]]
-- dms logic
function p.alterAttributes(attributes, mode, heading)
result, errors, backward = parseDMS(args[1], args[2], args[3], args[4],
-- indicate which template called it
args[5], args[6], args[7], args[8], args.format)
if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
if args[10] then
if not string.find(attributes, 'type:camera') then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
attributes = 'type:camera_' .. attributes
end
if not result then
return errorPrinter(errors)
end
result.param = getParam(args, 9)
elseif dmsTest(args[3], args[6]) then
-- dm logic
result, errors, backward = parseDMS(args[1], args[2], nil, args[3],
args[4], args[5], nil, args[6], args['format'])
if args[8] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
if not result then
return errorPrinter(errors)
end
result.param = getParam(args, 7)
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
end
elseif mode=='object'  then              -- Used by {{Object location}}
if not result then
if mode=='object' and not string.find(attributes, 'type:') then
return errorPrinter(errors)
attributes = 'type:object_' .. attributes
end
end
if not string.find(attributes, 'class:object') then
result.param = getParam(args, 5)
attributes = 'class:object_' .. attributes
else
-- Error
return errorPrinter({{"formatTest", "Unknown argument format"}}) .. '[[Category:Pages with malformed coordinate tags]]'
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
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
end
local hStr = ''
 
if heading then -- if heading is a  number
local ret = specPrinter(args, result)
hStr = string.format('heading:%6.2f', heading)
if #errors > 0 then
ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
end
return ret, backward
end
 
--[[
Generate Wikidata tracking categories.
]]
local function makeWikidataCategories(qid)
local ret
local qid = qid or mw.wikibase.getEntityIdForCurrentPage()
if mw.wikibase and current_page.namespace == 0 then
if qid and mw.wikibase.entityExists(qid) and mw.wikibase.getBestStatements(qid, "P625") and mw.wikibase.getBestStatements(qid, "P625")[1] then
local snaktype = mw.wikibase.getBestStatements(qid, "P625")[1].mainsnak.snaktype
if snaktype == 'value' then
-- coordinates exist both here and on Wikidata, and can be compared.
ret = 'Coordinates on Wikidata'
elseif snaktype == 'somevalue' then
ret = 'Coordinates on Wikidata set to unknown value'
elseif snaktype == 'novalue' then
ret = 'Coordinates on Wikidata set to no value'
end
else
-- We have to either import the coordinates to Wikidata or remove them here.
ret = 'Coordinates not on Wikidata'
end
end
end
if not string.find(attributes, 'heading:') then
if ret then
attributes = attributes .. '_' .. hStr
return string.format('[[Category:%s]]', ret)
else
else
attributes = string.gsub(attributes,'heading:[^_]*', hStr) -- replace heading in form heading:N with heading=0
return ''
attributes = string.gsub(attributes,'__', '_')
end
end
end
--[[
link


return string.gsub(attributes,' ', '')
Simple function to export the coordinates link for other uses.
 
Usage:
{{#invoke:Coordinates | link }}
 
]]
function coordinates.link(frame)
return coord_link;
end
end
 
--[[============================================================================
--[[
Create link to GeoHack tool which displays latitude and longitude coordinates
dec2dms
in DMS format
 
INPUTS:
Wrapper to allow templates to call dec2dms directly.
  * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,  
 
                Ganymede are also supported but are unused as of 2013.
Usage:
* lat        = latitude in degrees
{{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix |
* lon        = longitude in degrees
negative_suffix | precision }}
* lang      = language code
 
* prec      = geolocation precision in meters
decimal_coordinate is converted to DMS format. If positive, the positive_suffix
* attributes = attributes to be passed to GeoHack
is appended (typical N or E), if negative, the negative suffix is appended. The
==============================================================================]]
specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
function p._GeoHack_link(args)
to use.
-- create link and coordintate string
]]
local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
coordinates.dec2dms = makeInvokeFunc('_dec2dms')
if latlon==NoLatLonString then
function coordinates._dec2dms(args)
return latlon
local coordinate = args[1]
local firstPostfix = args[2] or ''
local secondPostfix = args[3] or ''
local precision = args[4] or ''
 
return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
end
 
--[[
Helper function to determine whether to use D, DM, or DMS
format depending on the precision of the decimal input.
]]
function coordinates.determineMode( value1, value2 )
local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) );
if precision <= 0 then
return 'd'
elseif precision <= 2 then
return 'dm';
else
else
local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
return 'dms';
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
end
end
end
end


function p.GeoHack_link(frame)
--[[
return p._GeoHack_link(core.getArgs(frame))
dms2dec
 
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
Main entry point for Lua function to replace {{coord}}
Usage:
{{#invoke:Coordinates | coord }}
{{#invoke:Coordinates | coord | lat | long }}
{{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
...
Refer to {{coord}} documentation page for many additional parameters and
configuration options.


--[[============================================================================
Note: This function provides the visual display elements of {{coord}}. In
Create full external links section of {{Location}} or {{Object location}}
order to load coordinates into the database, the {{#coordinates:}} parser
  templates, based on:
function must also be called, this is done automatically in the Lua
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
version of {{coord}}.
* mode      = Possible options:
]]
  - camera - call from {{location}}
coordinates.coord = makeInvokeFunc('_coord')
  - object - call from {{Object location}}
function coordinates._coord(args)
  - globe  - call from {{Globe location}}
if not tonumber(args[1]) and not args[2] then
* lat        = latitude in degrees
args[3] = args[1]; args[1] = nil
* lon        = longitude in degrees
local entity = mw.wikibase.getEntityObject(args.qid)
* lang      = language code
if entity
* namespace  = namespace name: File, Category, (Gallery)
and entity.claims
==============================================================================]]
and entity.claims.P625
function p._externalLinksSection(args)
and entity.claims.P625[1].mainsnak.snaktype == 'value'
local lang = args.lang
then
if not args.namespace then
local precision = entity.claims.P625[1].mainsnak.datavalue.value.precision
args.namespace = mw.title.getCurrentTitle().nsText
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
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)
local contents, backward = formatTest(args)
return p._externalLinksSection(core.getArgs(frame))
local Notes = args.notes or ''
end
local Display = args.display and args.display:lower() or 'inline'


--[[============================================================================
local function isInline(s)
Core section of template:Location, template:Object location and template:Globe location.
-- Finds whether coordinates are displayed inline.
This method requires several arguments to be passed to it or it's parent method/template:
return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti'
* 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
end
if args.namespace=='' then -- if empty than it is a gallery
local function isInTitle(s)
args.namespace = 'Gallery'
-- Finds whether coordinates are displayed in the title.
return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti'
end
end
local bare  = core.yesno(args.bare,false)
 
local Status = 'primary' -- used by {{#coordinates:}}
local function coord_wrapper(in_args)
if core.yesno(args.secondary,false) then
-- Calls the parser function {{#coordinates:}}.
Status = 'secondary'
return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
end
end
args.globe = mw.language.new('en'):ucfirst(args.globe or 'Earth')
 
local text = ''
-- Convert coordinates from string to numbers
if isInline(Display) then
local lat = tonumber(args.lat)
text = text .. displayinline(contents, Notes)
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
if isInTitle(Display) then
-- If wikidata link provided than compare coordinates
text = text
local Categories, geoMicroFormat, coorTag, edit_icon, wikidata_link = '', '', '', '', '', '', ''
.. displaytitle(contents, Notes)
local entity, coord, sd, cmp
.. makeWikidataCategories(args.qid)
local loc = {lat=lat, lon=lon, heading=heading, source='loc'}
local ID = args.wikidata
if (ID==nil) then  
entity = mw.wikibase.getEntity()
elseif type(ID)=='string' and ID:match( '^[QqMm]%d+$' ) then
entity = mw.wikibase.getEntity(ID)
elseif type(ID)~='string' and ID.id then
entity = ID -- entities can be passed from outside
end
end
if not args.nosave then
if entity then
local page_title, count = mw.title.getCurrentTitle(), 1
if (args.mode=='object' or args.mode=='globe') then
if backward then
sd = getSDCoords(entity,'P9149')  -- fetch coordinates of depicted place
local tmp = {}
if not sd.lat then
while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
sd = getSDCoords(entity,'P625')  -- fallback to coordinate location
tmp.count = count; count = 2*(count-1)
end
while count >= tmp.count do table.insert(tmp, 1, (args[count] or '')); count = count-1 end
elseif (args.mode=='camera') then
for i, v in ipairs(tmp) do args[i] = v end
sd = getSDCoords(entity,'P1259') -- fetch camera coordinates or coordinates of the point of view
else
end
while count <= 9 do args[count] = (args[count] or ''); count = count+1 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
end
elseif (args.namespace=='File') then
if isInTitle(Display) and not page_title.isTalkPage and page_title.subpageText ~= 'doc' and page_title.subpageText ~= 'testcases' then args[10] = 'primary' end
Categories = string.format(CoorCat.strucData4, args.mode, 'SDC')
args.notes, args.format, args.display = nil
text = text .. coord_wrapper(args)
end
end
return text
end
--[[
coord2text
Extracts a single value from a transclusion of {{Coord}}.
IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
Usage:


args.lat  = string.format('%010.6f', lat or 0)
    {{#invoke:Coordinates | coord2text | {{Coord}} | parameter }}
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
Valid values for the second parameter are: lat (signed integer), long (signed integer), type, scale, dim, region, globe, source
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 = '','','','','',''
function coordinates.coord2text(frame)
coor = p._GeoHack_link(args) -- the p and link to GeoHack
if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
coor = string.format('<span class=plainlinks>%s</span>%s', coor, edit_icon)
frame.args[2] = mw.text.trim(frame.args[2])
if heading then
if frame.args[2] == 'lat' or frame.args[2] == 'long' then
local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ')
local fname = heading_icon[k]
if frame.args[2] == 'lat' then
coor = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=Heading=%]]</span>', coor, heading, fname, heading)
result, negative = result[1], 'S'
end
else
if args.globe=='Earth' then
result, negative = result[2], 'W'
local icon = 'marker'
if args.mode=='camera' then
icon = 'camera'
end
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
result = mw.text.split(result, '°')
end
if result[2] == negative then result[1] = '-'..result[1] end
local external_link = p._externalLinksSection(args) -- external link section
return result[1]
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
-- choose name of the field and create row
return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
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)
--[[
local args = core.getArgs(frame)
coordinsert
args.namespace = mw.title.getCurrentTitle().nsText
 
if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
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 args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W"
IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] )
 
args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
Usage:
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"
    {{#invoke:Coordinates | coordinsert | {{Coord}} | parameter:value | parameter:value | … }}
args.lat = args[1]
 
args.lon = args[2]
Do not make Geohack unhappy by inserting something which isn't mentioned in the {{Coord}} documentation.
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
function coordinates.coordinsert(frame)
args.lat, args.lon = v[1], v[2]
for i, v in ipairs(frame.args) do
args.attributes = args.attributes or args[2]
if i ~= 1 then
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
local cat = ''
if frame.args.name then
if args.lat and args.lon then
if not mw.ustring.find(frame.args[1], '<span class="vcard">') then
local lat = tonumber(args.lat)
local namestr = frame.args.name
local lon = tonumber(args.lon)
frame.args[1] = mw.ustring.gsub(frame.args[1],
if not lat or not lon then
'(<span class="geo%-default">)(<span[^<>]*>[^<>]*</span><span[^<>]*>[^<>]*<span[^<>]*>[^<>]*</span></span>)(</span>)',
args.lat = dms2deg(args.lat or '')
'%1<span class="vcard">%2<span style="display:none">&#xfeff; (<span class="fn org">' .. namestr .. '</span>)</span></span>%3')
args.lon = dms2deg(args.lon or '')
frame.args[1] = mw.ustring.gsub(frame.args[1], '(&params=[^&"<>%[%] ]*) ', '%1&title=' .. mw.uri.encode(namestr) .. ' ')
if (args.namespace == 'File' or args.namespace == 'Category') then
cat = CoorCat.dms
end
end
end
end
end
local templateText, Categories, coorTag = p._LocationTemplateCore(args)
return frame.args[1]
return templateText .. Categories .. cat .. coorTag
end
end


return p
return coordinates

Latest revision as of 13:01, 22 October 2022

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

--[[
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
coordinate values.

{{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
degree values to DMS format.

{{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
to decimal degree format.

{{#invoke:Coordinates | link }} : Export the link used to reach the tools

]]

require('strict')

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

local current_page = mw.title.getCurrentTitle()
local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
local coord_link = '//geohack.toolforge.org/geohack.php?pagename=' .. page_name .. '&params='
local templatestyles = 'Module:Coordinates/styles.css'

--[[ Helper function, replacement for {{coord/display/title}} ]]
local function displaytitle(s, notes)
	local l = "[[Geographic coordinate system|Coordinates]]: " .. s
	local co = '<span id="coordinates">' .. l .. notes .. '</span>';
	return '<span style="font-size: small;">' .. co .. '</span>';
end

--[[ Helper function, Replacement for {{coord/display/inline}} ]]
local function displayinline(s, notes)
	return s .. notes
end

--[[ Helper function, used in detecting DMS formatting ]]
local function dmsTest(first, second)
	if type(first) ~= 'string' or type(second) ~= 'string' then
		return nil
	end
	local s = (first .. second):upper()
	return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
end


--[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
local function makeInvokeFunc(funcName)
	return function (frame)
		local args = require('Module:Arguments').getArgs(frame, {
			wrappers = 'Template:Coord'
		})
		return coordinates[funcName](args, frame)
	end
end

--[[ Helper function, handle optional args. ]]
local function optionalArg(arg, supplement)
	return arg and arg .. supplement or ''
end

--[[
Formats any error messages generated for display
]]
local function errorPrinter(errors)
	local result = ""
	for i,v in ipairs(errors) do
		local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>'
		result = result .. errorHTML .. "<br />"
	end
	return result
end

--[[
Determine the required CSS class to display coordinates

Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
default is the mode as specificied by the user when calling the {{coord}} template
mode is the display mode (dec or dms) that we will need to determine the css class for
]]
local function displayDefault(default, mode)
	if default == "" then
		default = "dec"
	end

	if default == mode then
		return "geo-default"
	else
		return "geo-nondefault"
	end
end

--[[
specPrinter

Output formatter.  Takes the structure generated by either parseDec
or parseDMS and formats it for inclusion on Wikipedia.
]]
local function specPrinter(args, coordinateSpec)
	local uriComponents = coordinateSpec["param"]
	if uriComponents == "" then
		-- RETURN error, should never be empty or nil
		return "ERROR param was empty"
	end
	if args["name"] then
		uriComponents = uriComponents .. "&title=" .. mw.uri.encode(coordinateSpec["name"])
	end

	local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
			.. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
			.. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
			.. '</span>'

	local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
	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

	local long = tonumber( coordinateSpec["dec-long"] ) or 0
	local geodeclong
	if long < 0 then
		-- FIXME does not handle unicode minus
		geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
	else
		geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
	end

	local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
			.. geodeclat .. ' '
			.. geodeclong
			.. '</span>'

	local geonumhtml = '<span class="geo">'
			.. coordinateSpec["dec-lat"] .. '; '
			.. coordinateSpec["dec-long"]
			.. '</span>'

	local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
				.. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
				.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';

	if not args["name"] then
		inner = inner .. geodechtml
				.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span></span>'
	else
		inner = inner .. '<span class="vcard">' .. geodechtml
				.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span>'
				.. '<span style="display:none">&#xfeff; (<span class="fn org">'
				.. args["name"] .. '</span>)</span></span></span>'
	end

	return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} }
	       .. '<span class="plainlinks nourlexpansion">'
	       .. '[' .. coord_link .. uriComponents .. ' ' .. inner .. ']'
	       .. '</span>'
end

--[[ Helper function, convert decimal to degrees ]]
local function convert_dec2dms_d(coordinate)
	local d = math_mod._round( coordinate, 0 ) .. "°"
	return d .. ""
end

--[[ Helper function, convert decimal to degrees and minutes ]]
local function convert_dec2dms_dm(coordinate)
	coordinate = math_mod._round( coordinate * 60, 0 );
	local m = coordinate % 60;
	coordinate = math.floor( (coordinate - m) / 60 );
	local d = coordinate % 360 .."°"

	return d .. string.format( "%02d′", m )
end

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

	return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
end

--[[
Helper function, convert decimal latitude or longitude to
degrees, minutes, and seconds format based on the specified precision.
]]
local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
	local coord = tonumber(coordinate)
	local postfix
	if coord >= 0 then
		postfix = firstPostfix
	else
		postfix = secondPostfix
	end

	precision = precision:lower();
	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

--[[
Convert DMS format into a N or E decimal coordinate
]]
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
	if direction == "S" or direction == "W" then
		factor = -1
	end

	local precision = 0
	if seconds_str then
		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

	local decimal = factor * (degrees+(minutes+seconds/60)/60)
	return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
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"})
		end
		]]
	end

	if lat_d > 90 then
		table.insert(errors, {source, "latitude degrees > 90"})
	end
	if lat_d < -90 then
		table.insert(errors, {source, "latitude degrees < -90"})
	end
	if lat_m >= 60 then
		table.insert(errors, {source, "latitude minutes >= 60"})
	end
	if lat_m < 0 then
		table.insert(errors, {source, "latitude minutes < 0"})
	end
	if lat_s >= 60 then
		table.insert(errors, {source, "latitude seconds >= 60"})
	end
	if lat_s < 0 then
		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

--[[
parseDec

Transforms decimal format latitude and longitude into the
structure to be used in displaying coordinates
]]
local function parseDec( lat, long, format )
	local coordinateSpec = {}
	local errors = {}

	if not long then
		return nil, {{"parseDec", "Missing longitude"}}
	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 );
	coordinateSpec["dec-lat"]  = lat;
	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
		coordinateSpec.default = "dec"
	end

	return coordinateSpec, errors
end

--[[
parseDMS

Transforms degrees, minutes, seconds format latitude and longitude
into the a structure to be used in displaying coordinates
]]
local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
	local coordinateSpec, errors, backward = {}, {}

	lat_f = lat_f:upper();
	long_f = long_f:upper();

	-- Check if specified backward
	if lat_f == 'E' or lat_f == 'W' then
		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;
	end

	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

	coordinateSpec["dms-lat"]  = lat_d.."°"..optionalArg(lat_m,"′") .. optionalArg(lat_s,"″") .. lat_f
	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
		coordinateSpec.default = "dms"
	end

	return coordinateSpec, errors, backward
end

--[[
Check the input arguments for coord to determine the kind of data being provided
and then make the necessary processing.
]]
local function formatTest(args)
	local result, errors
	local backward, primary = false, false

	local function getParam(args, lim)
		local ret = {}
		for i = 1, lim do
			ret[i] = args[i] or ''
		end
		return table.concat(ret, '_')
	end

	if not args[1] then
		-- no lat logic
		return errorPrinter( {{"formatTest", "Missing latitude"}} )
	elseif not tonumber(args[1]) then
		-- bad lat logic
		return errorPrinter( {{"formatTest", "Unable to parse latitude as a number:" .. args[1]}} )
	elseif not args[4] and not args[5] and not args[6] then
		-- dec logic
		result, errors = parseDec(args[1], args[2], args.format)
		if not result then
			return errorPrinter(errors);
		end
		-- formatting for geohack: geohack expects D_N_D_E notation or D;D notation
		-- wikiminiatlas doesn't support D;D notation
		-- #coordinates parserfunction doesn't support negative decimals with NSWE
		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
		if not result then
			return errorPrinter(errors)
		end
		result.param = getParam(args, 9)
	elseif dmsTest(args[3], args[6]) then
		-- dm logic
		result, errors, backward = parseDMS(args[1], args[2], nil, args[3],
			args[4], args[5], nil, args[6], args['format'])
		if args[8] then
			table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
		end
		if not result then
			return errorPrinter(errors)
		end
		result.param = getParam(args, 7)
	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
		-- Error
		return errorPrinter({{"formatTest", "Unknown argument format"}}) .. '[[Category:Pages with malformed coordinate tags]]'
	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

	local ret = specPrinter(args, result)
	if #errors > 0 then
		ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
	end
	return ret, backward
end

--[[
Generate Wikidata tracking categories.
]]
local function makeWikidataCategories(qid)
	local ret
	local qid = qid or mw.wikibase.getEntityIdForCurrentPage()
	if mw.wikibase and current_page.namespace == 0 then
		if qid and mw.wikibase.entityExists(qid) and mw.wikibase.getBestStatements(qid, "P625") and mw.wikibase.getBestStatements(qid, "P625")[1] then
			local snaktype = mw.wikibase.getBestStatements(qid, "P625")[1].mainsnak.snaktype
			if snaktype == 'value' then
				-- coordinates exist both here and on Wikidata, and can be compared.
				ret = 'Coordinates on Wikidata'
			elseif snaktype == 'somevalue' then
				ret = 'Coordinates on Wikidata set to unknown value'
			elseif snaktype == 'novalue' then
				ret = 'Coordinates on Wikidata set to no value'
			end
		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
		return ''
	end
end

--[[
link

Simple function to export the coordinates link for other uses.

Usage:
	{{#invoke:Coordinates | link }}

]]
function coordinates.link(frame)
	return coord_link;
end

--[[
dec2dms

Wrapper to allow templates to call dec2dms directly.

Usage:
	{{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix |
		negative_suffix | precision }}

decimal_coordinate is converted to DMS format.  If positive, the positive_suffix
is appended (typical N or E), if negative, the negative suffix is appended.  The
specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
to use.
]]
coordinates.dec2dms = makeInvokeFunc('_dec2dms')
function coordinates._dec2dms(args)
	local coordinate = args[1]
	local firstPostfix = args[2] or ''
	local secondPostfix = args[3] or ''
	local precision = args[4] or ''

	return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
end

--[[
Helper function to determine whether to use D, DM, or DMS
format depending on the precision of the decimal input.
]]
function coordinates.determineMode( value1, value2 )
	local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) );
	if precision <= 0 then
		return 'd'
	elseif precision <= 2 then
		return 'dm';
	else
		return 'dms';
	end
end

--[[
dms2dec

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

--[[
coord

Main entry point for Lua function to replace {{coord}}

Usage:
	{{#invoke:Coordinates | coord }}
	{{#invoke:Coordinates | coord | lat | long }}
	{{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
	...

	Refer to {{coord}} documentation page for many additional parameters and
	configuration options.

Note: This function provides the visual display elements of {{coord}}.  In
order to load coordinates into the database, the {{#coordinates:}} parser
function must also be called, this is done automatically in the Lua
version of {{coord}}.
]]
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

	local contents, backward = formatTest(args)
	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
	local function isInTitle(s)
		-- Finds whether coordinates are displayed in the title.
		return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti'
	end

	local function coord_wrapper(in_args)
		-- Calls the parser function {{#coordinates:}}.
		return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
	end

	local text = ''
	if isInline(Display) then
		text = text .. displayinline(contents, Notes)
	end
	if isInTitle(Display) then
		text = text
			.. displaytitle(contents, Notes)
			.. makeWikidataCategories(args.qid)
	end
	if not args.nosave then
		local page_title, count = mw.title.getCurrentTitle(), 1
		if backward then
			local tmp = {}
			while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
			tmp.count = count; count = 2*(count-1)
			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
		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
	return text
end

--[[
coord2text

Extracts a single value from a transclusion of {{Coord}}.
IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.

Usage:

    {{#invoke:Coordinates | coord2text | {{Coord}} | parameter }}

Valid values for the second parameter are: lat (signed integer), long (signed integer), type, scale, dim, region, globe, source

]]
function coordinates.coord2text(frame)
	if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
	frame.args[2] = mw.text.trim(frame.args[2])
	if frame.args[2] == 'lat' or frame.args[2] == 'long' then
		local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ')
		if frame.args[2] == 'lat' then
			result, negative = result[1], 'S'
		else
			result, negative = result[2], 'W'
		end
		result = mw.text.split(result, '°')
		if result[2] == negative then result[1] = '-'..result[1] end
		return result[1]
	else
		return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
	end
end

--[[
coordinsert

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 THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.

Usage:

    {{#invoke:Coordinates | coordinsert | {{Coord}} | parameter:value | parameter:value | … }}

Do not make Geohack unhappy by inserting something which isn't mentioned in the {{Coord}} documentation.

]]
function coordinates.coordinsert(frame)
	for i, v in ipairs(frame.args) do
		if i ~= 1 then
			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
	if frame.args.name then
		if not mw.ustring.find(frame.args[1], '<span class="vcard">') then
			local namestr = frame.args.name
			frame.args[1] = mw.ustring.gsub(frame.args[1],
				'(<span class="geo%-default">)(<span[^<>]*>[^<>]*</span><span[^<>]*>[^<>]*<span[^<>]*>[^<>]*</span></span>)(</span>)',
				'%1<span class="vcard">%2<span style="display:none">&#xfeff; (<span class="fn org">' .. namestr .. '</span>)</span></span>%3')
			frame.args[1] = mw.ustring.gsub(frame.args[1], '(&params=[^&"<>%[%] ]*) ', '%1&title=' .. mw.uri.encode(namestr) .. ' ')
		end
	end
	return frame.args[1]
end

return coordinates