Difference between revisions of "Module:Coordinates"

From Dharmawiki
Jump to navigation Jump to search
Template>Frietjes
(add support for optional qid for template:infobox observatory)
m (1 revision imported)
 
(2 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
--[[
 
--[[
This module is intended to replace the functionality of {{Coord}} and related
+
  __  __          _      _        ____                    _ _            _           
templates. It provides several methods, including
+
|  \/  | ___  __| |_  _| | ___ _ / ___|___  ___  _ __ __| (_)_ __  __ _| |_ ___  ___
 +
| |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
 +
| |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
 +
  |_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
 +
                                                                                         
  
{{#invoke:Coordinates | coord }} : General function formatting and displaying
+
This module is intended to provide functionality of {{location}} and related
coordinate values.
+
templates. It was developed on Wikimedia Commons, so if you find this code on
 +
other sites, check there for updates and discussions.
  
{{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
+
Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing
degree values to DMS format.
+
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
  
{{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
+
Authors and maintainers:
to decimal degree format.
+
* User:Jarekt
 +
* User:Ebraminio
  
{{#invoke:Coordinates | link }} : Export the link used to reach the tools
+
Functions:
 +
*function p.LocationTemplateCore(frame)
 +
**function p.GeoHack_link(frame)
 +
***function p.lat_lon(frame)
 +
****function p._deg2dms(deg,lang)
 +
***function p.externalLink(frame)
 +
****function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
 +
**function p._getHeading(attributes)
 +
**function p.externalLinksSection(frame)
 +
***function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
 +
*function p.getHeading(frame) 
 +
*function p.deg2dms(frame)
  
 
]]
 
]]
  
require('Module:No globals')
+
-- =======================================
 +
-- === Dependencies ======================
 +
-- =======================================
 +
require('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables
 +
local i18n  = require('Module:I18n/coordinates')    -- get localized translations of site names
 +
local yesno = require('Module:Yesno')
  
local math_mod = require("Module:Math")
+
-- =======================================
local coordinates = {};
+
-- === Hardwired parameters ==============
 +
-- =======================================
  
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 = '//tools.wmflabs.org/geohack/geohack.php?pagename=' .. page_name .. '&params='
+
  N    = 0,
 +
  NBE  = 11.25,
 +
  NNE  = 22.5,
 +
  NEBN = 33.75,
 +
  NE  = 45,
 +
  NEBE = 56.25,
 +
  ENE  = 67.5,
 +
  EBN  = 78.75,
 +
  E    = 90,
 +
  EBS  = 101.25,
 +
  ESE  = 112.5,
 +
  SEBE = 123.75,
 +
  SE  = 135,
 +
  SEBS = 146.25,
 +
  SSE  = 157.5,
 +
  SBE  = 168.75,
 +
  S    = 180,
 +
  SBW  = 191.25,
 +
  SSW  = 202.5,
 +
  SWBS = 213.75,
 +
  SW  = 225,
 +
  SWBW = 236.25,
 +
  WSW  = 247.5,
 +
  WBS  = 258.75,
 +
  W    = 270,
 +
  WBN  = 281.25,
 +
  WNW  = 292.5,
 +
  NWBW = 303.75,
 +
  NW  = 315,
 +
  NWBN = 326.25,
 +
  NNW  = 337.5,
 +
  NBW  = 348.75,
 +
}
  
--[[ Helper function, replacement for {{coord/display/title}} ]]
+
-- 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        = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
 +
GoogleEarth    = '//tools.wmflabs.org/geocommons/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
 +
Proximityrama  = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
 +
WikimediaMap  = '//maps.wikimedia.org/#16/$lat/$lon',
 +
OpenStreetMap1 = '//tools.wmflabs.org/wiwosm/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
 +
OpenStreetMap2 = '//tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
 +
GoogleMaps = {  
 +
Mars  = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
 +
Moon  = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
 +
Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic'
 +
}
 +
}
  
--[[ 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()
+
wikidata0    = '[[Category:Pages with coordinates from Wikidata]]',
return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
+
wikidata1    = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]',
end
+
wikidata2    = '[[Category:Pages with local coordinates and similar Wikidata coordinates]]',
 +
wikidata3    = '[[Category:Pages with local coordinates and mismatching Wikidata coordinates]]',
 +
wikidata4    = '[[Category:Pages with local coordinates and missing Wikidata coordinates]]',
 +
wikidata5    = '[[Category:Pages with locations and Wikidata ID to wrong type of entry]]',
 +
globe        = '[[Category:Media with %s locations]]',
 +
default      = '[[Category:Media with default locations]]',
 +
attribute    = '[[Category:Media with erroneous geolocation attributes]]',
 +
erroneous    = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n'
 +
}
  
 +
-- =======================================
 +
-- === Local Functions ===================
 +
-- =======================================
  
--[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
+
local function getArgs(frame)
local function makeInvokeFunc(funcName)
+
local args = frame.args
return function (frame)
+
if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then
local args = require('Module:Arguments').getArgs(frame, {
+
args.lang = frame:callParserFunction("int","lang") -- get user's chosen language
wrappers = 'Template:Coord'
 
})
 
return coordinates[funcName](args, frame)
 
 
end
 
end
 +
return args
 
end
 
end
  
--[[ Helper function, handle optional args. ]]
+
local NoLatLonString = 'latitude, longitude'
local function optionalArg(arg, supplement)
 
return arg and arg .. supplement or ''
 
end
 
  
--[[
+
local function langSwitch(list,lang)
Formats any error messages generated for display
+
local langList = mw.language.getFallbacksFor(lang)
]]
+
table.insert(langList,1,lang)
local function errorPrinter(errors)
+
for i,language in ipairs(langList) do
local result = ""
+
if list[language] then
for i,v in ipairs(errors) do
+
return list[language]
local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>'
+
end
result = result .. errorHTML .. "<br />"
 
 
end
 
end
return result
 
 
end
 
end
  
--[[
+
local function add_maplink(lat, lon, marker, text)
Determine the required CSS class to display coordinates
+
local tstr = ''
 
+
if text then
Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
+
tstr = string.format('text="%s" ', text)
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
 +
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
  
--[[
+
local function add_maplink2(lat1, lon1, lat2, lon2)
specPrinter
+
return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
 +
'  "type": "Feature",'..
 +
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
 +
'  "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons"  }'..
 +
'},{'..
 +
'  "type": "Feature",'..
 +
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
 +
'  "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata"  }'..
 +
'}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2)
 +
end
  
Output formatter.  Takes the structure generated by either parseDec
+
local function info_box(text)
or parseDMS and formats it for inclusion on Wikipedia.
+
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 specPrinter(args, coordinateSpec)
+
'<tr>'..
local uriComponents = coordinateSpec["param"]
+
'<td class="mbox-image" style="padding-left:.9em;">'..
if uriComponents == "" then
+
' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
-- RETURN error, should never be empty or nil
+
'<td class="mbox-text" style="">%s</td>'..
return "ERROR param was empty"
+
'</tr></table>', text)
end
+
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 function mergeWithWikidata(q, lat1, lon1)
local geodeclat
+
-- we are given wikidata q-code so look up the coordinates
if lat < 0 then
+
local dist_str=''
-- FIXME this breaks the pre-existing precision
+
local entity
geodeclat = tostring(coordinateSpec["dec-lat"]):sub(2) .. "°S"
+
-- Wikiata coordinates
 +
if q:match( '^[Qq]%d+$' ) then
 +
entity = mw.wikibase.getEntity(q)
 
else
 
else
geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
+
entity = q
 +
end
 +
q = entity.id
 +
local v, lat2, lon2, precision
 +
if entity then
 +
local P625 = entity:getBestStatements( 'P625' ) --  coordinate location
 +
local P159 = entity:getBestStatements( 'P159' ) --  headquarters location
 +
if P625[1] and P625[1].mainsnak.datavalue.value.latitude then
 +
v = P625[1].mainsnak.datavalue.value
 +
elseif P159[1] and P159[1].qualifiers and P159[1].qualifiers.P625 then
 +
v = P159[1].qualifiers.P625[1].datavalue.value
 +
end
 +
if v and v.globe == 'http://www.wikidata.org/entity/Q2' then
 +
lat2 = v.latitude
 +
lon2 = v.longitude
 +
precision = v.precision or 1e-4
 +
precision = math.floor(precision*111000) -- convert precision from degrees to meters and round
 +
precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree
 +
end
 
end
 
end
 +
 +
-- compare coordinates
 +
local cat = ''
 +
if not lat1 or not lon1 then -- wikidata coordinates only
 +
lat1 = lat2
 +
lon1 = lon2
 +
cat = CoorCat.wikidata0
 +
elseif lat1 and lon1 and not lat2 and not lon2 then
 +
cat = string.format('The above coordinates are missing from linked Wikidata item [[d:%s|%s]].  Click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
 +
"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span> to copy it",
 +
q, q, q, lat1, lon1)
 +
cat = CoorCat.wikidata4 .. info_box(cat)
 +
elseif lat1 and lon1 and lat2 and lon2 then
 +
-- calculate distance
 +
local dLat = math.rad(lat1-lat2)
 +
local dLon = math.rad(lon1-lon2)
 +
local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
 +
d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d))  -- angular distance in radians
 +
d = 6371000 * d      -- radians to meters conversion
 +
d = math.floor(d+0.5) -- rind it to even meters
 +
 +
local frame = mw.getCurrentFrame()
 +
local info = frame:preprocess(add_maplink2(lat1, lon1, lat2, lon2)) -- fancy link to OSM
 +
info = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at linked Wikidata item [[d:%s|%s]] (%s, precision: %i m). "..
 +
'Please reconcile them. To copy Commons coordinates to Wikidata, click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
 +
"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span>",
 +
d, q, q, info, precision, q, lat1, lon1)
  
local long = tonumber( coordinateSpec["dec-long"] ) or 0
+
if d<20 or d<precision then -- will consider location within 20 meters or precisi0on distance as the same
local geodeclong
+
cat = CoorCat.wikidata1
if long < 0 then
+
dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', d) -- will be displayed when hovering a mouse above wikidata icon
-- FIXME does not handle unicode minus
+
elseif d>1000 and d>5*precision then -- locations 1 km off and 5 precision distances away are likely wrong
geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
+
cat = CoorCat.wikidata3 .. info_box(info)  
else
+
else
geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
+
cat = CoorCat.wikidata2 .. info_box(info)  
 +
end
 
end
 
end
 
 
local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
+
-- verify proper P31 (instance of). List is based on https://www.wikidata.org/wiki/Property_talk:P625
.. geodeclat .. ' '
+
local QCodes =
.. geodeclong
+
Q5        = 1, -- human
.. '</span>'
+
Q11879590 = 1, -- female given name
 
+
Q202444  = 1, -- given name
local geonumhtml = '<span class="geo">'
+
Q12308941 = 1, -- male given name
.. coordinateSpec["dec-lat"] .. '; '
+
Q4167836  = 1, -- Wikimedia category
.. coordinateSpec["dec-long"]
+
Q4167410  = 1, -- Wikimedia disambiguation page
.. '</span>'
+
Q783794  = 2, -- company
 
+
Q4830453  = 2, -- business enterprise
local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
+
}
.. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
+
local s = entity:getBestStatements( 'P31' )
.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';
+
if s[1] and s[1].mainsnak.datavalue.value['id'] then
 
+
local instanceOf = s[1].mainsnak.datavalue.value['id']
if not args["name"] then
+
if QCodes[instanceOf] then
inner = inner .. geodechtml
+
cat = '' -- wipe out categories
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span></span>'
+
if QCodes[instanceOf]==1 then -- add problem category
else
+
cat = CoorCat.wikidata5
inner = inner .. '<span class="vcard">' .. geodechtml
+
end
.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span>'
+
end
.. '<span style="display:none">&#xfeff; (<span class="fn org">'
 
.. args["name"] .. '</span>)</span></span></span>'
 
 
end
 
end
 
return '<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 )
+
return lat1, lon1, q, cat, dist_str
 
end
 
end
  
--[[ Helper function, convert decimal to degrees, minutes, and seconds ]]
+
-- =======================================
local function convert_dec2dms_dms(coordinate)
+
-- === External Functions ================
coordinate = math_mod._round( coordinate * 60 * 60, 0 );
+
-- =======================================
local s = coordinate % 60
+
local p = {}
coordinate = math.floor( (coordinate - s) / 60 );
 
local m = coordinate % 60
 
coordinate = math.floor( (coordinate - m) / 60 );
 
local d = coordinate % 360 .."°"
 
  
return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
+
-- parse attribute variable returning desired field (used for debugging)
 +
function p.parseAttribute(frame)
 +
  return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
 
end
 
end
  
--[[
+
-- Helper 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)
+
end
local coord = tonumber(coordinate)
+
local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
local postfix
+
if hStr == nil then
if coord >= 0 then
+
return nil
postfix = firstPostfix
+
end
else
+
local hNum = tonumber( hStr )
postfix = secondPostfix
+
if hNum == nil then
 +
hStr = string.upper (hStr)
 +
hNum = compass_points[hStr] 
 
end
 
end
 
+
if hNum ~= nil then
precision = precision:lower();
+
hNum = hNum%360
if precision == "dms" then
 
return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
 
elseif precision == "dm" then
 
return convert_dec2dms_dm( math.abs( coord ) ) .. postfix;
 
elseif precision == "d" then
 
return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
 
 
end
 
end
 +
return hNum
 
end
 
end
  
--[[
+
--[[============================================================================
Convert DMS format into a N or E decimal coordinate
+
Parse attribute variable returning heading field. If heading is a string than
]]
+
try to convert it to an angle
local function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
+
==============================================================================]]
local degrees = tonumber(degrees_str)
+
 
local minutes = tonumber(minutes_str) or 0
+
function p.getHeading(frame)
local seconds = tonumber(seconds_str) or 0
+
local attributes
+
if frame.args[1] then
local factor = 1
+
attributes = frame.args[1]
if direction == "S" or direction == "W" then
+
elseif frame.args.attributes then
factor = -1
+
attributes = frame.args.attributes
 +
else
 +
return ''
 
end
 
end
+
local hNum = p._getHeading(attributes)
local precision = 0
+
if hNum == nil then
if seconds_str then
+
return ''
precision = 5 + math.max( math_mod._precision(seconds_str), 0 );
 
elseif minutes_str and minutes_str ~= '' then
 
precision = 3 + math.max( math_mod._precision(minutes_str), 0 );
 
else
 
precision = math.max( math_mod._precision(degrees_str), 0 );
 
 
end
 
end
+
return tostring(hNum)
local decimal = factor * (degrees+(minutes+seconds/60)/60)
 
return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
 
 
end
 
end
  
--[[
 
Checks input values to for out of range errors.
 
]]
 
local function validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, source, strong )
 
local errors = {};
 
lat_d = tonumber( lat_d ) or 0;
 
lat_m = tonumber( lat_m ) or 0;
 
lat_s = tonumber( lat_s ) or 0;
 
long_d = tonumber( long_d ) or 0;
 
long_m = tonumber( long_m ) or 0;
 
long_s = tonumber( long_s ) or 0;
 
  
if strong then
+
--[[============================================================================
if lat_d < 0 then
+
Helper core function for deg2dms. deg2dms can be called by templates, while
table.insert(errors, {source, "latitude degrees < 0 with hemisphere flag"})
+
_deg2dms should be called from Lua.
end
+
Inputs:
if long_d < 0 then
+
* degree - positive coordinate in degrees
table.insert(errors, {source, "longitude degrees < 0 with hemisphere flag"})
+
* degPrec - coordinate precision in degrees will result in different angle format
end
+
* lang - language to used when formatting the number
--[[
+
==============================================================================]]
#coordinates is inconsistent about whether this is an error. If globe: is
+
function p._deg2dms(degree, degPrec, lang)
specified, it won't error on this condition, but otherwise it will.
+
local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero
+
local Lang = mw.language.new(lang)
For not simply disable this check.
+
 
+
-- adjust number display based on precision
if long_d > 180 then
+
secPrec = degPrec*3600.0                    -- coordinate precision in seconds
table.insert(errors, {source, "longitude degrees > 180 with hemisphere flag"})
+
if secPrec<0.05 then                        -- degPrec<1.3889e-05
end
+
formatStr = '%s°&nbsp;%s′&nbsp;%s″'     -- use DD° MM′ SS.SS″ format
]]
+
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
if lat_d > 90 then
+
c = 36000
table.insert(errors, {source, "latitude degrees > 90"})
+
elseif degPrec*60.0<0.5 then                 -- 1.3889e-04<degPrec<0.0083
 +
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
 +
c = 3600
 +
elseif degPrec<0.5 then                      -- 0.0083<degPrec<0.5
 +
formatStr = '%s°&nbsp;%s′'              -- use DD° MM′ format
 +
c = 60
 +
else -- if degPrec>0.5 then                
 +
formatStr = '%s°'                        -- use DD° format
 +
c = 1
 
end
 
end
if lat_d < -90 then
+
table.insert(errors, {source, "latitude degrees < -90"})
+
-- create degree, minute and seconds numbers and string
end
+
d = c/60
if lat_m >= 60 then
+
= 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
table.insert(errors, {source, "latitude minutes >= 60"})
+
dNum = math.floor(k/c) % 360          -- degree number (integer in 0-360 range)
end
+
mNum = math.floor(k/d) %  60         -- minute number (integer in 0-60 range)
if lat_m < 0 then
+
sNum =      3600*(k%d) / c            -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
table.insert(errors, {source, "latitude minutes < 0"})
+
dStr = Lang:formatNum(dNum)          -- degree string
end
+
mStr = Lang:formatNum(mNum)           -- minute string
if lat_s >= 60 then
+
sStr = Lang:formatNum(sNum)          -- second string
table.insert(errors, {source, "latitude seconds >= 60"})
+
zero = Lang:formatNum(0)              -- zero string in local language
end
+
if mNum<10 then
if lat_s < 0 then
+
mStr = zero .. mStr                -- pad with zero if a single digit
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
 
end
if long_m < 0 then
+
if sNum<10 then
table.insert(errors, {source, "longitude minutes < 0"})
+
sStr = zero .. sStr                -- pad with zero if less than ten
 
end
 
end
if long_s >= 60 then
+
return string.format(formatStr, dStr, mStr, sStr);
table.insert(errors, {source, "longitude seconds >= 60"})
 
end
 
if long_s < 0 then
 
table.insert(errors, {source, "longitude seconds < 0"})
 
end
 
 
return errors;
 
 
end
 
end
  
--[[
+
--[[============================================================================
parseDec
+
Convert degrees to degrees/minutes/seconds notation commonly used when displaying
 +
coordinates.
 +
Inputs:
 +
1) latitude or longitude angle in degrees
 +
2) georeference precision in degrees
 +
3) language used in formatting of the number
 +
==============================================================================]]
 +
function p.deg2dms(frame)
 +
local args = getArgs(frame)
 +
local degree  = tonumber(args[1])
 +
local degPrec = tonumber(args[2]) or 0-- precision in degrees
  
Transforms decimal format latitude and longitude into the
+
if degree==nil then
structure to be used in displaying coordinates
+
return args[1];
]]
+
else
local function parseDec( lat, long, format )
+
return p._deg2dms(degree, degPrec, args.lang)
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
 
end
+
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}}}}}}}
+
Format coordinate location string, by creating and joining DMS strings for
coordinateSpec["dms-long"] = convert_dec2dms( long, "E", "W", mode) -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
+
latitude and longitude. Also convert precision from meters to degrees.
+
INPUTS:
if format then
+
* lat        = latitude in degrees
coordinateSpec.default = format
+
* lon        = longitude in degrees
 +
