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}}
ParameterUsage
typemaplink or mapframe depending on which function should be invoked
geotypePoint for individual points, Polygon for polygons
titleObject name
latitude and longitude
zoomZoom level of the map
marker-symbolSymbol, letter, or number for displaying on the map as marker
marker-colorColor of the map marker
groupGroup of markers (see, eat, drink, etc.)
showWhich marker groups to show (by default shows the most common groups like see, eat, drink, ...)
datadata=values fills the polygon given by data
data=world;;values fills the area outside of the polygon
imageName of the image shown in the thumbnail
width and heightmap width and map height in px or % of screen width, only for mapframe

local getArgs = require('Moduuli:Arguments').getArgslocal p = {}function dbg(v, msg)    mw.log((msg or '') .. mw.text.jsonEncode(v))end-- Parse all unnamed string parameters in a form of "latitude, longitude" into the real number pairsfunction 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 coordsend--   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 resultendfunction 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 okfunction 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'endfunction 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,    }    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 linktagArgs.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.longitudeif args.image thenargs.description = (args.description or '') .. '[[file:' .. args.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' }    end    return tagname, geojson and mw.text.jsonEncode(geojson) or '', tagArgsendfunction p.tag(frame)    local args = getArgs(frame)    local tag, geojson, tagArgs = p._tag(args)    return frame:extensionTag(tag, geojson, tagArgs)endreturn p