Module:Coordinates
Jump to navigation
Jump to search
This Lua module is used on 895,600 pages. To avoid large-scale disruption and unnecessary server load, any changes to this module should first be tested in its /sandbox or /testcases subpages. The tested changes can then be added to this page in one single edit. Please consider discussing any changes on the talk page before implementing them. |
--[[ This module is intended to provide functionality of {{location}} and related templates. 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 coordinates.LocationTemplateCore(frame) **function coordinates.GeoHack_link(frame) ***function coordinates.lat_lon(frame) ****function coordinates._deg2dms(deg,lang) ***function coordinates.externalLink(frame) ****function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes) **function coordinates._getHeading(attributes) **function coordinates.externalLinksSection(frame) ***function coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes) *function coordinates.getHeading(frame) *function coordinates.deg2dms(frame) ]] coordinates = {}; -- ======================================= -- === Dependencies ====================== -- ======================================= local i18n = require('Module:I18n/coordinates') -- get localized translations of site names local Fallback = require('Module:Fallback') -- get fallback functions local yesno = require('Module:Yesno') -- ======================================= -- === Hardwired parameters ============== -- ======================================= -- Angles associated with each abriviation 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, } -- 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 = '{{fullurl:toollabs:geocommons/earth.kml|latdegdec=$lat&londegdec=$lon&scale=10000&commons=1}}', Proximityrama = '{{fullurl:toollabs:geocommons/proximityrama|latlon=$lat,$lon}}', OpenStreetMap = '{{fullurl:toollabs:wiwosm/osm-on-ol/commons-on-osm.php|zoom=16&lat=$lat&lon=$lon}}', GoogleMaps = { Mars = '//www.google.com/mars/#lat=$lat&lon=$lon&zoom=8', Moon = '//www.google.com/moon/#lat=$lat&lon=$lon&zoom=8', Earth = 'https://maps.google.com/maps?ll=$lat,$lon&spn=0.01,0.01&t=k&q=http://tools.wmflabs.org/geocommons/web.kml&hl=$lang' } } -- Categories local CoorCat = { File = '[[Category:Media with locations]]', Gallery = '[[Category:Galleries with coordinates]]', Category = '[[Category:Categories with coordinates]]', globe = '[[Category:Media with %s locations]]', default = '[[Category:Media with default locations]]', erroneous = '[[Category:Media with erroneous locations]]<span style="color:red;font-weight:bold">Error: Invalid parameters!</span>\n' } local NoLatLonString = 'latitude, longitude' -- ======================================= -- === Functions ========================= -- ======================================= -- parse attribute variable returning desired field function coordinates.parseAttribute(frame) return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or '' end --[[============================================================================ Parse attribute variable returning heading field. If heading is a string than try to convert it to an angle ==============================================================================]] function coordinates.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 = coordinates._getHeading(attributes) if hNum == nil then return '' end return tostring(hNum) end -- Helper core function for getHeading. function coordinates._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 --[[============================================================================ Convert degrees to degrees/minutes/seconds notation comonly used when displaying coordinates. Inputs: 1) latitude or longitude angle in degrees 2) georeference precission in degrees 3) language used in formating of the number ==============================================================================]] function coordinates.deg2dms(frame) local deg = tonumber(frame.args[1]) local degPrec = tonumber(frame.args[2]) or 0-- precision in degrees local lang if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then lang = frame.args.lang else -- get user's chosen language lang = mw.message.new( "lang" ):plain() end if deg==nil then return frame.args[1]; else return coordinates._deg2dms(deg, degPrec, lang) end end --[[============================================================================ Helper core function for deg2dms. deg2dms can be called by templates, while _deg2dms should be called from Lua. Inputs: * deg - positive coordinate in degrees * degPrec - coordinate precission in degrees will result in different angle format * lang - language to used when formating the number ==============================================================================]] function coordinates._deg2dms(deg, degPrec, lang) local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k local Lang = mw.language.new(lang) -- adjust number display based on precission secPrec = degPrec*3600.0 -- coordinate precission 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*(deg%360)+0.49) -- convert float to an integer. This step HAS to be identical for all conversions to avoid incorrect results due to different rounding dNum = math.floor(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 if mNum<10 then mStr = '0' ..mStr -- pad with zero if a single digit end if sNum<10 then sStr = '0' ..sStr -- pad with zero if less than ten end return string.format(formatStr, dStr, mStr, sStr); end --[[============================================================================ Format coordinate location string, by creating and joining DMS strings for latutude and longitude. Also convert precission from meters to degrees. INPUTS: * lat = latitude in degrees * lon = longitude in degrees * lang = language code * prec = geolocation precission in meters ==============================================================================]] function coordinates.lat_lon(frame) local lat = tonumber(frame.args.lat) local lon = tonumber(frame.args.lon) local prec = math.abs(tonumber(frame.args.prec) or 0) -- location precision in meters if lon then -- get longitude t0 be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end local lang if frame.args.lang and mw.language.isSupportedLanguage(frame.args.lang) then lang = frame.args.lang else -- get user's chosen language lang = mw.message.new( "lang" ):plain() end if lat==nil or lon==nil then return NoLatLonString else local nsew = Fallback._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 precission 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 = coordinates._deg2dms(math.abs(lat), prec/lat2m, lang) -- Convert latitude degrees to degrees/minutes/seconds lonStr = coordinates._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 --[[============================================================================ Create URL for different sites. INPUTS: * site = Possinle 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 coordinates.externalLink(frame) args = frame.args if args.lang and mw.language.isSupportedLanguage(args.lang) then lang = args.lang else -- get user's chosen language lang = mw.message.new( "lang" ):plain() end local str = coordinates._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, lang, args.attributes or '') return frame:preprocess(str) end --[[============================================================================ Helper core function for externalLink. Create URL for different sites: INPUTS: * site = Possinle 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 coordinates._externalLink(site, globe, latStr, lonStr, lang, attributes) local URLstr = SiteURL[site]; if site == 'GoogleMaps' then URLstr = SiteURL.GoogleMaps[globe] elseif site == 'GeoHack' then attributes = string.format('globe:%s_%s', globe, attributes) local pageName = mw.uri.encode( mw.title.getCurrentTitle().prefixedText, 'WIKI' ) pageName = mw.ustring.gsub( pageName, '%%', '%%%%') URLstr = mw.ustring.gsub( URLstr, '$attr', attributes) URLstr = mw.ustring.gsub( URLstr, '$page', pageName) URLstr = mw.ustring.gsub( URLstr, ' ', '_') end URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr) URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr) URLstr = mw.ustring.gsub( URLstr, '$lang', lang) return URLstr 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 coordinates.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 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 precission in meters * attributes = attributes to be passed to GeoHack ==============================================================================]] function coordinates.GeoHack_link(frame) -- create link and coordintate string local latlon = coordinates.lat_lon(frame) if latlon==NoLatLonString then return latlon else frame.args.site = 'GeoHack' local url = coordinates.externalLink(frame) return string.format('<span class="plainlinksneverexpand">[%s %s]</span>', url, latlon) --<span class="plainlinks nourlexpansion"> 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 coordinates.externalLinksSection(frame) args = frame.args if args.lang and mw.language.isSupportedLanguage(args.lang) then lang = args.lang else -- get user's chosen language lang = mw.message.new( "lang" ):plain() end if not args.namespace then args.namespace = mw.title.getCurrentTitle().namespace end local str if args.globe=='Earth' then -- Earth locations will have 3 or 4 links str = string.format('[%s %s] - [%s %s]', -- - [%s %s]', coordinates._externalLink('OpenStreetMap', 'Earth', args.lat, args.lon, lang, ''), Fallback._langSwitch(i18n.OpenStreetMaps, lang), -- GoogleMap link no longer works due to changes in the website software --coordinates._externalLink('GoogleMaps' , 'Earth', args.lat, args.lon, lang, ''), --Fallback._langSwitch(i18n.GoogleMaps, lang), coordinates._externalLink('GoogleEarth' , 'Earth', args.lat, args.lon, lang, ''), Fallback._langSwitch(i18n.GoogleEarth, lang)) if args.namespace=="Category" then str = string.format('%s - [%s %s]', str, coordinates._externalLink('Proximityrama', 'Earth', args.lat, args.lon, lang, ''), Fallback._langSwitch(i18n.Proximityrama, lang)) end elseif args.globe=='Mars' or args.globe=='Moon' then str = string.format('[%s %s]', coordinates._externalLink('GoogleMaps', args.globe, args.lat, args.lon, lang, ''), Fallback._langSwitch(i18n.GoogleMaps, lang)) end return frame:preprocess(str) -- use preprocess to expand {{#fullurl}} --return str 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 metchod/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 precission in meters ==============================================================================]] function coordinates.LocationTemplateCore(frame) -- prepare arguments args = frame.args if not args or not args.lat then -- if no arguments provided than use parent arguments args = mw.getCurrentFrame():getParent().args end if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then args.lang = mw.message.new( "lang" ):plain() -- get user's chosen language end if not (args.namespace) then -- if namespace not provided than look it up args.namespace = mw.title.getCurrentTitle().namespace end if args.namespace=='' then -- if empty than it is a gallery args.namespace = 'Gallery' end local bare = yesno(args.bare,false) local Status = 'primary' -- used by {{#coordinates:}} if yesno(args.secondary,false) then Status = 'secondary' end args.attributes = coordinates.alterAttributes(args.attributes or '', args.mode) frame.args = args -- check for errors and add Geo (microformat) code for machine readability. local lat = tonumber(args.lat) local lon = tonumber(args.lon) if lon then -- get longitude t0 be in -180 to 180 range lon=lon%360 if lon>180 then lon = lon-360 end end local Categories, geoMicroFormat, coorTag = '', '', '' -- 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 = CoorCat.default end if args.noError==0 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 = string.format('{{#coordinates:primary|%10.6f|%11.6f|%s}}', 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 str1, str2, str3, str4, inner_table, heading str1 = coordinates.GeoHack_link(frame) -- the coordinates and link to GeoHack heading = coordinates._getHeading(frame.args.attributes) -- get heading arrow section if heading then --str1 = string.format('%s <span style="{{Transform-rotate|%f}}">[[File:North Pointer.svg|20px|link=|alt=]]</span>', str1, 360-heading) local fname = string.format('{{Compass rose file|%f|style=heading}}', heading) str1 = string.format('%s <span title="%s°">[[%s|25px|link=|alt=%s°]]</span>', str1, heading, fname, heading) end str2 = Fallback._langSwitch(i18n.LocationTemplateLinkLabel, args.lang) -- header of the link section str3 = coordinates.externalLinksSection(frame) or '' -- external link section str4 = '[[File:Circle-information.svg|18x18px|alt=info|link=Commons:Geocoding]]' inner_table = string.format('<td style="border:none;">%s</td><td style="border:none;">%s %s</td><td style="border:none;">%s%s</td>', str1, str2, str3, str4, geoMicroFormat) -- 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 = Fallback._langSwitch(i18n.CameraLocation, args.lang) elseif args.mode=='object' then field_name = Fallback._langSwitch(i18n.ObjectLocation, args.lang) elseif args.mode=='globe' then field_list = Fallback._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 local style = frame:expandTemplate{ title="Infobar-Layout", args={ ["lang"] = lang, ["class"] = 'commons-file-information-table' } } templateText = string.format('<table lang="%s" %s><tr><th class="type fileinfo-paramfield">%s</th>%s</tr></table>', lang, style, field_name, inner_table) end return frame:preprocess(templateText .. Categories .. coorTag) end return coordinates