* lang      = language code
 +
* prec      = geolocation precision in meters
 +
==============================================================================]]
 +
function p._lat_lon(lat, lon, prec, lang)
 +
lat  = tonumber(lat)
 +
lon = tonumber(lon)
 +
prec = math.abs(tonumber(prec) or 0)
 +
if lon then -- get longitude to be in -180 to 180 range
 +
lon=lon%360
 +
if lon>180 then
 +
lon = lon-360
 +
end
 +
end
 +
if lat==nil or lon==nil then
 +
return NoLatLonString
 
else
 
else
coordinateSpec.default = "dec"
+
local nsew = langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language
 +
local SN, EW, latStr, lonStr, lon2m, lat2m, phi
 +
if lat<0 then SN = nsew.S else SN = nsew.N end              -- choose S or N depending on latitude  degree sign
 +
if lon<0 then EW = nsew.W else EW = nsew.E end              -- choose W or E depending on longitude degree sign
 +
lat2m=1
 +
lon2m=1
 +
if prec>0 then -- if user specified the precision of the geo location...
 +
phi  = math.abs(lat)*math.pi/180  -- latitude in radiants
 +
lon2m = 6378137*math.cos(phi)*math.pi/180  -- see https://en.wikipedia.org/wiki/Longitude
 +
