モジュール:Coordinates
このモジュールについての説明文ページを モジュール:Coordinates/doc に作成できます
--[[
__ __ _ _ ____ _ _ _
| \/ | ___ __| |_ _| | ___ _ / ___|___ ___ _ __ __| (_)_ __ __ _| |_ ___ ___
| |\/| |/ _ \ / _` | | | | |/ _ (_) | / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __|
| | | | (_) | (_| | |_| | | __/_| |__| (_) | (_) | | | (_| | | | | | (_| | || __/\__ \
|_| |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_| \__,_|_|_| |_|\__,_|\__\___||___/
This module is intended to provide functionality of {{location}} and related
templates. It was developed on Wikimedia Commons, so if you find this code on
other sites, check there for updates and discussions.
Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing
at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases.
Authors and maintainers:
* User:Jarekt
* User:Ebraminio
Functions:
*function p.LocationTemplateCore(frame)
**function p.GeoHack_link(frame)
***function p.lat_lon(frame)
****function p._deg2dms(deg,lang)
***function p.externalLink(frame)
****function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
**function p._getHeading(attributes)
**function p.externalLinksSection(frame)
***function p._externalLink(site, globe, latStr, lonStr, lang, attributes)
*function p.getHeading(frame)
*function p.deg2dms(frame)
]]
-- =======================================
-- === Dependencies ======================
-- =======================================
require('Module:No globals') -- used for debugging purposes as it detects cases of unintended global variables
local i18n = require('Module:I18n/coordinates') -- get localized translations of site names
local yesno = require('Module:Yesno')
-- =======================================
-- === Hardwired parameters ==============
-- =======================================
-- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]]
local compass_points = {
N = 0,
NBE = 11.25,
NNE = 22.5,
NEBN = 33.75,
NE = 45,
NEBE = 56.25,
ENE = 67.5,
EBN = 78.75,
E = 90,
EBS = 101.25,
ESE = 112.5,
SEBE = 123.75,
SE = 135,
SEBS = 146.25,
SSE = 157.5,
SBE = 168.75,
S = 180,
SBW = 191.25,
SSW = 202.5,
SWBS = 213.75,
SW = 225,
SWBW = 236.25,
WSW = 247.5,
WBS = 258.75,
W = 270,
WBN = 281.25,
WNW = 292.5,
NWBW = 303.75,
NW = 315,
NWBN = 326.25,
NNW = 337.5,
NBW = 348.75,
}
-- files to use for different headings
local heading_icon = {
[ 1] = 'File:Compass-icon bb N.svg',
[ 2] = 'File:Compass-icon bb NbE.svg',
[ 3] = 'File:Compass-icon bb NNE.svg',
[ 4] = 'File:Compass-icon bb NEbN.svg',
[ 5] = 'File:Compass-icon bb NE.svg',
[ 6] = 'File:Compass-icon bb NEbE.svg',
[ 7] = 'File:Compass-icon bb ENE.svg',
[ 8] = 'File:Compass-icon bb EbN.svg',
[ 9] = 'File:Compass-icon bb E.svg',
[10] = 'File:Compass-icon bb EbS.svg',
[11] = 'File:Compass-icon bb ESE.svg',
[12] = 'File:Compass-icon bb SEbE.svg',
[13] = 'File:Compass-icon bb SE.svg',
[14] = 'File:Compass-icon bb SEbS.svg',
[15] = 'File:Compass-icon bb SSE.svg',
[16] = 'File:Compass-icon bb SbE.svg',
[17] = 'File:Compass-icon bb S.svg',
[18] = 'File:Compass-icon bb SbW.svg',
[19] = 'File:Compass-icon bb SSW.svg',
[20] = 'File:Compass-icon bb SWbS.svg',
[21] = 'File:Compass-icon bb SW.svg',
[22] = 'File:Compass-icon bb SWbW.svg',
[23] = 'File:Compass-icon bb WSW.svg',
[24] = 'File:Compass-icon bb WbS.svg',
[25] = 'File:Compass-icon bb W.svg',
[26] = 'File:Compass-icon bb WbN.svg',
[27] = 'File:Compass-icon bb WNW.svg',
[28] = 'File:Compass-icon bb NWbW.svg',
[29] = 'File:Compass-icon bb NW.svg',
[30] = 'File:Compass-icon bb NWbN.svg',
[31] = 'File:Compass-icon bb NNW.svg',
[32] = 'File:Compass-icon bb NbW.svg'
}
-- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be
-- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings.
local SiteURL = {
GeoHack = '//tools.wmflabs.org/geohack/geohack.php?pagename=$page¶ms=$lat_N_$lon_E_$attr&language=$lang',
GoogleEarth = '//tools.wmflabs.org/geocommons/earth.kml?latdegdec=$lat&londegdec=$lon&scale=10000&commons=1',
Proximityrama = '//tools.wmflabs.org/geocommons/proximityrama?latlon=$lat,$lon',
WikimediaMap = '//maps.wikimedia.org/#16/$lat/$lon',
OpenStreetMap1 = '//tools.wmflabs.org/wiwosm/osm-on-ol/commons-on-osm.php?zoom=16&lat=$lat&lon=$lon',
OpenStreetMap2 = '//tools.wmflabs.org/osm4wiki/cgi-bin/wiki/wiki-osm.pl?project=Commons&article=$page&l=$level',
GoogleMaps = {
Mars = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8',
Moon = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8',
Earth = '//tools.wmflabs.org/wp-world/googlmaps-proxy.php?page=http://tools.wmflabs.org/kmlexport/%3Fproject%3DCommons%26article%3D$page&l=$level&output=classic'
}
}
-- Categories
local CoorCat = {
File = '[[Category:Media with locations]]',
Gallery = '[[Category:Galleries with coordinates]]',
Category = '[[Category:Categories with coordinates]]',
wikidata0 = '[[Category:Pages with coordinates from Wikidata]]',
wikidata1 = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]',
wikidata2 = '[[Category:Pages with local coordinates and similar Wikidata coordinates]]',
wikidata3 = '[[Category:Pages with local coordinates and mismatching Wikidata coordinates]]',
wikidata4 = '[[Category:Pages with local coordinates and missing Wikidata coordinates]]',
wikidata5 = '[[Category:Pages with locations and Wikidata ID to wrong type of entry]]',
sdc0 = '[[Category:Pages with coordinates from SDC]]',
sdc1 = '[[Category:Pages with local coordinates and matching SDC coordinates]]',
sdc2 = '[[Category:Pages with local coordinates and similar SDC coordinates]]',
sdc3 = '[[Category:Pages with local coordinates and mismatching SDC coordinates]]',
sdc4 = '[[Category:Pages with local coordinates and missing SDC coordinates]]',
globe = '[[Category:Media with %s locations]]',
default = '[[Category:Media with default locations]]',
attribute = '[[Category:Media with erroneous geolocation attributes]]',
erroneous = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n',
dms = '[[Category:Media with coordinates in DMS format]]'
}
-- =======================================
-- === Local Functions ===================
-- =======================================
local function normalize_input_args(input_args, output_args)
for name, value in pairs( input_args ) do
if value ~= '' then -- nuke empty strings
if type(name)=='string' then
name = string.lower(name)
end
output_args[name] = string.gsub(value, "^%s*(.-)%s*$", "%1") -- trim whitespaces from the beggining and the end of the string
end
end
return output_args
end
local function getArgs(frame)
local args = {}
args = normalize_input_args(frame:getParent().args, args)
args = normalize_input_args(frame.args, args)
if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then
args.lang = frame:callParserFunction("int","lang") -- get user's chosen language
end
return args
end
local NoLatLonString = 'latitude, longitude'
local function langSwitch(list,lang)
local langList = mw.language.getFallbacksFor(lang)
table.insert(langList,1,lang)
for i,language in ipairs(langList) do
if list[language] then
return list[language]
end
end
end
local function add_maplink(lat, lon, marker, text)
local tstr = ''
if text then
tstr = string.format('text="%s" ', text)
end
return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'..
' "type": "Feature",'..
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
' "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0" }'..
'}</maplink>', tstr, lat, lon, lon, lat, marker)
end
local function add_maplink2(lat1, lon1, lat2, lon2)
return string.format('<maplink zoom="13" latitude="%f" longitude="%f" class="no-icon">[{'..
' "type": "Feature",'..
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
' "properties": { "marker-symbol":"c", "marker-size": "large", "marker-color": "0050d0", "title": "Location on Wikimedia Commons" }'..
'},{'..
' "type": "Feature",'..
' "geometry": { "type":"Point", "coordinates":[%f, %f] },'..
' "properties": { "marker-symbol":"w", "marker-size": "large", "marker-color": "228b22", "title": "Location on Wikidata" }'..
'}]</maplink>', lat2, lon2, lon1, lat1, lon2, lat2)
end
local function info_box(text)
return string.format('<table class="messagebox plainlinks layouttemplate" style="border-collapse:collapse; border-width:2px; border-style:solid; width:100%%; clear: both; '..
'border-color:#f28500; background:#ffe;direction:ltr; border-left-width: 8px; ">'..
'<tr>'..
'<td class="mbox-image" style="padding-left:.9em;">'..
' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'..
'<td class="mbox-text" style="">%s</td>'..
'</tr></table>', text)
end
local function distance(lat1, lon1, lat2, lon2)
-- calculate distance
local dLat = math.rad(lat1-lat2)
local dLon = math.rad(lon1-lon2)
local d = math.pow(math.sin(dLat/2),2) + math.pow(math.sin(dLon/2),2) * math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
d = 2 * math.atan2(math.sqrt(d), math.sqrt(1-d)) -- angular distance in radians
d = 6371000 * d -- radians to meters conversion
d = math.floor(d+0.5) -- round it to even meters
return d
end
local function mergeWithWikidata(qID, lat1, lon1)
-- we are given wikidata q-code so look up the coordinates
local dist_str=''
local entity
-- Wikiata coordinates
if qID==nil then
entity = mw.wikibase.getEntity()
elseif type(qID)=='string' and qID:match( '^[Qq]%d+$' ) then
entity = mw.wikibase.getEntity(qID)
else
entity = qID
end
if not entity then
return lat1, lon1, nil, '', dist_str
end
qID = entity.id
local v, lat2, lon2, precision
if entity then
local P625 = entity:getBestStatements( 'P625' ) -- coordinate location
local P159 = entity:getBestStatements( 'P159' ) -- headquarters location
if P625[1] and P625[1].mainsnak.datavalue.value.latitude then
v = P625[1].mainsnak.datavalue.value
elseif P159[1] and P159[1].qualifiers and P159[1].qualifiers.P625 then
v = P159[1].qualifiers.P625[1].datavalue.value
end
if v and v.globe == 'http://www.wikidata.org/entity/Q2' then
lat2 = v.latitude
lon2 = v.longitude
precision = v.precision or 1e-4
precision = math.floor(precision*111000) -- convert precision from degrees to meters and round
precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree
end
end
-- compare coordinates
local cat = ''
if not lat1 or not lon1 then -- wikidata coordinates only
lat1 = lat2
lon1 = lon2
cat = CoorCat.wikidata0
elseif lat1 and lon1 and not lat2 and not lon2 then
cat = string.format('The above coordinates are missing from linked Wikidata item [[d:%s|%s]]. Click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span> to copy it",
qID, qID, qID, lat1, lon1)
cat = CoorCat.wikidata4 .. info_box(cat)
elseif lat1 and lon1 and lat2 and lon2 then
local d = distance(lat1, lon1, lat2, lon2) -- calculate distance
local frame = mw.getCurrentFrame()
local info = frame:preprocess(add_maplink2(lat1, lon1, lat2, lon2)) -- fancy link to OSM
info = string.format("There is a discrepancy of %i meters between the above coordinates and the ones stored at linked Wikidata item [[d:%s|%s]] (%s, precision: %i m). "..
'Please reconcile them. To copy Commons coordinates to Wikidata, click <span class=\"plainlinks\" title=\"Click to copy to wikidata\">'..
"[https://tools.wmflabs.org/quickstatements/index_old.html#v1=%s%%09P625%%09@%09.5f/%09.5f%%09S143%%09Q565 here]</span>",
d, qID, qID, info, precision, qID, lat1, lon1)
if d<20 or d<precision then -- will consider location within 20 meters or precisi0on distance as the same
cat = CoorCat.wikidata1
dist_str = string.format(' (discrepancy of %i meters between the above coordinates and the ones stored on Wikidata)', d) -- will be displayed when hovering a mouse above wikidata icon
elseif d>1000 and d>5*precision then -- locations 1 km off and 5 precision distances away are likely wrong
cat = CoorCat.wikidata3 .. info_box(info)
else
cat = CoorCat.wikidata2 .. info_box(info)
end
end
-- verify proper P31 (instance of). List is based on https://www.wikidata.org/wiki/Property_talk:P625
local QCodes = {
Q5 = 1, -- human
Q11879590 = 1, -- female given name
Q202444 = 1, -- given name
Q12308941 = 1, -- male given name
Q4167836 = 1, -- Wikimedia category
Q4167410 = 1, -- Wikimedia disambiguation page
Q783794 = 2, -- company
Q4830453 = 2, -- business enterprise
}
local s = entity:getBestStatements( 'P31' )
if s[1] and s[1].mainsnak.datavalue.value['id'] then
local instanceOf = s[1].mainsnak.datavalue.value['id']
if QCodes[instanceOf] then
cat = '' -- wipe out categories
if QCodes[instanceOf]==1 then -- add problem category
cat = CoorCat.wikidata5
end
end
end
return lat1, lon1, qID, cat, dist_str
end
local function mergeWithSDC(lat1, lon1, heading1)
-- we are given SDC m-code so look up the coordinates
local entity = nil
-- Wikiata coordinates
entity = mw.wikibase.getEntity()
if not entity and lat1 and lon1 then
return lat1, lon1, heading1, nil, CoorCat.sdc4
elseif not entity then
return lat1, lon1, heading1, nil, ''
end
local v, lat2, lon2, heading2, precision
if entity then
local P1259 = entity:getBestStatements( 'P1259' ) -- coordinates of the point of view
if P1259[1] and P1259[1].mainsnak.datavalue.value.latitude then
if P1259[1].qualifiers and P1259[1].qualifiers.P7787 then
v = P1259[1].qualifiers.P7787[1].datavalue.value
if v.unit == "http://www.wikidata.org/entity/Q28390" then -- in degrees
heading2 = v.amount
elseif v.unit == "http://www.wikidata.org/entity/Q33680" then -- in radians
heading2 = v.amount*57.2957795131
end
end
v = P1259[1].mainsnak.datavalue.value -- get coordinates
end
if v and v.globe == 'http://www.wikidata.org/entity/Q2' then
lat2 = v.latitude
lon2 = v.longitude
precision = v.precision or 1e-4
precision = math.floor(precision*111000) -- convert precision from degrees to meters and round
precision = math.max(math.min(precision,111000),5) -- bound precision to a number between 5 meters and 1 degree
end
end
-- compare coordinates
local cat = ''
if not lat1 or not lon1 then -- SDC coordinates only
lat1 = lat2
lon1 = lon2
heading1 = heading2
cat = CoorCat.sdc0
elseif lat1 and lon1 and not lat2 and not lon2 then
cat = CoorCat.sdc4
elseif lat1 and lon1 and lat2 and lon2 then
local d = distance(lat1, lon1, lat2, lon2) -- calculate distance
if d<20 or d<precision then -- will consider location within 20 meters or precision distance as the same
cat = CoorCat.sdc1
else
cat = CoorCat.sdc2
end
end
return lat1, lon1, heading1, entity.id, cat
end
local function dms2deg_ ( d, m, s, h )
d,m,s = tonumber(d), tonumber(m), tonumber(s)
if not (d and m and s and h) then
return nil
end
local LUT = {N=1, S=-1, E=1, W=-1} -- look up table
h = LUT[mw.ustring.upper( h )]
if not h then
return nil
end
return h * (d + m/60.0 + s/3600.0)
end
local function dms2deg ( dms )
local ltab = mw.text.split(dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", ""), "/")
local degre = dms2deg_ (ltab[1], ltab[2], ltab[3], ltab[4])
--return dms .. '->' .. dms:gsub("[°'′″\",%s]+" , "/" ):gsub("^%/", "") .. '->' .. (degre or 'nil')
return degre or dms
end
-- =======================================
-- === External Functions ================
-- =======================================
local p = {}
-- parse attribute variable returning desired field (used for debugging)
function p.parseAttribute(frame)
return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or ''
end
-- Helper core function for getHeading.
function p._getHeading(attributes)
if attributes == nil then
return nil
end
local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)')
if hStr == nil then
return nil
end
local hNum = tonumber( hStr )
if hNum == nil then
hStr = string.upper (hStr)
hNum = compass_points[hStr]
end
if hNum ~= nil then
hNum = hNum%360
end
return hNum
end
--[[============================================================================
Parse attribute variable returning heading field. If heading is a string than
try to convert it to an angle
==============================================================================]]
function p.getHeading(frame)
local attributes
if frame.args[1] then
attributes = frame.args[1]
elseif frame.args.attributes then
attributes = frame.args.attributes
else
return ''
end
local hNum = p._getHeading(attributes)
if hNum == nil then
return ''
end
return tostring(hNum)
end
--[[============================================================================
Helper core function for deg2dms. deg2dms can be called by templates, while
_deg2dms should be called from Lua.
Inputs:
* degree - positive coordinate in degrees
* degPrec - coordinate precision in degrees will result in different angle format
* lang - language to used when formatting the number
==============================================================================]]
function p._deg2dms(degree, degPrec, lang)
local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero
local Lang = mw.language.new(lang)
-- adjust number display based on precision
secPrec = degPrec*3600.0 -- coordinate precision in seconds
if secPrec<0.05 then -- degPrec<1.3889e-05
formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.SS″ format
c = 360000
elseif secPrec<0.5 then -- 1.3889e-05<degPrec<1.3889e-04
formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.S″ format
c = 36000
elseif degPrec*60.0<0.5 then -- 1.3889e-04<degPrec<0.0083
formatStr = '%s° %s′ %s″' -- use DD° MM′ SS″ format
c = 3600
elseif degPrec<0.5 then -- 0.0083<degPrec<0.5
formatStr = '%s° %s′' -- use DD° MM′ format
c = 60
else -- if degPrec>0.5 then
formatStr = '%s°' -- use DD° format
c = 1
end
-- create degree, minute and seconds numbers and string
d = c/60
k = math.floor(c*(degree%360)+0.49) -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding
dNum = math.floor(k/c) % 360 -- degree number (integer in 0-360 range)
mNum = math.floor(k/d) % 60 -- minute number (integer in 0-60 range)
sNum = 3600*(k%d) / c -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits)
dStr = Lang:formatNum(dNum) -- degree string
mStr = Lang:formatNum(mNum) -- minute string
sStr = Lang:formatNum(sNum) -- second string
zero = Lang:formatNum(0) -- zero string in local language
if mNum<10 then
mStr = zero .. mStr -- pad with zero if a single digit
end
if sNum<10 then
sStr = zero .. sStr -- pad with zero if less than ten
end
return string.format(formatStr, dStr, mStr, sStr);
end
--[[============================================================================
Convert degrees to degrees/minutes/seconds notation commonly used when displaying
coordinates.
Inputs:
1) latitude or longitude angle in degrees
2) georeference precision in degrees
3) language used in formatting of the number
==============================================================================]]
function p.deg2dms(frame)
local args = getArgs(frame)
local degree = tonumber(args[1])
local degPrec = tonumber(args[2]) or 0-- precision in degrees
if degree==nil then
return args[1];
else
return p._deg2dms(degree, degPrec, args.lang)
end
end
function p.dms2deg(frame)
return dms2deg(frame.args[1])
end
--[[============================================================================
Format coordinate location string, by creating and joining DMS strings for
latitude and longitude. Also convert precision from meters to degrees.
INPUTS:
* lat = latitude in degrees
* lon = longitude in degrees
* lang = language code
* prec = geolocation precision in meters
==============================================================================]]
function p._lat_lon(lat, lon, prec, lang)
lat = tonumber(lat)
lon = tonumber(lon)
prec = math.abs(tonumber(prec) or 0)
if lon then -- get longitude to be in -180 to 180 range
lon=lon%360
if lon>180 then
lon = lon-360
end
end
if lat==nil or lon==nil then
return NoLatLonString
else
local nsew = langSwitch(i18n.NSEW, lang) -- find set of localized translation of N, S, W and E in the desired language
local SN, EW, latStr, lonStr, lon2m, lat2m, phi
if lat<0 then SN = nsew.S else SN = nsew.N end -- choose S or N depending on latitude degree sign
if lon<0 then EW = nsew.W else EW = nsew.E end -- choose W or E depending on longitude degree sign
lat2m=1
lon2m=1
if prec>0 then -- if user specified the precision of the geo location...
phi = math.abs(lat)*math.pi/180 -- latitude in radiants
lon2m = 6378137*math.cos(phi)*math.pi/180 -- see https://en.wikipedia.org/wiki/Longitude
lat2m = 111000 -- average latitude degree size in meters
end
latStr = p._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude degrees to degrees/minutes/seconds
lonStr = p._deg2dms(math.abs(lon), prec/lon2m, lang) -- Convert longitude degrees to degrees/minutes/seconds
return string.format('%s %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 = getArgs(frame)
return p._lat_lon(args.lat, args.lon, args.prec, args.lang)
end
--[[============================================================================
Helper core function for externalLink. Create URL for different sites:
INPUTS:
* site = Possible sites: GeoHack, GoogleEarth, Proximityrama,
OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
Ganymede are also supported but are unused as of 2013.
* latStr = latitude string or number
* lonStr = longitude string or number
* lang = language code
* attributes = attributes to be passed to GeoHack
==============================================================================]]
function p._externalLink(site, globe, latStr, lonStr, lang, attributes, level)
local URLstr = SiteURL[site];
level = level or 1
local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' )
pageName = mw.ustring.gsub( pageName, '%%', '%%%%')
if site == 'GoogleMaps' then
URLstr = SiteURL.GoogleMaps[globe]
elseif site == 'GeoHack' then
attributes = string.format('globe:%s_%s', globe, attributes)
URLstr = mw.ustring.gsub( URLstr, '$attr', attributes)
end
URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr)
URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr)
URLstr = mw.ustring.gsub( URLstr, '$lang' , lang)
URLstr = mw.ustring.gsub( URLstr, '$level', level)
URLstr = mw.ustring.gsub( URLstr, '$page' , pageName)
URLstr = mw.ustring.gsub( URLstr, '+', '')
URLstr = mw.ustring.gsub( URLstr, ' ', '_')
return URLstr
end
--[[============================================================================
Create URL for different sites.
INPUTS:
* site = Possible sites: GeoHack, GoogleEarth, Proximityrama,
OpenStreetMap, GoogleMaps (for Earth, Mars and Moon)
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
Ganymede are also supported but are unused as of 2013.
* lat = latitude string or number
* lon = longitude string or number
* lang = language code
* attributes = attributes to be passed to GeoHack
==============================================================================]]
function p.externalLink(frame)
local args = getArgs(frame)
return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
end
--[[============================================================================
Adjust GeoHack attributes depending on the template that calls it
INPUTS:
* attributes = attributes to be passed to GeoHack
* mode = set by each calling template
==============================================================================]]
function p.alterAttributes(attributes, mode)
-- indicate which template called it
if mode=='camera' then -- Used by {{Location}} and {{Location dec}}
if string.find(attributes, 'type:camera')==nil then
attributes = 'type:camera_' .. attributes
end
elseif mode=='object'or mode =='globe' then -- Used by {{Object location}}
if mode=='object' and string.find(attributes, 'type:')==nil then
attributes = 'type:object_' .. attributes
end
if string.find(attributes, 'class:object')==nil then
attributes = 'class:object_' .. attributes
end
elseif mode=='inline' then -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment)
elseif mode=='user' then -- Used by {{User location}}
attributes = 'type:user_location'
elseif mode=='institution' then --Used by {{Institution/coordinates}} (categories only)
attributes = 'type:institution'
end
return attributes
end
--[[============================================================================
Create link to GeoHack tool which displays latitude and longitude coordinates
in DMS format
INPUTS:
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan,
Ganymede are also supported but are unused as of 2013.
* lat = latitude in degrees
* lon = longitude in degrees
* lang = language code
* prec = geolocation precision in meters
* attributes = attributes to be passed to GeoHack
==============================================================================]]
function p._GeoHack_link(args)
-- create link and coordintate string
local latlon = p._lat_lon(args.lat, args.lon, args.prec, args.lang)
if latlon==NoLatLonString then
return latlon
else
local url = p._externalLink('GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '')
return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion">
end
end
function p.GeoHack_link(frame)
return p._GeoHack_link(getArgs(frame))
end
--[[============================================================================
Create full external links section of {{Location}} or {{Object location}}
templates, based on:
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
* mode = Possible options:
- camera - call from {{location}}
- object - call from {{Object location}}
- globe - call from {{Globe location}}
* lat = latitude in degrees
* lon = longitude in degrees
* lang = language code
* namespace = namespace name: File, Category, (Gallery)
==============================================================================]]
function p._externalLinksSection(args)
local lang = args.lang
if not args.namespace then
args.namespace = mw.title.getCurrentTitle().nsText
end
local str, link1, link2, link3, link4
if args.globe=='Earth' and args.namespace~="Category" then -- Earth locations for files will have 2 links
link1 = p._externalLink('OpenStreetMap1', 'Earth', args.lat, args.lon, lang, '')
link2 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '')
str = string.format('[%s %s] - [%s %s]',
link1, langSwitch(i18n.OpenStreetMaps, lang),
link2, langSwitch(i18n.GoogleEarth, lang))
elseif args.globe=='Earth' and args.namespace=="Category" then -- Earth locations for categories will have 4 links
link1 = p._externalLink('OpenStreetMap2', 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
--link2 = p._externalLink('GoogleMaps' , 'Earth', args.lat, args.lon, lang, '', args.catRecurse)
link3 = p._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, '')
link4 = p._externalLink('Proximityrama' , 'Earth', args.lat, args.lon, lang, '')
str = string.format('[%s %s] - [%s %s] - [%s %s]',
link1, langSwitch(i18n.OpenStreetMaps, lang),
--link2, langSwitch(i18n.GoogleMaps, lang),
link3, langSwitch(i18n.GoogleEarth, lang),
link4, langSwitch(i18n.Proximityrama, lang))
elseif args.globe=='Mars' or args.globe=='Moon' then
link1 = p._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, '')
str = string.format('[%s %s]', link1, langSwitch(i18n.GoogleMaps, lang))
end
return str
end
function p.externalLinksSection(frame)
return p._externalLinksSection(getArgs(frame))
end
--[[============================================================================
Core section of template:Location, template:Object location and template:Globe location.
This method requires several arguments to be passed to it or it's parent method/template:
* globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013.
* mode = Possible options:
- camera - call from {{location}}
- object - call from {{Object location}}
- globe - call from {{Globe location}}
* lat = latitude in degrees
* lon = longitude in degrees
* attributes = attributes
* lang = language code
* namespace = namespace: File, Category, Gallery
* prec = geolocation precision in meters
==============================================================================]]
function p._LocationTemplateCore(args)
-- prepare arguments
if not (args.namespace) then -- if namespace not provided than look it up
args.namespace = mw.title.getCurrentTitle().nsText
end
if args.namespace=='' then -- if empty than it is a gallery
args.namespace = 'Gallery'
end
local bare = yesno(args.bare,false)
local Status = 'primary' -- used by {{#coordinates:}}
if yesno(args.secondary,false) then
Status = 'secondary'
end
args.globe = args.globe or 'Earth'
local attributes0 = args.attributes
args.attributes = p.alterAttributes(args.attributes or '', args.mode)
-- Convert coordinates from string to numbers
local lat = tonumber(args.lat)
local lon = tonumber(args.lon)
local heading = p._getHeading(attributes0) -- get heading arrow section
if lon then -- get longitude to be in -180 to 180 range
lon=lon%360
if lon>180 then
lon = lon-360
end
end
-- If wikidata link provided than compare coordinates
local Categories, geoMicroFormat, coorTag, wikidata_link = '', '', '', ''
if (args.mode=='object') and (args.namespace~='File') then
local dist_str, qID
-- look up the coordinates on Wikidata
lat, lon, qID, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon)
if qID then
wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, qID);
args.wikidata = args.wikidata or qID
end
elseif (args.mode=='camera') and (args.namespace=='File') then
local dist_str, mID
-- look up lat/lon on SDC
lat, lon, heading, mID, Categories = mergeWithSDC(lat, lon, heading)
if mID then
wikidata_link = "\n[[File:Commons structured data logo.svg|16px|Edit coordinates on Structured Data on Commons|link=]]";
end
end
args.lat = string.format('%010.6f', lat or 0)
args.lon = string.format('%011.6f', lon or 0)
local frame = mw.getCurrentFrame()
-- Categories, {{#coordinates}} and geoMicroFormat will be only added to File, Category and Gallery pages
if (args.namespace == 'File' or args.namespace == 'Category' or args.namespace == 'Gallery') then
if lat and lon then -- if lat and lon are numbers...
if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources
Categories = Categories .. CoorCat.default
end
if attributes0 and string.find(attributes0, '=') then
Categories = Categories .. CoorCat.attribute
end
if (math.abs(lon)>180) or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors )
Categories = Categories .. CoorCat.erroneous
end
local cat = CoorCat[args.namespace]
if cat then -- add category based on namespace
Categories = Categories .. cat
end
-- if not earth than add a category for each globe
if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then
Categories = Categories .. string.format(CoorCat[args.mode], args.globe)
end
-- add <span class="geo"> Geo (microformat) code: it is included for machine readability
geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon)
-- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData
if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then
coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } )
elseif args.namespace == 'File' and args.mode=='object' then
coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } )
end
else -- if lat and lon are not numbers then add error category
Categories = Categories .. CoorCat.erroneous
end
end
-- Call helper functions to render different parts of the template
local coor, info_link, inner_table, OSM = '','','','','',''
coor = p._GeoHack_link(args) -- the p and link to GeoHack
if heading then
local k = math.fmod(math.floor(0.5+math.fmod(heading+360,360)/11.25),32)+1
local fname = heading_icon[k]
coor = string.format('%s <span title="%s°">[[%s|25px|link=|alt=Heading=%s°]]</span>', coor, heading, fname, heading)
end
if args.globe=='Earth' then
local icon = 'marker'
if args.mode=='camera' then
icon = 'camera'
end
OSM = frame:preprocess(add_maplink(args.lat, args.lon, icon, '[[File:Openstreetmap logo.svg|20px|link=|Kartographer map based on OpenStreetMap.]]')) -- fancy link to OSM
end
local external_link = p._externalLinksSection(args) -- external link section
if external_link and args.namespace == 'File' then
external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template
elseif external_link then
external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template
end
info_link = string.format('[[File:OOjs UI icon help.svg|18x18px|alt=info|link=%s]]', langSwitch(i18n.COM_GEO, args.lang) )
inner_table = string.format('<td style="border:none;">%s %s</td><td style="border:none;">%s</td><td style="border:none;">%s%s%s</td>',
coor, OSM, external_link or '', wikidata_link, info_link, geoMicroFormat)
-- combine strings into a table
local templateText
if bare then
templateText = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table)
else
-- choose name of the field
local field_name = 'Location'
if args.mode=='camera' then
field_name = langSwitch(i18n.CameraLocation, args.lang)
elseif args.mode=='object' then
field_name = langSwitch(i18n.ObjectLocation, args.lang)
elseif args.mode=='globe' then
local field_list = langSwitch(i18n.GlobeLocation, args.lang)
if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized
field_name = field_list[args.globe]
end
end
--Create HTML text
local dir, text_align
if mw.language.new( args.lang ):isRTL() then
dir = 'rtl'
text_align = 'right'
else
dir = 'ltr'
text_align = 'left'
end
local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"',
args.lang, dir, text_align, args.lang)
templateText = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', args.lang, style, field_name, inner_table)
end
return templateText, Categories, coorTag
end
function p.LocationTemplateCore(frame)
local args = getArgs(frame)
args.namespace = mw.title.getCurrentTitle().nsText
if not args.lat and not args.lon then -- if no lat and lon but numbered arguments present
if args[4] then -- DMS with pipes format, ex. "34|5|32.36|N|116|9|24|55|W"
args.lat = dms2deg_ ( args[1], args[2], args[3], args[4] )
args.lon = dms2deg_ ( args[5], args[6], args[7], args[8] )
args.attributes = args.attributes or args[9]
elseif args[2] and not (type(args[2])=='string' and args[2]:find(":")) then -- decimal format or DMS with one pipe, ex. "34° 05′ 32.36″ N| 116° 09′ 24.55″ W"
args.lat = args[1]
args.lon = args[2]
args.attributes = args.attributes or args[3]
elseif args[1] then -- detect a single argument in the form "34° 05′ 32.36″ N, 116° 09′ 24.55″ W" or similar
local v = mw.text.split(args[1]:gsub("([NnSs])", "%1/" ), "/") -- split into lat and lon using splitting point after any letter
args.lat, args.lon = v[1], v[2]
args.attributes = args.attributes or args[2]
end
end
local cat = ''
if args.lat and args.lon then
local lat = tonumber(args.lat)
local lon = tonumber(args.lon)
if not lat or not lon then
args.lat = dms2deg(args.lat or '')
args.lon = dms2deg(args.lon or '')
if (args.namespace == 'File' or args.namespace == 'Category') then
cat = CoorCat.dms
end
end
end
local templateText, Categories, coorTag = p._LocationTemplateCore(args)
return templateText .. Categories .. cat .. coorTag
end
return p