Module:Coordinates

![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
Note: The code which this module's main function (coord
) outputs is directly parsed and/or manipulated by Module:Location map and other functions of this module itself (coord2text
and coordinsert
). If the structure of the output changes (for example, to use the <mapframe>
and <maplink>
tags), please update the aforementioned scripts as well.
Using the module with coordinsert
When using the {{Coord}} template inside another template, like an infobox, there may be parameters (like type:airport
) which should be added automatically. To do so, do something like this:
{{#if:{{{coordinates|}}}|{{#invoke:Coordinates|coordinsert|{{{coordinates|}}}|parameter1:value1|parameter2:value2|parameter3:value3…}}|
Do not add more vertical bars |
than necessary.
Using the module with coord2text to extract latitude or longitude
Developers maintaining legacy code may need to extract latitude or longitude to use a parameters in other code, or a mathematical expression. The module's "coord2text" function can be used to extract data from the {{Coord}} template. To extract the latitude from a Coord template, use:
{{#invoke:coordinates|coord2text|{{Coord|57|18|22|N|4|27|32|E}}|lat}}
→ Lua error in package.lua at line 80: module 'Module:Coord_prec_dec' not found.
To extract the longitude, use:
{{#invoke:coordinates|coord2text|{{Coord|57|18|22|N|4|27|32|E}}|long}}
→ Lua error in package.lua at line 80: module 'Module:Coord_prec_dec' not found.
Modules using this module directly
Tracking categories
- Category:Pages with malformed coordinate tags (8)
- Category:Coordinates not on Wikidata (167)
- Category:Coordinates on Wikidata (32,377)
- Category:Coordinates on Wikidata set to no value (1)
- Category:Coordinates on Wikidata set to unknown value (0)
-- A module that mimics the functionality of Template:Coord and its sub templates
-- The attempt is to actually mimic a conversion of an often used en.wp template in the way
-- that most templates will actually be converted by the wiki users.
-- Attempt is not really to write a nice and proper module from scratch :D
local coordinates = {
prec_dec = require "Module:Coord_prec_dec",
precision = require "Module:Precision",
wikitext = require "Module:Wikitext"
}
globalFrame = nil
--- Replacement for {{coord/display/title}}
function displaytitle (s)
local l = "[[Geographic coordinate system|Coordinates]]: " .. s
local co = mw.text.tag({name="span",contents=l,params={id="coordinates"}})
local p = {}
p["font-size"] = "small"
return mw.text.tag({name="span",contents=co,params=p})
end
--- Replacement for {{coord/display/inline}}
function displayinline (s)
return s
end
--- Test if the arguments imply that DMS might be in use
local dmsTest = function(first, second)
local concatenated = first .. second;
if concatenated == "NE" or concatenated == "NW" or concatenated == "SE" or concatenated == "SW" then
return true;
end
return false;
end
--- Parse the frame assuming that it is in dec format.
-- @frame
-- @returns a table with all information needed to display coordinates
function parseDec(args)
local coordinateSpec = {}
local errors = {}
if args[2] == "" or args[2] == nil then
return nil, {{"parseDec", "Missing longitude"}}
end
coordinateSpec["dec-lat"] = args[1]
coordinateSpec["dec-long"] = args[2]
local precision = coordinates.prec_dec.max_precision(args[1], args[2])
coordinateSpec["dms-lat"] = convert_dec2dms(args[1], "N", "S", precision) -- {{coord/dec2dms|{{{1}}}|N|S|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
coordinateSpec["dms-long"] = convert_dec2dms(args[2], "E", "W", precision) -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
coordinateSpec.param = args[1] .."_N_" .. args[2] .. "_E_" .. args[3]
if args["format"] ~= "" then
coordinateSpec.default = args["format"]
else
coordinateSpec.default = "dec"
end
coordinateSpec.name = args["name"]
-- TODO refactor the validations into separate functions
if (tonumber(args[1]) or 0) > 90 then
table.insert(errors, {"parseDec","latd>90"})
end
if (tonumber(args[1]) or 0) < -90 then
table.insert(errors, {"parseDec", "latd<-90"})
end
if (tonumber(args[2]) or 0) >= 360 then
table.insert(errors, {"parseDec", "longd>=360"})
end
if (tonumber(args[2]) or 0) <= -360 then
table.insert(errors, {"parseDec", "longd<=-360"})
end
return coordinateSpec, errors
end
function optionalArg(arg, suplement)
if arg ~= nil and arg ~= "" then
return arg .. suplement
end
return ""
end
function isEmpty(arg)
if arg == nil or arg == "" then
return true
end
return false
end
function parseDMS(args)
local coordinateSpec = {}
local errors = {}
coordinateSpec["dms-lat"] = args[1].."°"..optionalArg(args[2],"′") .. optionalArg(args[3],"″") .. args[4]
coordinateSpec["dms-long"] = args[5].."°"..optionalArg(args[6],"′") .. optionalArg(args[7],"″") .. args[8]
coordinateSpec["dec-lat"] = convert_dms2dec(args[4],args[1],args[2],args[3]) -- {{coord/dms2dec|{{{4}}}|{{{1}}}|0{{{2}}}|0{{{3}}}}}
coordinateSpec["dec-long"] = convert_dms2dec(args[8],args[5],args[6],args[7]) -- {{coord/dms2dec|{{{8}}}|{{{5}}}|0{{{6}}}|0{{{7}}}}}
-- TODO Use loop when we know it won't break everything
coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5] .. "_" .. args[6] .. "_" .. args[7] .. "_" .. args[8] .. "_" .. args[9]
if args["format"] ~= "" then
coordinateSpec.default = args["format"]
else
coordinateSpec.default = "dms"
end
coordinateSpec.name = args["name"]
-- Error reporting
if isEmpty(args[5]) then
table.insert(errors, {"parseDM", "Missing longitude" })
end
if not isEmpty(args[10]) then
table.insert(errors, {"parseDM", "Unexpected extra parameters"})
end
if (tonumber(args[1]) or 0) > 90 then
table.insert(errors, {"parseDMS", "latd>90"})
end
if (tonumber(args[1]) or 0) < -90 then
table.insert(errors, {"parseDMS", "latd<-90"})
end
if (tonumber(args[2]) or 0) >= 60 then
table.insert(errors, {"parseDMS", "latm>=60"})
end
if (tonumber(args[2]) or 0) < 0 then
table.insert(errors, {"parseDMS", "latm<0"})
end
if (tonumber(args[3]) or 0) >= 60 then
table.insert(errors, {"parseDMS", "lats>=60"})
end
if (tonumber(args[3]) or 0) < 0 then
table.insert(errors, {"parseDMS", "lats<0"})
end
if (tonumber(args[5]) or 0) >= 360 then
table.insert(errors, {"parseDMS", "longd>=360"})
end
if (tonumber(args[5]) or 0) <= -360 then
table.insert(errors, {"parseDMS", "longd<=-360"})
end
if (tonumber(args[6]) or 0) >= 60 then
table.insert(errors, {"parseDMS", "longm>=60"})
end
if (tonumber(args[6]) or 0) < 0 then
table.insert(errors, {"parseDMS", "longm<0"})
end
if (tonumber(args[7]) or 0) >= 60 then
table.insert(errors, {"parseDMS", "longs>=60"})
end
if (tonumber(args[7]) or 0) < 0 then
table.insert(errors, {"parseDMS", "longs<0"})
end
return coordinateSpec, errors
end
function parseDM(args)
local coordinateSpec = {}
local errors = {}
coordinateSpec["dms-lat"] = args[1].."°"..optionalArg(args[2],"′") .. args[3]
coordinateSpec["dms-long"] = args[4].."°"..optionalArg(args[5],"′") .. args[6]
coordinateSpec["dec-lat"] = convert_dms2dec(args[3],args[1],args[2]) -- {{coord/dms2dec|{{{3}}}|{{{1}}}|0{{{2}}}}}
coordinateSpec["dec-long"] = convert_dms2dec(args[6],args[4],args[5]) -- {{coord/dms2dec|{{{6}}}|{{{4}}}|0{{{5}}}}}
-- TODO Use loop when we know it won't break everything
coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5] .. "_" .. args[6] .. "_" .. args[7]
if args["format"] ~= "" then
coordinateSpec.default = args["format"]
else
coordinateSpec.default = "dms"
end
coordinateSpec.name = args["name"]
-- Error reporting
if isEmpty(args[4]) then
table.insert(errors, {"parseDM", "Missing longitude" })
end
if not (isEmpty(args[8]) and isEmpty(args[9]) and isEmpty(args[10])) then
table.insert(errors, {"parseDM", "Unexpected extra parameters"})
end
if (tonumber(args[1]) or 0) > 90 then
table.insert(errors, {"parseDM", "latd>90"})
end
if (tonumber(args[1]) or 0) < -90 then
table.insert(errors, {"parseDM", "latd<-90"})
end
if (tonumber(args[2]) or 0) >= 60 then
table.insert(errors, {"parseDM", "latm>=60"})
end
if (tonumber(args[2]) or 0) < 0 then
table.insert(errors, {"parseDM", "latm<0"})
end
if (tonumber(args[4]) or 0) >= 360 then
table.insert(errors, {"parseDM", "longd>=360"})
end
if (tonumber(args[4]) or 0) <= -360 then
table.insert(errors, {"parseDM", "longd<=-360"})
end
if (tonumber(args[5]) or 0) >= 60 then
table.insert(errors, {"parseDM", "longm>=60"})
end
if (tonumber(args[5]) or 0) < 0 then
table.insert(errors, {"parseDM", "longm<0"})
end
return coordinateSpec, errors
end
function parseD(args)
local coordinateSpec = {}
local errors = {}
coordinateSpec["dec-lat"] = args[1]
if args[2] =="S" then
coordinateSpec["dec-lat"] = "-" .. coordinateSpec["dec-lat"]
end
coordinateSpec["dec-long"] = args[4]
if args[4] =="W" then
coordinateSpec["dec-long"] = "-" .. coordinateSpec["dec-long"]
end
coordinateSpec["dec-lat-display"] = args[1] .. "°" .. args[2]
coordinateSpec["dec-long-display"] = args[3] .. "°" .. args[4]
local function postfixInverter(NE, latlong)
if NE == "N" and latlong == "lat" then
return "S"
elseif NE == "E" and latlong == "long" then
return "W"
elseif latlong == "lat" then
return "N"
else
return "E"
end
end
local precision = coordinates.prec_dec.max_precision(args[1], args[3])
coordinateSpec["dms-lat"] = convert_dec2dms(args[1], args[2], postfixInverter(args[2],"lat"), precision) -- {{coord/dec2dms|{{{1}}}|{{{2}}}|{{#ifeq:{{{2}}}|N|S|N}}|{{coord/prec dec|{{{1}}}|{{{3}}}}}}}
coordinateSpec["dms-long"] = convert_dec2dms(args[3], args[4], postfixInverter(args[4],"long"), precision) -- {{coord/dec2dms|{{{3}}}|{{{4}}}|{{#ifeq:{{{4}}}|E|W|E}}|{{coord/prec dec|{{{1}}}|{{{3}}}}}}}
-- TODO Use loop when we know it won't break everything
coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5]
if args["format"] ~= "" then
coordinateSpec.default = args["format"]
else
-- {{#ifeq:{{coord/prec dec|{{{1}}}|{{{3}}}}}|d|dms|dec}}
if precision == "d" then
coordinateSpec.default = "dms"
else
coordinateSpec.default = "dec"
end
end
coordinateSpec.name = args["name"]
-- Error reporting
if isEmpty(args[3]) then
table.insert(errors, {"parseD", "Missing longitude" })
args[3] = 0 -- to avoid error in tonumber() later on
end
if not (isEmpty(args[6]) and isEmpty(args[7]) and isEmpty(args[8]) and isEmpty(args[9]) and isEmpty(args[10])) then
table.insert(errors, {"parseD", "Unexpected extra parameters"})
end
if (tonumber(args[1]) or 0) > 90 then
table.insert(errors, {"parseD", "latd>90"})
end
if (tonumber(args[1]) or 0) < -90 then
table.insert(errors, {"parseD", "latd<-90"})
end
if (tonumber(args[3]) or 0) >= 360 then
table.insert(errors, {"parseD", "longd>=360"})
end
if (tonumber(args[3]) or 0) <= -360 then
table.insert(errors, {"parseD", "longd<=-360"})
end
return coordinateSpec, errors
end
--- BAD BAD URL escape
-- replace this later with the actual helper template
function urlEscape(arg)
return arg:gsub("%s+", '%%20'):gsub("%<", "%%3C"):gsub("%>", "%%3E")
end
--- A function that prints a table of coordinate specifications to HTML
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"] ~= "" and args["name"] ~= nil then
uriComponents = uriComponents .. "&title=" .. urlEscape(coordinateSpec["name"])
end
-- TODO i18n
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 geodeclat = coordinateSpec["dec-lat-display"]
if geodeclat == nil then
local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
if lat < 0 then
-- FIXME this breaks the pre-existing precision
geodeclat = coordinateSpec["dec-lat"]:sub(2) .. "°S"
else
geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
end
end
-- FIXME ugly code duplication, but lazy :D
local geodeclong = coordinateSpec["dec-long-display"]
if geodeclong == nil then
local long = tonumber( coordinateSpec["dec-long"] ) or 0
if long < 0 then
-- FIXME does not handle unicode minus
geodeclong = coordinateSpec["dec-long"]:sub(2) .. "°W"
else
geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
end
end
-- TODO requires DEC formatting
-- TODO requires vcard
local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
.. '<span class="latitude">' .. geodeclat .. '</span> '
.. '<span class="longitude">' .. geodeclong .. '</span>'
.. '</span>'
local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
.. '<span class="geo-multi-punct"> / </span>'
.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">' .. geodechtml .. '</span>'
return '<span class="plainlinks nourlexpansion">' .. globalFrame:preprocess('[http://toolserver.org/~geohack/geohack.php?pagename={{FULLPAGENAMEE}}¶ms=' .. uriComponents .. ' ' .. inner .. ']') .. '</span>'
end
function errorPrinter(errors)
local result = ""
for i,v in ipairs(errors) do
local errorHTML = '<strong class="error">' .. v[2] .. ' in Module:Coordinates.' .. v[1] .."()" .. '</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
function displayDefault(default, mode)
if default == "" then
default = "dec"
end
if default == mode then
return "geo-default"
else
return "geo-nondefault"
end
end
--- Check the arguments to determine what type of coordinates has been input
function formatTest(args)
if args[1] == "" then
-- no lat logic
return errorPrinter( {{"formatTest", "Missing latitude"}} )
elseif args[4] == "" and args[5] == "" and args[6] == "" then
-- dec logic
local decResult, errors = parseDec(args)
return specPrinter(args,decResult) .. " " .. errorPrinter(errors)
elseif dmsTest(args[4], args[8]) then
-- dms logic
local dmsResult, errors = parseDMS(args)
return specPrinter(args, dmsResult ) .. " " .. errorPrinter(errors)
-- return "dms"
elseif dmsTest(args[3], args[6]) then
-- dm logic
local dmResult, errors = parseDM(args)
return specPrinter(args, dmResult ) .. " " .. errorPrinter(errors)
-- return "dm"
elseif dmsTest(args[2], args[4]) then
-- d logic
local dResult, errors = parseD(args)
return specPrinter(args, dResult ) .. " " .. errorPrinter(errors)
-- return "d"
end
-- Error
return errorPrinter( {{"formatTest", "Unknown argument format"}} )
end
function convert_dec2dms_d(coordinate)
local d = coordinates.precision.round( coordinate, 0) .. "°"
return d .. ""
end
function convert_dec2dms_dm(coordinate)
-- {{#expr:{{{1}}} mod 360}}°{{padleft:{{#expr:({{{1}}} * 600 round 0) mod 600 / 10 round 0}}|2|0}}′
local d = math.floor(coordinate % 360) .."°"
local m = string.format( "%02d′", coordinates.precision.round( coordinates.precision.round(coordinate * 600, 0) % 600 / 10, 0) )
return d .. m
end
function convert_dec2dms_dms(coordinate)
--{{#expr:(((({{{1|0}}}) * 3600) round 0) / 3600) mod 360}}°{{padleft:{{#expr:(((3600 * ({{{1|0}}})) round 0) / 60) mod 60}}|2|0}}′{{padleft:{{#expr:((360000 * ({{{1|0}}})) round -2) mod 6000 div 100}}|2|0}}″
local d = math.floor(coordinate % 360) .. "°"
local m = string.format( "%02d′", math.floor(60 * coordinate) % 60 )
local s = string.format( "%02d″", (coordinates.precision.round(360000 * coordinate, -2) % 6000) / 100 )
return d .. m .. s
end
--- Convert a latitude or longitude to the DMS format
function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
-- {{Coord/dec2dms/{{{4}}}|{{#ifexpr:{{{1}}} >= 0||-}}{{{1}}}}}{{#ifexpr:{{{1}}} >= 0|{{{2}}}|{{{3}}}}}
local coord = tonumber(coordinate) or 0
local postfix
if coord >= 0 then
postfix = firstPostfix
else
postfix = secondPostfix
end
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
-- return "" .. globalFrame:expandTemplate{ title = 'coord/dec2dms', args = {coordinate, firstPostfix, secondPostfix, precision}}
end
--- Convert DMS into a N or E decimal coordinate
-- @param coordinate direction. either "N" "S" "E" or "W"
-- @param degrees: string or number
-- @param minutes: string or number
-- @param seconds: string or number
-- @return a number with the N or E normalized decimal coordinate of the input
function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
local degrees = tonumber(degrees_str) or 0
local minutes = tonumber(minutes_str) or 0
local seconds = tonumber(seconds_str) or 0
-- {{#expr:{{#switch:{{{1}}}|N|E=1|S|W=-1}}*({{{2|0}}}+({{{3|0}}}+{{{4|0}}}/60)/60) round {{{precdec|{{#if:{{{4|}}}|5|{{#if:{{{3|}}}|3|0}}}}+{{precision1|{{{4|{{{3|{{{2}}}}}}}}}}}}}}}}
local factor
if direction == "N" or direction == "E" then
factor = 1
else
factor = -1
end
local precision = 0
if not isEmpty(seconds_str) then
precision = 5 + coordinates.precision.prec1(seconds_str)
elseif not isEmpty(minutes_str) then
precision = 3 + coordinates.precision.prec1(minutes_str)
else
precision = coordinates.precision.prec1(degrees_str)
end
-- nil -> 0
local decimal = factor * (degrees+(minutes+seconds/60)/60)
return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
--return "" .. globalFrame:expandTemplate{ title = 'coord/dms2dec', args = {direction, degrees, minutes, seconds}}
end
--- TODO not yet in use
function validateDegreesLatitude(degrees)
if 0+tonumber(degrees) > 90 then
return "latd>90"
end
if 0+tonumber(degrees) < -90 then
return "latd<-90"
end
return true
end
--- TODO not yet in use
function validateDegreesLongtitude(degrees)
if 0+tonumber(degrees) >= 360 then
return "longd>=360"
end
if 0+tonumber(degrees) <= -360 then
return "longd<=-360"
end
return true
end
--- TODO not yet in use
function validateMinutes(minutes)
if 0+tonumber(minutes) >= 60 then
return "m>=60"
end
if 0+tonumber(minutes) < 0 then
return "m<0"
end
return true
end
--- TODO not yet in use
function validateSeconds(seconds)
if 0+tonumber(seconds) >= 60 then
return "s>=60"
end
if 0+tonumber(seconds) < 0 then
return "s<0"
end
return true
end
--- The display function we exposed to Module:Coordinates
function coordinates.input(frame)
globalFrame = frame;
return formatTest(frame.args)
end
--- The dec2dms function exposed to Module:Coordinates
function coordinates.dec2dms(frame)
globalFrame = frame
local coordinate = frame.args[1]
local firstPostfix = frame.args[2]
local secondPostfix = frame.args[3]
local precision = frame.args[4]
return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
end
--- The dec2dms function exposed to Module:Coordinates
function coordinates.dms2dec(frame)
globalFrame = frame
local direction = frame.args[1]
local degrees = frame.args[2]
local minutes = frame.args[3]
local seconds = frame.args[4]
return convert_dms2dec(direction, degrees, minutes, seconds)
end
--- This is used by {{coord}}.
function coordinates.coord(frame)
globalFrame = frame
local pframe = frame:getParent()
local args = pframe.args
local config = frame.args
for i=1,10 do
if ( nil == args[i] ) then args[i] = "" end
end
local contents = formatTest(args)
local Notes = args.notes or ""
local Display = args.display or "inline"
local text
if ( "title" ~= Display ) then
text = displayinline(contents)
else
text = displaytitle(contents)
end
return text .. Notes
end
return coordinates