lat2m = 111000  -- average latitude degree size in meters
 +
end
 +
latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude  degrees to degrees/minutes/seconds
 +
lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds
 +
return string.format('%s&nbsp;%s, %s&nbsp;%s', latStr, SN, lonStr, EW)
 +
--return string.format('<span class="latitude">%s %s</span>, <span class="longitude">%s %s</span>', latStr, SN, lonStr, EW)
 
end
 
end
 +
end
  
return coordinateSpec, errors
+
function p.lat_lon(frame)
 +
local args = getArgs(frame)
 +
return p._lat_lon(args.lat, args.lon, args.prec, args.lang)
 
end
 
end
  
--[[
+
--[[============================================================================
parseDMS
+
Helper core function for externalLink. Create URL for different sites:
 +
INPUTS:
 +
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
 +
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 +
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
 +
                Ganymede are also supported but are unused as of 2013.
 +
* latStr    = latitude string or number
 +
* lonStr    = longitude string or number
 +
* lang      = language code
 +
* attributes = attributes to be passed to GeoHack
 +
==============================================================================]]
 +
function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level)
 +
local URLstr = SiteURL[site];
 +
level = level or 1
 +
local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' )
 +
pageName = mw.ustring.gsub( pageName, '%%', '%%%%')
  
Transforms degrees, minutes, seconds format latitude and longitude
+
if site == 'GoogleMaps' then
into the a structure to be used in displaying coordinates
+
URLstr = SiteURL.GoogleMaps[globe]
]]
+
elseif site == 'GeoHack' then
local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
+
attributes = string.format('globe:%s_%s', globe, attributes)
local coordinateSpec, errors, backward = {}, {}
+
URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
 
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
 
