Line 1: |
Line 1: |
| --[[ | | --[[ |
− | This module is intended to replace the functionality of {{Coord}} and related
| + | __ __ _ _ ____ _ _ _ |
− | templates. It provides several methods, including
| + | | \/ | ___ __| |_ _| | ___ _ / ___|___ ___ _ __ __| (_)_ __ __ _| |_ ___ ___ |
| + | | |\/| |/ _ \ / _` | | | | |/ _ (_) | / _ \ / _ \| '__/ _` | | '_ \ / _` | __/ _ \/ __| |
| + | | | | | (_) | (_| | |_| | | __/_| |__| (_) | (_) | | | (_| | | | | | (_| | || __/\__ \ |
| + | |_| |_|\___/ \__,_|\__,_|_|\___(_)\____\___/ \___/|_| \__,_|_|_| |_|\__,_|\__\___||___/ |
| + | |
| | | |
− | {{#invoke:Coordinates | coord }} : General function formatting and displaying | + | This module is intended to provide functionality of {{location}} and related |
− | coordinate values.
| + | templates. It was developed on Wikimedia Commons, so if you find this code on |
| + | other sites, check there for updates and discussions. |
| | | |
− | {{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
| + | Please do not modify this code without applying the changes first at Module:Coordinates/sandbox and testing |
− | degree values to DMS format.
| + | at Module:Coordinates/sandbox/testcases and Module talk:Coordinates/sandbox/testcases. |
| | | |
− | {{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
| + | Authors and maintainers: |
− | to decimal degree format.
| + | * User:Jarekt |
| + | * User:Ebraminio |
| | | |
− | {{#invoke:Coordinates | link }} : Export the link used to reach the tools
| + | Functions: |
| + | *function p.LocationTemplateCore(frame) |
| + | **function p.GeoHack_link(frame) |
| + | ***function p.lat_lon(frame) |
| + | ****function p._deg2dms(deg,lang) |
| + | ***function p.externalLink(frame) |
| + | ****function p._externalLink(site, globe, latStr, lonStr, lang, attributes) |
| + | **function p._getHeading(attributes) |
| + | **function p.externalLinksSection(frame) |
| + | ***function p._externalLink(site, globe, latStr, lonStr, lang, attributes) |
| + | *function p.getHeading(frame) |
| + | *function p.deg2dms(frame) |
| | | |
| ]] | | ]] |
| | | |
− | require('Module:No globals') | + | -- ======================================= |
| + | -- === 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') |
| | | |
− | local math_mod = require("Module:Math")
| + | -- ======================================= |
− | local coordinates = {};
| + | -- === Hardwired parameters ============== |
| + | -- ======================================= |
| | | |
− | local current_page = mw.title.getCurrentTitle() | + | -- Angles associated with each abbreviation of compass point names. See [[:en:Points of the compass]] |
− | local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
| + | local compass_points = { |
− | local coord_link = '//tools.wmflabs.org/geohack/geohack.php?pagename=' .. page_name .. '¶ms='
| + | 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, |
| + | } |
| | | |
− | --[[ Helper function, replacement for {{coord/display/title}} ]] | + | -- files to use for different headings |
− | local function displaytitle(s, notes)
| + | local heading_icon = { |
− | local l = "[[Geographic coordinate system|Coordinates]]: " .. s | + | [ 1] = 'File:Compass-icon bb N.svg', |
− | local co = '<span id="coordinates">' .. l .. notes .. '</span>'; | + | [ 2] = 'File:Compass-icon bb NbE.svg', |
− | return '<span style="font-size: small;">' .. co .. '</span>'; | + | [ 3] = 'File:Compass-icon bb NNE.svg', |
− | end
| + | [ 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' |
| + | } |
| | | |
− | --[[ Helper function, Replacement for {{coord/display/inline}} ]] | + | -- URL definitions for different sites. Strings: $lat, $lon, $lang, $attr, $page will be |
− | local function displayinline(s, notes)
| + | -- replaced with latitude, longitude, language code, GeoHack attribution parameters and full-page-name strings. |
− | return s .. notes
| + | local SiteURL = { |
− | end
| + | 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' |
| + | } |
| + | } |
| | | |
− | --[[ Helper function, used in detecting DMS formatting ]] | + | -- Categories |
− | local function dmsTest(first, second)
| + | local CoorCat = { |
− | if type(first) ~= 'string' or type(second) ~= 'string' then | + | File = '[[Category:Media with locations]]', |
− | return nil
| + | Gallery = '[[Category:Galleries with coordinates]]', |
− | end | + | Category = '[[Category:Categories with coordinates]]', |
− | local s = (first .. second):upper() | + | wikidata0 = '[[Category:Pages with coordinates from Wikidata]]', |
− | return s:find('^[NS][EW]$') or s:find('^[EW][NS]$') | + | wikidata1 = '[[Category:Pages with local coordinates and matching Wikidata coordinates]]', |
− | end
| + | 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]]', |
| + | 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' |
| + | } |
| | | |
| + | -- ======================================= |
| + | -- === Local Functions =================== |
| + | -- ======================================= |
| | | |
− | --[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
| + | local function getArgs(frame) |
− | local function makeInvokeFunc(funcName) | + | local args = frame.args |
− | return function (frame) | + | if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then |
− | local args = require('Module:Arguments').getArgs(frame, {
| + | args.lang = frame:callParserFunction("int","lang") -- get user's chosen language |
− | wrappers = 'Template:Coord'
| |
− | })
| |
− | return coordinates[funcName](args, frame)
| |
| end | | end |
| + | return args |
| end | | end |
| | | |
− | --[[ Helper function, handle optional args. ]]
| + | local NoLatLonString = 'latitude, longitude' |
− | local function optionalArg(arg, supplement) | |
− | return arg and arg .. supplement or ''
| |
− | end
| |
| | | |
− | --[[
| + | local function langSwitch(list,lang) |
− | Formats any error messages generated for display
| + | local langList = mw.language.getFallbacksFor(lang) |
− | ]]
| + | table.insert(langList,1,lang) |
− | local function errorPrinter(errors) | + | for i,language in ipairs(langList) do |
− | local result = "" | + | if list[language] then |
− | for i,v in ipairs(errors) do | + | return list[language] |
− | local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>' | + | end |
− | result = result .. errorHTML .. "<br />" | |
| end | | end |
− | return result
| |
| end | | end |
| | | |
− | --[[
| + | local function add_maplink(lat, lon, marker, text) |
− | Determine the required CSS class to display coordinates
| + | local tstr = '' |
− | | + | if text then |
− | Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
| + | tstr = string.format('text="%s" ', text) |
− | default is the mode as specificied by the user when calling the {{coord}} template
| |
− | mode is the display mode (dec or dms) that we will need to determine the css class for
| |
− | ]]
| |
− | local function displayDefault(default, mode) | |
− | if default == "" then | |
− | default = "dec"
| |
− | end
| |
− |
| |
− | if default == mode then | |
− | return "geo-default" | |
− | else
| |
− | return "geo-nondefault"
| |
| end | | end |
| + | return string.format('<maplink %szoom="13" latitude="%f" longitude="%f" class="no-icon">{'.. |
| + | ' "type": "Feature",'.. |
| + | ' "geometry": { "type":"Point", "coordinates":[%f, %f] },'.. |
| + | ' "properties": { "marker-symbol":"%s", "marker-size": "large", "marker-color": "0050d0" }'.. |
| + | '}</maplink>', tstr, lat, lon, lon, lat, marker) |
| end | | end |
| | | |
− | --[[ | + | local function add_maplink2(lat1, lon1, lat2, lon2) |
− | specPrinter
| + | 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 |
| | | |
− | Output formatter. Takes the structure generated by either parseDec
| + | local function info_box(text) |
− | or parseDMS and formats it for inclusion on Wikipedia.
| + | 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; ">'.. |
− | local function specPrinter(args, coordinateSpec) | + | '<tr>'.. |
− | local uriComponents = coordinateSpec["param"] | + | '<td class="mbox-image" style="padding-left:.9em;">'.. |
− | if uriComponents == "" then
| + | ' [[File:Commons-emblem-issue.svg|class=noviewer|45px]]</td>'.. |
− | -- RETURN error, should never be empty or nil | + | '<td class="mbox-text" style="">%s</td>'.. |
− | return "ERROR param was empty"
| + | '</tr></table>', text) |
− | end
| + | end |
− | if args["name"] then
| |
− | uriComponents = uriComponents .. "&title=" .. mw.uri.encode(coordinateSpec["name"]) | |
− | end
| |
− |
| |
− | local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
| |
− | .. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
| |
− | .. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
| |
− | .. '</span>'
| |
| | | |
− | local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
| + | local function mergeWithWikidata(q, lat1, lon1) |
− | local geodeclat | + | -- we are given wikidata q-code so look up the coordinates |
− | if lat < 0 then | + | local dist_str='' |
− | -- FIXME this breaks the pre-existing precision
| + | local entity |
− | geodeclat = tostring(coordinateSpec["dec-lat"]):sub(2) .. "°S"
| + | -- Wikiata coordinates |
| + | if q:match( '^[Qq]%d+$' ) then |
| + | entity = mw.wikibase.getEntity(q) |
| else | | else |
− | geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N" | + | entity = q |
| + | end |
| + | q = 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 | | 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", |
| + | q, q, q, lat1, lon1) |
| + | cat = CoorCat.wikidata4 .. info_box(cat) |
| + | elseif lat1 and lon1 and lat2 and lon2 then |
| + | -- 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) -- rind it to even meters |
| + | |
| + | 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, q, q, info, precision, q, lat1, lon1) |
| | | |
− | local long = tonumber( coordinateSpec["dec-long"] ) or 0
| + | if d<20 or d<precision then -- will consider location within 20 meters or precisi0on distance as the same |
− | local geodeclong
| + | cat = CoorCat.wikidata1 |
− | if long < 0 then
| + | 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 |
− | -- FIXME does not handle unicode minus | + | elseif d>1000 and d>5*precision then -- locations 1 km off and 5 precision distances away are likely wrong |
− | geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
| + | cat = CoorCat.wikidata3 .. info_box(info) |
− | else
| + | else |
− | geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
| + | cat = CoorCat.wikidata2 .. info_box(info) |
| + | end |
| end | | end |
| | | |
− | local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">' | + | -- verify proper P31 (instance of). List is based on https://www.wikidata.org/wiki/Property_talk:P625 |
− | .. geodeclat .. ' '
| + | local QCodes = { |
− | .. geodeclong
| + | Q5 = 1, -- human |
− | .. '</span>'
| + | Q11879590 = 1, -- female given name |
− | | + | Q202444 = 1, -- given name |
− | local geonumhtml = '<span class="geo">' | + | Q12308941 = 1, -- male given name |
− | .. coordinateSpec["dec-lat"] .. '; '
| + | Q4167836 = 1, -- Wikimedia category |
− | .. coordinateSpec["dec-long"]
| + | Q4167410 = 1, -- Wikimedia disambiguation page |
− | .. '</span>'
| + | Q783794 = 2, -- company |
− | | + | Q4830453 = 2, -- business enterprise |
− | local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
| + | } |
− | .. '<span class="geo-multi-punct"> / </span>'
| + | local s = entity:getBestStatements( 'P31' ) |
− | .. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';
| + | if s[1] and s[1].mainsnak.datavalue.value['id'] then |
− | | + | local instanceOf = s[1].mainsnak.datavalue.value['id'] |
− | if not args["name"] then | + | if QCodes[instanceOf] then |
− | inner = inner .. geodechtml
| + | cat = '' -- wipe out categories |
− | .. '<span style="display:none"> / ' .. geonumhtml .. '</span></span>'
| + | if QCodes[instanceOf]==1 then -- add problem category |
− | else
| + | cat = CoorCat.wikidata5 |
− | inner = inner .. '<span class="vcard">' .. geodechtml | + | end |
− | .. '<span style="display:none"> / ' .. geonumhtml .. '</span>'
| + | end |
− | .. '<span style="display:none"> (<span class="fn org">'
| |
− | .. args["name"] .. '</span>)</span></span></span>' | |
| end | | end |
− |
| |
− | return '<span class="plainlinks nourlexpansion">' ..
| |
− | '[' .. coord_link .. uriComponents .. ' ' .. inner .. ']' .. '</span>'
| |
− | end
| |
− |
| |
− | --[[ Helper function, convert decimal to degrees ]]
| |
− | local function convert_dec2dms_d(coordinate)
| |
− | local d = math_mod._round( coordinate, 0 ) .. "°"
| |
− | return d .. ""
| |
− | end
| |
− |
| |
− | --[[ Helper function, convert decimal to degrees and minutes ]]
| |
− | local function convert_dec2dms_dm(coordinate)
| |
− | coordinate = math_mod._round( coordinate * 60, 0 );
| |
− | local m = coordinate % 60;
| |
− | coordinate = math.floor( (coordinate - m) / 60 );
| |
− | local d = coordinate % 360 .."°"
| |
| | | |
− | return d .. string.format( "%02d′", m ) | + | return lat1, lon1, q, cat, dist_str |
| end | | end |
| | | |
− | --[[ Helper function, convert decimal to degrees, minutes, and seconds ]] | + | -- ======================================= |
− | local function convert_dec2dms_dms(coordinate)
| + | -- === External Functions ================ |
− | coordinate = math_mod._round( coordinate * 60 * 60, 0 );
| + | -- ======================================= |
− | local s = coordinate % 60
| + | local p = {} |
− | coordinate = math.floor( (coordinate - s) / 60 );
| |
− | local m = coordinate % 60
| |
− | coordinate = math.floor( (coordinate - m) / 60 );
| |
− | local d = coordinate % 360 .."°"
| |
| | | |
− | return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
| + | -- parse attribute variable returning desired field (used for debugging) |
| + | function p.parseAttribute(frame) |
| + | return string.match(mw.text.decode(frame.args[1]), mw.text.decode(frame.args[2]) .. ':' .. '([^_]*)') or '' |
| end | | end |
| | | |
− | --[[ | + | -- Helper core function for getHeading. |
− | Helper function, convert decimal latitude or longitude to | + | function p._getHeading(attributes) |
− | degrees, minutes, and seconds format based on the specified precision.
| + | if attributes == nil then |
− | ]]
| + | return nil |
− | local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision) | + | end |
− | local coord = tonumber(coordinate) | + | local hStr = string.match(mw.text.decode(attributes), 'heading:([^_]*)') |
− | local postfix
| + | if hStr == nil then |
− | if coord >= 0 then | + | return nil |
− | postfix = firstPostfix | + | end |
− | else
| + | local hNum = tonumber( hStr ) |
− | postfix = secondPostfix | + | if hNum == nil then |
| + | hStr = string.upper (hStr) |
| + | hNum = compass_points[hStr] |
| end | | end |
− | | + | if hNum ~= nil then |
− | precision = precision:lower();
| + | hNum = hNum%360 |
− | if precision == "dms" then | |
− | return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
| |
− | elseif precision == "dm" then
| |
− | return convert_dec2dms_dm( math.abs( coord ) ) .. postfix; | |
− | elseif precision == "d" then
| |
− | return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
| |
| end | | end |
| + | return hNum |
| end | | end |
| | | |
− | --[[ | + | --[[============================================================================ |
− | Convert DMS format into a N or E decimal coordinate
| + | Parse attribute variable returning heading field. If heading is a string than |
− | ]] | + | try to convert it to an angle |
− | local function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
| + | ==============================================================================]] |
− | local degrees = tonumber(degrees_str) | + | |
− | local minutes = tonumber(minutes_str) or 0 | + | function p.getHeading(frame) |
− | local seconds = tonumber(seconds_str) or 0
| + | local attributes |
− | | + | if frame.args[1] then |
− | local factor = 1
| + | attributes = frame.args[1] |
− | if direction == "S" or direction == "W" then | + | elseif frame.args.attributes then |
− | factor = -1 | + | attributes = frame.args.attributes |
| + | else |
| + | return '' |
| end | | end |
− |
| + | local hNum = p._getHeading(attributes) |
− | local precision = 0 | + | if hNum == nil then |
− | if seconds_str then
| + | return '' |
− | precision = 5 + math.max( math_mod._precision(seconds_str), 0 );
| |
− | elseif minutes_str and minutes_str ~= '' then | |
− | precision = 3 + math.max( math_mod._precision(minutes_str), 0 ); | |
− | else
| |
− | precision = math.max( math_mod._precision(degrees_str), 0 );
| |
| end | | end |
− |
| + | return tostring(hNum) |
− | local decimal = factor * (degrees+(minutes+seconds/60)/60)
| |
− | return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based. | |
| end | | end |
| | | |
− | --[[
| |
− | Checks input values to for out of range errors.
| |
− | ]]
| |
− | local function validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, source, strong )
| |
− | local errors = {};
| |
− | lat_d = tonumber( lat_d ) or 0;
| |
− | lat_m = tonumber( lat_m ) or 0;
| |
− | lat_s = tonumber( lat_s ) or 0;
| |
− | long_d = tonumber( long_d ) or 0;
| |
− | long_m = tonumber( long_m ) or 0;
| |
− | long_s = tonumber( long_s ) or 0;
| |
| | | |
− | if strong then
| + | --[[============================================================================ |
− | if lat_d < 0 then
| + | Helper core function for deg2dms. deg2dms can be called by templates, while |
− | table.insert(errors, {source, "latitude degrees < 0 with hemisphere flag"})
| + | _deg2dms should be called from Lua. |
− | end
| + | Inputs: |
− | if long_d < 0 then
| + | * degree - positive coordinate in degrees |
− | table.insert(errors, {source, "longitude degrees < 0 with hemisphere flag"})
| + | * degPrec - coordinate precision in degrees will result in different angle format |
− | end
| + | * lang - language to used when formatting the number |
− | --[[
| + | ==============================================================================]] |
− | #coordinates is inconsistent about whether this is an error. If globe: is
| + | function p._deg2dms(degree, degPrec, lang) |
− | specified, it won't error on this condition, but otherwise it will. | + | local dNum, mNum, sNum, dStr, mStr, sStr, formatStr, secPrec, c, k, d, zero |
− | | + | local Lang = mw.language.new(lang) |
− | For not simply disable this check. | + | |
− | | + | -- adjust number display based on precision |
− | if long_d > 180 then
| + | secPrec = degPrec*3600.0 -- coordinate precision in seconds |
− | table.insert(errors, {source, "longitude degrees > 180 with hemisphere flag"})
| + | if secPrec<0.05 then -- degPrec<1.3889e-05 |
− | end | + | formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.SS″ format |
− | ]] | + | c = 360000 |
− | end | + | elseif secPrec<0.5 then -- 1.3889e-05<degPrec<1.3889e-04 |
− | | + | formatStr = '%s° %s′ %s″' -- use DD° MM′ SS.S″ format |
− | if lat_d > 90 then | + | c = 36000 |
− | table.insert(errors, {source, "latitude degrees > 90"}) | + | elseif degPrec*60.0<0.5 then -- 1.3889e-04<degPrec<0.0083 |
| + | formatStr = '%s° %s′ %s″' -- use DD° MM′ SS″ format |
| + | c = 3600 |
| + | elseif degPrec<0.5 then -- 0.0083<degPrec<0.5 |
| + | formatStr = '%s° %s′' -- use DD° MM′ format |
| + | c = 60 |
| + | else -- if degPrec>0.5 then |
| + | formatStr = '%s°' -- use DD° format |
| + | c = 1 |
| end | | end |
− | if lat_d < -90 then | + | |
− | table.insert(errors, {source, "latitude degrees < -90"})
| + | -- create degree, minute and seconds numbers and string |
− | end | + | d = c/60 |
− | if lat_m >= 60 then | + | 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 |
− | table.insert(errors, {source, "latitude minutes >= 60"})
| + | dNum = math.floor(k/c) % 360 -- degree number (integer in 0-360 range) |
− | end | + | mNum = math.floor(k/d) % 60 -- minute number (integer in 0-60 range) |
− | if lat_m < 0 then
| + | sNum = 3600*(k%d) / c -- seconds number (float in 0-60 range with 0, 1 or 2 decimal digits) |
− | table.insert(errors, {source, "latitude minutes < 0"})
| + | dStr = Lang:formatNum(dNum) -- degree string |
− | end | + | mStr = Lang:formatNum(mNum) -- minute string |
− | if lat_s >= 60 then
| + | sStr = Lang:formatNum(sNum) -- second string |
− | table.insert(errors, {source, "latitude seconds >= 60"})
| + | zero = Lang:formatNum(0) -- zero string in local language |
− | end | + | if mNum<10 then |
− | if lat_s < 0 then
| + | mStr = zero .. mStr -- pad with zero if a single digit |
− | table.insert(errors, {source, "latitude seconds < 0"})
| |
− | end | |
− | if long_d >= 360 then | |
− | table.insert(errors, {source, "longitude degrees >= 360"})
| |
− | end | |
− | if long_d <= -360 then | |
− | table.insert(errors, {source, "longitude degrees <= -360"})
| |
− | end
| |
− | if long_m >= 60 then | |
− | table.insert(errors, {source, "longitude minutes >= 60"}) | |
| end | | end |
− | if long_m < 0 then | + | if sNum<10 then |
− | table.insert(errors, {source, "longitude minutes < 0"}) | + | sStr = zero .. sStr -- pad with zero if less than ten |
| end | | end |
− | if long_s >= 60 then | + | return string.format(formatStr, dStr, mStr, sStr); |
− | table.insert(errors, {source, "longitude seconds >= 60"})
| |
− | end
| |
− | if long_s < 0 then
| |
− | table.insert(errors, {source, "longitude seconds < 0"})
| |
− | end
| |
− |
| |
− | return errors;
| |
| end | | end |
| | | |
− | --[[ | + | --[[============================================================================ |
− | parseDec
| + | 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 |
| | | |
− | Transforms decimal format latitude and longitude into the
| + | if degree==nil then |
− | structure to be used in displaying coordinates
| + | return args[1]; |
− | ]]
| + | else |
− | local function parseDec( lat, long, format )
| + | return p._deg2dms(degree, degPrec, args.lang) |
− | local coordinateSpec = {} | |
− | local errors = {}
| |
− |
| |
− | if not long then
| |
− | return nil, {{"parseDec", "Missing longitude"}} | |
− | elseif not tonumber(long) then | |
− | return nil, {{"parseDec", "Longitude could not be parsed as a number: " .. long}} | |
| end | | end |
− |
| + | end |
− | errors = validate( lat, nil, nil, long, nil, nil, 'parseDec', false );
| |
− | coordinateSpec["dec-lat"] = lat;
| |
− | coordinateSpec["dec-long"] = long;
| |
| | | |
− | local mode = coordinates.determineMode( lat, long );
| + | --[[============================================================================ |
− | coordinateSpec["dms-lat"] = convert_dec2dms( lat, "N", "S", mode) -- {{coord/dec2dms|{{{1}}}|N|S|{{coord/prec dec|{{{1}}}|{{{2}}}}}}} | + | Format coordinate location string, by creating and joining DMS strings for |
− | coordinateSpec["dms-long"] = convert_dec2dms( long, "E", "W", mode) -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}} | + | latitude and longitude. Also convert precision from meters to degrees. |
− | | + | INPUTS: |
− | if format then | + | * lat = latitude in degrees |
− | coordinateSpec.default = format | + | * 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 | | else |
− | coordinateSpec.default = "dec" | + | 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 |
| + | end |
| | | |
− | return coordinateSpec, errors | + | function p.lat_lon(frame) |
| + | local args = getArgs(frame) |
| + | return p._lat_lon(args.lat, args.lon, args.prec, args.lang) |
| end | | end |
| | | |
− | --[[ | + | --[[============================================================================ |
− | parseDMS
| + | 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, '%%', '%%%%') |
| | | |
− | Transforms degrees, minutes, seconds format latitude and longitude
| + | if site == 'GoogleMaps' then |
− | into the a structure to be used in displaying coordinates
| + | URLstr = SiteURL.GoogleMaps[globe] |
− | ]]
| + | elseif site == 'GeoHack' then |
− | local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
| + | attributes = string.format('globe:%s_%s', globe, attributes) |
− | local coordinateSpec, errors, backward = {}, {} | + | URLstr = mw.ustring.gsub( URLstr, '$attr', attributes) |
− |
| |
− | lat_f = lat_f:upper();
| |
− | long_f = long_f:upper();
| |
− | | |
− | -- Check if specified backward
| |
− | if lat_f == 'E' or lat_f == 'W' then
| |
− | lat_d, long_d, lat_m, long_m, lat_s, long_s, lat_f, long_f, backward = long_d, lat_d, long_m, lat_m, long_s, lat_s, long_f, lat_f, true; | |
− | end
| |
− |
| |
− | errors = validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, 'parseDMS', true );
| |
− | if not long_d then
| |
− | return nil, {{"parseDMS", "Missing longitude" }}
| |
− | elseif not tonumber(long_d) then
| |
− | return nil, {{"parseDMS", "Longitude could not be parsed as a number:" .. long_d }}
| |
| end | | end |
− | | + | URLstr = mw.ustring.gsub( URLstr, '$lat' , latStr) |
− | if not lat_m and not lat_s and not long_m and not long_s and #errors == 0 then
| + | URLstr = mw.ustring.gsub( URLstr, '$lon' , lonStr) |
− | if math_mod._precision( lat_d ) > 0 or math_mod._precision( long_d ) > 0 then
| + | URLstr = mw.ustring.gsub( URLstr, '$lang' , lang) |
− | if lat_f:upper() == 'S' then
| + | URLstr = mw.ustring.gsub( URLstr, '$level', level) |
− | lat_d = '-' .. lat_d;
| + | URLstr = mw.ustring.gsub( URLstr, '$page' , pageName) |
− | end
| + | URLstr = mw.ustring.gsub( URLstr, '+', '') |
− | if long_f:upper() == 'W' then
| + | URLstr = mw.ustring.gsub( URLstr, ' ', '_') |
− | long_d = '-' .. long_d;
| + | return URLstr |
− | end
| + | end |
− |
| |
− | return parseDec( lat_d, long_d, format );
| |
− | end
| |
− | end
| |
− |
| |
− | coordinateSpec["dms-lat"] = lat_d.."°"..optionalArg(lat_m,"′") .. optionalArg(lat_s,"″") .. lat_f | |
− | coordinateSpec["dms-long"] = long_d.."°"..optionalArg(long_m,"′") .. optionalArg(long_s,"″") .. long_f | |
− | coordinateSpec["dec-lat"] = convert_dms2dec(lat_f, lat_d, lat_m, lat_s) -- {{coord/dms2dec|{{{4}}}|{{{1}}}|0{{{2}}}|0{{{3}}}}} | |
− | coordinateSpec["dec-long"] = convert_dms2dec(long_f, long_d, long_m, long_s) -- {{coord/dms2dec|{{{8}}}|{{{5}}}|0{{{6}}}|0{{{7}}}}} | |
| | | |
− | if format then
| + | --[[============================================================================ |
− | coordinateSpec.default = format
| + | Create URL for different sites. |
− | else
| + | INPUTS: |
− | coordinateSpec.default = "dms"
| + | * site = Possible sites: GeoHack, GoogleEarth, Proximityrama, |
− | end | + | OpenStreetMap, GoogleMaps (for Earth, Mars and Moon) |
− | | + | * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, |
− | return coordinateSpec, errors, backward | + | Ganymede are also supported but are unused as of 2013. |
| + | * lat = latitude string or number |
| + | * lon = longitude string or number |
| + | * lang = language code |
| + | * attributes = attributes to be passed to GeoHack |
| + | ==============================================================================]] |
| + | function p.externalLink(frame) |
| + | local args = getArgs(frame) |
| + | return p._externalLink(args.site or 'GeoHack', args.globe or 'Earth', args.lat, args.lon, args.lang, args.attributes or '') |
| end | | end |
| | | |
− | --[[ | + | --[[============================================================================ |
− | Check the input arguments for coord to determine the kind of data being provided
| + | Adjust GeoHack attributes depending on the template that calls it |
− | and then make the necessary processing.
| + | INPUTS: |
− | ]]
| + | * attributes = attributes to be passed to GeoHack |
− | local function formatTest(args)
| + | * mode = set by each calling template |
− | local result, errors
| + | ==============================================================================]] |
− | local backward, primary = false, false
| + | function p.alterAttributes(attributes, mode) |
− | | + | -- indicate which template called it |
− | local function getParam(args, lim)
| + | if mode=='camera' then -- Used by {{Location}} and {{Location dec}} |
− | local ret = {}
| + | if string.find(attributes, 'type:camera')==nil then |
− | for i = 1, lim do
| + | attributes = 'type:camera_' .. attributes |
− | ret[i] = args[i] or ''
| |
− | end
| |
− | return table.concat(ret, '_')
| |
− | end | |
− |
| |
− | if not args[1] then | |
− | -- no lat logic
| |
− | return errorPrinter( {{"formatTest", "Missing latitude"}} )
| |
− | elseif not tonumber(args[1]) then
| |
− | -- bad lat logic
| |
− | return errorPrinter( {{"formatTest", "Unable to parse latitude as a number:" .. args[1]}} )
| |
− | elseif not args[4] and not args[5] and not args[6] then
| |
− | -- dec logic
| |
− | result, errors = parseDec(args[1], args[2], args.format)
| |
− | if not result then | |
− | return errorPrinter(errors);
| |
− | end
| |
− | -- formatting for geohack: geohack expects D_N_D_E notation or D;D notation
| |
− | -- wikiminiatlas doesn't support D;D notation
| |
− | -- #coordinates parserfunction doesn't support negative decimals with NSWE
| |
− | result.param = table.concat({
| |
− | math.abs(tonumber(args[1])),
| |
− | ((tonumber(args[1]) or 0) < 0) and 'S' or 'N',
| |
− | math.abs(tonumber(args[2])),
| |
− | ((tonumber(args[2]) or 0) < 0) and 'W' or 'E',
| |
− | args[3] or ''}, '_')
| |
− | elseif dmsTest(args[4], args[8]) then
| |
− | -- dms logic
| |
− | result, errors, backward = parseDMS(args[1], args[2], args[3], args[4],
| |
− | args[5], args[6], args[7], args[8], args.format)
| |
− | if args[10] then
| |
− | table.insert(errors, {'formatTest', 'Extra unexpected parameters'}) | |
− | end
| |
− | if not result then
| |
− | return errorPrinter(errors)
| |
− | end
| |
− | result.param = getParam(args, 9)
| |
− | elseif dmsTest(args[3], args[6]) then
| |
− | -- dm logic
| |
− | result, errors, backward = parseDMS(args[1], args[2], nil, args[3],
| |
− | args[4], args[5], nil, args[6], args['format'])
| |
− | if args[8] then
| |
− | table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
| |
| end | | end |
− | if not result then | + | elseif mode=='object'or mode =='globe' then -- Used by {{Object location}} |
− | return errorPrinter(errors) | + | if mode=='object' and string.find(attributes, 'type:')==nil then |
| + | attributes = 'type:object_' .. attributes |
| end | | end |
− | result.param = getParam(args, 7) | + | if string.find(attributes, 'class:object')==nil then |
− | elseif dmsTest(args[2], args[4]) then
| + | attributes = 'class:object_' .. attributes |
− | -- d logic
| |
− | result, errors, backward = parseDMS(args[1], nil, nil, args[2],
| |
− | args[3], nil, nil, args[4], args.format)
| |
− | if args[6] then
| |
− | table.insert(errors, {'formatTest', 'Extra unexpected parameters'}) | |
− | end
| |
− | if not result then
| |
− | return errorPrinter(errors)
| |
| end | | end |
− | result.param = getParam(args, 5)
| + | elseif mode=='inline' then -- Used by {{Inline coordinates}} (actually that template does not set any attributes at the moment) |
− | else | + | elseif mode=='user' then -- Used by {{User location}} |
− | -- Error | + | attributes = 'type:user_location' |
− | return errorPrinter({{"formatTest", "Unknown argument format"}})
| + | elseif mode=='institution' then --Used by {{Institution/coordinates}} (categories only) |
| + | attributes = 'type:institution' |
| end | | end |
− | result.name = args.name | + | return attributes |
| + | end |
| | | |
− | local extra_param = {'dim', 'globe', 'scale', 'region', 'source', 'type'}
| + | --[[============================================================================ |
− | for _, v in ipairs(extra_param) do | + | Create link to GeoHack tool which displays latitude and longitude coordinates |
− | if args[v] then
| + | in DMS format |
− | table.insert(errors, {'formatTest', 'Parameter: "' .. v .. '=" should be "' .. v .. ':"' })
| + | INPUTS: |
− | end
| + | * 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 |
− |
| |
− | local ret = specPrinter(args, result)
| |
− | if #errors > 0 then
| |
− | ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
| |
− | end
| |
− | return ret, backward
| |
| end | | end |
| | | |
− | --[[
| + | function p.GeoHack_link(frame) |
− | Generate Wikidata tracking categories.
| + | return p._GeoHack_link(getArgs(frame)) |
− | ]]
| |
− | local function makeWikidataCategories(qid)
| |
− | local ret | |
− | if mw.wikibase and current_page.namespace == 0 then
| |
− | local entity = qid and mw.wikibase.getEntityObject(qid) or mw.wikibase.getEntityObject()
| |
− | if entity and entity.claims and entity.claims.P625 and entity.claims.P625[1] then
| |
− | local snaktype = entity.claims.P625[1].mainsnak.snaktype
| |
− | if snaktype == 'value' then
| |
− | -- coordinates exist both here and on Wikidata, and can be compared.
| |
− | ret = 'Coordinates on Wikidata'
| |
− | elseif snaktype == 'somevalue' then
| |
− | ret = 'Coordinates on Wikidata set to unknown value'
| |
− | elseif snaktype == 'novalue' then
| |
− | ret = 'Coordinates on Wikidata set to no value'
| |
− | end
| |
− | else
| |
− | -- We have to either import the coordinates to Wikidata or remove them here.
| |
− | ret = 'Coordinates not on Wikidata'
| |
− | end
| |
− | end
| |
− | if ret then
| |
− | return string.format('[[Category:%s]]', ret)
| |
− | else
| |
− | return ''
| |
− | end
| |
| end | | end |
| | | |
− | --[[
| |
− | link
| |
| | | |
− | Simple function to export the coordinates link for other uses.
| + | --[[============================================================================ |
− | | + | Create full external links section of {{Location}} or {{Object location}} |
− | Usage:
| + | templates, based on: |
− | {{#invoke:Coordinates | link }}
| + | * 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().namespace |
| + | 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 |
− | function coordinates.link(frame)
| |
− | return coord_link; | |
| end | | end |
| | | |
− | --[[
| + | function p.externalLinksSection(frame) |
− | dec2dms
| + | return p._externalLinksSection(getArgs(frame)) |
| + | end |
| | | |
− | Wrapper to allow templates to call dec2dms directly.
| + | --[[============================================================================ |
− | | + | Core section of template:Location, template:Object location and template:Globe location. |
− | Usage:
| + | This method requires several arguments to be passed to it or it's parent method/template: |
− | {{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix | | + | * globe = Possible options: Earth, Mars or Moon. Venus, Mercury, Titan, Ganymede are also supported but are unused as of 2013. |
− | negative_suffix | precision }} | + | * 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().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 |
| + | local attributes0 = args.attributes |
| + | args.attributes = p.alterAttributes(args.attributes or '', args.mode) |
| | | |
− | decimal_coordinate is converted to DMS format. If positive, the positive_suffix
| + | -- Convert coordinates from string to numbers |
− | is appended (typical N or E), if negative, the negative suffix is appended. The
| + | local lat = tonumber(args.lat) |
− | specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
| + | local lon = tonumber(args.lon) |
− | to use. | + | if lon then -- get longitude to be in -180 to 180 range |
− | ]]
| + | lon=lon%360 |
− | coordinates.dec2dms = makeInvokeFunc('_dec2dms')
| + | if lon>180 then |
− | function coordinates._dec2dms(args)
| + | lon = lon-360 |
− | local coordinate = args[1] | + | end |
− | local firstPostfix = args[2] or ''
| |
− | local secondPostfix = args[3] or ''
| |
− | local precision = args[4] or ''
| |
− | | |
− | return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
| |
− | end
| |
− | | |
− | --[[
| |
− | Helper function to determine whether to use D, DM, or DMS
| |
− | format depending on the precision of the decimal input.
| |
− | ]]
| |
− | function coordinates.determineMode( value1, value2 )
| |
− | local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) ); | |
− | if precision <= 0 then | |
− | return 'd' | |
− | elseif precision <= 2 then
| |
− | return 'dm'; | |
− | else
| |
− | return 'dms'; | |
| end | | end |
− | end
| |
− |
| |
− | --[[
| |
− | dms2dec
| |
− |
| |
− | Wrapper to allow templates to call dms2dec directly.
| |
− |
| |
− | Usage:
| |
− | {{#invoke:Coordinates | dms2dec | direction_flag | degrees |
| |
− | minutes | seconds }}
| |
| | | |
− | Converts DMS values specified as degrees, minutes, seconds too decimal format.
| + | -- If wikidata link provided than compare coordinates |
− | direction_flag is one of N, S, E, W, and determines whether the output is
| + | local Categories, geoMicroFormat, coorTag, wikidata_link = '', '', '', '' |
− | positive (i.e. N and E) or negative (i.e. S and W).
| + | if args.wikidata and args.wikidata~='' then |
− | ]]
| + | local dist_str, q |
− | coordinates.dms2dec = makeInvokeFunc('_dms2dec')
| + | -- if lat/lon is not provided but we are given wikidata q-code than look up the coordinates |
− | function coordinates._dms2dec(args)
| + | lat, lon, q, Categories, dist_str = mergeWithWikidata(args.wikidata, lat, lon) |
− | local direction = args[1]
| + | wikidata_link = string.format("\n[[File:Wikidata-logo.svg|20px|Edit coordinates on Wikidata%s|link=wikidata:%s]]", dist_str, q); |
− | local degrees = args[2]
| + | end |
− | local minutes = args[3]
| |
− | local seconds = args[4] | |
| | | |
− | return convert_dms2dec(direction, degrees, minutes, seconds) | + | args.lat = string.format('%010.6f', lat or 0) |
− | end
| + | args.lon = string.format('%011.6f', lon or 0) |
| | | |
− | --[[
| + | local frame = mw.getCurrentFrame() |
− | coord
| |
| | | |
− | Main entry point for Lua function to replace {{coord}}
| + | -- 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 |
− | Usage:
| + | if lat and lon then -- if lat and lon are numbers... |
− | {{#invoke:Coordinates | coord }} | + | if lat==0 and lon==0 then -- lat=0 and lon=0 is a common issue when copying from flickr and other sources |
− | {{#invoke:Coordinates | coord | lat | long }} | + | Categories = Categories .. CoorCat.default |
− | {{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
| + | end |
− | ...
| + | if attributes0 and string.find(attributes0, '=') then |
− |
| + | Categories = Categories .. CoorCat.attribute |
− | Refer to {{coord}} documentation page for many additional parameters and
| + | end |
− | configuration options.
| + | if args.error=='1' or (math.abs(lat)>90) then -- check for errors ({{#coordinates:}} also checks for errors ) |
− |
| + | Categories = Categories .. CoorCat.erroneous |
− | Note: This function provides the visual display elements of {{coord}}. In
| + | end |
− | order to load coordinates into the database, the {{#coordinates:}} parser
| + | local cat = CoorCat[args.namespace] |
− | function must also be called, this is done automatically in the Lua
| + | if cat then -- add category based on namespace |
− | version of {{coord}}.
| + | Categories = Categories .. cat |
− | ]]
| + | end |
− | coordinates.coord = makeInvokeFunc('_coord')
| + | -- if not earth than add a category for each globe |
− | function coordinates._coord(args)
| + | if args.mode and args.globe and args.mode=='globe' and args.globe~='Earth' then |
− | if (not args[1] or not tonumber(args[1])) and not args[2] and mw.wikibase.getEntityObject() then
| + | Categories = Categories .. string.format(CoorCat[args.mode], args.globe) |
− | args[3] = args[1]; args[1] = nil
| + | end |
− | local entity = args.qid and mw.wikibase.getEntityObject(args.qid) or mw.wikibase.getEntityObject()
| + | -- add <span class="geo"> Geo (microformat) code: it is included for machine readability |
− | if entity
| + | geoMicroFormat = string.format('<span class="geo" style="display:none">%10.6f; %11.6f</span>',lat, lon) |
− | and entity.claims | + | -- add {{#coordinates}} tag, see https://www.mediawiki.org/wiki/Extension:GeoData |
− | and entity.claims.P625
| + | if args.namespace == 'File' and Status == 'primary' and args.mode=='camera' then |
− | and entity.claims.P625[1].mainsnak.snaktype == 'value'
| + | coorTag = frame:callParserFunction( '#coordinates', { 'primary', lat, lon, args.attributes } ) |
− | then
| + | elseif args.namespace == 'File' and args.mode=='object' then |
− | local precision = entity.claims.P625[1].mainsnak.datavalue.value.precision
| + | coorTag = frame:callParserFunction( '#coordinates', { lat, lon, args.attributes } ) |
− | args[1]=entity.claims.P625[1].mainsnak.datavalue.value.latitude | |
− | args[2]=entity.claims.P625[1].mainsnak.datavalue.value.longitude | |
− | if precision then | |
− | precision=-math_mod._round(math.log(precision)/math.log(10),0) | |
− | args[1]=math_mod._round(args[1],precision)
| |
− | args[2]=math_mod._round(args[2],precision) | |
| end | | end |
| + | else -- if lat and lon are not numbers then add error category |
| + | Categories = Categories .. CoorCat.erroneous |
| end | | end |
| end | | end |
− |
| |
− | local contents, backward = formatTest(args)
| |
− | local Notes = args.notes or ''
| |
− | local Display = args.display and args.display:lower() or 'inline'
| |
| | | |
− | local function isInline(s) | + | -- Call helper functions to render different parts of the template |
− | -- Finds whether coordinates are displayed inline.
| + | local coor, info_link, inner_table, heading, OSM = '','','','','','','' |
− | return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti' | + | coor = p._GeoHack_link(args) -- the p and link to GeoHack |
| + | heading = p._getHeading(attributes0) -- get heading arrow section |
| + | 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 | | end |
− | local function isInTitle(s) | + | if args.globe=='Earth' then |
− | -- Finds whether coordinates are displayed in the title.
| + | local icon = 'marker' |
− | return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti' | + | 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 | | end |
− |
| + | local external_link = p._externalLinksSection(args) -- external link section |
− | local function coord_wrapper(in_args) | + | if external_link and args.namespace == 'File' then |
− | -- Calls the parser function {{#coordinates:}}. | + | external_link = langSwitch(i18n.LocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{location}} template |
− | return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or '' | + | elseif external_link then |
| + | external_link = langSwitch(i18n.ObjectLocationTemplateLinkLabel, args.lang) .. ' ' .. external_link -- header of the link section for {{Object location}} template |
| end | | 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 '', info_link, wikidata_link, geoMicroFormat) |
| | | |
− | local text = '' | + | -- combine strings into a table |
− | if isInline(Display) then | + | local templateText |
− | text = text .. displayinline(contents, Notes) | + | if bare then |
− | end | + | templateText = string.format('<table style="width:100%%"><tr>%s</tr></table>', inner_table) |
− | if isInTitle(Display) then
| + | else |
− | text = text | + | -- choose name of the field |
− | .. displaytitle(contents, Notes)
| + | local field_name = 'Location' |
− | .. makeWikidataCategories(args.qid) | + | if args.mode=='camera' then |
− | end
| + | field_name = langSwitch(i18n.CameraLocation, args.lang) |
− | if not args.nosave then
| + | elseif args.mode=='object' then |
− | local page_title, count = mw.title.getCurrentTitle(), 1
| + | field_name = langSwitch(i18n.ObjectLocation, args.lang) |
− | if backward then | + | elseif args.mode=='globe' then |
− | local tmp = {} | + | local field_list = langSwitch(i18n.GlobeLocation, args.lang) |
− | while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
| + | if args.globe and i18n.GlobeLocation['en'][args.globe] then -- verify globe is provided and is recognized |
− | tmp.count = count; count = 2*(count-1)
| + | field_name = field_list[args.globe] |
− | while count >= tmp.count do table.insert(tmp, 1, (args[count] or '')); count = count-1 end | + | end |
− | for i, v in ipairs(tmp) do args[i] = v end
| |
− | else
| |
− | while count <= 9 do args[count] = (args[count] or ''); count = count+1 end
| |
| end | | end |
− | if isInTitle(Display) and not page_title.isTalkPage and page_title.subpageText ~= 'doc' and page_title.subpageText ~= 'testcases' then args[10] = 'primary' end | + | --Create HTML text |
− | args.notes, args.format, args.display = nil
| + | local dir, text_align |
− | text = text .. coord_wrapper(args)
| + | if mw.language.new( args.lang ):isRTL() then |
− | end
| + | dir = 'rtl' |
− | return text
| + | text_align = 'right' |
− | end
| |
− | | |
− | --[[ | |
− | coord2text
| |
− | | |
− | Extracts a single value from a transclusion of {{Coord}}.
| |
− | IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
| |
− | | |
− | Usage:
| |
− | | |
− | {{#invoke:Coordinates | coord2text | {{Coord}} | parameter }}
| |
− | | |
− | Valid values for the second parameter are: lat (signed integer), long (signed integer), type, scale, dim, region, globe, source
| |
− | | |
− | ]]
| |
− | function coordinates.coord2text(frame)
| |
− | if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
| |
− | frame.args[2] = mw.text.trim(frame.args[2])
| |
− | if frame.args[2] == 'lat' or frame.args[2] == 'long' then
| |
− | local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ') | |
− | if frame.args[2] == 'lat' then
| |
− | result, negative = result[1], 'S' | |
| else | | else |
− | result, negative = result[2], 'W' | + | dir = 'ltr' |
− | end | + | text_align = 'left' |
− | result = mw.text.split(result, '°') | + | end |
− | if result[2] == negative then result[1] = '-'..result[1] end
| + | local style = string.format('class="toccolours mw-content-%s layouttemplate commons-file-information-table" cellpadding="2" style="width: 100%%; direction:%s;" lang="%s"', |
− | return result[1] | + | args.lang, dir, text_align, args.lang) |
− | else
| + | 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) |
− | return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
| |
| end | | end |
| + | return templateText, Categories, coorTag |
| end | | end |
| | | |
− | --[[
| + | function p.LocationTemplateCore(frame) |
− | coordinsert
| + | local args = frame.args |
− | | + | if not args or not args.lat then -- if no arguments provided than use parent arguments |
− | Injects some text into the Geohack link of a transclusion of {{Coord}} (if that text isn't already in the transclusion). Outputs the modified transclusion of {{Coord}}.
| + | args = mw.getCurrentFrame():getParent().args |
− | IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
| |
− | | |
− | Usage:
| |
− | | |
− | {{#invoke:Coordinates | coordinsert | {{Coord}} | parameter:value | parameter:value | … }}
| |
− | | |
− | Do not make Geohack unhappy by inserting something which isn't mentioned in the {{Coord}} documentation.
| |
− | | |
− | ]]
| |
− | function coordinates.coordinsert(frame) | |
− | for i, v in ipairs(frame.args) do | |
− | if i ~= 1 then
| |
− | if not mw.ustring.find(frame.args[1], (mw.ustring.match(frame.args[i], '^(.-:)') or '')) then
| |
− | frame.args[1] = mw.ustring.gsub(frame.args[1], '(params=.-)_? ', '%1_'..frame.args[i]..' ')
| |
− | end
| |
− | end
| |
| end | | end |
− | if frame.args.name then | + | if not (args.lang and mw.language.isSupportedLanguage(args.lang)) then |
− | if not mw.ustring.find(frame.args[1], '<span class="vcard">') then
| + | args.lang = frame:callParserFunction("int","lang") -- get user's chosen language |
− | local namestr = frame.args.name
| |
− | frame.args[1] = mw.ustring.gsub(frame.args[1],
| |
− | '(<span class="geo%-default">)(<span[^<>]*>[^<>]*</span><span[^<>]*>[^<>]*<span[^<>]*>[^<>]*</span></span>)(</span>)',
| |
− | '%1<span class="vcard">%2<span style="display:none"> (<span class="fn org">' .. namestr .. '</span>)</span></span>%3')
| |
− | frame.args[1] = mw.ustring.gsub(frame.args[1], '(¶ms=[^&"<>%[%] ]*) ', '%1&title=' .. mw.uri.encode(namestr) .. ' ')
| |
− | end
| |
| end | | end |
− | return frame.args[1] | + | local templateText, Categories, coorTag = p._LocationTemplateCore(args) |
| + | return templateText .. Categories .. coorTag |
| end | | end |
| | | |
− | return coordinates | + | return p |