Logo Voyage

Module:Map Voyage Tips and guide

You can check the original Wikivoyage article Here
[view] [edit] [history] [purge] Documentation

    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}}
    
    Parameters for Module:Map
    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] = '&#32;</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
    


    Discover



    Powered by GetYourGuide