end
+
URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr)
if not lat_m and not lat_s and not long_m and not long_s and #errors == 0 then
+
URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr)
if math_mod._precision( lat_d ) > 0 or math_mod._precision( long_d ) > 0 then
+
URLstr = mw.ustring.gsub( URLstr, '$lang' , lang)
if lat_f:upper() == 'S' then
+
URLstr = mw.ustring.gsub( URLstr, '$level', level)
lat_d = '-' .. lat_d;
+
URLstr = mw.ustring.gsub( URLstr, '$page' , pageName)
end
+
URLstr = mw.ustring.gsub( URLstr, '+', '')
if long_f:upper() == 'W' then
+
URLstr = mw.ustring.gsub( URLstr, ' ', '_')
long_d = '-' .. long_d;
+
return URLstr
end
+
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
+
Create URL for different sites.
else
+
INPUTS:
coordinateSpec.default = "dms"
+
* site      = Possible sites: GeoHack, GoogleEarth, Proximityrama,
end 
+
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 
+
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
return coordinateSpec, errors, backward
+
                Ganymede are also supported but are unused as of 2013.
 +
* lat        = latitude string or number
 +
* lon        = longitude string or number
 +
* lang      = language code
 +
* attributes = attributes to be passed to GeoHack
 +
==============================================================================]]
 +
function p.externalLink(frame)
 +
local args = getArgs(frame)
 +
return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
 
end
 
end
  
--[[  
+
--[[============================================================================
Check the input arguments for coord to determine the kind of data being provided
+
Adjust GeoHack attributes depending on the template that calls it
and then make the necessary processing.
+
INPUTS:
]]
+
* attributes = attributes to be passed to GeoHack
local function formatTest(args)
+
* mode = set by each calling template
local result, errors
+
==============================================================================]]
local backward, primary = false, false
+
function p.alterAttributes(attributes, mode)
 
+
-- indicate which template called it
local function getParam(args, lim)
+
if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
local ret = {}
+
if string.find(attributes, 'type:camera')==nil then
for i = 1, lim do
+
attributes = 'type:camera_' .. attributes
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
 
end
if not result then
+
elseif mode=='object'or mode =='globe' then              -- Used by {{Object location}}
return errorPrinter(errors)
+
if mode=='object' and string.find(attributes, 'type:')==nil then
 +
attributes = 'type:object_' .. attributes
 
end
 
end
result.param = getParam(args, 7)
+
if string.find(attributes, 'class:object')==nil then
elseif dmsTest(args[2], args[4]) then
+
attributes = 'class:object_' .. attributes
-- 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
 
end
result.param = getParam(args, 5)
+
elseif mode=='inline' then                              -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
else
+
elseif mode=='user' then                                -- Used by {{User location}}
-- Error
+
attributes = 'type:user_location'
return errorPrinter({{"formatTest", "Unknown argument format"}})
+
elseif mode=='institution' then                          --Used by {{Institution/coordinates}} (categories only)
 +
attributes = 'type:institution'
 
end
 
end
result.name = args.name
+
return attributes
 +
end
 
 
local extra_param = {'dim', 'globe', 'scale', 'region', 'source', 'type'}
+
--[[============================================================================
for _, v in ipairs(extra_param) do
+
Create link to GeoHack tool which displays latitude and longitude coordinates
if args[v] then  
+
in DMS format
table.insert(errors, {'formatTest', 'Parameter: "' .. v .. '=" should be "' .. v .. ':"' })
+
INPUTS:
end
+
* globe     = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,  
 +
                Ganymede are also supported but are unused as of 2013.
 +
* lat        = latitude in degrees
 +
* lon        = longitude in degrees
 +
* lang      = language code
 +
* prec      = geolocation precision in meters
 +
* attributes = attributes to be passed to GeoHack
 +
==============================================================================]]
 +
function p._GeoHack_link(args)
 +
-- create link and coordintate string
 +
local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
 +
if latlon==NoLatLonString then
 +
return latlon
 +
else
 +
local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
 +
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
 
end
 
end
 
local ret = specPrinter(args, result)
 
if #errors > 0 then
 
ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
 
end
 
return ret, backward
 
 
end
 
end
  
--[[
+
function p.GeoHack_link(frame)
Generate Wikidata tracking categories.
+
return p._GeoHack_link(getArgs(frame))
]]
 
local function makeWikidataCategories(qid)
 
local ret
 
if mw.wikibase and current_page.namespace == 0 then
 
local entity = qid and mw.wikibase.getEntityObject(qid) or mw.wikibase.getEntityObject()
 
if entity and entity.claims and entity.claims.P625 and entity.claims.P625[1] then
 
local snaktype = entity.claims.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
 
end
  
--[[
 
link
 
  
Simple function to export the coordinates link for other uses.
+
--[[============================================================================
 
+
Create full external links section of {{Location}} or {{Object location}}
Usage:
+
templates, based on:
{{#invoke:Coordinates | link }}
+
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 +
* mode      = Possible options:
 +
  - camera - call from {{location}}
 +
  - object - call from {{Object location}}
 +
  - globe  - call from {{Globe location}}
 +
* lat        = latitude in degrees
 +
* lon        = longitude in degrees
 +
* lang      = language code
 +
* namespace  = namespace name: File, Category, (Gallery)
 +
==============================================================================]]
 +
function p._externalLinksSection(args)
 +
local lang = args.lang
 +
if not args.namespace then
 +
args.namespace = mw.title.getCurrentTitle().namespace
 +
end
 +
 +
local str, link1, link2, link3, link4
 +
if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links
 +
link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '')
 +
link2 = p._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, '')
 +
str = string.format('[%s %s] - [%s %s]',
 +
link1, langSwitch(i18n.OpenStreetMaps, lang),
 +
link2, langSwitch(i18n.GoogleEarth, lang))
 +
elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links
 +
link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
 +
--link2 = p._externalLink('GoogleMaps'    , 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
 +
link3 = p._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, '')
 +
link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '')
 +
str = string.format('[%s %s] - [%s %s] - [%s %s]',
 +
link1, langSwitch(i18n.OpenStreetMaps, lang),
 +
--link2, langSwitch(i18n.GoogleMaps, lang),
 +
link3, langSwitch(i18n.GoogleEarth, lang),
 +
link4, langSwitch(i18n.Proximityrama, lang))
 +
elseif args.globe=='Mars' or args.globe=='Moon' then
 +
link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
 +
str = string.format('[%s %s]', link1, langSwitch(i18n.GoogleMaps, lang))
 +
end
 
 
]]
+
return str
function coordinates.link(frame)
 
return coord_link;
 
 
end
 
end
  
