Module:Coordinates: Difference between revisions
From WWII Archives
w>WOSlinker (use require('strict') instead of require('Module:No globals')) |
Paul Sidle (talk | contribs) m (1 revision imported) |
||
(2 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
--[[ | --[[ | ||
__ __ _ _ ____ _ _ _ | |||
| \/ | ___ __| |_ _| | ___ _ / ___|___ ___ _ __ __| (_)_ __ __ _| |_ ___ ___ | |||
| |\/| |/ _ \ / _` | | | | |/ _ (_) | / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __| | |||
| | | | (_) | (_| | |_| | | __/_| |__| (_) | (_) | | | (_| | | | | | (_| | || __/\__ \ | |||
|_| |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_| \__,_|_|_| |_|\__,_|\__\___||___/ | |||
{{ | 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) | |||
]] | ]] | ||
require('strict') | -- ======================================= | ||
-- === Dependencies ====================== | |||
-- ======================================= | |||
require('strict') -- used for debugging purposes as it detects cases of unintended global variables | |||
local i18n = require('Module:I18n/coordinates') -- get localized translations of site names | |||
local core = require('Module:Core') | |||
-- ======================================= | |||
-- === Hardwired parameters ============== | |||
-- ======================================= | |||
local | -- =========================================================== | ||
-- 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 = { | |||
local | [ 1] = 'File:Compass-icon bb N.svg', | ||
[ 2] = 'File:Compass-icon bb NbE.svg', | |||
[ 3] = 'File:Compass-icon bb NNE.svg', | |||
[ 4] = 'File:Compass-icon bb NEbN.svg', | |||
[ 5] = 'File:Compass-icon bb NE.svg', | |||
[ 6] = 'File:Compass-icon bb NEbE.svg', | |||
[ 7] = 'File:Compass-icon bb ENE.svg', | |||
[ 8] = 'File:Compass-icon bb EbN.svg', | |||
[ 9] = 'File:Compass-icon bb E.svg', | |||
[10] = 'File:Compass-icon bb EbS.svg', | |||
[11] = 'File:Compass-icon bb ESE.svg', | |||
[12] = 'File:Compass-icon bb SEbE.svg', | |||
[13] = 'File:Compass-icon bb SE.svg', | |||
[14] = 'File:Compass-icon bb SEbS.svg', | |||
[15] = 'File:Compass-icon bb SSE.svg', | |||
[16] = 'File:Compass-icon bb SbE.svg', | |||
[17] = 'File:Compass-icon bb S.svg', | |||
[18] = 'File:Compass-icon bb SbW.svg', | |||
[19] = 'File:Compass-icon bb SSW.svg', | |||
[20] = 'File:Compass-icon bb SWbS.svg', | |||
[21] = 'File:Compass-icon bb SW.svg', | |||
[22] = 'File:Compass-icon bb SWbW.svg', | |||
[23] = 'File:Compass-icon bb WSW.svg', | |||
[24] = 'File:Compass-icon bb WbS.svg', | |||
[25] = 'File:Compass-icon bb W.svg', | |||
[26] = 'File:Compass-icon bb WbN.svg', | |||
[27] = 'File:Compass-icon bb WNW.svg', | |||
[28] = 'File:Compass-icon bb NWbW.svg', | |||
[29] = 'File:Compass-icon bb NW.svg', | |||
[30] = 'File:Compass-icon bb NWbN.svg', | |||
[31] = 'File:Compass-icon bb NNW.svg', | |||
[32] = 'File:Compass-icon bb NbW.svg' | |||
} | |||
-- =========================================================== | |||
-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be | |||
-- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings. | |||
local SiteURL = { | |||
GeoHack = 'https://geohack.toolforge.org/geohack.php?pagename=$page¶ms=$lat_N_$lon_E_$attr&language=$lang', | |||
--GoogleEarth = '//geocommons.toolforge.org/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1', | |||
Proximityrama = 'https://tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon', | |||
WikimediaMap = 'https://maps.wikimedia.org/#16/$lat/$lon', | |||
--OpenStreetMap1 = '//wiwosm.toolforge.org/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon', | |||
OpenStreetMap1 = 'https://wikimap.toolforge.org/?wp=false&cluster=false&zoom=16&lat=$lat&lon=$lon', | |||
OpenStreetMap2 = 'https://tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level', | |||
GoogleMaps = { | |||
Mars = 'https://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8', | |||
Moon = 'https://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8', | |||
Earth = 'https://wp-world.toolforge.org/googlmaps-proxy.php?page=http://kmlexport.toolforge.org/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic' | |||
} | |||
} | |||
-- =========================================================== | |||
-- Categories | |||
local CoorCat = { | |||
-- File = '[[Category:Media with locations]]', | |||
-- Gallery = '[[Category:Galleries with coordinates]]', | |||
-- Category = '[[Category:Categories with coordinates]]', | |||
strucData0 = '[[Category:Pages with %s coordinates from %s]]', | |||
strucData1 = '[[Category:Pages with local %s coordinates and matching %s coordinates]]', | |||
strucData2 = '[[Category:Pages with local %s coordinates and similar %s coordinates]]', | |||
strucData3 = '[[Category:Pages with local %s coordinates and mismatching %s coordinates]]', | |||
strucData4 = '[[Category:Pages with local %s coordinates and missing %s coordinates]]', | |||
sHeading3 = '[[Category:Pages with local %s heading and mismatching %s heading]]', | |||
sHeading4 = '[[Category:Pages with local %s heading and missing %s heading]]', | |||
sHeading5 = '[[Category:Pages with local %s heading:0 and missing %s heading]]', | |||
globe = '[[Category:Media with %s locations]]', | |||
default = '[[Category:Media with default locations]]', | |||
attribute = '[[Category:Media with erroneous geolocation attributes]]', | |||
erroneous = '[[Category:Media with erroneous locations]]', | |||
dms = '[[Category:Media with coordinates in DMS format]]' | |||
} | |||
local globeLUT = { Q2='Earth', Q111='Mars', Q405='Moon'} | |||
local NoLatLonString = 'latitude, longitude' | |||
-- ======================================= | |||
-- === Local Functions =================== | |||
-- ======================================= | |||
-- | ------------------------------------------------------------------------------- | ||
local function | local function getProperty(entity, prop) | ||
return | return (core.parseStatements(entity:getBestStatements( prop ), nil) or {nil})[1] | ||
end | end | ||
-- | -- =========================================================== | ||
local function | local function add_maplink(lat, lon, marker, text) | ||
if | local tstr = '' | ||
if text then | |||
tstr = string.format('text="%s" ', text) | |||
end | end | ||
return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'.. | |||
' "type": "Feature",'.. | |||
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. | |||
' "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0" }'.. | |||
'}</maplink>', tstr, lat, lon, lon, lat, marker) | |||
end | end | ||
-- =========================================================== | |||
-- | local function add_maplink2(lat1, lon1, lat2, lon2) | ||
local function | return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'.. | ||
return | ' "type": "Feature",'.. | ||
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. | |||
' "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons" }'.. | |||
} | '},{'.. | ||
' "type": "Feature",'.. | |||
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. | |||
' "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata" }'.. | |||
'}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2) | |||
end | end | ||
-- | -- =========================================================== | ||
local function | local function info_box(text) | ||
return | return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '.. | ||
'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'.. | |||
'<tr>'.. | |||
'<td class="mbox-image" style="padding-left:.9em;">'.. | |||
' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'.. | |||
'<td class="mbox-text" style="">%s</td>'.. | |||
'</tr></table>', text) | |||
end | end | ||
-- | -- =========================================================== | ||
local function distance(lat1, lon1, lat2, lon2) | |||
-- calculate distance | |||
local function | local dLat = math.rad(lat1-lat2) | ||
local | 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 | return d | ||
end | end | ||
-- | -- =========================================================== | ||
local function getSDCoords(entity, prop) | |||
-- get coordinates from structured data (either wikidata or SDC) | |||
local coords = {id=entity.id, source=prop} | |||
if not entity or not entity.claims or not entity.claims[prop]then | |||
return coords | |||
local | |||
if | |||
end | end | ||
for _, statement in pairs( entity:getBestStatements( prop )) do | |||
local v = statement.mainsnak.datavalue.value -- get coordinates | |||
if v.latitude then | |||
coords.lat = v.latitude | |||
return | coords.lon = v.longitude | ||
coords.prec = v.precision or 1e-4 | |||
coords.prec = math.floor(coords.prec*111000) -- convert precision from degrees to meters and round | |||
coords.prec = math.max(math.min(coords.prec,111000),5) -- bound precision to a number between 5 meters and 1 degree | |||
coords.globe = string.gsub(v.globe, 'http://www.wikidata.org/entity/','') | |||
coords.globe = globeLUT[coords.globe] | |||
if statement.qualifiers and statement.qualifiers.P7787 then | |||
v = statement.qualifiers.P7787[1].datavalue.value | |||
if v.unit == "http://www.wikidata.org/entity/Q28390" then -- in degrees | |||
coords.heading = v.amount | |||
elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians | |||
coords.heading = v.amount*57.2957795131 | |||
end | |||
end | |||
return coords | |||
end | |||
end | end | ||
return coords | |||
end | end | ||
-- | -- =========================================================== | ||
local function compareCoords(loc, sd, mode, source) | |||
-- compare coordinates | |||
--INPUTS: | |||
-- * loc - local coordinates | |||
-- * sd - structured data coords | |||
local coord = loc | |||
local cat, dist_str = '', '' | |||
local case, dist, qs, mapLink, message | |||
dist=0 | |||
if not loc.lat or not loc.lon then -- structured data/wikidata coordinates only | |||
coord = sd | |||
cat = string.format(CoorCat.strucData0, mode, source) | |||
local | case = 0 | ||
elseif loc.lat and loc.lon and not sd.lat and not sd.lon then | |||
cat = string.format(CoorCat.strucData4, mode, source) | |||
-- | case = 4 -- local coordinates only | ||
elseif loc.lat and loc.lon and sd.lat and sd.lon then | |||
dist = distance(loc.lat, loc.lon, sd.lat, sd.lon) -- calculate distance | |||
dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', dist) -- will be displayed when hovering a mouse above wikidata icon | |||
if dist<20 or dist<sd.prec then -- will consider location within 20 meters or precision distance as the same | |||
if source=='Wikidata' then | |||
cat = string.format(CoorCat.strucData1, mode, source) | |||
end | |||
case = 1 | |||
elseif (dist<1000 or dist<5*sd.prec) and mode=='object' then | |||
--cat = string.format(CoorCat.strucData2, mode, source) | |||
case = 2 | |||
else -- locations 1 km off and 5 precision distances away are likely wrong. The issue might be with wrong precission | |||
mapLink = mw.getCurrentFrame():preprocess(add_maplink2(loc.lat, loc.lon, sd.lat, sd.lon)) -- fancy link to OSM | |||
message = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at %s (%s, precision: %i m). Please [[Commons:Structured data/Reconciliation|reconcile them]]. ", | |||
dist, source, mapLink, sd.prec) | |||
cat = string.format(CoorCat.strucData3, mode, source) .. info_box(message) | |||
case = 3 | |||
end | |||
end | end | ||
if | if not loc.heading and sd.heading then -- structured data/wikidata heading only | ||
coord.heading = sd.heading | |||
elseif loc.heading==0 and not sd.heading and sd.lat and sd.lon then -- local heading only | |||
cat = cat .. string.format(CoorCat.sHeading5, mode, source) | |||
elseif loc.heading and not sd.heading and sd.lat and sd.lon then -- local heading only | |||
cat = cat .. string.format(CoorCat.sHeading4, mode, source) | |||
elseif loc.heading and sd.heading then | |||
local dh = math.abs(math.fmod(loc.heading,360) - math.fmod(sd.heading,360)) | |||
if dh>1 and dh<359 then | |||
message = string.format("There is a discrepancy of %i degrees between the above camera heading (set to %i) and the ones stored at %s (set to %i). Please [[Commons:Structured data/Reconciliation|reconcile them]]. ", dh, loc.heading, source, sd.heading) | |||
cat = cat .. string.format(CoorCat.sHeading3, mode, source) .. info_box(message) | |||
end | |||
end | end | ||
if source=='Wikidata' and case>=3 then | |||
local url = mw.title.getCurrentTitle():canonicalUrl() | |||
local today = '+' .. os.date('!%F') .. 'T00:00:00Z/11' -- today's date in QS format | |||
qs = string.format('%s|P625|@%09.5f/%09.5f|S143|Q565|S813|%s|S4656|"%s"', sd.wID, loc.lat, loc.lon, today, url) | |||
qs = string.gsub (mw.uri.encode(qs),'%%2520','%%20') | |||
qs = 'https://quickstatements.toolforge.org/#/v1=' .. qs -- create full URL link | |||
qs = string.format("[[File:Commons_to_Wikidata_QuickStatements.svg|15px|link=%s|Copy geo coordinates to Wikidata]]", qs) | |||
end | end | ||
local ret = {dist_str=dist_str, case=case, qs=qs } | |||
return coord, cat, ret | |||
end | |||
local | -- Check if location of creation (P1071) is set | ||
local | local function checkLocationOfCreation(entity, lat, lon) | ||
if | local cat = '' | ||
local latFloor, lonFloor, latAbs, lonAbs | |||
if entity and entity.statements and entity.statements['P1071'] then | |||
return cat | |||
end | end | ||
latFloor = math.floor(lat) | |||
lonFloor = math.floor(lon) | |||
latAbs = math.abs(latFloor) | |||
lonAbs = math.abs(lonFloor) | |||
-- This is a rough bounding box of the Netherlands and part of neighbor countries as a pilot | |||
if (48 <= latFloor) and (latFloor < 54) and (0 <= lonFloor) and (lonFloor < 12) then | |||
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° E)]]", latAbs, lonAbs) | |||
return cat | |||
-- Part of the United Kingdom | |||
elseif (50 <= latFloor) and (latFloor < 56) and (-5 <= lonFloor) and (lonFloor < 0) then | |||
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° W)]]", latAbs, lonAbs) | |||
return cat | |||
-- Canberra and Sydney | |||
elseif (-36 <= latFloor) and (latFloor < -33) and (149 <= lonFloor) and (lonFloor < 152 ) then | |||
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° E)]]", latAbs, lonAbs) | |||
return cat | |||
-- Buenos Aires and Montevideo | |||
elseif (-36 <= latFloor) and (latFloor < -33) and (-58 <= lonFloor) and (lonFloor < -55) then | |||
cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° W)]]", latAbs, lonAbs) | |||
return cat | |||
end | end | ||
cat = '[[Category:Files with coordinates missing SDC location of creation]]' | |||
return cat | |||
end | end | ||
-- | -- =========================================================== | ||
local function | local function dms2deg_ ( d, m, s, h ) | ||
d,m,s = tonumber(d), tonumber(m), tonumber(s) | |||
return d .. | if not (d and m and s and h) then | ||
return nil | |||
end | |||
local LUT = {N=1, S=-1, E=1, W=-1} -- look up table | |||
h = LUT[mw.ustring.upper( h )] | |||
if not h then | |||
return nil | |||
end | |||
return h * (d + m/60.0 + s/3600.0) | |||
end | end | ||
-- | -- =========================================================== | ||
local function | 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 | |||
return | |||
end | end | ||
-- | -- ======================================= | ||
-- === External Functions ================ | |||
-- ======================================= | |||
local p = {} | |||
p.debug = 'nothing' | |||
-- parse attribute variable returning desired field (used for debugging) | |||
function p.parseAttribute(frame) | |||
return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or '' | |||
end | end | ||
-- | -- =========================================================== | ||
Helper function | -- Helper core function for getHeading. | ||
function p._getHeading(attributes) | |||
if attributes == nil then | |||
local | return nil | ||
local | end | ||
local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)') | |||
if | if hStr == nil then | ||
return nil | |||
end | |||
local hNum = tonumber( hStr ) | |||
if hNum == nil then | |||
hStr = string.upper (hStr) | |||
hNum = compass_points[hStr] | |||
end | end | ||
if hNum then | |||
hNum = hNum%360 | |||
if | |||
end | end | ||
return hNum | |||
end | end | ||
--[[ | --[[============================================================================ | ||
Parse attribute variable returning heading field. If heading is a string than | |||
try to convert it to an angle | |||
==============================================================================]] | |||
local | 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 | end | ||
local hNum = p._getHeading(attributes) | |||
local | if hNum == nil then | ||
return '' | |||
end | end | ||
return tostring(hNum) | |||
return | |||
end | 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) | |||
if | -- 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 | |||
if | 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 | 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 | end | ||
if | if sNum<10 then | ||
sStr = zero .. sStr -- pad with zero if less than ten | |||
end | end | ||
return string.format(formatStr, dStr, mStr, sStr); | |||
end | 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 | |||
local | ==============================================================================]] | ||
local | function p.deg2dms(frame) | ||
local args = core.getArgs(frame) | |||
local degree = tonumber(args[1]) | |||
local degPrec = tonumber(args[2]) or 0-- precision in degrees | |||
if degree==nil then | |||
return args[1]; | |||
else | else | ||
return p._deg2dms(degree, degPrec, args.lang) | |||
end | end | ||
end | |||
return | function p.dms2deg(frame) | ||
return dms2deg(frame.args[1]) | |||
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 | ||
end | end | ||
if lat==nil or lon==nil then | |||
return NoLatLonString | |||
else | else | ||
local nsew = core.langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language | |||
local SN, EW, latStr, lonStr, lon2m, lat2m, phi | |||
if lat<0 then SN = nsew.S else SN = nsew.N end -- choose S or N depending on latitude degree sign | |||
if lon<0 then EW = nsew.W else EW = nsew.E end -- choose W or E depending on longitude degree sign | |||
lat2m=1 | |||
lon2m=1 | |||
if prec>0 then -- if user specified the precision of the geo location... | |||
phi = math.abs(lat)*math.pi/180 -- latitude in radiants | |||
lon2m = 6378137*math.cos(phi)*math.pi/180 -- see https://en.wikipedia.org/wiki/Longitude | |||
lat2m = 111000 -- average latitude degree size in meters | |||
end | |||
latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude degrees to degrees/minutes/seconds | |||
lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds | |||
return string.format('%s %s, %s %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 | function p.lat_lon(frame) | ||
local args = core.getArgs(frame) | |||
return p._lat_lon(args.lat, args.lon, args.prec, args.lang) | |||
end | end | ||
--[[ | --[[============================================================================ | ||
Helper core function for externalLink. Create URL for different sites: | |||
and | INPUTS: | ||
]] | * site = Possible sites: GeoHack, GoogleEarth, Proximityrama, | ||
OpenStreetMap, GoogleMaps (for Earth, Mars and Moon) | |||
local | * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, | ||
local | 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 | end | ||
URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr) | |||
URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr) | |||
URLstr = mw.ustring.gsub( URLstr, '$lang' , lang) | |||
URLstr = mw.ustring.gsub( URLstr, '$level', level) | |||
URLstr = mw.ustring.gsub( URLstr, '$page' , pageName) | |||
URLstr = mw.ustring.gsub( URLstr, '+', '') | |||
URLstr = mw.ustring.gsub( URLstr, ' ', '_') | |||
return URLstr | |||
end | |||
--[[============================================================================ | |||
Create URL for different sites. | |||
INPUTS: | |||
* site = Possible sites: GeoHack, GoogleEarth, Proximityrama, | |||
OpenStreetMap, GoogleMaps (for Earth, Mars and Moon) | |||
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, | |||
Ganymede are also supported but are unused as of 2013. | |||
* lat = latitude string or number | |||
* lon = longitude string or number | |||
* lang = language code | |||
* attributes = attributes to be passed to GeoHack | |||
==============================================================================]] | |||
function p.externalLink(frame) | |||
local args = core.getArgs(frame) | |||
return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '') | |||
end | |||
--[[============================================================================ | |||
Adjust GeoHack attributes depending on the template that calls it | |||
INPUTS: | |||
* attributes = attributes to be passed to GeoHack | |||
* mode = set by each calling template | |||
==============================================================================]] | |||
function p.alterAttributes(attributes, mode, heading) | |||
-- indicate which template called it | |||
if mode=='camera' then -- Used by {{Location}} and {{Location dec}} | |||
if not string.find(attributes, 'type:camera') then | |||
attributes = 'type:camera_' .. attributes | |||
end | end | ||
elseif mode=='object' then -- Used by {{Object location}} | |||
if mode=='object' and not string.find(attributes, 'type:') then | |||
attributes = 'type:object_' .. attributes | |||
end | end | ||
if not | if not string.find(attributes, 'class:object') then | ||
attributes = 'class:object_' .. attributes | |||
end | end | ||
elseif mode=='inline' then -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment) | |||
elseif | elseif mode=='user' then -- Used by {{User location}} | ||
attributes = 'type:user_location' | |||
elseif mode=='institution' then --Used by {{Institution/coordinates}} (categories only) | |||
attributes = 'type:institution' | |||
end | |||
local hStr = '' | |||
if heading then -- if heading is a number | |||
hStr = string.format('heading:%6.2f', heading) | |||
end | |||
if not string.find(attributes, 'heading:') then | |||
attributes = attributes .. '_' .. hStr | |||
else | else | ||
attributes = string.gsub(attributes,'heading:[^_]*', hStr) -- replace heading in form heading:N with heading=0 | |||
attributes = string.gsub(attributes,'__', '_') | |||
end | end | ||
return string.gsub(attributes,' ', '') | |||
end | 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 | |||
if | |||
return | |||
else | else | ||
return '' | local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '') | ||
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion"> | |||
end | end | ||
end | end | ||
function p.GeoHack_link(frame) | |||
return p._GeoHack_link(core.getArgs(frame)) | |||
end | end | ||
--[[============================================================================ | |||
Create full external links section of {{Location}} or {{Object location}} | |||
templates, based on: | |||
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. | |||
* mode = Possible options: | |||
- camera - call from {{location}} | |||
- object - call from {{Object location}} | |||
- globe - call from {{Globe location}} | |||
* lat = latitude in degrees | |||
* lon = longitude in degrees | |||
* lang = language code | |||
* namespace = namespace name: File, Category, (Gallery) | |||
==============================================================================]] | |||
function p._externalLinksSection(args) | |||
local lang = args.lang | |||
if not args.namespace then | |||
args.namespace = mw.title.getCurrentTitle().nsText | |||
end | |||
local str, link1, link2, link3, link4 | |||
if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links | |||
link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '') | |||
--link2 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '') | |||
str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang)) | |||
--link2, core.langSwitch(i18n.GoogleEarth, lang)) | |||
elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links | |||
link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse) | |||
--link2 = p._externalLink('GoogleMaps' , 'Earth', args.lat, args.lon, lang, '', args.catRecurse) | |||
--link3 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '') | |||
--link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '') | |||
str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang)) | |||
--link2, core.langSwitch(i18n.GoogleMaps, lang), | |||
--link3, core.langSwitch(i18n.GoogleEarth, lang), | |||
--link4, core.langSwitch(i18n.Proximityrama, lang)) | |||
elseif args.globe=='Mars' or args.globe=='Moon' then | |||
link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '') | |||
str = string.format('[%s %s]', link1, core.langSwitch(i18n.GoogleMaps, lang)) | |||
end | end | ||
return str | |||
end | end | ||
function p.externalLinksSection(frame) | |||
return p._externalLinksSection(core.getArgs(frame)) | |||
end | 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 | ||
function | |||
if not | |||
end | end | ||
if args.namespace=='' then -- if empty than it is a gallery | |||
args.namespace = 'Gallery' | |||
end | end | ||
local | local bare = core.yesno(args.bare,false) | ||
local Status = 'primary' -- used by {{#coordinates:}} | |||
if core.yesno(args.secondary,false) then | |||
Status = 'secondary' | |||
end | end | ||
args.globe = mw.language.new('en'):ucfirst(args.globe or 'Earth') | |||
-- Convert coordinates from string to numbers | |||
local lat = tonumber(args.lat) | |||
local lon = tonumber(args.lon) | |||
local precission = tonumber(args.prec or '0') | |||
local heading = p._getHeading(args.attributes) -- get heading arrow section | |||
if lon then -- get longitude to be in -180 to 180 range | |||
lon=lon%360 | |||
if lon>180 then | |||
lon = lon-360 | |||
end | |||
end | end | ||
local | -- If wikidata link provided than compare coordinates | ||
if | local Categories, geoMicroFormat, coorTag, edit_icon, wikidata_link = '', '', '', '', '', '', '' | ||
local entity, coord, sd, cmp, locationCat | |||
local loc = {lat=lat, lon=lon, heading=heading, source='loc'} | |||
local ID = args.wikidata | |||
if (ID==nil) then | |||
entity = mw.wikibase.getEntity() | |||
if entity and args.namespace == 'Category' then | |||
-- this is category connected to Wikidata through sitelink | |||
ID = getProperty(entity, "P301") | |||
if getProperty(entity, "P31") == 'Q4167836' and ID then | |||
-- wikidata item is a "category item" with "category's main topic (P301)" | |||
-- follow P301 to the actual item for this category | |||
entity = mw.wikibase.getEntity(ID) | |||
end | |||
end | |||
elseif type(ID)=='string' and ID:match( '^[QqMm]%d+$' ) then | |||
entity = mw.wikibase.getEntity(ID) | |||
elseif type(ID)~='string' and ID.id then | |||
entity = ID -- entities can be passed from outside | |||
end | end | ||
if | |||
if entity then | |||
.. | if (args.mode=='object' or args.mode=='globe') then | ||
.. | sd = getSDCoords(entity,'P9149') -- fetch coordinates of depicted place | ||
if not sd.lat then | |||
sd = getSDCoords(entity,'P625') -- fallback to coordinate location | |||
end | |||
elseif (args.mode=='camera') then | |||
sd = getSDCoords(entity,'P1259') -- fetch camera coordinates or coordinates of the point of view | |||
end | |||
if (args.namespace=='File') then -- look up lat/lon on SDC | |||
coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'SDC') | |||
if coord.source~='loc' then | |||
edit_icon = core.editAtSDC(coord.source, args.lang) | |||
lat, lon, heading, precission = coord.lat, coord.lon, coord.heading, coord.prec | |||
end | |||
elseif (args.namespace == 'Category') then -- look up lat/lon on wikidata | |||
sd.wID = entity.id | |||
coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'Wikidata') | |||
if coord.source~='loc' then | |||
local str = "\n[[File:Wikidata-logo.svg|20px|Field with data from Wikidata's %s property<br/>%s|link=wikidata:%s#%s]]" | |||
edit_icon = core.editAtWikidata(entity.id, coord.source, args.lang) | |||
lat, lon, heading, precission = coord.lat, coord.lon, coord.heading, coord.prec | |||
end | |||
if cmp.qs then | |||
wikidata_link = cmp.qs | |||
end | |||
end | |||
elseif (args.namespace=='File') then | |||
Categories = string.format(CoorCat.strucData4, args.mode, 'SDC') | |||
end | end | ||
if | |||
-- Check if location of creation (P1071) is set and if not, add tracker | |||
if args.namespace == 'File' and lat and lon then | |||
locationCat = checkLocationOfCreation(entity, lat, lon) | |||
if locationCat then | |||
Categories = Categories .. locationCat | |||
end | end | ||
end | end | ||
args.lat = string.format('%010.6f', lat or 0) | |||
args.lon = string.format('%011.6f', lon or 0) | |||
args.prec = precission | |||
args.attributes = p.alterAttributes(args.attributes or '', args.mode, heading) | |||
local frame = mw.getCurrentFrame() | |||
-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages | |||
if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then | |||
if lat and lon then -- if lat and lon are numbers... | |||
if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources | |||
Categories = Categories .. CoorCat.default | |||
end | |||
if args.attributes and string.find(args.attributes, '=') then | |||
Categories = Categories .. CoorCat.attribute | |||
end | |||
if (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors ) | |||
Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are outside allowed range)</span>\n' .. CoorCat.erroneous | |||
end | |||
-- local cat = CoorCat[args.namespace] | |||
-- if cat then -- add category based on namespace | |||
-- Categories = Categories .. cat | |||
-- end | |||
-- if not earth than add a category for each globe | |||
if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then | |||
Categories = Categories .. string.format(CoorCat[args.mode], args.globe) | |||
end | |||
-- add <span class="geo"> Geo (microformat) code: it is included for machine readability | |||
geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon) | |||
-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData | |||
if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then | |||
coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } ) | |||
elseif args.namespace == 'File' and args.mode=='object' then | |||
coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } ) | |||
end | |||
else -- if lat and lon are not numbers then add error category | |||
Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are missing or not numeric)</span>\n' .. CoorCat.erroneous | |||
end | |||
end | |||
-- Call helper functions to render different parts of the template | |||
local coor, info_link, inner_table, OSM = '','','','','','' | |||
coor = p._GeoHack_link(args) -- the p and link to GeoHack | |||
coor = string.format('<span class=plainlinks>%s</span>%s', coor, edit_icon) | |||
if heading then | |||
local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1 | |||
local fname = heading_icon[k] | |||
coor = string.format('%s <span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading) | |||
end | |||
if args.globe=='Earth' then | |||
local icon = 'marker' | |||
local | if args.mode=='camera' then | ||
icon = 'camera' | |||
end | end | ||
OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM | |||
end | |||
local external_link = p._externalLinksSection(args) -- external link section | |||
if external_link and args.namespace == 'File' then | |||
external_link = core.langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template | |||
elseif external_link then | |||
external_link = core.langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template | |||
end | |||
info_link = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', core.langSwitch(i18n.COM_GEO, args.lang) ) | |||
inner_table = string.format('<td style="border:none;">%s %s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>', | |||
coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat) | |||
-- combine strings into a table | |||
local templateText | |||
if bare then | |||
templateText = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table) | |||
else | else | ||
-- choose name of the field and create row | |||
local field_name = 'Location' | |||
if args.mode=='camera' then | |||
field_name = core.langSwitch(i18n.CameraLocation, args.lang) | |||
elseif args.mode=='object' then | |||
field_name = core.langSwitch(i18n.ObjectLocation, args.lang) | |||
elseif args.mode=='globe' then | |||
local field_list = core.langSwitch(i18n.GlobeLocation, args.lang) | |||
if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized | |||
field_name = field_list[args.globe] | |||
end | |||
end | |||
templateText = string.format('<tr><th class="type fileinfo-paramfield">%s</th>%s</tr>', field_name, inner_table) | |||
--Create HTML text | |||
local dir = mw.language.new( args.lang ):getDir() -- get text direction | |||
local style = 'class="toccolours mw-content-%s layouttemplate commons-file-information-table" style="width: 100%%;" dir="%s" lang="%s"' | |||
style = string.format(style, dir, dir, args.lang) | |||
templateText = string.format('<table %s>\n%s\n</table>', style, templateText) | |||
end | end | ||
return templateText, Categories, coorTag | |||
end | end | ||
function p.LocationTemplateCore(frame) | |||
local args = core.getArgs(frame) | |||
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 | ||
end | end | ||
if | 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 | end | ||
return | local templateText, Categories, coorTag = p._LocationTemplateCore(args) | ||
return templateText .. Categories .. cat .. coorTag | |||
end | end | ||
return | return p |
Latest revision as of 13:21, 30 May 2024
Documentation for this module may be created at Module:Coordinates/doc
--[[ __ __ _ _ ____ _ _ _ | \/ | ___ __| |_ _| | ___ _ / ___|___ ___ _ __ __| (_)_ __ __ _| |_ ___ ___ | |\/| |/ _ \ / _` | | | | |/ _ (_) | / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __| | | | | (_) | (_| | |_| | | __/_| |__| (_) | (_) | | | (_| | | | | | (_| | || __/\__ \ |_| |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_| \__,_|_|_| |_|\__,_|\__\___||___/ This module is intended to provide functionality of {{location}} and related templates. It was developed on Wikimedia Commons, so if you find this code on other sites, check there for updates and discussions. Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases. Authors and maintainers: * User:Jarekt * User:Ebraminio Functions: *function p.LocationTemplateCore(frame) **function p.GeoHack_link(frame) ***function p.lat_lon(frame) ****function p._deg2dms(deg,lang) ***function p.externalLink(frame) ****function p._externalLink(site, globe, latStr, lonStr, lang, attributes) **function p._getHeading(attributes) **function p.externalLinksSection(frame) ***function p._externalLink(site, globe, latStr, lonStr, lang, attributes) *function p.getHeading(frame) *function p.deg2dms(frame) ]] -- ======================================= -- === Dependencies ====================== -- ======================================= require('strict') -- used for debugging purposes as it detects cases of unintended global variables local i18n = require('Module:I18n/coordinates') -- get localized translations of site names local core = require('Module:Core') -- ======================================= -- === Hardwired parameters ============== -- ======================================= -- =========================================================== -- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]] local compass_points = { N = 0, NBE = 11.25, NNE = 22.5, NEBN = 33.75, NE = 45, NEBE = 56.25, ENE = 67.5, EBN = 78.75, E = 90, EBS = 101.25, ESE = 112.5, SEBE = 123.75, SE = 135, SEBS = 146.25, SSE = 157.5, SBE = 168.75, S = 180, SBW = 191.25, SSW = 202.5, SWBS = 213.75, SW = 225, SWBW = 236.25, WSW = 247.5, WBS = 258.75, W = 270, WBN = 281.25, WNW = 292.5, NWBW = 303.75, NW = 315, NWBN = 326.25, NNW = 337.5, NBW = 348.75, } -- =========================================================== -- files to use for different headings local heading_icon = { [ 1] = 'File:Compass-icon bb N.svg', [ 2] = 'File:Compass-icon bb NbE.svg', [ 3] = 'File:Compass-icon bb NNE.svg', [ 4] = 'File:Compass-icon bb NEbN.svg', [ 5] = 'File:Compass-icon bb NE.svg', [ 6] = 'File:Compass-icon bb NEbE.svg', [ 7] = 'File:Compass-icon bb ENE.svg', [ 8] = 'File:Compass-icon bb EbN.svg', [ 9] = 'File:Compass-icon bb E.svg', [10] = 'File:Compass-icon bb EbS.svg', [11] = 'File:Compass-icon bb ESE.svg', [12] = 'File:Compass-icon bb SEbE.svg', [13] = 'File:Compass-icon bb SE.svg', [14] = 'File:Compass-icon bb SEbS.svg', [15] = 'File:Compass-icon bb SSE.svg', [16] = 'File:Compass-icon bb SbE.svg', [17] = 'File:Compass-icon bb S.svg', [18] = 'File:Compass-icon bb SbW.svg', [19] = 'File:Compass-icon bb SSW.svg', [20] = 'File:Compass-icon bb SWbS.svg', [21] = 'File:Compass-icon bb SW.svg', [22] = 'File:Compass-icon bb SWbW.svg', [23] = 'File:Compass-icon bb WSW.svg', [24] = 'File:Compass-icon bb WbS.svg', [25] = 'File:Compass-icon bb W.svg', [26] = 'File:Compass-icon bb WbN.svg', [27] = 'File:Compass-icon bb WNW.svg', [28] = 'File:Compass-icon bb NWbW.svg', [29] = 'File:Compass-icon bb NW.svg', [30] = 'File:Compass-icon bb NWbN.svg', [31] = 'File:Compass-icon bb NNW.svg', [32] = 'File:Compass-icon bb NbW.svg' } -- =========================================================== -- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be -- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings. local SiteURL = { GeoHack = 'https://geohack.toolforge.org/geohack.php?pagename=$page¶ms=$lat_N_$lon_E_$attr&language=$lang', --GoogleEarth = '//geocommons.toolforge.org/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1', Proximityrama = 'https://tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon', WikimediaMap = 'https://maps.wikimedia.org/#16/$lat/$lon', --OpenStreetMap1 = '//wiwosm.toolforge.org/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon', OpenStreetMap1 = 'https://wikimap.toolforge.org/?wp=false&cluster=false&zoom=16&lat=$lat&lon=$lon', OpenStreetMap2 = 'https://tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level', GoogleMaps = { Mars = 'https://www.google.com/mars/#lat=$lat&lon=$lon&zoom=8', Moon = 'https://www.google.com/moon/#lat=$lat&lon=$lon&zoom=8', Earth = 'https://wp-world.toolforge.org/googlmaps-proxy.php?page=http://kmlexport.toolforge.org/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic' } } -- =========================================================== -- Categories local CoorCat = { -- File = '[[Category:Media with locations]]', -- Gallery = '[[Category:Galleries with coordinates]]', -- Category = '[[Category:Categories with coordinates]]', strucData0 = '[[Category:Pages with %s coordinates from %s]]', strucData1 = '[[Category:Pages with local %s coordinates and matching %s coordinates]]', strucData2 = '[[Category:Pages with local %s coordinates and similar %s coordinates]]', strucData3 = '[[Category:Pages with local %s coordinates and mismatching %s coordinates]]', strucData4 = '[[Category:Pages with local %s coordinates and missing %s coordinates]]', sHeading3 = '[[Category:Pages with local %s heading and mismatching %s heading]]', sHeading4 = '[[Category:Pages with local %s heading and missing %s heading]]', sHeading5 = '[[Category:Pages with local %s heading:0 and missing %s heading]]', globe = '[[Category:Media with %s locations]]', default = '[[Category:Media with default locations]]', attribute = '[[Category:Media with erroneous geolocation attributes]]', erroneous = '[[Category:Media with erroneous locations]]', dms = '[[Category:Media with coordinates in DMS format]]' } local globeLUT = { Q2='Earth', Q111='Mars', Q405='Moon'} local NoLatLonString = 'latitude, longitude' -- ======================================= -- === Local Functions =================== -- ======================================= ------------------------------------------------------------------------------- local function getProperty(entity, prop) return (core.parseStatements(entity:getBestStatements( prop ), nil) or {nil})[1] end -- =========================================================== local function add_maplink(lat, lon, marker, text) local tstr = '' if text then tstr = string.format('text="%s" ', text) end return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0" }'.. '}</maplink>', tstr, lat, lon, lon, lat, marker) end -- =========================================================== local function add_maplink2(lat1, lon1, lat2, lon2) return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons" }'.. '},{'.. ' "type": "Feature",'.. ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. ' "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata" }'.. '}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2) end -- =========================================================== local function info_box(text) return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '.. 'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'.. '<tr>'.. '<td class="mbox-image" style="padding-left:.9em;">'.. ' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'.. '<td class="mbox-text" style="">%s</td>'.. '</tr></table>', text) end -- =========================================================== local function distance(lat1, lon1, lat2, lon2) -- calculate distance local dLat = math.rad(lat1-lat2) local dLon = math.rad(lon1-lon2) local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2)) d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d)) -- angular distance in radians d = 6371000 * d -- radians to meters conversion d = math.floor(d+0.5) -- round it to even meters return d end -- =========================================================== local function getSDCoords(entity, prop) -- get coordinates from structured data (either wikidata or SDC) local coords = {id=entity.id, source=prop} if not entity or not entity.claims or not entity.claims[prop]then return coords end for _, statement in pairs( entity:getBestStatements( prop )) do local v = statement.mainsnak.datavalue.value -- get coordinates if v.latitude then coords.lat = v.latitude coords.lon = v.longitude coords.prec = v.precision or 1e-4 coords.prec = math.floor(coords.prec*111000) -- convert precision from degrees to meters and round coords.prec = math.max(math.min(coords.prec,111000),5) -- bound precision to a number between 5 meters and 1 degree coords.globe = string.gsub(v.globe, 'http://www.wikidata.org/entity/','') coords.globe = globeLUT[coords.globe] if statement.qualifiers and statement.qualifiers.P7787 then v = statement.qualifiers.P7787[1].datavalue.value if v.unit == "http://www.wikidata.org/entity/Q28390" then -- in degrees coords.heading = v.amount elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians coords.heading = v.amount*57.2957795131 end end return coords end end return coords end -- =========================================================== local function compareCoords(loc, sd, mode, source) -- compare coordinates --INPUTS: -- * loc - local coordinates -- * sd - structured data coords local coord = loc local cat, dist_str = '', '' local case, dist, qs, mapLink, message dist=0 if not loc.lat or not loc.lon then -- structured data/wikidata coordinates only coord = sd cat = string.format(CoorCat.strucData0, mode, source) case = 0 elseif loc.lat and loc.lon and not sd.lat and not sd.lon then cat = string.format(CoorCat.strucData4, mode, source) case = 4 -- local coordinates only elseif loc.lat and loc.lon and sd.lat and sd.lon then dist = distance(loc.lat, loc.lon, sd.lat, sd.lon) -- calculate distance dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', dist) -- will be displayed when hovering a mouse above wikidata icon if dist<20 or dist<sd.prec then -- will consider location within 20 meters or precision distance as the same if source=='Wikidata' then cat = string.format(CoorCat.strucData1, mode, source) end case = 1 elseif (dist<1000 or dist<5*sd.prec) and mode=='object' then --cat = string.format(CoorCat.strucData2, mode, source) case = 2 else -- locations 1 km off and 5 precision distances away are likely wrong. The issue might be with wrong precission mapLink = mw.getCurrentFrame():preprocess(add_maplink2(loc.lat, loc.lon, sd.lat, sd.lon)) -- fancy link to OSM message = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at %s (%s, precision: %i m). Please [[Commons:Structured data/Reconciliation|reconcile them]]. ", dist, source, mapLink, sd.prec) cat = string.format(CoorCat.strucData3, mode, source) .. info_box(message) case = 3 end end if not loc.heading and sd.heading then -- structured data/wikidata heading only coord.heading = sd.heading elseif loc.heading==0 and not sd.heading and sd.lat and sd.lon then -- local heading only cat = cat .. string.format(CoorCat.sHeading5, mode, source) elseif loc.heading and not sd.heading and sd.lat and sd.lon then -- local heading only cat = cat .. string.format(CoorCat.sHeading4, mode, source) elseif loc.heading and sd.heading then local dh = math.abs(math.fmod(loc.heading,360) - math.fmod(sd.heading,360)) if dh>1 and dh<359 then message = string.format("There is a discrepancy of %i degrees between the above camera heading (set to %i) and the ones stored at %s (set to %i). Please [[Commons:Structured data/Reconciliation|reconcile them]]. ", dh, loc.heading, source, sd.heading) cat = cat .. string.format(CoorCat.sHeading3, mode, source) .. info_box(message) end end if source=='Wikidata' and case>=3 then local url = mw.title.getCurrentTitle():canonicalUrl() local today = '+' .. os.date('!%F') .. 'T00:00:00Z/11' -- today's date in QS format qs = string.format('%s|P625|@%09.5f/%09.5f|S143|Q565|S813|%s|S4656|"%s"', sd.wID, loc.lat, loc.lon, today, url) qs = string.gsub (mw.uri.encode(qs),'%%2520','%%20') qs = 'https://quickstatements.toolforge.org/#/v1=' .. qs -- create full URL link qs = string.format("[[File:Commons_to_Wikidata_QuickStatements.svg|15px|link=%s|Copy geo coordinates to Wikidata]]", qs) end local ret = {dist_str=dist_str, case=case, qs=qs } return coord, cat, ret end -- Check if location of creation (P1071) is set local function checkLocationOfCreation(entity, lat, lon) local cat = '' local latFloor, lonFloor, latAbs, lonAbs if entity and entity.statements and entity.statements['P1071'] then return cat end latFloor = math.floor(lat) lonFloor = math.floor(lon) latAbs = math.abs(latFloor) lonAbs = math.abs(lonFloor) -- This is a rough bounding box of the Netherlands and part of neighbor countries as a pilot if (48 <= latFloor) and (latFloor < 54) and (0 <= lonFloor) and (lonFloor < 12) then cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° E)]]", latAbs, lonAbs) return cat -- Part of the United Kingdom elseif (50 <= latFloor) and (latFloor < 56) and (-5 <= lonFloor) and (lonFloor < 0) then cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° N, %s° W)]]", latAbs, lonAbs) return cat -- Canberra and Sydney elseif (-36 <= latFloor) and (latFloor < -33) and (149 <= lonFloor) and (lonFloor < 152 ) then cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° E)]]", latAbs, lonAbs) return cat -- Buenos Aires and Montevideo elseif (-36 <= latFloor) and (latFloor < -33) and (-58 <= lonFloor) and (lonFloor < -55) then cat = string.format("[[Category:Files with coordinates missing SDC location of creation (%s° S, %s° W)]]", latAbs, lonAbs) return cat end cat = '[[Category:Files with coordinates missing SDC location of creation]]' return cat end -- =========================================================== local function dms2deg_ ( d, m, s, h ) d,m,s = tonumber(d), tonumber(m), tonumber(s) if not (d and m and s and h) then return nil end local LUT = {N=1, S=-1, E=1, W=-1} -- look up table h = LUT[mw.ustring.upper( h )] if not h then return nil end return h * (d + m/60.0 + s/3600.0) end -- =========================================================== local function dms2deg ( dms ) local ltab = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/") local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4]) --return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "") .. '->' .. (degre or 'nil') return degre or dms end -- ======================================= -- === External Functions ================ -- ======================================= local p = {} p.debug = 'nothing' -- parse attribute variable returning desired field (used for debugging) function p.parseAttribute(frame) return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or '' end -- =========================================================== -- Helper core function for getHeading. function p._getHeading(attributes) if attributes == nil then return nil end local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)') if hStr == nil then return nil end local hNum = tonumber( hStr ) if hNum == nil then hStr = string.upper (hStr) hNum = compass_points[hStr] end if hNum then hNum = hNum%360 end return hNum end --[[============================================================================ Parse attribute variable returning heading field. If heading is a string than try to convert it to an angle ==============================================================================]] function p.getHeading(frame) local attributes if frame.args[1] then attributes = frame.args[1] elseif frame.args.attributes then attributes = frame.args.attributes else return '' end local hNum = p._getHeading(attributes) if hNum == nil then return '' end return tostring(hNum) end --[[============================================================================ Helper core function for deg2dms. deg2dms can be called by templates, while _deg2dms should be called from Lua. Inputs: * degree - positive coordinate in degrees * degPrec - coordinate precision in degrees will result in different angle format * lang - language to used when formatting the number ==============================================================================]] function p._deg2dms(degree, degPrec, lang) local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero local Lang = mw.language.new(lang) -- adjust number display based on precision secPrec = degPrec*3600.0 -- coordinate precision in seconds if secPrec<0.05 then -- degPrec<1.3889e-05 formatStr = '%s° %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 = core.getArgs(frame) local degree = tonumber(args[1]) local degPrec = tonumber(args[2]) or 0-- precision in degrees if degree==nil then return args[1]; else return p._deg2dms(degree, degPrec, args.lang) end end function p.dms2deg(frame) return dms2deg(frame.args[1]) end --[[============================================================================ Format coordinate location string, by creating and joining DMS strings for latitude and longitude. Also convert precision from meters to degrees. INPUTS: * lat = latitude in degrees * lon = longitude in degrees * lang = language code * prec = geolocation precision in meters ==============================================================================]] function p._lat_lon(lat, lon, prec, lang) lat = tonumber(lat) lon = tonumber(lon) prec = math.abs(tonumber(prec) or 0) if lon then -- get longitude to be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end if lat==nil or lon==nil then return NoLatLonString else local nsew = core.langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language local SN, EW, latStr, lonStr, lon2m, lat2m, phi if lat<0 then SN = nsew.S else SN = nsew.N end -- choose S or N depending on latitude degree sign if lon<0 then EW = nsew.W else EW = nsew.E end -- choose W or E depending on longitude degree sign lat2m=1 lon2m=1 if prec>0 then -- if user specified the precision of the geo location... phi = math.abs(lat)*math.pi/180 -- latitude in radiants lon2m = 6378137*math.cos(phi)*math.pi/180 -- see https://en.wikipedia.org/wiki/Longitude lat2m = 111000 -- average latitude degree size in meters end latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude degrees to degrees/minutes/seconds lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds return string.format('%s %s, %s %s', latStr, SN, lonStr, EW) --return string.format('<span class="latitude">%s %s</span>, <span class="longitude">%s %s</span>', latStr, SN, lonStr, EW) end end function p.lat_lon(frame) local args = core.getArgs(frame) return p._lat_lon(args.lat, args.lon, args.prec, args.lang) end --[[============================================================================ Helper core function for externalLink. Create URL for different sites: INPUTS: * site = Possible sites: GeoHack, GoogleEarth, Proximityrama, OpenStreetMap, GoogleMaps (for Earth, Mars and Moon) * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. * latStr = latitude string or number * lonStr = longitude string or number * lang = language code * attributes = attributes to be passed to GeoHack ==============================================================================]] function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level) local URLstr = SiteURL[site]; level = level or 1 local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' ) pageName = mw.ustring.gsub( pageName, '%%', '%%%%') if site == 'GoogleMaps' then URLstr = SiteURL.GoogleMaps[globe] elseif site == 'GeoHack' then attributes = string.format('globe:%s_%s', globe, attributes) URLstr = mw.ustring.gsub( URLstr, '$attr', attributes) end URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr) URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr) URLstr = mw.ustring.gsub( URLstr, '$lang' , lang) URLstr = mw.ustring.gsub( URLstr, '$level', level) URLstr = mw.ustring.gsub( URLstr, '$page' , pageName) URLstr = mw.ustring.gsub( URLstr, '+', '') URLstr = mw.ustring.gsub( URLstr, ' ', '_') return URLstr end --[[============================================================================ Create URL for different sites. INPUTS: * site = Possible sites: GeoHack, GoogleEarth, Proximityrama, OpenStreetMap, GoogleMaps (for Earth, Mars and Moon) * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. * lat = latitude string or number * lon = longitude string or number * lang = language code * attributes = attributes to be passed to GeoHack ==============================================================================]] function p.externalLink(frame) local args = core.getArgs(frame) return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '') end --[[============================================================================ Adjust GeoHack attributes depending on the template that calls it INPUTS: * attributes = attributes to be passed to GeoHack * mode = set by each calling template ==============================================================================]] function p.alterAttributes(attributes, mode, heading) -- indicate which template called it if mode=='camera' then -- Used by {{Location}} and {{Location dec}} if not string.find(attributes, 'type:camera') then attributes = 'type:camera_' .. attributes end elseif mode=='object' then -- Used by {{Object location}} if mode=='object' and not string.find(attributes, 'type:') then attributes = 'type:object_' .. attributes end if not string.find(attributes, 'class:object') then attributes = 'class:object_' .. attributes end elseif mode=='inline' then -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment) elseif mode=='user' then -- Used by {{User location}} attributes = 'type:user_location' elseif mode=='institution' then --Used by {{Institution/coordinates}} (categories only) attributes = 'type:institution' end local hStr = '' if heading then -- if heading is a number hStr = string.format('heading:%6.2f', heading) end if not string.find(attributes, 'heading:') then attributes = attributes .. '_' .. hStr else attributes = string.gsub(attributes,'heading:[^_]*', hStr) -- replace heading in form heading:N with heading=0 attributes = string.gsub(attributes,'__', '_') end return string.gsub(attributes,' ', '') end --[[============================================================================ Create link to GeoHack tool which displays latitude and longitude coordinates in DMS format INPUTS: * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. * lat = latitude in degrees * lon = longitude in degrees * lang = language code * prec = geolocation precision in meters * attributes = attributes to be passed to GeoHack ==============================================================================]] function p._GeoHack_link(args) -- create link and coordintate string local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang) if latlon==NoLatLonString then return latlon else local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '') return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion"> end end function p.GeoHack_link(frame) return p._GeoHack_link(core.getArgs(frame)) end --[[============================================================================ Create full external links section of {{Location}} or {{Object location}} templates, based on: * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. * mode = Possible options: - camera - call from {{location}} - object - call from {{Object location}} - globe - call from {{Globe location}} * lat = latitude in degrees * lon = longitude in degrees * lang = language code * namespace = namespace name: File, Category, (Gallery) ==============================================================================]] function p._externalLinksSection(args) local lang = args.lang if not args.namespace then args.namespace = mw.title.getCurrentTitle().nsText end local str, link1, link2, link3, link4 if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '') --link2 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '') str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang)) --link2, core.langSwitch(i18n.GoogleEarth, lang)) elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse) --link2 = p._externalLink('GoogleMaps' , 'Earth', args.lat, args.lon, lang, '', args.catRecurse) --link3 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '') --link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '') str = string.format('[%s %s]', link1, core.langSwitch(i18n.OpenStreetMaps, lang)) --link2, core.langSwitch(i18n.GoogleMaps, lang), --link3, core.langSwitch(i18n.GoogleEarth, lang), --link4, core.langSwitch(i18n.Proximityrama, lang)) elseif args.globe=='Mars' or args.globe=='Moon' then link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '') str = string.format('[%s %s]', link1, core.langSwitch(i18n.GoogleMaps, lang)) end return str end function p.externalLinksSection(frame) return p._externalLinksSection(core.getArgs(frame)) end --[[============================================================================ Core section of template:Location, template:Object location and template:Globe location. This method requires several arguments to be passed to it or it's parent method/template: * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. * mode = Possible options: - camera - call from {{location}} - object - call from {{Object location}} - globe - call from {{Globe location}} * lat = latitude in degrees * lon = longitude in degrees * attributes = attributes * lang = language code * namespace = namespace: File, Category, Gallery * prec = geolocation precision in meters ==============================================================================]] function p._LocationTemplateCore(args) -- prepare arguments if not (args.namespace) then -- if namespace not provided than look it up args.namespace = mw.title.getCurrentTitle().nsText end if args.namespace=='' then -- if empty than it is a gallery args.namespace = 'Gallery' end local bare = core.yesno(args.bare,false) local Status = 'primary' -- used by {{#coordinates:}} if core.yesno(args.secondary,false) then Status = 'secondary' end args.globe = mw.language.new('en'):ucfirst(args.globe or 'Earth') -- Convert coordinates from string to numbers local lat = tonumber(args.lat) local lon = tonumber(args.lon) local precission = tonumber(args.prec or '0') local heading = p._getHeading(args.attributes) -- get heading arrow section if lon then -- get longitude to be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end -- If wikidata link provided than compare coordinates local Categories, geoMicroFormat, coorTag, edit_icon, wikidata_link = '', '', '', '', '', '', '' local entity, coord, sd, cmp, locationCat local loc = {lat=lat, lon=lon, heading=heading, source='loc'} local ID = args.wikidata if (ID==nil) then entity = mw.wikibase.getEntity() if entity and args.namespace == 'Category' then -- this is category connected to Wikidata through sitelink ID = getProperty(entity, "P301") if getProperty(entity, "P31") == 'Q4167836' and ID then -- wikidata item is a "category item" with "category's main topic (P301)" -- follow P301 to the actual item for this category entity = mw.wikibase.getEntity(ID) end end elseif type(ID)=='string' and ID:match( '^[QqMm]%d+$' ) then entity = mw.wikibase.getEntity(ID) elseif type(ID)~='string' and ID.id then entity = ID -- entities can be passed from outside end if entity then if (args.mode=='object' or args.mode=='globe') then sd = getSDCoords(entity,'P9149') -- fetch coordinates of depicted place if not sd.lat then sd = getSDCoords(entity,'P625') -- fallback to coordinate location end elseif (args.mode=='camera') then sd = getSDCoords(entity,'P1259') -- fetch camera coordinates or coordinates of the point of view end if (args.namespace=='File') then -- look up lat/lon on SDC coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'SDC') if coord.source~='loc' then edit_icon = core.editAtSDC(coord.source, args.lang) lat, lon, heading, precission = coord.lat, coord.lon, coord.heading, coord.prec end elseif (args.namespace == 'Category') then -- look up lat/lon on wikidata sd.wID = entity.id coord, Categories, cmp = compareCoords(loc, sd, args.mode, 'Wikidata') if coord.source~='loc' then local str = "\n[[File:Wikidata-logo.svg|20px|Field with data from Wikidata's %s property<br/>%s|link=wikidata:%s#%s]]" edit_icon = core.editAtWikidata(entity.id, coord.source, args.lang) lat, lon, heading, precission = coord.lat, coord.lon, coord.heading, coord.prec end if cmp.qs then wikidata_link = cmp.qs end end elseif (args.namespace=='File') then Categories = string.format(CoorCat.strucData4, args.mode, 'SDC') end -- Check if location of creation (P1071) is set and if not, add tracker if args.namespace == 'File' and lat and lon then locationCat = checkLocationOfCreation(entity, lat, lon) if locationCat then Categories = Categories .. locationCat end end args.lat = string.format('%010.6f', lat or 0) args.lon = string.format('%011.6f', lon or 0) args.prec = precission args.attributes = p.alterAttributes(args.attributes or '', args.mode, heading) local frame = mw.getCurrentFrame() -- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then if lat and lon then -- if lat and lon are numbers... if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources Categories = Categories .. CoorCat.default end if args.attributes and string.find(args.attributes, '=') then Categories = Categories .. CoorCat.attribute end if (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors ) Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are outside allowed range)</span>\n' .. CoorCat.erroneous end -- local cat = CoorCat[args.namespace] -- if cat then -- add category based on namespace -- Categories = Categories .. cat -- end -- if not earth than add a category for each globe if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then Categories = Categories .. string.format(CoorCat[args.mode], args.globe) end -- add <span class="geo"> Geo (microformat) code: it is included for machine readability geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon) -- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } ) elseif args.namespace == 'File' and args.mode=='object' then coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } ) end else -- if lat and lon are not numbers then add error category Categories = Categories .. '<span style="color:red;font-weight:bold">Error: Invalid parameters! (coordinates are missing or not numeric)</span>\n' .. CoorCat.erroneous end end -- Call helper functions to render different parts of the template local coor, info_link, inner_table, OSM = '','','','','','' coor = p._GeoHack_link(args) -- the p and link to GeoHack coor = string.format('<span class=plainlinks>%s</span>%s', coor, edit_icon) if heading then local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1 local fname = heading_icon[k] coor = string.format('%s <span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading) end if args.globe=='Earth' then local icon = 'marker' if args.mode=='camera' then icon = 'camera' end OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM end local external_link = p._externalLinksSection(args) -- external link section if external_link and args.namespace == 'File' then external_link = core.langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template elseif external_link then external_link = core.langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template end info_link = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', core.langSwitch(i18n.COM_GEO, args.lang) ) inner_table = string.format('<td style="border:none;">%s %s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>', coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat) -- combine strings into a table local templateText if bare then templateText = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table) else -- choose name of the field and create row local field_name = 'Location' if args.mode=='camera' then field_name = core.langSwitch(i18n.CameraLocation, args.lang) elseif args.mode=='object' then field_name = core.langSwitch(i18n.ObjectLocation, args.lang) elseif args.mode=='globe' then local field_list = core.langSwitch(i18n.GlobeLocation, args.lang) if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized field_name = field_list[args.globe] end end templateText = string.format('<tr><th class="type fileinfo-paramfield">%s</th>%s</tr>', field_name, inner_table) --Create HTML text local dir = mw.language.new( args.lang ):getDir() -- get text direction local style = 'class="toccolours mw-content-%s layouttemplate commons-file-information-table" style="width: 100%%;" dir="%s" lang="%s"' style = string.format(style, dir, dir, args.lang) templateText = string.format('<table %s>\n%s\n</table>', style, templateText) end return templateText, Categories, coorTag end function p.LocationTemplateCore(frame) local args = core.getArgs(frame) args.namespace = mw.title.getCurrentTitle().nsText if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present if args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W" args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] ) args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] ) args.attributes = args.attributes or args[9] elseif args[2] and not (type(args[2])=='string' and args[2]:find(":")) then -- decimal format or DMS with one pipe, ex. "34° 05′ 32.36″ N| 116° 09′ 24.55″ W" args.lat = args[1] args.lon = args[2] args.attributes = args.attributes or args[3] elseif args[1] then -- detect a single argument in the form "34° 05′ 32.36″ N, 116° 09′ 24.55″ W" or similar local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter args.lat, args.lon = v[1], v[2] args.attributes = args.attributes or args[2] end end local cat = '' if args.lat and args.lon then local lat = tonumber(args.lat) local lon = tonumber(args.lon) if not lat or not lon then args.lat = dms2deg(args.lat or '') args.lon = dms2deg(args.lon or '') if (args.namespace == 'File' or args.namespace == 'Category') then cat = CoorCat.dms end end end local templateText, Categories, coorTag = p._LocationTemplateCore(args) return templateText .. Categories .. cat .. coorTag end return p