Aller au contenu

Module:Map

De Wikivoyage

Ce module fournit des paramètres d'entrée pour les fonctions map et maplink prises en charge par Extension:Kartographer.

Utilisation

[modifier]
{{#invoke:map|tag|type=|text=|geotype=|title=|latitude=|longitude=|zoom=|marker-symbol=|marker-size=|marker-color=|group=|show=|data=|image=|width=|height=}}
Paramètre Utilisation Syntaxe
type maplink ou mapframe selon la fonction qui doit être invoquée
text Texte qui apparaîtra dans le coin inférieur gauche de la carte ou en lieu et place des coordonnées
geotype Point pour les points individuels, Polygon pour les polygones
title Nom de l'objet
latitude et longitude
zoom Niveau de zoom de la carte Le niveau par défaut est de « 13 »
marker-symbol Symbole, lettre ou numéro à afficher sur la carte comme marqueur
  • Symboles les plus courants : town-hall hôtel de ville, information bureau d'information, aiport aéroport, harbor, rail, bus bus, rail-metro, museum , castle, restaurant restaurant, lodging, campsite, bar bar, theatre théâtre, bank banque, hospital hôpital, embassy ambassade (liste complète des icônes dans la section « Voir aussi »)
  • Lettres : -letter
  • Numéros : -number
marker-size Taille du symbole, lettre ou numéro à afficher sur la carte comme marqueur
  • Vide par défaut
  • large pour une plus grande taille
marker-color Couleur du marqueur de carte
  • 808080 couleur par défaut si le paramètre est vide
  • 008000 pour le symbole « town-hall » (ou 4682B4 s'il est dans la section « Voir ») ainsi que les symboles « information », « bank » et « hospital »
  • 8B0000 pour les symboles « airport », « harbor », « rail » et « bus » ainsi que les lettres et chiffres de la section « Aller »
  • 800080 pour les symboles « bus » et « rail-metro » ainsi que les lettres et chiffres de la section « Circuler »
  • 4682B4 pour les symboles « museum » et « castle » ainsi que les lettres et chiffres de la section « Voir »
  • FF4500 pour le symbole « restaurant » et les lettres et chiffres de la section « Manger »
  • 000000 pour les symboles « bar » et « theatre » ainsi que les lettres et chiffres de la section « Boire un verre / Sortir »
  • 000080 pour les symboles « lodging » et « campsite » ainsi que les lettres et chiffres de la section « Dormir »
  • FFD700 pour le symbole « embassy » ainsi que les lettres et chiffres de la section « Ambassades et consulats »
  • A0522D pour les lettres et chiffres des sections « Autres destinations » et « Aux environs »
group Groupe du marqueur (voir, manger, boire, etc.)
show Quels groupes de marqueurs afficher (par défaut, les groupes les plus courants comme voir, manger, boire, etc.)
data data=values remplit le polygone donné par data
data=world;;values remplit la zone à l'extérieur du polygone
image Nom de l'image affichée dans la vignette
width et height Largeur et hauteur de la carte en px ou % de la largeur de l'écran (uniquement pour mapframe)

Exemple

[modifier]

Avec maplink :

Avec mapframe :

{{#invoke:map|tag|type=mapframe|text=Carte de [[Liège]]|geotype=Point|title=Musée du Grand Curtius|latitude=50.64709 |longitude=5.58370 |zoom=15|image=LIEGE Palais Curtius - actuel Musée Curtius quai de Maestricht 13 (13-2013).JPG|marker-symbol=museum|marker-size=large|marker-color=4682B4|group=voir}}
Carte
Carte de Liège

Voir aussi

[modifier]

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,
        alt = args.alt,
        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.fr ~= nil then
		    	-- always try to fetch title, to get a reference in 'Wikidata entities used in this page'
	    		title = e.labels.fr.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.alt then
		tagArgs.alt = ''
	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,
                alt = args.alt,
                ['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:Article avec marqueur]]"
			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
			out[#out + 1] = title
		if tagArgs.alt ~= '' then
			alt = ' (<i>' .. tagArgs.alt .. '</i>)'
		else
			alt = ''
		end
			out[#out + 1] = alt
		return table.concat(out, "")
	else
		return frame:extensionTag(tag, geojson, tagArgs)
	end
end

return p