--[[
+
function p.externalLinksSection(frame)
dec2dms
+
return p._externalLinksSection(getArgs(frame))
 +
end
  
Wrapper to allow templates to call dec2dms directly.
+
--[[============================================================================
 
+
Core section of template:Location, template:Object location and template:Globe location.
Usage:
+
This method requires several arguments to be passed to it or it's parent method/template:
{{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix |
+
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
negative_suffix | precision }}
+
* mode      = Possible options:
 +
  - camera - call from {{location}}
 +
  - object - call from {{Object location}}
 +
  - globe  - call from {{Globe location}}
 +
* lat        = latitude in degrees
 +
* lon        = longitude in degrees
 +
* attributes = attributes
 +
* lang      = language code
 +
* namespace  = namespace: File, Category, Gallery
 +
* prec      = geolocation precision in meters
 +
==============================================================================]]
 +
function p._LocationTemplateCore(args)
 +
-- prepare arguments
 +
if not (args.namespace) then -- if namespace not provided than look it up
 +
args.namespace = mw.title.getCurrentTitle().namespace
 +
end
 +
if args.namespace=='' then -- if empty than it is a gallery
 +
args.namespace = 'Gallery'
 +
end
 +
local bare  = yesno(args.bare,false)
 +
local Status = 'primary' -- used by {{#coordinates:}}
 +
if yesno(args.secondary,false) then
 +
Status = 'secondary'
 +
end
 +
local attributes0 = args.attributes
 +
args.attributes = p.alterAttributes(args.attributes or '', args.mode)
 
 
decimal_coordinate is converted to DMS format.  If positive, the positive_suffix
+
-- Convert coordinates from string to numbers
is appended (typical N or E), if negative, the negative suffix is appended.  The
+
local lat = tonumber(args.lat)
specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
+
local lon = tonumber(args.lon)
to use.
+
if lon then -- get longitude to be in -180 to 180 range
]]
+
lon=lon%360
coordinates.dec2dms = makeInvokeFunc('_dec2dms')
+
if lon>180 then
function coordinates._dec2dms(args)
+
lon = lon-360
local coordinate = args[1]
+
end
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
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.
+
-- If wikidata link provided than compare coordinates
direction_flag is one of N, S, E, W, and determines whether the output is
+
local Categories, geoMicroFormat, coorTag, wikidata_link = '', '', '', ''
positive (i.e. N and E) or negative (i.e. S and W).
+
if args.wikidata and args.wikidata~='' then
]]
+
local dist_str, q
coordinates.dms2dec = makeInvokeFunc('_dms2dec')
+
  -- if lat/lon is not provided but we are given wikidata q-code than look up the coordinates
function coordinates._dms2dec(args)
+
lat, lon, q, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon)
local direction = args[1]
+
wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, q);
local degrees = args[2]
+
end
local minutes = args[3]
 
local seconds = args[4]
 
  
return convert_dms2dec(direction, degrees, minutes, seconds)
+
args.lat = string.format('%010.6f', lat or 0)
end
+
args.lon = string.format('%011.6f', lon or 0)
  
