Module:Coordinates
Jump to navigation
Jump to search
Lua
CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules
Careful, this function is live, and called by {{Location}} template. The documentation might not be complete
This module provides most of the logic behind {{Location}} and related geolocation templates. It provides several methods visible to the templates:
- {{#Invoke:Coordinates | parseAttribute| attribute string | attribute name }} : parse {{Location}} attribute parameter attribute string and return value of the attribute name parameter
- {{#Invoke:Coordinates | getHeader| attribute string }} : parse {{Location}} attribute parameter attribute string and return the numeric value of the header attribute.
- {{#Invoke:Coordinates | GeoHack_link| lat=... | lon=... |lang=xx | site=... | globe=... }}: creates link to GeoHack tool and display location coordinates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth.
- {{#Invoke:Coordinates | lat_lon| lat=... | lon=... |lang=xx }}: create coordinate location string based on decimal degrees latitude and longitude number. Language is used to localize the presentation of the numbers.
- {{#Invoke:Coordinates | deg2dms| degrees|lang=xx }}: create dms (degree/minute/second) string based on decimal degrees number. Language is used to localize the presentation of the numbers.
- {{#Invoke:Coordinates | externalLink| site=... | globe=... | lat=... | lon=... |lang=xx }}: create URLs for different sites which are used by coordinate location templates. The URLs are based on website and latitude/longitude coordinates. Language is used so it can be passes to the website. Globe parameter is used to allow specifying coordinates on planets other than earth.
Examples
Functions:
- deg2dms(degree, degree_precision, language)
- {{#invoke:Coordinates| deg2dms | 12.3456789}} will display "12° 20′ 44.44″"
- {{#invoke:Coordinates| deg2dms | 12.3456789 |1}} will display "12°"
- {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-1}} will display "12° 21′"
- {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-3}} will display "12° 20′ 44″"
- {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-4}} will display "12° 20′ 44.4″"
- {{#invoke:Coordinates| deg2dms | 12.3456789 |1e-5}} will display "12° 20′ 44.44″"
- lat_lon
- {{#invoke:Coordinates| lat_lon | lat=51.48 | lon=0}} will display "51° 28′ 48″ N, 0° 00′ 00″ E"
- GeoHack_link
- {{#invoke:Coordinates| GeoHack_link | lat=51.48123 | lon=0}} will display
- {{#invoke:Coordinates| GeoHack_link | lat=51.48123 | lon=0 | lang=en }} will display
- {{#invoke:Coordinates| GeoHack_link | lat=51.48123 | lon=0 | lang=ru }} will display
- {{#invoke:Coordinates| GeoHack_link | lat= | lon=0 | lang=ru }} (with missing latitude value) will display "latitude, longitude"
- externalLinksSection
- {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "OpenStreetMap"
- {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=Category}} displays "OpenStreetMap"
- {{#invoke:Coordinates| externalLinksSection | globe = Earth| lat=51.48 | lon=0 | lang=ru | namespace=Category}} displays "OpenStreetMap"
- {{#invoke:Coordinates| externalLinksSection | globe = Mars| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
- {{#invoke:Coordinates| externalLinksSection | globe = Moon| lat=51.48 | lon=0 | lang=en | namespace=File}} displays "Google Maps"
- LocationTemplateCore
- {{#invoke:Coordinates| LocationTemplateCore | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File| attributes=elevation:10_heading:W | mode=camera | bare = 1| secondary=1}} displays:
View all coordinates using: OpenStreetMap |
- {{#invoke:Coordinates| LocationTemplateCore | globe = Earth| lat=51.48 | lon=0 | lang=en | namespace=File| attributes=elevation:10_heading:W | mode=camera | bare = 0| secondary=1}} displays:
Camera location | View all coordinates using: OpenStreetMap |
---|
See testcases to see more examples.
Dependencies
Relies on Module:I18n/coordinates for all of the internationalization translations.
Code
--[[
__ __ _ _ ____ _ _ _
| \/ | ___ __| |_ _| | ___ _ / ___|___ ___ _ __ __| (_)_ __ __ _| |_ ___ ___
| |\/| |/ _ \ / _` | | | | |/ _ (_) | / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
| | | | (_) | (_| | |_| | | __/_| |__| (_) | (_) | | | (_| | | | | | (_| | || __/\__ \
|_| |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_| \__,_|_|_| |_|\__,_|\__\___||___/
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',
OpenStreetMap2 = 'https://osm4wiki.toolforge.org/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
-- ============================================================================
local function P625_categories(args, entity)
local cat = ''
local data = {}
if ((not entity.id:match( '^[Mm]%d+$' )) or (not getProperty(entity, 'P625'))) then
return cat
end
local P625 = getSDCoords(entity,'P625')
local P1259 = getSDCoords(entity,'P1259')
local P9149 = getSDCoords(entity,'P9149')
if (P1259.lat and distance(P625.lat, P625.lon, P1259.lat, P1259.lon)<=1) then
cat = '[[Category:Media with the same P625 and P1259 coordinates]]'
elseif (P1259.lat) then
cat = '[[Category:Media with P625 and P1259 coordinates]]'
end
if (P9149.lat and distance(P625.lat, P625.lon, P9149.lat, P9149.lon)<=1) then
cat = cat .. '[[Category:Media with the same P625 and P9149 coordinates]]'
elseif (P9149.lat) then
cat = cat .. '[[Category:Media with P625 and P9149 coordinates]]'
end
if (cat=='' and args.lat and distance(P625.lat, P625.lon, args.lat, args.lon)<=1) then
cat = cat .. string.format('[[Category:Media with the same P625 and %s coordinates]]', args.mode)
end
if (cat=='' and args.lat) then
cat = string.format('[[Category:Media with P625 and %s coordinates]]', args.mode)
else
cat = '[[Category:Media with P625 coordinates]]'
end
return cat
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, mode=args.mode, 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
Categories = Categories .. P625_categories(loc, entity)
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 = 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|class=skin-invert]]', 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 layouttemplate commons-file-information-table" style="width: 100%%;" dir="%s" lang="%s"'
style = string.format(style, 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