「モジュール:Coordinates」の版間の差分

提供:脳科学辞典
ナビゲーションに移動 検索に移動
(1版)
 
bsd>Jarekt
(support of SDC)
1行目: 1行目:
--[[
--[[
  __  __          _      _        ____                    _ _            _           
|  \/  | ___  __| |_  _| | ___ _ / ___|___  ___  _ __ __| (_)_ __  __ _| |_ ___  ___
| |\/| |/ _ \ / _` | | | | |/ _ (_) |  / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
| |  | | (_) | (_| | |_| | |  __/_| |__| (_) | (_) | | | (_| | | | | | (_| | ||  __/\__ \
|_|  |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_|  \__,_|_|_| |_|\__,_|\__\___||___/
                                                                                         


This module is intended to (eventually) replace some or all functionality of {{location}} and related
This module is intended to provide functionality of {{location}} and related
templates. At the moment it is collection of methods related to geolocation.  
templates. It was developed on Wikimedia Commons, so if you find this code on
other sites, check there for updates and discussions.


*function coordinates.LocationTemplateCore(frame)
Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing
**function coordinates.GeoHack_link(frame)
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
***function coordinates.lat_lon(frame)
 
****function coordinates._deg2dms(deg,lang)
Authors and maintainers:
***function coordinates.externalLink(frame)
* User:Jarekt
****function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
* User:Ebraminio
**function coordinates._getHeading(attributes)
 
**function coordinates.externalLinksSection(frame)
Functions:
***function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
*function p.LocationTemplateCore(frame)
*function coordinates.getHeading(frame)   
**function p.GeoHack_link(frame)
*function coordinates.deg2dms(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)


]]
]]
coordinates = {};


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


-- =======================================
-- =======================================
31行目: 44行目:
-- =======================================
-- =======================================


-- Angles associated with each abriviation of compass point names. See [[:en:Points of the compass]]
-- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
local compass_points = {
local compass_points = {
   N    = 0,
   N    = 0,
67行目: 80行目:
}
}


-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr will be replaced with latitude, longitude, language code and GeoHack attribution parameters strings.
-- 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 = {
local SiteURL = {
GeoHack        = 'http://tools.wmflabs.org/geohack/geohack.php?pagename={{FULLPAGENAMEE}}&params=$lat_N_$lon_E_$attr&language=$lang',
GeoHack        = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page&params=$lat_N_$lon_E_$attr&language=$lang',
GoogleEarth    = '{{fullurl:tools:~para/GeoCommons/earth.php|latdegdec=$lat&londegdec=$lon&scale=10000&commons=1}}',
GoogleEarth    = '//tools.wmflabs.org/geocommons/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
Proximityrama  = '{{fullurl:tools:~para/GeoCommons/proximityrama|latlon=$lat,$lon}}',
Proximityrama  = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
OpenStreetMap  = '{{fullurl:tools:~kolossos/openlayers/commons-on-osm.php|zoom=16&lat=$lat&lon=$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 = {  
GoogleMaps = {  
Mars  = 'http://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
Mars  = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
Moon  = 'http://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
Moon  = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
Earth = 'http://maps.google.com/maps?ll=$lat,$lon&spn=0.01,0.01&t=k&q=http://toolserver.org/~para/GeoCommons/GeoCommons-simple.kml&hl=$lang'
Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic'
}
}
}
}
85行目: 137行目:
Gallery      = '[[Category:Galleries with coordinates]]',
Gallery      = '[[Category:Galleries with coordinates]]',
Category      = '[[Category:Categories with coordinates]]',
Category      = '[[Category:Categories with coordinates]]',
wikidata0    = '[[Category:Pages with coordinates from Wikidata]]',
wikidata1    = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]',
wikidata2    = '[[Category:Pages with local coordinates and similar Wikidata coordinates]]',
wikidata3    = '[[Category:Pages with local coordinates and mismatching Wikidata coordinates]]',
wikidata4    = '[[Category:Pages with local coordinates and missing Wikidata coordinates]]',
wikidata5    = '[[Category:Pages with locations and Wikidata ID to wrong type of entry]]',
sdc0          = '[[Category:Pages with coordinates from SDC]]',
sdc1          = '[[Category:Pages with local coordinates and matching SDC coordinates]]',
sdc2          = '[[Category:Pages with local coordinates and similar SDC coordinates]]',
sdc3          = '[[Category:Pages with local coordinates and mismatching SDC coordinates]]',
sdc4          = '[[Category:Pages with local coordinates and missing SDC coordinates]]',
globe        = '[[Category:Media with %s locations]]',
globe        = '[[Category:Media with %s locations]]',
default      = '[[Category:Media with default locations]]',
default      = '[[Category:Media with default locations]]',
erroneous    = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n'
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',
dms          = '[[Category:Media with coordinates in DMS format]]'
}
}


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


-- parse attribute variable returning desired field
local function normalize_input_args(input_args, output_args)
function coordinates.parseAttribute(frame)
for name, value in pairs( input_args ) do
  return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
if value ~= '' then -- nuke empty strings
if type(name)=='string' then
name = string.lower(name)
end
output_args[name] = string.gsub(value, "^%s*(.-)%s*$", "%1") -- trim whitespaces from the beggining and the end of the string
end
end
return output_args
end
 
local function getArgs(frame)
local args = {}
args = normalize_input_args(frame:getParent().args, args)
args = normalize_input_args(frame.args, args)
if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then
args.lang = frame:callParserFunction("int","lang")  -- get user's chosen language
end
return args
end
 
local NoLatLonString = 'latitude, longitude'
 
local function langSwitch(list,lang)
local langList = mw.language.getFallbacksFor(lang)
table.insert(langList,1,lang)
for i,language in ipairs(langList) do
if list[language] then
return list[language]
end
end
end
 
local function add_maplink(lat, lon, marker, text)
local tstr = ''
if text then
tstr = string.format('text="%s" ', text)
end
return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'..
'  "type": "Feature",'..
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
'  "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0"  }'..
'}</maplink>', tstr, lat, lon, lon, lat, marker)
end
 
local function add_maplink2(lat1, lon1, lat2, lon2)
return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
'  "type": "Feature",'..
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
'  "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons"  }'..
'},{'..
'  "type": "Feature",'..
'  "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
'  "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata"  }'..
'}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2)
end
 
local function info_box(text)
return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '..
'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'..
'<tr>'..
'<td class="mbox-image" style="padding-left:.9em;">'..
' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
'<td class="mbox-text" style="">%s</td>'..
'</tr></table>', text)
end
 
local function distance(lat1, lon1, lat2, lon2)
-- calculate distance
local dLat = math.rad(lat1-lat2)
local dLon = math.rad(lon1-lon2)
local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d))  -- angular distance in radians
d = 6371000 * d      -- radians to meters conversion
d = math.floor(d+0.5) -- round it to even meters
return d
end
end


-- Parse attribute variable returning heading field. If heading is a string than try to convert it to an angle
local function mergeWithWikidata(qID, lat1, lon1)
function coordinates.getHeading(frame) 
-- we are given wikidata q-code so look up the coordinates
local attributes
local dist_str=''
if frame.args[1] then
local entity
attributes = frame.args[1]
-- Wikiata coordinates
elseif frame.args.attributes then
if qID==nil then
attributes = frame.args.attributes
entity = mw.wikibase.getEntity()
elseif type(qID)=='string' and qID:match( '^[Qq]%d+$' ) then
entity = mw.wikibase.getEntity(qID)
else
else
return ''
entity = qID
end
if not entity then
return lat1, lon1, nil, '', dist_str
end
qID = entity.id
local v, lat2, lon2, precision
if entity then
local P625 = entity:getBestStatements( 'P625' ) --  coordinate location
local P159 = entity:getBestStatements( 'P159' ) --  headquarters location
if P625[1] and P625[1].mainsnak.datavalue.value.latitude then
v = P625[1].mainsnak.datavalue.value
elseif P159[1] and P159[1].qualifiers and P159[1].qualifiers.P625 then
v = P159[1].qualifiers.P625[1].datavalue.value
end
if v and v.globe == 'http://www.wikidata.org/entity/Q2' then
lat2 = v.latitude
lon2 = v.longitude
precision = v.precision or 1e-4
precision = math.floor(precision*111000) -- convert precision from degrees to meters and round
precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree
end
end
-- compare coordinates
local cat = ''
if not lat1 or not lon1 then -- wikidata coordinates only
lat1 = lat2
lon1 = lon2
cat = CoorCat.wikidata0
elseif lat1 and lon1 and not lat2 and not lon2 then
cat = string.format('The above coordinates are missing from linked Wikidata item [[d:%s|%s]].  Click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span> to copy it",
qID, qID, qID, lat1, lon1)
cat = CoorCat.wikidata4 .. info_box(cat)
elseif lat1 and lon1 and lat2 and lon2 then
local d = distance(lat1, lon1, lat2, lon2) -- calculate distance
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, qID, qID, info, precision, qID, 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, qID, cat, dist_str
end 
 
local function mergeWithSDC(lat1, lon1, heading1)
-- we are given SDC m-code so look up the coordinates
local entity  = nil
-- Wikiata coordinates
entity = mw.wikibase.getEntity()
 
if not entity and lat1 and lon1 then
return lat1, lon1, heading1, nil, CoorCat.sdc4
elseif not entity then
return lat1, lon1, heading1, nil, ''
end
 
local v, lat2, lon2, heading2, precision
if entity then
local P1259 = entity:getBestStatements( 'P1259' ) --  coordinates of the point of view
if P1259[1] and P1259[1].mainsnak.datavalue.value.latitude then
if P1259[1].qualifiers and P1259[1].qualifiers.P7787 then
v = P1259[1].qualifiers.P7787[1].datavalue.value
if v.unit == "http://www.wikidata.org/entity/Q28390" then    -- in degrees
heading2 = v.amount
elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians
heading2 = v.amount*57.2957795131
end
end
v = P1259[1].mainsnak.datavalue.value -- get coordinates
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 -- SDC coordinates only
lat1 = lat2
lon1 = lon2
heading1 = heading2
cat = CoorCat.sdc0
elseif lat1 and lon1 and not lat2 and not lon2 then
cat = CoorCat.sdc4
elseif lat1 and lon1 and lat2 and lon2 then
local d = distance(lat1, lon1, lat2, lon2) -- calculate distance
if d<20 or d<precision then -- will consider location within 20 meters or precision distance as the same
cat = CoorCat.sdc1
else
cat = CoorCat.sdc2
end
end
return lat1, lon1, heading1, entity.id, cat
end
 
 
local function dms2deg_ ( d, m, s, h )
  d,m,s = tonumber(d), tonumber(m), tonumber(s)
  if not (d and m and s and h) then
return nil
end
end
local hNum = coordinates._getHeading(attributes)
local LUT = {N=1, S=-1, E=1, W=-1} -- look up table
if hNum == nil then
h = LUT[mw.ustring.upper( h )]
return ''
if not h then
return nil
end
end
return tostring(hNum)
return h * (d + m/60.0 + s/3600.0)
end
 
local function dms2deg ( dms )
  local ltab  = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/")
  local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4])
--return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "")  .. '->' .. (degre or 'nil')
return degre or dms
end
 
-- =======================================
-- === External Functions ================
-- =======================================
local p = {}
 
-- 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 core function for getHeading.  
function coordinates._getHeading(attributes)
function p._getHeading(attributes)
if attributes == nil then
if attributes == nil then
return nil
return nil
135行目: 434行目:
end
end


-- Convert degrees to degrees/minutes/seconds notation comonly used when displaying coordinates
--[[============================================================================
function coordinates.deg2dms(frame)
Parse attribute variable returning heading field. If heading is a string than
local deg = tonumber(frame.args[1])
try to convert it to an angle
local lang
==============================================================================]]
if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then  
 
lang = frame.args.lang
function p.getHeading(frame)
else -- get user's chosen language
local attributes
lang = frame:preprocess( "{{int:lang}}" )
if frame.args[1] then
attributes = frame.args[1]
elseif frame.args.attributes then
attributes = frame.args.attributes
else
return ''
end
end
if deg==nil then
local hNum = p._getHeading(attributes)
return frame.args[1];
if hNum == nil then
else
return ''
return coordinates._deg2dms(deg,lang)
end
end
return tostring(hNum)
end
end
-- Helper core function for deg2dms.  
 
function coordinates._deg2dms(deg,lang)
 
local dNum, mNum, sNum, dStr, mStr, sStr
--[[============================================================================
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)
local Lang = mw.language.new(lang)
deg = math.floor(360000*(deg%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(deg/360000) % 360          -- degree number (integer in 0-360 range)
-- adjust number display based on precision
mNum = math.floor(deg/6000  ) %  60          -- minute number (integer in 0-60 range)
secPrec = degPrec*3600.0                    -- coordinate precision in seconds
sNum =           (deg%6000  ) / 100          -- seconds number (float with 2 decimal digits in 0-60 range)
if secPrec<0.05 then                        -- degPrec<1.3889e-05
dStr = Lang:formatNum(dNum)                 -- degree string  
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
mStr = Lang:formatNum(mNum)                 -- minute string  
c = 360000
sStr = Lang:formatNum(sNum)                 -- second string  
elseif secPrec<0.5 then                      -- 1.3889e-05<degPrec<1.3889e-04
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
c = 36000
elseif degPrec*60.0<0.5 then                -- 1.3889e-04<degPrec<0.0083
formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
c = 3600
elseif degPrec<0.5 then                      -- 0.0083<degPrec<0.5
formatStr = '%s°&nbsp;%s′'              -- use DD° MM′ format
c = 60
else -- if degPrec>0.5 then                 
formatStr = '%s°'                        -- use DD° format
c = 1
end
-- create degree, minute and seconds numbers and string
d = c/60
k = math.floor(c*(degree%360)+0.49) -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding
dNum = math.floor(k/c) % 360          -- degree number (integer in 0-360 range)
mNum = math.floor(k/d) %  60          -- minute number (integer in 0-60 range)
sNum =     3600*(k%d) / c            -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
dStr = Lang:formatNum(dNum)           -- degree string  
mStr = Lang:formatNum(mNum)           -- minute string  
sStr = Lang:formatNum(sNum)           -- second string  
zero = Lang:formatNum(0)              -- zero string in local language
if mNum<10 then
if mNum<10 then
mStr = '0' ..mStr                       -- pad with zero if a single digit
mStr = zero .. mStr                 -- pad with zero if a single digit
end
end
if sNum<10 then
if sNum<10 then
sStr = '0' ..sStr                       -- pad with zero if less than ten
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
str = string.format('%s°&nbsp;%s′&nbsp;%s″', dStr, mStr, sStr);
return str
end
end


-- format coordinate location string  
function p.dms2deg(frame)
function coordinates.lat_lon(frame)
return dms2deg(frame.args[1])
local lat = tonumber(frame.args.lat)
end
local lon = tonumber(frame.args.lon)
 
if lon then -- get longitude t0 be in -180 to 180 range
--[[============================================================================
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
lon=lon%360
if lon>180 then
if lon>180 then
lon = lon-360
lon = lon-360
end
end
end
local lang
if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then
lang = frame.args.lang
else -- get user's chosen language
lang = frame:preprocess( "{{int:lang}}" )
end
end
if lat==nil or lon==nil then
if lat==nil or lon==nil then
return 'latitude, longitude'
return NoLatLonString
else
else
local nsew = Fallback._langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language  
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
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 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
if lon<0 then EW = nsew.W else EW = nsew.E end             -- choose W or E depending on longitude degree sign
latStr = coordinates._deg2dms(math.abs(lat), lang)     -- Convert latitude  degrees to degrees/minutes/seconds
lat2m=1
lonStr = coordinates._deg2dms(math.abs(lon), lang)     -- Convert longitude degrees to degrees/minutes/seconds
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('%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)
--return string.format('<span class="latitude">%s %s</span>, <span class="longitude">%s %s</span>', latStr, SN, lonStr, EW)
201行目: 570行目:
end
end


-- Create URL for different sites based on globe (planet), latitude, longitude, language code and GeoHack attribution parameters
function p.lat_lon(frame)
function coordinates.externalLink(frame)
local args = getArgs(frame)
args = frame.args
return p._lat_lon(args.lat, args.lon, args.prec, args.lang)
if args.lang and mw.language.isSupportedLanguage(args.lang) then
lang = args.lang
else -- get user's chosen language
lang = frame:preprocess( "{{int:lang}}" )
end
return coordinates._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, lang, args.attributes or '')
end
end
-- Helper core function for externalLink  
 
function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes)
--[[============================================================================
local str
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
if site == 'GoogleMaps' then
str = SiteURL.GoogleMaps[globe]
URLstr = SiteURL.GoogleMaps[globe]
else
elseif site == 'GeoHack' then
str = SiteURL[site];
attributes = string.format('globe:%s_%s', globe, attributes)
attributes = string.format('globe:%s_%s', globe, attributes)
str = mw.ustring.gsub( str, '$attr', attributes)
URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
end
end
str = mw.ustring.gsub( str, '$lat', latStr)
URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr)
str = mw.ustring.gsub( str, '$lon', lonStr)
URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr)
str = mw.ustring.gsub( str, '$lang', lang)
URLstr = mw.ustring.gsub( URLstr, '$lang' , lang)
return str
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
end


-- adjust attributes depending on the template that calls it
--[[============================================================================
function coordinates.alterAttributes(attributes, mode)
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
-- indicate which template called it
if mode=='camera' then                                  -- Used by {{Location}} and {{Location dec}}
if mode=='camera' then                                  -- Used by {{Location}} and {{Location dec}}
234行目: 638行目:
attributes = 'type:camera_' .. attributes
attributes = 'type:camera_' .. attributes
end
end
elseif mode=='object'or mode =='globe' then                           -- Used by {{Object location}}
elseif mode=='object'or mode =='globe' then             -- Used by {{Object location}}
if mode=='object' and string.find(attributes, 'type:')==nil then
attributes = 'type:object_' .. attributes
end
if string.find(attributes, 'class:object')==nil then
if string.find(attributes, 'class:object')==nil then
attributes = 'class:object_' .. attributes
attributes = 'class:object_' .. attributes
247行目: 654行目:
end
end
-- Create link to GeoHack tool which displays latitude and longitude coordinates in DMS format
--[[============================================================================
function coordinates.GeoHack_link(frame)
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
-- create link and coordintate string
local latlon = coordinates.lat_lon(frame)
local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
if latlon=='lattiude, longitude' then
if latlon==NoLatLonString then
return latlon
return latlon
else
else
frame.args.site = 'GeoHack'
local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
local url = frame:preprocess(coordinates.externalLink(frame)) -- use preprocess to get page name
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
end
end
end
end


function coordinates.externalLinksSection(frame)
function p.GeoHack_link(frame)
args = frame.args
return p._GeoHack_link(getArgs(frame))
if args.lang and mw.language.isSupportedLanguage(args.lang) then
end
lang = args.lang
 
else -- get user's chosen language  
 
lang = frame:preprocess( "{{int:lang}}" )
--[[============================================================================
end
Create full external links section of {{Location}} or {{Object location}}
if not args.namespaceNum then
templates, based on:
args.namespace = frame:preprocess( "{{NAMESPACE}}" )
* globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
* mode      = Possible options:
  - camera - call from {{location}}
  - object - call from {{Object location}}
  - globe  - call from {{Globe location}}
* lat        = latitude in degrees
* lon        = longitude in degrees
* lang      = language code
* namespace  = namespace name: File, Category, (Gallery)
==============================================================================]]
function p._externalLinksSection(args)
local lang = args.lang
if not args.namespace then
args.namespace = mw.title.getCurrentTitle().nsText
end
end
local str
local str, link1, link2, link3, link4
if args.globe=='Earth' then -- Earth locations will have 3 or 4 links
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]',  
str = string.format('[%s %s] - [%s %s] - [%s %s]',  
coordinates._externalLink('OpenStreetMap', 'Earth', args.lat, args.lon, lang, ''),
link1, langSwitch(i18n.OpenStreetMaps, lang),
Fallback._langSwitch(i18n.OpenStreetMaps, lang),
--link2, langSwitch(i18n.GoogleMaps, lang),
coordinates._externalLink('GoogleMaps'  , 'Earth', args.lat, args.lon, lang, ''),
link3, langSwitch(i18n.GoogleEarth, lang),
Fallback._langSwitch(i18n.GoogleMaps, lang),
link4, langSwitch(i18n.Proximityrama, lang))
coordinates._externalLink('GoogleEarth'  , 'Earth', args.lat, args.lon, lang, ''), 
Fallback._langSwitch(i18n.GoogleEarth, lang))
if args.namespace=="Category" then
str = string.format('%s - [%s %s]', str,
coordinates._externalLink('Proximityrama', 'Earth', args.lat, args.lon, lang, ''), 
Fallback._langSwitch(i18n.Proximityrama, lang))
end
elseif args.globe=='Mars' or args.globe=='Moon' then
elseif args.globe=='Mars' or args.globe=='Moon' then
str = string.format('[%s %s]',
link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
coordinates._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
str = string.format('[%s %s]', link1, langSwitch(i18n.GoogleMaps, lang))
Fallback._langSwitch(i18n.GoogleMaps, lang))
end
end
--return frame:preprocess(str) -- use preprocess to expand {{#fullurl}}
return str
return str
end
end


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


--[[============================================================================
Core section of template:Location, template:Object location and template:Globe location.
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 metchod/template:
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.
  * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
  * mode      = Possible options:  
  * mode      = Possible options:  
308行目: 742行目:
  * attributes = attributes
  * attributes = attributes
  * lang      = language code
  * lang      = language code
  * namespace  = namespace name: File, Category, (Gallery)
  * namespace  = namespace: File, Category, Gallery
]]
* prec      = geolocation precision in meters
function coordinates.LocationTemplateCore(frame)
==============================================================================]]
function p._LocationTemplateCore(args)
-- prepare arguments
-- prepare arguments
args = frame.args
if not (args.namespace) then -- if namespace not provided than look it up
if not args or not args.lat then -- if no arguments provided than use parent arguments
args.namespace = mw.title.getCurrentTitle().nsText
args = mw.getCurrentFrame():getParent().args
end
if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then  
args.lang = frame:preprocess( "{{int:lang}}" ) -- get user's chosen language
end
if not (args.namespace) then -- if not provided than look up
args.namespace = frame:preprocess( "{{NAMESPACE}}" )
end
end
if args.namespace=='' then -- if empty than it is a gallery
if args.namespace=='' then -- if empty than it is a gallery
330行目: 758行目:
Status = 'secondary'
Status = 'secondary'
end
end
    args.attributes = coordinates.alterAttributes(args.attributes or '', args.mode)
args.globe = args.globe or 'Earth'
frame.args = args
local attributes0 = args.attributes
args.attributes = p.alterAttributes(args.attributes or '', args.mode)
-- check for errors and add Geo (microformat) code for machine readability.
-- Convert coordinates from string to numbers
local lat = tonumber(args.lat)
local lat = tonumber(args.lat)
local lon = tonumber(args.lon)
local lon = tonumber(args.lon)
if lon then -- get longitude t0 be in -180 to 180 range
local heading = p._getHeading(attributes0) -- get heading arrow section
if lon then -- get longitude to be in -180 to 180 range
lon=lon%360
lon=lon%360
if lon>180 then
if lon>180 then
342行目: 772行目:
end
end
end
end
local Categories, geoMicroFormat, coorTag = '', '', ''
-- If wikidata link provided than compare coordinates
local Categories, geoMicroFormat, coorTag, wikidata_link = '', '', '', ''
if (args.mode=='object') and (args.namespace~='File') then
local dist_str, qID
  -- look up the coordinates on Wikidata
lat, lon, qID, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon)
if qID then
wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, qID);
args.wikidata = args.wikidata or qID
end
elseif (args.mode=='camera') and (args.namespace=='File') then
local dist_str, mID
  -- look up lat/lon on SDC
lat, lon, heading, mID, Categories = mergeWithSDC(lat, lon, heading)
if mID then
wikidata_link = "\n[[File:Commons structured data logo.svg|16px|Edit coordinates on Structured Data on Commons|link=]]";
end
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
-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
348行目: 800行目:
if lat and lon then -- if lat and lon are numbers...
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
if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources
Categories = CoorCat.default
Categories = Categories .. CoorCat.default
end
if attributes0 and string.find(attributes0, '=') then
Categories = Categories .. CoorCat.attribute
end
end
if args.noError==0 or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
if (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
Categories = Categories .. CoorCat.erroneous
Categories = Categories .. CoorCat.erroneous
end
end
363行目: 818行目:
-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
-- https://www.mediawiki.org/wiki/Extension:GeoData
-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
if args.namespace == 'File' and Status ~= 'secondary' then -- TODO enable for secondary cases without throwing errors
if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then  
--coorTag = string.format('{{#coordinates:%f|%f|%s|%s}}', frame.args.lat, frame.args.lon, args.attributes, Status)
coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } )
coorTag = string.format('{{#coordinates:%10.6f|%11.6f|%s}}', lat, lon, args.attributes)
elseif args.namespace == 'File' and args.mode=='object' then
coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } )
end
end
else -- if lat and lon are not numbers then add error category
else -- if lat and lon are not numbers then add error category
Categories = Categories .. CoorCat.erroneous
Categories = Categories .. CoorCat.erroneous
end
end
end
end


-- Call helper functions to render different parts of the template
-- Call helper functions to render different parts of the template
local str1, str2, str3, str4, inner_table, heading
local coor, info_link, inner_table, OSM = '','','','','',''
str1 = coordinates.GeoHack_link(frame) -- the coordinates and link to GeoHack
coor = p._GeoHack_link(args) -- the p and link to GeoHack
heading = coordinates._getHeading(frame.args.attributes) -- get heading arrow section
if heading then 
if heading then
local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
--str1 = string.format('%s&nbsp;&nbsp;<span style="{{Transform-rotate|%f}}">[[File:North Pointer.svg|20px|link=|alt=]]</span>', str1, 360-heading)
local fname = heading_icon[k]
local fname = string.format('{{Compass rose file|%f|style=heading}}', heading)
coor = string.format('%s&nbsp;&nbsp;<span title="%">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading)
str1 =  string.format('%s&nbsp;&nbsp;[[%s|25px|link=|alt=]]', str1, fname, heading)
end
end
str2 = Fallback._langSwitch(i18n.LocationTemplateLinkLabel, args.lang) -- header of the link section
if args.globe=='Earth' then
str3 = coordinates.externalLinksSection(frame) or '' -- external link section
local icon = 'marker'
str4 = '[[File:Circle-information.svg|18x18px|alt=info|link=Commons:Geocoding]]'
if args.mode=='camera' then
inner_table = string.format('<td style="border:none;">%s</td><td style="border:none;">%s %s</td><td style="border:none;">%s%s</td>', str1, str2, str3, str4, geoMicroFormat)
icon = 'camera'
end
OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM
end
local external_link = p._externalLinksSection(args) -- external link section
if external_link and args.namespace == 'File' then
external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template
elseif external_link then
external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template
end
info_link  = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', langSwitch(i18n.COM_GEO, args.lang) )
inner_table = string.format('<td style="border:none;">%s&nbsp;%s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>',  
coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat)
-- combine strings into a table
-- combine strings into a table
396行目: 862行目:
local field_name = 'Location'
local field_name = 'Location'
if args.mode=='camera' then  
if args.mode=='camera' then  
field_name = Fallback._langSwitch(i18n.CameraLocation, args.lang)
field_name = langSwitch(i18n.CameraLocation, args.lang)
elseif args.mode=='object' then  
elseif args.mode=='object' then  
field_name = Fallback._langSwitch(i18n.ObjectLocation, args.lang)
field_name = langSwitch(i18n.ObjectLocation, args.lang)
elseif args.mode=='globe' then
elseif args.mode=='globe' then
field_list = Fallback._langSwitch(i18n.GlobeLocation, args.lang)
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
if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
field_name = field_list[args.globe]
field_name = field_list[args.globe]
end
end
end
end
local style = frame:preprocess(string.format('{{Infobar-Layout|lang=%s}}',lang))
--Create HTML text
templateText  = string.format('<table %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', style, field_name, inner_table)
local dir, text_align
if mw.language.new( args.lang ):isRTL() then
dir = 'rtl'
text_align = 'right'
else
dir = 'ltr'
text_align = 'left'
end
local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"',  
args.lang, dir, text_align, args.lang)
templateText  = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', args.lang, style, field_name, inner_table)
end
return templateText, Categories, coorTag
end
 
function p.LocationTemplateCore(frame)
local args = getArgs(frame)
args.namespace = mw.title.getCurrentTitle().nsText
if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
if args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W"
args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] )
args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
args.attributes = args.attributes or args[9]
elseif args[2] and not (type(args[2])=='string' and args[2]:find(":")) then -- decimal format or DMS with one pipe, ex. "34° 05′ 32.36″ N| 116° 09′ 24.55″ W"
args.lat = args[1]
args.lon = args[2]
args.attributes = args.attributes or args[3]
elseif args[1] then -- detect a single argument in the form "34° 05′ 32.36″ N, 116° 09′ 24.55″ W" or similar
local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter
args.lat, args.lon = v[1], v[2]
args.attributes = args.attributes or args[2]
end
end
local cat = ''
if args.lat and args.lon then
local lat = tonumber(args.lat)
local lon = tonumber(args.lon)
if not lat or not lon then
args.lat = dms2deg(args.lat or '')
args.lon = dms2deg(args.lon or '')
if (args.namespace == 'File' or args.namespace == 'Category') then
cat = CoorCat.dms
end
end
end
end
return frame:preprocess(templateText .. Categories .. coorTag)
local templateText, Categories, coorTag = p._LocationTemplateCore(args)
return templateText .. Categories .. cat .. coorTag
end
end


return coordinates
return p

2020年1月17日 (金) 13:29時点における版

このモジュールについての説明文ページを モジュール:Coordinates/doc に作成できます

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

This module is intended to provide functionality of {{location}} and related
templates. It was developed on Wikimedia Commons, so if you find this code on
other sites, check there for updates and discussions.

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

Authors and maintainers:
* User:Jarekt
* User:Ebraminio

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

]]

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

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

-- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
local compass_points = {
  N    = 0,
  NBE  = 11.25,
  NNE  = 22.5,
  NEBN = 33.75,
  NE   = 45,
  NEBE = 56.25,
  ENE  = 67.5,
  EBN  = 78.75,
  E    = 90,
  EBS  = 101.25,
  ESE  = 112.5,
  SEBE = 123.75,
  SE   = 135,
  SEBS = 146.25,
  SSE  = 157.5,
  SBE  = 168.75,
  S    = 180,
  SBW  = 191.25,
  SSW  = 202.5,
  SWBS = 213.75,
  SW   = 225,
  SWBW = 236.25,
  WSW  = 247.5,
  WBS  = 258.75,
  W    = 270,
  WBN  = 281.25,
  WNW  = 292.5,
  NWBW = 303.75,
  NW   = 315,
  NWBN = 326.25,
  NNW  = 337.5,
  NBW  = 348.75,
}

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

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

-- Categories
local CoorCat = {
	File          = '[[Category:Media with locations]]',
	Gallery       = '[[Category:Galleries with coordinates]]',
	Category      = '[[Category:Categories with coordinates]]',
	wikidata0     = '[[Category:Pages with coordinates from Wikidata]]',
	wikidata1     = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]',
	wikidata2     = '[[Category:Pages with local coordinates and similar Wikidata coordinates]]',
	wikidata3     = '[[Category:Pages with local coordinates and mismatching Wikidata coordinates]]',
	wikidata4     = '[[Category:Pages with local coordinates and missing Wikidata coordinates]]',
	wikidata5     = '[[Category:Pages with locations and Wikidata ID to wrong type of entry]]',
	sdc0          = '[[Category:Pages with coordinates from SDC]]',
	sdc1          = '[[Category:Pages with local coordinates and matching SDC coordinates]]',
	sdc2          = '[[Category:Pages with local coordinates and similar SDC coordinates]]',
	sdc3          = '[[Category:Pages with local coordinates and mismatching SDC coordinates]]',
	sdc4          = '[[Category:Pages with local coordinates and missing SDC coordinates]]',
	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',
	dms           = '[[Category:Media with coordinates in DMS format]]'
}

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

local function normalize_input_args(input_args, output_args)
	for name, value in pairs( input_args ) do 
		if value ~= '' then -- nuke empty strings
			if type(name)=='string' then 
				name = string.lower(name)
			end
			output_args[name] = string.gsub(value, "^%s*(.-)%s*$", "%1") -- trim whitespaces from the beggining and the end of the string
		end
	end
	return output_args
end

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

local NoLatLonString = 'latitude, longitude'

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

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

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

local function info_box(text)
	return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '..
		'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'..
		'<tr>'..
		'<td class="mbox-image" style="padding-left:.9em;">'..
		' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
		'<td class="mbox-text" style="">%s</td>'..
		'</tr></table>', text)
end

local function distance(lat1, lon1, lat2, lon2)
	-- calculate distance
	local dLat = math.rad(lat1-lat2)
	local dLon = math.rad(lon1-lon2)
	local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
	d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d))  -- angular distance in radians
	d = 6371000 * d       -- radians to meters conversion
	d = math.floor(d+0.5) -- round it to even meters
	return d
end

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

local function mergeWithSDC(lat1, lon1, heading1)
	-- we are given SDC m-code so look up the coordinates
	local entity  = nil
	-- Wikiata coordinates
	entity = mw.wikibase.getEntity()

	if not entity and lat1 and lon1 then
		return lat1, lon1, heading1, nil, CoorCat.sdc4
	elseif not entity then
		return lat1, lon1, heading1, nil, ''
	end

	local v, lat2, lon2, heading2, precision
	if entity then 
		local P1259 = entity:getBestStatements( 'P1259' ) --  coordinates of the point of view
		if P1259[1] and P1259[1].mainsnak.datavalue.value.latitude then
			if P1259[1].qualifiers and P1259[1].qualifiers.P7787 then
				v = P1259[1].qualifiers.P7787[1].datavalue.value
				if v.unit == "http://www.wikidata.org/entity/Q28390" then     -- in degrees
					heading2 = v.amount
				elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians
					heading2 = v.amount*57.2957795131
				end	
			end			
			v = P1259[1].mainsnak.datavalue.value	-- get coordinates
		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 -- SDC coordinates only
		lat1 = lat2
		lon1 = lon2
		heading1 = heading2
		cat = CoorCat.sdc0
	elseif lat1 and lon1 and not lat2 and not lon2 then	
		cat = CoorCat.sdc4
	elseif lat1 and lon1 and lat2 and lon2 then
		local d = distance(lat1, lon1, lat2, lon2) -- calculate distance
		if d<20 or d<precision then -- will consider location within 20 meters or precision distance as the same
			cat = CoorCat.sdc1
		else
			cat = CoorCat.sdc2 
		end
	end
	
	return lat1, lon1, heading1, entity.id, cat
end 


local function dms2deg_ ( d, m, s, h )
  	d,m,s = tonumber(d), tonumber(m), tonumber(s)
  	if not (d and m and s and h) then
		return nil
	end
	local LUT = {N=1, S=-1, E=1, W=-1} -- look up table
	h = LUT[mw.ustring.upper( h )]
	if not h then
		return nil
	end
	return h * (d + m/60.0 + s/3600.0)
end

local function dms2deg ( dms )
  	local ltab  = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/")
  	local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4])
	--return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "")  .. '->' .. (degre or 'nil')
	return degre or dms
end

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

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

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

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

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


--[[============================================================================
Helper core function for deg2dms. deg2dms can be called by templates, while 
_deg2dms should be called from Lua.
Inputs:
* degree - positive coordinate in degrees
* degPrec - coordinate precision in degrees will result in different angle format
* lang - language to used when formatting the number
==============================================================================]]
function p._deg2dms(degree, degPrec, lang)
	local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero
	local Lang = mw.language.new(lang)

	-- adjust number display based on precision
	secPrec = degPrec*3600.0                     -- coordinate precision in seconds
	if secPrec<0.05 then                         -- degPrec<1.3889e-05
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.SS″ format
		c = 360000
	elseif secPrec<0.5 then                      -- 1.3889e-05<degPrec<1.3889e-04
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS.S″ format
		c = 36000
	elseif degPrec*60.0<0.5 then                 -- 1.3889e-04<degPrec<0.0083
		formatStr = '%s°&nbsp;%s′&nbsp;%s″'      -- use DD° MM′ SS″ format
		c = 3600
	elseif degPrec<0.5 then                      -- 0.0083<degPrec<0.5
		formatStr = '%s°&nbsp;%s′'               -- use DD° MM′ format
		c = 60
	else -- if degPrec>0.5 then                  
		formatStr = '%s°'                        -- use DD° format
		c = 1
	end
	
	-- create degree, minute and seconds numbers and string
	d = c/60
	k  = math.floor(c*(degree%360)+0.49)  -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding
	dNum = math.floor(k/c) % 360          -- degree number (integer in 0-360 range)
	mNum = math.floor(k/d) %  60          -- minute number (integer in 0-60 range)
	sNum =      3600*(k%d) / c            -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
	dStr = Lang:formatNum(dNum)           -- degree string 
	mStr = Lang:formatNum(mNum)           -- minute string 
	sStr = Lang:formatNum(sNum)           -- second string 
	zero = Lang:formatNum(0)              -- zero string in local language
	if mNum<10 then
		mStr = zero .. mStr                 -- pad with zero if a single digit
	end
	if sNum<10 then
		sStr = zero .. sStr                 -- pad with zero if less than ten
	end
	return string.format(formatStr, dStr, mStr, sStr);
end

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

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

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

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

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

--[[============================================================================
Helper core function for externalLink. Create URL for different sites:
INPUTS:
 * site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * latStr     = latitude string or number
 * lonStr     = longitude string or number
 * lang       = language code
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level)
	local URLstr = SiteURL[site];
	level = level or 1
	local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' )
	pageName = mw.ustring.gsub( pageName, '%%', '%%%%')

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

--[[============================================================================
Create URL for different sites.
INPUTS:
 * site       = Possible sites: GeoHack, GoogleEarth, Proximityrama, 
                OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * lat        = latitude string or number
 * lon        = longitude string or number
 * lang       = language code
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function p.externalLink(frame)
	local args = getArgs(frame)
	return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
end

--[[============================================================================
Adjust GeoHack attributes depending on the template that calls it
INPUTS:
 * attributes = attributes to be passed to GeoHack
 * mode = set by each calling template
==============================================================================]]
function p.alterAttributes(attributes, mode)
	-- indicate which template called it
	if mode=='camera' then                                   -- Used by {{Location}} and {{Location dec}}
		if string.find(attributes, 'type:camera')==nil then
			attributes = 'type:camera_' .. attributes
		end
	elseif mode=='object'or mode =='globe' then              -- Used by {{Object location}}
		if mode=='object' and string.find(attributes, 'type:')==nil then
			attributes = 'type:object_' .. attributes
		end
		if string.find(attributes, 'class:object')==nil then
			attributes = 'class:object_' .. attributes
		end
	elseif mode=='inline' then                               -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
	elseif mode=='user' then                                 -- Used by {{User location}}
		attributes = 'type:user_location'
	elseif mode=='institution' then                          --Used by {{Institution/coordinates}} (categories only)	
		attributes = 'type:institution'
	end
	return attributes
end
	
--[[============================================================================
 Create link to GeoHack tool which displays latitude and longitude coordinates 
 in DMS format
 INPUTS:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, 
                Ganymede are also supported but are unused as of 2013.
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * lang       = language code
 * prec       = geolocation precision in meters
 * attributes = attributes to be passed to GeoHack
==============================================================================]]
function p._GeoHack_link(args)
	-- create link and coordintate string
	local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
	if latlon==NoLatLonString then
		return latlon
	else
		local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
		return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
	end
end

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


--[[============================================================================
 Create full external links section of {{Location}} or {{Object location}} 
 templates, based on:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 * mode       = Possible options: 
  - camera - call from {{location}}
  - object - call from {{Object location}}
  - globe  - call from {{Globe location}}
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * lang       = language code
 * namespace  = namespace name: File, Category, (Gallery)
==============================================================================]]
function p._externalLinksSection(args)
	local lang = args.lang
	if not args.namespace then
		args.namespace = mw.title.getCurrentTitle().nsText
	end
	
	local str, link1, link2, link3, link4
	if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links
		link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '')
		link2 = p._externalLink('GoogleEarth'   , 'Earth', args.lat, args.lon, lang, '')
		str = string.format('[%s %s] - [%s %s]', 
			link1, langSwitch(i18n.OpenStreetMaps, lang),
			link2, langSwitch(i18n.GoogleEarth, lang)) 
	elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links
		link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
		--link2 = p._externalLink('GoogleMaps'    , 'Earth', args.lat, args.lon, lang, '', args.catRecurse) 
		link3 = p._externalLink('GoogleEarth'   , 'Earth', args.lat, args.lon, lang, '')
		link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '')
		str = string.format('[%s %s] - [%s %s] - [%s %s]', 
			link1, langSwitch(i18n.OpenStreetMaps, lang),
			--link2, langSwitch(i18n.GoogleMaps, lang),
			link3, langSwitch(i18n.GoogleEarth, lang),
			link4, langSwitch(i18n.Proximityrama, lang))
	elseif args.globe=='Mars' or args.globe=='Moon' then
		link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
		str = string.format('[%s %s]', link1, langSwitch(i18n.GoogleMaps, lang))
	end
	
	return str
end

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

--[[============================================================================
Core section of template:Location, template:Object location and template:Globe location.
This method requires several arguments to be passed to it or it's parent method/template:
 * globe      = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
 * mode       = Possible options: 
  - camera - call from {{location}}
  - object - call from {{Object location}}
  - globe  - call from {{Globe location}}
 * lat        = latitude in degrees
 * lon        = longitude in degrees
 * attributes = attributes
 * lang       = language code
 * namespace  = namespace: File, Category, Gallery
 * prec       = geolocation precision in meters
==============================================================================]]
function p._LocationTemplateCore(args)
	-- prepare arguments
	if not (args.namespace) then -- if namespace not provided than look it up
		args.namespace = mw.title.getCurrentTitle().nsText
	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
	args.globe = args.globe or 'Earth'
	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)
	local heading = p._getHeading(attributes0)	-- get heading arrow section
	if lon then -- get longitude to be in -180 to 180 range
		lon=lon%360
		if lon>180 then
			lon = lon-360
		end
	end
	
	-- If wikidata link provided than compare coordinates
	local Categories, geoMicroFormat, coorTag, wikidata_link = '', '', '', ''
	if (args.mode=='object') and (args.namespace~='File') then
		local dist_str, qID
	  -- look up the coordinates on Wikidata
		lat, lon, qID, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon)
		if qID then 
			wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, qID);
			args.wikidata = args.wikidata or qID
		end
	elseif (args.mode=='camera') and (args.namespace=='File') then
		local dist_str, mID
	  -- look up lat/lon on SDC
		lat, lon, heading, mID, Categories = mergeWithSDC(lat, lon, heading)
		if mID then 
			wikidata_link = "\n[[File:Commons structured data logo.svg|16px|Edit coordinates on Structured Data on Commons|link=]]";
		end	
	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 (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
				Categories = Categories .. CoorCat.erroneous
			end
			local cat = CoorCat[args.namespace]
			if cat then -- add category based on namespace
				Categories = Categories .. cat
			end
			-- if not earth than add a category for each globe
			if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then
				Categories = Categories .. string.format(CoorCat[args.mode], args.globe)
			end
			-- add  <span class="geo"> Geo (microformat) code: it is included for machine readability
			geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
			-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
			if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then 
				coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } )
			elseif args.namespace == 'File' and args.mode=='object' then 
				coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } )
			end
		else -- if lat and lon are not numbers then add error category
			Categories = Categories .. CoorCat.erroneous
		end
	end

	-- Call helper functions to render different parts of the template
	local coor,  info_link, inner_table, OSM = '','','','','',''
	coor = p._GeoHack_link(args)  			-- the p and link to GeoHack
	if heading then  
		local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
		local fname = heading_icon[k]
		coor = string.format('%s&nbsp;&nbsp;<span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading)
	end
	if args.globe=='Earth' then
		local icon = 'marker'
		if args.mode=='camera' then 
			icon = 'camera'
		end
		OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM
	end
	local external_link = p._externalLinksSection(args) 					-- external link section
	if external_link and args.namespace == 'File' then
		external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link 	-- header of the link section for {{location}} template
	elseif external_link then
		external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template
	end
	info_link   = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', langSwitch(i18n.COM_GEO, args.lang) )
	inner_table = string.format('<td style="border:none;">%s&nbsp;%s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>', 
		coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat)
	
	-- combine strings into a table
	local templateText
	if bare then
		templateText  = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table)
	else
		-- choose name of the field
		local field_name = 'Location'
		if args.mode=='camera' then 
			field_name = langSwitch(i18n.CameraLocation, args.lang)
		elseif args.mode=='object' then 
			field_name = langSwitch(i18n.ObjectLocation, args.lang)
		elseif args.mode=='globe' then
			local field_list = langSwitch(i18n.GlobeLocation, args.lang)
			if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
				field_name = field_list[args.globe]
			end
		end
		--Create HTML text
		local dir, text_align
		if mw.language.new( args.lang ):isRTL() then
			dir = 'rtl'
			text_align = 'right'
		else
			dir = 'ltr'
			text_align = 'left'
		end 
		local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"', 
		args.lang, dir, text_align, args.lang)
		templateText  = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', args.lang, style, field_name, inner_table)
	end
	return templateText, Categories, coorTag
end

function p.LocationTemplateCore(frame)
	local args = getArgs(frame)
	args.namespace = mw.title.getCurrentTitle().nsText
	if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
		if args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W"
			args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] )
			args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
			args.attributes = args.attributes or args[9]
		elseif args[2] and not (type(args[2])=='string' and args[2]:find(":")) then -- decimal format or DMS with one pipe, ex. "34° 05′ 32.36″ N| 116° 09′ 24.55″ W"
			args.lat = args[1]
			args.lon = args[2]
			args.attributes = args.attributes or args[3]
		elseif args[1] then -- detect a single argument in the form "34° 05′ 32.36″ N, 116° 09′ 24.55″ W" or similar
			local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter
			args.lat, args.lon = v[1], v[2]
			args.attributes = args.attributes or args[2]
		end
	end
	local cat = ''
	if args.lat and args.lon then
		local lat = tonumber(args.lat)
		local lon = tonumber(args.lon)
		if not lat or not lon then
			args.lat = dms2deg(args.lat or '')
			args.lon = dms2deg(args.lon or '')
			if (args.namespace == 'File' or args.namespace == 'Category') then
				cat = CoorCat.dms
			end
		end
	end
	local templateText, Categories, coorTag = p._LocationTemplateCore(args)
	return templateText .. Categories .. cat .. coorTag
end

return p