--[[
+
local frame = mw.getCurrentFrame()
coord
 
  
Main entry point for Lua function to replace {{coord}}
+
-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
 
+
if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then
Usage:
+
if lat and lon then -- if lat and lon are numbers...
{{#invoke:Coordinates | coord }}
+
if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources
{{#invoke:Coordinates | coord | lat | long }}
+
Categories = Categories .. CoorCat.default
{{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
+
end
...
+
if attributes0 and string.find(attributes0, '=') then
+
Categories = Categories .. CoorCat.attribute
Refer to {{coord}} documentation page for many additional parameters and  
+
end
configuration options.
+
if args.error=='1' or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
+
Categories = Categories .. CoorCat.erroneous
Note: This function provides the visual display elements of {{coord}}. In
+
end
order to load coordinates into the database, the {{#coordinates:}} parser
+
local cat = CoorCat[args.namespace]
function must also be called, this is done automatically in the Lua
+
if cat then -- add category based on namespace
version of {{coord}}.
+
Categories = Categories .. cat
]]
+
end
coordinates.coord = makeInvokeFunc('_coord')
+
-- if not earth than add a category for each globe
function coordinates._coord(args)
+
if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then
if (not args[1] or not tonumber(args[1])) and not args[2] and mw.wikibase.getEntityObject() then
+
Categories = Categories .. string.format(CoorCat[args.mode], args.globe)
args[3] = args[1]; args[1] = nil
+
end
local entity = args.qid and mw.wikibase.getEntityObject(args.qid) or mw.wikibase.getEntityObject()
+
-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
if entity
+
geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
and entity.claims
+
-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
and entity.claims.P625
+
if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then  
and entity.claims.P625[1].mainsnak.snaktype == 'value'
+
coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } )
then
+
elseif args.namespace == 'File' and args.mode=='object' then
local precision = entity.claims.P625[1].mainsnak.datavalue.value.precision
+
coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } )
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
 +
else -- if lat and lon are not numbers then add error category
 +
Categories = Categories .. CoorCat.erroneous
 
end
 
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)
+
-- Call helper functions to render different parts of the template
-- Finds whether coordinates are displayed inline.
+
local coor,  info_link, inner_table, heading, OSM = '','','','','','',''
return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti'
+
coor = p._GeoHack_link(args) -- the p and link to GeoHack
 +
heading = p._getHeading(attributes0) -- get heading arrow section
 +
if heading then 
 +
local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
 +
local fname = heading_icon[k]
 +
coor = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading)
 
end
 
end
local function isInTitle(s)
+
if args.globe=='Earth' then
-- Finds whether coordinates are displayed in the title.
+
local icon = 'marker'
return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti'
+
if args.mode=='camera' then
 +
icon = 'camera'
 +
end
 +
OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM
 
end
 
end
+
local external_link = p._externalLinksSection(args) -- external link section
local function coord_wrapper(in_args)
+
if external_link and args.namespace == 'File' then
-- Calls the parser function {{#coordinates:}}.
+
external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template
return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
+
elseif external_link then
 +
external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template
 
end
 
end
 +
info_link  = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', langSwitch(i18n.COM_GEO, args.lang) )
 +
inner_table = string.format('<td style="border:none;">%s&nbsp;%s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>',
 +
coor, OSM, external_link or '', info_link, wikidata_link, geoMicroFormat)
 
 
local text = ''
+
-- combine strings into a table
if isInline(Display) then
+
local templateText
text = text .. displayinline(contents, Notes)
+
if bare then
end
+
templateText  = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table)
if isInTitle(Display) then
+
else
text = text
+
-- choose name of the field
.. displaytitle(contents, Notes)
+
local field_name = 'Location'
.. makeWikidataCategories(args.qid)
+
if args.mode=='camera' then
end
+
field_name = langSwitch(i18n.CameraLocation, args.lang)
if not args.nosave then
+
elseif args.mode=='object' then  
local page_title, count = mw.title.getCurrentTitle(), 1
+
field_name = langSwitch(i18n.ObjectLocation, args.lang)
if backward then
+
elseif args.mode=='globe' then
local tmp = {}
+
local field_list = langSwitch(i18n.GlobeLocation, args.lang)
while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
+
if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
tmp.count = count; count = 2*(count-1)
+
field_name = field_list[args.globe]
while count >= tmp.count do table.insert(tmp, 1, (args[count] or '')); count = count-1 end
+
end
for i, v in ipairs(tmp) do args[i] = v end
 
else
 
while count <= 9 do args[count] = (args[count] or ''); count = count+1 end
 
 
end
 
end
if isInTitle(Display) and not page_title.isTalkPage and page_title.subpageText ~= 'doc' and page_title.subpageText ~= 'testcases' then args[10] = 'primary' end
+
--Create HTML text
args.notes, args.format, args.display = nil
+
local dir, text_align
text = text .. coord_wrapper(args)
+
if mw.language.new( args.lang ):isRTL() then
end
+
dir = 'rtl'
return text
+
text_align = 'right'
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
 
else
result, negative = result[2], 'W'
+
dir = 'ltr'
end
+
text_align = 'left'
result = mw.text.split(result, '°')
+
end  
if result[2] == negative then result[1] = '-'..result[1] end
+
local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"',
return result[1]
+
args.lang, dir, text_align, args.lang)
else
+
templateText  = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', args.lang, style, field_name, inner_table)
return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
 
 
end
 
end
 +
return templateText, Categories, coorTag
 
end
 
end
  
--[[
+
function p.LocationTemplateCore(frame)
coordinsert
+
local args = frame.args
 
+
if not args or not args.lat then -- if no arguments provided than use parent arguments
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}}.
+
args = mw.getCurrentFrame():getParent().args
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
 
end
if frame.args.name then
+
if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then  
if not mw.ustring.find(frame.args[1], '<span class="vcard">') then
+
args.lang = frame:callParserFunction("int","lang") -- get user's chosen language
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
 
end
return frame.args[1]
+
local templateText, Categories, coorTag = p._LocationTemplateCore(args)
 +
return templateText .. Categories .. coorTag
 
end
 
end
  
return coordinates
+
return p

Latest revision as of 22:22, 9 February 2019

--[[

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

This module is intended to provide functionality of Script error: The function "coord" does not exist. and related templates. It was developed on Wikimedia Commons, so if you find this code on other sites, check there for updates and discussions.

Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.

Authors and maintainers:

  • User:Jarekt
  • User:Ebraminio

Functions:

  • function p.LocationTemplateCore(frame)
    • function p.GeoHack_link(frame)
      • function p.lat_lon(frame)
        • function p._deg2dms(deg,lang)
      • function p.externalLink(frame)
        • function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
    • function p._getHeading(attributes)
    • function p.externalLinksSection(frame)
      • function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
  • function p.getHeading(frame)
  • function p.deg2dms(frame)

]]

-- ======================================= -- === Dependencies ====================== -- ======================================= require('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables local i18n = require('Module:I18n/coordinates') -- get localized translations of site names local yesno = require('Module:Yesno')

-- ======================================= -- === Hardwired parameters ============== -- =======================================

-- Angles associated with each abbreviation of compass point names. See en:Points of the compass local compass_points = {

 N    = 0,
 NBE  = 11.25,
 NNE  = 22.5,
 NEBN = 33.75,
 NE   = 45,
 NEBE = 56.25,
 ENE  = 67.5,
 EBN  = 78.75,
 E    = 90,
 EBS  = 101.25,
 ESE  = 112.5,
 SEBE = 123.75,
 SE   = 135,
 SEBS = 146.25,
 SSE  = 157.5,
 SBE  = 168.75,
 S    = 180,
 SBW  = 191.25,
 SSW  = 202.5,
 SWBS = 213.75,
 SW   = 225,
 SWBW = 236.25,
 WSW  = 247.5,
 WBS  = 258.75,
 W    = 270,
 WBN  = 281.25,
 WNW  = 292.5,
 NWBW = 303.75,
 NW   = 315,
 NWBN = 326.25,
 NNW  = 337.5,
 NBW  = 348.75,

}

-- files to use for different headings local heading_icon = { [ 1] = 'File:Compass-icon bb N.svg', [ 2] = 'File:Compass-icon bb NbE.svg', [ 3] = 'File:Compass-icon bb NNE.svg', [ 4] = 'File:Compass-icon bb NEbN.svg', [ 5] = 'File:Compass-icon bb NE.svg', [ 6] = 'File:Compass-icon bb NEbE.svg', [ 7] = 'File:Compass-icon bb ENE.svg', [ 8] = 'File:Compass-icon bb EbN.svg', [ 9] = 'File:Compass-icon bb E.svg', [10] = 'File:Compass-icon bb EbS.svg', [11] = 'File:Compass-icon bb ESE.svg', [12] = 'File:Compass-icon bb SEbE.svg', [13] = 'File:Compass-icon bb SE.svg', [14] = 'File:Compass-icon bb SEbS.svg', [15] = 'File:Compass-icon bb SSE.svg', [16] = 'File:Compass-icon bb SbE.svg', [17] = 'File:Compass-icon bb S.svg', [18] = 'File:Compass-icon bb SbW.svg', [19] = 'File:Compass-icon bb SSW.svg', [20] = 'File:Compass-icon bb SWbS.svg', [21] = 'File:Compass-icon bb SW.svg', [22] = 'File:Compass-icon bb SWbW.svg', [23] = 'File:Compass-icon bb WSW.svg', [24] = 'File:Compass-icon bb WbS.svg', [25] = 'File:Compass-icon bb W.svg', [26] = 'File:Compass-icon bb WbN.svg', [27] = 'File:Compass-icon bb WNW.svg', [28] = 'File:Compass-icon bb NWbW.svg', [29] = 'File:Compass-icon bb NW.svg', [30] = 'File:Compass-icon bb NWbN.svg', [31] = 'File:Compass-icon bb NNW.svg', [32] = 'File:Compass-icon bb NbW.svg' }

-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be -- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings. local SiteURL = { GeoHack = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang', GoogleEarth = '//tools.wmflabs.org/geocommons/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1', Proximityrama = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon', WikimediaMap = '//maps.wikimedia.org/#16/$lat/$lon', OpenStreetMap1 = '//tools.wmflabs.org/wiwosm/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon', OpenStreetMap2 = '//tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level', GoogleMaps = { Mars = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8', Moon = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8', Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic' } }

-- Categories local CoorCat = { File = , Gallery = , Category = , wikidata0 = , wikidata1 = , wikidata2 = , wikidata3 = , wikidata4 = , wikidata5 = , globe = , default = , attribute = , erroneous = 'Error: Invalid parameters!\n' }

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

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

local NoLatLonString = 'latitude, longitude'

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

local function add_maplink(lat, lon, marker, text) local tstr = if text then tstr = string.format('text="%s" ', text) end return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0" }'.. '}</maplink>', tstr, lat, lon, lon, lat, marker) end

local function add_maplink2(lat1, lon1, lat2, lon2) return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons" }'.. '},{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata" }'.. '}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2) end

local function info_box(text)

return string.format('

'.. ''.. ''.. ''.. '

', text)

end

local function mergeWithWikidata(q, lat1, lon1) -- we are given wikidata q-code so look up the coordinates local dist_str= local entity -- Wikiata coordinates if q:match( '^[Qq]%d+$' ) then entity = mw.wikibase.getEntity(q) else entity = q end q = entity.id local v, lat2, lon2, precision if entity then local P625 = entity:getBestStatements( 'P625' ) -- coordinate location local P159 = entity:getBestStatements( 'P159' ) -- headquarters location if P625[1] and P625[1].mainsnak.datavalue.value.latitude then v = P625[1].mainsnak.datavalue.value elseif P159[1] and P159[1].qualifiers and P159[1].qualifiers.P625 then v = P159[1].qualifiers.P625[1].datavalue.value end if v and v.globe == 'http://www.wikidata.org/entity/Q2' then lat2 = v.latitude lon2 = v.longitude precision = v.precision or 1e-4 precision = math.floor(precision*111000) -- convert precision from degrees to meters and round precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree end end

-- compare coordinates local cat = if not lat1 or not lon1 then -- wikidata coordinates only lat1 = lat2 lon1 = lon2 cat = CoorCat.wikidata0 elseif lat1 and lon1 and not lat2 and not lon2 then cat = string.format('The above coordinates are missing from linked Wikidata item %s. Click '.. "here to copy it", q, q, q, lat1, lon1) cat = CoorCat.wikidata4 .. info_box(cat) elseif lat1 and lon1 and lat2 and lon2 then -- calculate distance local dLat = math.rad(lat1-lat2) local dLon = math.rad(lon1-lon2) local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2)) d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d)) -- angular distance in radians d = 6371000 * d -- radians to meters conversion d = math.floor(d+0.5) -- rind it to even meters

local frame = mw.getCurrentFrame() local info = frame:preprocess(add_maplink2(lat1, lon1, lat2, lon2)) -- fancy link to OSM info = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at linked Wikidata item %s (%s, precision: %i m). ".. 'Please reconcile them. To copy Commons coordinates to Wikidata, click '.. "here", d, q, q, info, precision, q, lat1, lon1)

