(also create category for Do) |
m (1 revision imported from wikivoyage:Module:Map) |
(No difference)
|
Latest revision as of 22:21, 21 May 2024
This module provides input parameters for mapframe and maplink functions supported by Extension:Kartographer.
Usage:
{{#invoke:map|tag|type=maplink|geotype=Point|title=Example|latitude=59.0|longitude=29.0}}
Parameter | Usage |
---|---|
type | maplink or mapframe depending on which function should be invoked
|
geotype | Point for individual points, Polygon for polygons
|
title | Object name |
latitude and longitude | use 'NA' to disable coordinates, including the ones from wikidata |
zoom | Zoom level of the map |
marker-symbol | Symbol, letter, or number for displaying on the map as marker |
marker-color | Color of the map marker |
group | Group of markers (see, eat, drink, etc.) |
show | Which marker groups to show (by default shows the most common groups like see, eat, drink, ...) |
data | data=values fills the polygon given by data data=world;;values fills the area outside of the polygon
|
image | Name of the image shown in the thumbnail |
width and height | map width and map height in px or % of screen width, only for mapframe |
wikidata | if specified, the missing title/lat/long/image fields will be fetched from the respective wikidata entry fields |
local getArgs = require('Module:Arguments').getArgs local p = {} function dbg(v, msg) mw.log((msg or '') .. mw.text.jsonEncode(v)) end local function has_value (tab, val) for index, value in ipairs(tab) do if value == val then return true end end return false end -- Parse all unnamed string parameters in a form of "latitude, longitude" into the real number pairs function getSequence(args) local coords = {} for ind, val in pairs( args ) do if type(ind) == "number" then local valid = false local val2 = mw.text.split( val, ',', true ) -- allow for elevation if #val2 >= 2 and #val2 <= 3 then local lat = tonumber(val2[1]) local lon = tonumber(val2[2]) if lat ~= nil and lon ~= nil then table.insert(coords, { lon, lat } ) valid = true end end if not valid then error('Unnamed parameter #' .. ind .. ' "' .. val .. '" is not recognized as a valid "latitude,longitude" value') end end end return coords end -- See http://geojson.org/geojson-spec.html -- Convert a comma and semicolon separated numbers into geojson coordinate arrays -- Each geotype expects a certain array depth: -- Point - [ lon, lat ] All other types use point as the basic type -- MultiPoint - array of points: [ point, ... ] -- LineString - array of 2 or more points: [ point, point, ... ] -- MultiLineString - array of LineStrings: [ [ point, point, ... ], ... ] -- Polygon - [ [ point, point, point, point, ... ], ... ] -- each LinearRing is an array of 4 or more points, where first and last must be the same -- first LinearRing is the exterior ring, subsequent rings are holes in it -- MultiPolygon - array of Polygons: [ [ [ point, point, point, point, ... ], ... ], ... ] -- -- For example, for the LineString, data "p1;p2;p3" would be converted to [p1,p2,p3] (each "p" is a [lon,lat] value) -- LineString has the depth of "1" -- array of points (each point being a two value array) -- For Polygon, the same sequence "p1;p2;p3" would be converted to [[p1,p2,p3]] -- Which is an array of array of points. But sometimes we need to specify two subarrays of points: -- [[p1,p2],[p3]] (last point is in a separate array), and we do it with "p1;p2;;p3" -- Similarly, for MultiPolygon, "p1;p2;;;p3" would generate [[[p1,p2]],[[p3]]] -- function p.parseGeoSequence(args) local result = p._parseGeoSequence(args) if type(result) == 'string' then error(result) end return result end function p._parseGeoSequence(args) local allTypes = { -- how many nested array levels until we get to the Point, -- second is the minimum number of values each Points array must have Point = { 1, 1 }, MultiPoint = { 1, 0 }, LineString = { 1, 2 }, MultiLineString = { 2, 2 }, Polygon = { 2, 4 }, MultiPolygon = { 3, 4 }, } if not allTypes[args.geotype] then return ('Unknown geotype ' .. args.geotype) end local levels, min = unpack(allTypes[args.geotype]) local result result = {} for i = 1, levels do result[i] = {} end local gap = 0 -- Example for levels==3, converting "p1 ; p2 ; ; ; p3 ; ; p4" => [[[p1, p2]], [[p3],[p4]]] -- This function will be called after each gap, and all values are done, so the above will call: -- before p3: gap=2, [],[],[p1,p2] => [[[p1,p2]]],[],[] -- before p4: gap=1, [[[p1,p2]]],[],[p3] => [[[p1,p2]]],[[p3]]],[] -- the end, gap=2, [[[p1,p2]]],[[p3]]],[p4] => [[[p1,p2]],[[p3],[p4]]],[],[] -- Here, convert at "p1 ; ; " from [[],[p1]] local closeArrays = function (gap) if #result[levels] < min then error('Each points array must be at least ' .. min .. ' values') elseif min == 1 and #result[levels] ~= 1 then -- Point error('Point must have exactly one data point') end -- attach arrays in reverse order to the higher order ones for i = levels, levels-gap+1, -1 do table.insert(result[i-1], result[i]) result[i] = {} end return 0 end local usedSequence = false for val in mw.text.gsplit(args.data, ';', true) do local val2 = mw.text.split(val, ',', true) -- allow for elevation if #val2 >= 2 and #val2 <= 3 and not usedSequence then if gap > 0 then gap = closeArrays(gap) end local lat = tonumber(val2[1]) local lon = tonumber(val2[2]) if lat == nil or lon == nil then return ('Bad data value "' .. val .. '"') end table.insert(result[levels], { lon, lat } ) else val = mw.text.trim(val) if val == '' then usedSequence = false gap = gap + 1 if (gap >= levels) then return ('Data must not skip more than ' .. levels-1 .. ' values') end elseif usedSequence then return ('Coordinates may not be added right after the named sequence') else if gap > 0 then gap = closeArrays(gap) elseif #result[levels] > 0 then return ('Named sequence "' .. val .. '" cannot be used in the middle of the sequence') end -- Parse value as a sequence name. Eventually we can load data from external data sources if val == 'values' then val = getSequence(args) elseif min == 4 and val == 'world' then val = {{36000,-180}, {36000,180}, {-36000,180}, {-36000,-180}, {36000,-180}} elseif tonumber(val) ~= nil then return ('Not a valid coordinate or a sequence name: ' .. val) else return ('Sequence "' .. val .. '" is not known. Try "values" or "world" (for Polygons), or specify values as lat,lon;lat,lon;... pairs') end result[levels] = val usedSequence = true end end end -- allow one empty last value (some might close the list with an extra semicolon) if (gap > 1) then return ('Data values must not have blanks at the end') end closeArrays(levels-1) return args.geotype == 'Point' and result[1][1] or result[1] end -- Run this function to check that the above works ok function p.parseGeoSequenceTest() local testSeq = function(data, expected) local result = getSequence(data) if type(result) == 'table' then local actual = mw.text.jsonEncode(result) result = actual ~= expected and 'data="' .. mw.text.jsonEncode(data) .. '", actual="' .. actual .. '", expected="' .. expected .. '"<br>\n' or '' else result = result .. '<br>\n' end return result end local test = function(geotype, data, expected, values) values = values or {} values.geotype = geotype; values.data = data; local result = p._parseGeoSequence(values) if type(result) == 'table' then local actual = mw.text.jsonEncode(result) result = actual ~= expected and 'geotype="' .. geotype .. '", data="' .. data .. '", actual="' .. actual .. '", expected="' .. expected .. '"<br>\n' or '' else result = 'geotype="' .. geotype .. '", data="' .. data .. '", error="' .. result .. '<br>\n' end return result end local values = {' 9 , 8 ','7,6'} local result = '' .. testSeq({}, '[]') .. testSeq({'\t\n 1 \r,-10'}, '[[-10,1]]') .. testSeq(values, '[[8,9],[6,7]]') .. test('Point', '1,2', '[2,1]') .. test('MultiPoint', '1,2;3,4;5,6', '[[2,1],[4,3],[6,5]]') .. test('LineString', '1,2;3,4', '[[2,1],[4,3]]') .. test('MultiLineString', '1,2;3,4', '[[[2,1],[4,3]]]') .. test('MultiLineString', '1,2;3,4;;5,6;7,8', '[[[2,1],[4,3]],[[6,5],[8,7]]]') .. test('Polygon', '1,2;3,4;5,6;1,2', '[[[2,1],[4,3],[6,5],[2,1]]]') .. test('MultiPolygon', '1,2;3,4;5,6;1,2', '[[[[2,1],[4,3],[6,5],[2,1]]]]') .. test('MultiPolygon', '1,2;3,4;5,6;1,2;;11,12;13,14;15,16;11,12', '[[[[2,1],[4,3],[6,5],[2,1]],[[12,11],[14,13],[16,15],[12,11]]]]') .. test('MultiPolygon', '1,2;3,4;5,6;1,2;;;11,12;13,14;15,16;11,12', '[[[[2,1],[4,3],[6,5],[2,1]]],[[[12,11],[14,13],[16,15],[12,11]]]]') .. test('MultiPolygon', '1,2;3,4;5,6;1,2;;;11,12;13,14;15,16;11,12;;21,22;23,24;25,26;21,22', '[[[[2,1],[4,3],[6,5],[2,1]]],[[[12,11],[14,13],[16,15],[12,11]],[[22,21],[24,23],[26,25],[22,21]]]]') .. test('MultiLineString', 'values;;1,2;3,4', '[[[8,9],[6,7]],[[2,1],[4,3]]]', values) .. test('Polygon', 'world;;world', '[[[36000,-180],[36000,180],[-36000,180],[-36000,-180],[36000,-180]],[[36000,-180],[36000,180],[-36000,180],[-36000,-180],[36000,-180]]]') .. '' return result ~= '' and result or 'Tests passed' end function p._tag(args) local tagname = args.type or 'maplink' if tagname ~= 'maplink' and tagname ~= 'mapframe' then error('unknown type "' .. tagname .. '"') end local geojson local tagArgs = { text = args.text, zoom = tonumber(args.zoom), latitude = tonumber(args.latitude), longitude = tonumber(args.longitude), group = args.group, show = args.show, class = args.class, url = args.url, image = args.image, } if (args.wikidata ~= nil) then local e = mw.wikibase.getEntity(args.wikidata) if e.claims ~= nil then if (not tagArgs.latitude or not tagArgs.longitude) then if e.claims.P625 ~= nil then tagArgs.latitude = e.claims.P625[1].mainsnak.datavalue.value.latitude tagArgs.longitude = e.claims.P625[1].mainsnak.datavalue.value.longitude end end if e.labels.en ~= nil then -- always try to fetch title, to get a reference in 'Wikidata entities used in this page' title = e.labels.en.value end if not args.title then args.title = title end --if not tagArgs.url then -- if e.claims.P856 ~= nil then -- tagArgs.url = e.claims.P856[1].mainsnak.datavalue.value -- end --end if not tagArgs.image then if e.claims.P18 ~= nil then tagArgs.image = e.claims.P18[1].mainsnak.datavalue.value end end end end if not args.title then args.title = '' end if not tagArgs.url then tagArgs.url = '' end if not tagArgs.image then tagArgs.image = '' end tagArgs.title = args.title if args.ismarker and (args.latitude == 'NA' or args.longitude == 'NA' or not tagArgs.latitude or not tagArgs.longitude) then return 'nowiki', '', tagArgs end if tagname == 'mapframe' then tagArgs.width = args.width == nil and 420 or args.width tagArgs.height = args.height == nil and 420 or args.height tagArgs.align = args.align == nil and 'right' or args.align elseif not args.class and (args.text == '' or args.text == '""') then -- Hide pushpin icon in front of an empty text link tagArgs.class = 'no-icon' end if args.data == '' then args.data = nil end if (not args.geotype) ~= (not args.data) then -- one is given, but not the other if args.data then error('Parameter "data" is given, but "geotype" is not set. Use one of these: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon') elseif args.geotype == "Point" and tagArgs.latitude ~= nil and tagArgs.longitude ~= nil then -- For Point geotype, it is enough to set latitude and logitude, and data will be set up automatically args.data = tagArgs.latitude .. ',' .. tagArgs.longitude else error('Parameter data must be set. Use "values" to use all unnamed parameters as coordinates (lat,lon|lat,lon|...), "world" for the whole world, a combination to make a mask, e.g. "world;;values", or direct values "lat,lon;lat,lon..." with ";" as value separator') end end -- Kartographer can now automatically calculate needed zoom & lat/long based on the data provided -- Current version ignores mapmasks, but that will also be fixed soon. Leaving this for now, but can be removed if all is good. -- tagArgs.zoom = tagArgs.zoom == nil and 14 or tagArgs.zoom -- tagArgs.latitude = tagArgs.latitude == nil and 51.47766 or tagArgs.latitude -- tagArgs.longitude = tagArgs.longitude == nil and -0.00115 or tagArgs.longitude if tagArgs.image ~= '' then args.description = (args.description or '') .. '[[file:' .. tagArgs.image .. '|300px]]' end if args.geotype then geojson = { type = "Feature", properties = { title = args.title, description = args.description, ['marker-size'] = args['marker-size'], ['marker-symbol'] = args['marker-symbol'], ['marker-color'] = args['marker-color'], stroke = args.stroke, ['stroke-opacity'] = tonumber(args['stroke-opacity']), ['stroke-width'] = tonumber(args['stroke-width']), fill = args.fill, ['fill-opacity'] = tonumber(args['fill-opacity']), }, geometry = { type = args.geotype, coordinates = p.parseGeoSequence(args) } } end if args.debug ~= nil then local html = mw.html.create(tagname, not geojson and {selfClosing=true} or nil) :attr(tagArgs) if geojson then html:wikitext( mw.text.jsonEncode(geojson, mw.text.JSON_PRETTY) ) end return 'syntaxhighlight', tostring(html) .. mw.text.jsonEncode(args, mw.text.JSON_PRETTY), { lang = 'json', latitude=0, longitude=0, title='', url='' } end return tagname, geojson and mw.text.jsonEncode(geojson) or '', tagArgs end function p.tag(frame) out = {} local args = getArgs(frame) local tag, geojson, tagArgs = p._tag(args) local listingTypes = {'see', 'eat', 'buy', 'drink', 'sleep'} if args.ismarker == 'yes' then if mw.title.getCurrentTitle().namespace == 0 and has_value({'do', unpack(listingTypes)}, string.lower(args.group)) -- prepend to copy of listingTypes, then out[#out + 1] = "[[Category:Has "..string.lower(args.group).." listing]]" end if geojson ~= '' then coordargs = {tagArgs.latitude, tagArgs.longitude, ['title'] = tagArgs.title} out[#out + 1] = '<span class="noprint listing-coordinates" style="display:none">' out[#out + 1] = '<span class="geo">' out[#out + 1] = '<abbr class="latitude">' .. tagArgs.latitude ..'</abbr>' out[#out + 1] = '<abbr class="longitude">' .. tagArgs.longitude ..'</abbr>' out[#out + 1] = '</span></span>' out[#out + 1] = '<span title="Map for this \''.. args.group ..'\' marker">' -- TODO out[#out + 1] = frame:extensionTag(tag, geojson, tagArgs) out[#out + 1] = ' </span>' if mw.title.getCurrentTitle().namespace == 0 then out[#out + 1] = "[[Category:Has map markers]]" end else if mw.title.getCurrentTitle().namespace == 0 and has_value(listingTypes, string.lower(args.group)) and (args.latitude ~= 'NA' and args.longitude ~= 'NA') then out[#out + 1] = "[[Category:"..string.lower(args.group).." listing with no coordinates]]" end end if mw.title.getCurrentTitle().namespace == 0 and has_value({'city', 'vicinity'}, string.lower(args.group)) and (args.wikidata == nil or args.wikidata == '') and (args.image == nil or args.image == '') then out[#out + 1] = "[[Category:Region markers without wikidata]]" end if tagArgs.title ~= '' then title = '<span id="'.. mw.uri.anchorEncode(tagArgs.title) ..'" class="fn org listing-name">\'\'\''.. tagArgs.title ..'\'\'\'</span>' else title = '' end if tagArgs.url ~= '' then out[#out + 1] = '['.. tagArgs.url ..' '..title..']' else out[#out + 1] = title end return table.concat(out, "") else return frame:extensionTag(tag, geojson, tagArgs) end end return p