if d<20 or d<precision then -- will consider location within 20 meters or precisi0on distance as the same cat = CoorCat.wikidata1 dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', d) -- will be displayed when hovering a mouse above wikidata icon elseif d>1000 and d>5*precision then -- locations 1 km off and 5 precision distances away are likely wrong cat = CoorCat.wikidata3 .. info_box(info) else cat = CoorCat.wikidata2 .. info_box(info) end end

-- verify proper P31 (instance of). List is based on https://www.wikidata.org/wiki/Property_talk:P625 local QCodes = { Q5 = 1, -- human Q11879590 = 1, -- female given name Q202444 = 1, -- given name Q12308941 = 1, -- male given name Q4167836 = 1, -- Wikimedia category Q4167410 = 1, -- Wikimedia disambiguation page Q783794 = 2, -- company Q4830453 = 2, -- business enterprise } local s = entity:getBestStatements( 'P31' ) if s[1] and s[1].mainsnak.datavalue.value['id'] then local instanceOf = s[1].mainsnak.datavalue.value['id'] if QCodes[instanceOf] then cat = -- wipe out categories if QCodes[instanceOf]==1 then -- add problem category cat = CoorCat.wikidata5 end end end

return lat1, lon1, q, cat, dist_str end

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

-- parse attribute variable returning desired field (used for debugging) function p.parseAttribute(frame)

 return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or 

end

-- Helper core function for getHeading. function p._getHeading(attributes) if attributes == nil then return nil end local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)') if hStr == nil then return nil end local hNum = tonumber( hStr ) if hNum == nil then hStr = string.upper (hStr) hNum = compass_points[hStr] end if hNum ~= nil then hNum = hNum%360 end return hNum end

--[[============================================================================ Parse attribute variable returning heading field. If heading is a string than try to convert it to an angle ==============================================================================]]

function p.getHeading(frame) local attributes if frame.args[1] then attributes = frame.args[1] elseif frame.args.attributes then attributes = frame.args.attributes else return end local hNum = p._getHeading(attributes) if hNum == nil then return end return tostring(hNum) end


--[[============================================================================ Helper core function for deg2dms. deg2dms can be called by templates, while _deg2dms should be called from Lua. Inputs:

  • degree - positive coordinate in degrees
  • degPrec - coordinate precision in degrees will result in different angle format
  • lang - language to used when formatting the number

==============================================================================]] function p._deg2dms(degree, degPrec, lang) local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero local Lang = mw.language.new(lang)

-- adjust number display based on precision secPrec = degPrec*3600.0 -- coordinate precision in seconds if secPrec<0.05 then -- degPrec<1.3889e-05 formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.SS″ format c = 360000 elseif secPrec<0.5 then -- 1.3889e-05<degPrec<1.3889e-04 formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.S″ format c = 36000 elseif degPrec*60.0<0.5 then -- 1.3889e-04<degPrec<0.0083 formatStr = '%s° %s′ %s″' -- use DD° MM′ SS″ format c = 3600 elseif degPrec<0.5 then -- 0.0083<degPrec<0.5 formatStr = '%s° %s′' -- use DD° MM′ format c = 60 else -- if degPrec>0.5 then formatStr = '%s°' -- use DD° format c = 1 end

-- create degree, minute and seconds numbers and string d = c/60 k = math.floor(c*(degree%360)+0.49) -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding dNum = math.floor(k/c) % 360 -- degree number (integer in 0-360 range) mNum = math.floor(k/d) % 60 -- minute number (integer in 0-60 range) sNum = 3600*(k%d) / c -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits) dStr = Lang:formatNum(dNum) -- degree string mStr = Lang:formatNum(mNum) -- minute string sStr = Lang:formatNum(sNum) -- second string zero = Lang:formatNum(0) -- zero string in local language if mNum<10 then mStr = zero .. mStr -- pad with zero if a single digit end if sNum<10 then sStr = zero .. sStr -- pad with zero if less than ten end return string.format(formatStr, dStr, mStr, sStr); end

--[[============================================================================ Convert degrees to degrees/minutes/seconds notation commonly used when displaying coordinates. Inputs: 1) latitude or longitude angle in degrees 2) georeference precision in degrees 3) language used in formatting of the number ==============================================================================]] function p.deg2dms(frame) local args = getArgs(frame) local degree = tonumber(args[1]) local degPrec = tonumber(args[2]) or 0-- precision in degrees

if degree==nil then return args[1]; else return p._deg2dms(degree, degPrec, args.lang) end end

--[[============================================================================ Format coordinate location string, by creating and joining DMS strings for latitude and longitude. Also convert precision from meters to degrees. INPUTS:

* lat        = latitude in degrees
* lon        = longitude in degrees
* lang       = language code
* prec       = geolocation precision in meters

==============================================================================]] function p._lat_lon(lat, lon, prec, lang) lat = tonumber(lat) lon = tonumber(lon) prec = math.abs(tonumber(prec) or 0) if lon then -- get longitude to be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end if lat==nil or lon==nil then return NoLatLonString else local nsew = langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language local SN, EW, latStr, lonStr, lon2m, lat2m, phi if lat<0 then SN = nsew.S else SN = nsew.N end -- choose S or N depending on latitude degree sign if lon<0 then EW = nsew.W else EW = nsew.E end -- choose W or E depending on longitude degree sign lat2m=1 lon2m=1 if prec>0 then -- if user specified the precision of the geo location... phi = math.abs(lat)*math.pi/180 -- latitude in radiants lon2m = 6378137*math.cos(phi)*math.pi/180 -- see https://en.wikipedia.org/wiki/Longitude lat2m = 111000 -- average latitude degree size in meters end latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude degrees to degrees/minutes/seconds lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds return string.format('%s %s, %s %s', latStr, SN, lonStr, EW) --return string.format('%s %s, %s %s', latStr, SN, lonStr, EW) end end

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

--[[============================================================================ Helper core function for externalLink. Create URL for different sites: INPUTS:

* site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
               OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
               Ganymede are also supported but are unused as of 2013.
* latStr     = latitude string or number
* lonStr     = longitude string or number
* lang       = language code
* attributes = attributes to be passed to GeoHack

==============================================================================]] function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level) local URLstr = SiteURL[site]; level = level or 1 local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' ) pageName = mw.ustring.gsub( pageName, '%%', '%%%%')

if site == 'GoogleMaps' then URLstr = SiteURL.GoogleMaps[globe] elseif site == 'GeoHack' then attributes = string.format('globe:%s_%s', globe, attributes) URLstr = mw.ustring.gsub( URLstr, '$attr', attributes) end URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr) URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr) URLstr = mw.ustring.gsub( URLstr, '$lang' , lang) URLstr = mw.ustring.gsub( URLstr, '$level', level) URLstr = mw.ustring.gsub( URLstr, '$page' , pageName) URLstr = mw.ustring.gsub( URLstr, '+', ) URLstr = mw.ustring.gsub( URLstr, ' ', '_') return URLstr end

--[[============================================================================ Create URL for different sites. INPUTS:

* site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
               OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
               Ganymede are also supported but are unused as of 2013.
* lat        = latitude string or number
* lon        = longitude string or number
* lang       = language code
* attributes = attributes to be passed to GeoHack

==============================================================================]] function p.externalLink(frame) local args = getArgs(frame) return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or ) end

--[[============================================================================ Adjust GeoHack attributes depending on the template that calls it INPUTS:

* attributes = attributes to be passed to GeoHack
* mode = set by each calling template

==============================================================================]] function p.alterAttributes(attributes, mode) -- indicate which template called it if mode=='camera' then -- Used by Script error: The function "coord" does not exist. and Template:Location dec if string.find(attributes, 'type:camera')==nil then attributes = 'type:camera_' .. attributes end elseif mode=='object'or mode =='globe' then -- Used by Template:Object location if mode=='object' and string.find(attributes, 'type:')==nil then attributes = 'type:object_' .. attributes end if string.find(attributes, 'class:object')==nil then attributes = 'class:object_' .. attributes end elseif mode=='inline' then -- Used by Template:Inline coordinates (actually that template does not set any attributes at the moment) elseif mode=='user' then -- Used by Template:User location attributes = 'type:user_location' elseif mode=='institution' then --Used by Template:Institution/coordinates (categories only) attributes = 'type:institution' end return attributes end

--[[============================================================================

Create link to GeoHack tool which displays latitude and longitude coordinates 
in DMS format
INPUTS:
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
               Ganymede are also supported but are unused as of 2013.
* lat        = latitude in degrees
* lon        = longitude in degrees
* lang       = language code
* prec       = geolocation precision in meters
* attributes = attributes to be passed to GeoHack

==============================================================================]] function p._GeoHack_link(args) -- create link and coordintate string local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang) if latlon==NoLatLonString then return latlon else local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or ) return string.format('[%s %s]', url, latlon) -- end end

function p.GeoHack_link(frame) return p._GeoHack_link(getArgs(frame)) end


--[[============================================================================

Create full external links section of Script error: The function "coord" does not exist. or Template:Object location 
templates, based on:
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
* mode       = Possible options: 
 - camera - call from Script error: The function "coord" does not exist.
 - object - call from Template:Object location
 - globe  - call from Template:Globe location
* lat        = latitude in degrees
* lon        = longitude in degrees
* lang       = language code
* namespace  = namespace name: File, Category, (Gallery)

==============================================================================]] function p._externalLinksSection(args) local lang = args.lang if not args.namespace then args.namespace = mw.title.getCurrentTitle().namespace end

local str, link1, link2, link3, link4 if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, ) link2 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, ) str = string.format('[%s %s] - [%s %s]', link1, langSwitch(i18n.OpenStreetMaps, lang), link2, langSwitch(i18n.GoogleEarth, lang)) elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, , args.catRecurse) --link2 = p._externalLink('GoogleMaps' , 'Earth', args.lat, args.lon, lang, , args.catRecurse) link3 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, ) link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, ) str = string.format('[%s %s] - [%s %s] - [%s %s]', link1, langSwitch(i18n.OpenStreetMaps, lang), --link2, langSwitch(i18n.GoogleMaps, lang), link3, langSwitch(i18n.GoogleEarth, lang), link4, langSwitch(i18n.Proximityrama, lang)) elseif args.globe=='Mars' or args.globe=='Moon' then link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, ) str = string.format('[%s %s]', link1, langSwitch(i18n.GoogleMaps, lang)) end

return str end

function p.externalLinksSection(frame) return p._externalLinksSection(getArgs(frame)) end

--[[============================================================================ Core section of template:Location, template:Object location and template:Globe location. This method requires several arguments to be passed to it or it's parent method/template:

* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
* mode       = Possible options: 
 - camera - call from Script error: The function "coord" does not exist.
 - object - call from Template:Object location
 - globe  - call from Template:Globe location
* lat        = latitude in degrees
* lon        = longitude in degrees
* attributes = attributes
* lang       = language code
* namespace  = namespace: File, Category, Gallery
* prec       = geolocation precision in meters

==============================================================================]] function p._LocationTemplateCore(args) -- prepare arguments if not (args.namespace) then -- if namespace not provided than look it up args.namespace = mw.title.getCurrentTitle().namespace end if args.namespace== then -- if empty than it is a gallery args.namespace = 'Gallery' end local bare = yesno(args.bare,false) local Status = 'primary' -- used by {{#coordinates:}} if yesno(args.secondary,false) then Status = 'secondary' end local attributes0 = args.attributes args.attributes = p.alterAttributes(args.attributes or , args.mode)

-- Convert coordinates from string to numbers local lat = tonumber(args.lat) local lon = tonumber(args.lon) if lon then -- get longitude to be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end

-- If wikidata link provided than compare coordinates local Categories, geoMicroFormat, coorTag, wikidata_link = , , , if args.wikidata and args.wikidata~= then local dist_str, q -- if lat/lon is not provided but we are given wikidata q-code than look up the coordinates lat, lon, q, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon) wikidata_link = string.format("\nEdit coordinates on Wikidata%s", dist_str, q); end

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

local frame = mw.getCurrentFrame()

-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then if lat and lon then -- if lat and lon are numbers... if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources Categories = Categories .. CoorCat.default end if attributes0 and string.find(attributes0, '=') then Categories = Categories .. CoorCat.attribute end if args.error=='1' or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors ) Categories = Categories .. CoorCat.erroneous end local cat = CoorCat[args.namespace] if cat then -- add category based on namespace Categories = Categories .. cat end -- if not earth than add a category for each globe if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then Categories = Categories .. string.format(CoorCat[args.mode], args.globe) end -- add Geo (microformat) code: it is included for machine readability geoMicroFormat = string.format('',lat, lon) -- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } ) elseif args.namespace == 'File' and args.mode=='object' then coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } ) end else -- if lat and lon are not numbers then add error category Categories = Categories .. CoorCat.erroneous end end

-- Call helper functions to render different parts of the template local coor, info_link, inner_table, heading, OSM = ,,,,,, coor = p._GeoHack_link(args) -- the p and link to GeoHack heading = p._getHeading(attributes0) -- get heading arrow section if heading then local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1 local fname = heading_icon[k] coor = string.format('%s  25px|link=|alt=Heading=%s°', coor, heading, fname, heading) end if args.globe=='Earth' then local icon = 'marker' if args.mode=='camera' then icon = 'camera' end OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, 'Kartographer map based on OpenStreetMap.')) -- fancy link to OSM end local external_link = p._externalLinksSection(args) -- external link section if external_link and args.namespace == 'File' then external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for Script error: The function "coord" does not exist. template elseif external_link then external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for Template:Object location template end info_link = string.format('18x18px', langSwitch(i18n.COM_GEO, args.lang) )

inner_table = string.format('%s %s%s%s%s%s',

coor, OSM, external_link or , info_link, wikidata_link, geoMicroFormat)

-- combine strings into a table local templateText if bare then

templateText = string.format('

%s

', inner_table)

else -- choose name of the field local field_name = 'Location' if args.mode=='camera' then field_name = langSwitch(i18n.CameraLocation, args.lang) elseif args.mode=='object' then field_name = langSwitch(i18n.ObjectLocation, args.lang) elseif args.mode=='globe' then local field_list = langSwitch(i18n.GlobeLocation, args.lang) if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized field_name = field_list[args.globe] end end --Create HTML text local dir, text_align if mw.language.new( args.lang ):isRTL() then dir = 'rtl' text_align = 'right' else dir = 'ltr' text_align = 'left' end local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"', args.lang, dir, text_align, args.lang)

templateText = string.format('

%s
%s

', args.lang, style, field_name, inner_table)

end return templateText, Categories, coorTag end

function p.LocationTemplateCore(frame) local args = frame.args if not args or not args.lat then -- if no arguments provided than use parent arguments args = mw.getCurrentFrame():getParent().args end if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then args.lang = frame:callParserFunction("int","lang") -- get user's chosen language end local templateText, Categories, coorTag = p._LocationTemplateCore(args) return templateText .. Categories .. coorTag end

return p