انتقل إلى المحتوى

مودول:Map

من ويكيپيديا

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

See also: Module:Mapframe

Utilisation

[بدل لكود]
{{#invoke:map|tag|type=|text=|geotype=|title=|latitude=|longitude=|zoom=|marker-symbol=|marker-size=|marker-color=|data=|image=|width=|height=}}
Paramètre Utilisation Syntaxe
type maplink ou mapframe selon la fonction qui doit être invoquée
text Texte qui apparaitra 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
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

exemples : 808080 ; 008000 ; 8B0000 ; 800080 ; 4682B4 ; FF4500 ; 000000 ; 000080 ; FFD700

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)

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}}
خريطة
Carte de Liège


Voir aussi

[بدل لكود]

موضيل:Projet Scribunto


Catégorie:Module en langage Lua


-- Credits:
-- Original from Wikivoyage
-- Developed for Kartographer version on Wikipedia by Vriullop @cawiki
-- Formulae:
--    CSGNetwork at http://www.csgnetwork.com/degreelenllavcalc.html via @enwiki
--    OpenStreetMap
-- Version: 20200509

local p = {}

-- Localization on [[Module:Map/i18n]]
local i18n = {
	["coordinate-invalid"] = "Parameter $1 is an invalid value of \"latitude,longitude\".",
	["type-invalid"] = "Type $1 is invalid. Use mapframe or maplink.",
	["geotype-invalid"] = "Geotype $1 is an invalid value.",
	["ids-invalid"] = "Parameter ids $1 is invalid.",
	["polygon-required-points"] = "A polygon requires a minimum of 4 coordinate points.",
	["polygon-not-closed"] = "A closed polygon requires last point equal to first one.",
	['ids-not-found'] = "Ids not found for external data.",
	--['not-from-content-page'] = "Do not invoke from content page. Use a template or use a module subpage like /sandbox for testing .",
	-- local categories
	['cat-several-features'] = "",
	['cat-linestring-drawn'] = "",
	['cat-polygon-drawn'] = "",
}

local cat = {['cat-several-features'] = false, ['cat-linestring-drawn'] = false, ['cat-polygon-drawn'] = false}

-- Credit to http://stackoverflow.com/a/1283608/2644759, cc-by-sa 3.0
local function tableMerge(t1, t2)
	for k, v in pairs(t2) do
		if type(v) == "table" then
			if type(t1[k] or false) == "table" then
				tableMerge(t1[k] or {}, t2[k] or {})
			else
				t1[k] = v
			end
		else
			t1[k] = v
		end
	end
	return t1
end

local function loadI18n()
	local exist, res = pcall(require, "Module:Map/i18n")
	if exist and next(res) ~= nil then
		tableMerge(i18n, res.i18n)
	end
end
loadI18n()

local errormessage
local function printError(key, par)
	-- just print first error
	errormessage = errormessage or ('<span class="error">' .. (par and mw.ustring.gsub(i18n[key], "$1", par) or i18n[key]) .. '</span>')
end

-- Convert coordinates input format to geojson table
local function parseGeoSequence(data, geotype)
	local coordsGeo = {}
	for line_coord in mw.text.gsplit(data, ':', true) do -- Polygon - linearRing:linearRing...
		local coordsLine = {}
		for point_coord in mw.text.gsplit(line_coord, ';', true) do -- LineString or MultiPoint - point;point...
			local valid = false
			local val = mw.text.split(point_coord, ',', true) -- Point - lat,lon
			-- allow for elevation
			if #val >= 2 and #val <= 3 then
				local lat = tonumber(val[1])
				local lon = tonumber(val[2])
				if lat ~= nil and lon ~= nil then
					table.insert(coordsLine, {lon, lat})
					valid = true
				end
			end
			if not valid and point_coord ~= '' then printError('coordinate-invalid', point_coord) end
		end
		if geotype == 'Polygon' then
			if #coordsLine < 4 then
				printError('polygon-required-points')
			elseif table.concat(coordsLine[1]) ~= table.concat(coordsLine[#coordsLine]) then
				printError('polygon-not-closed')
			end
		end
		table.insert(coordsGeo, coordsLine)
	end
	
	if geotype == 'Point' then
		coordsGeo = coordsGeo[1][1]
	elseif geotype == "LineString" or geotype == "MultiPoint" then
		coordsGeo = coordsGeo[1]
	elseif geotype ~= 'Polygon' then
		printError('geotype-invalid', geotype)
	end
	
    return coordsGeo
end

-- data Point - {lon,lat}
-- data LineString - { {lon,lat}, {lon,lat}, ... }
-- data Polygon - { { {lon,lat}, {lon,lat} }, { {lon,lat}, {lon,lat} }, ... }
-- output as LineString format
local function mergePoints(stack, merger)
	if merger == nil then return stack end
	for _, val in ipairs(merger) do
		if type(val) == "number" then -- Point format
			stack[#stack + 1] = merger
			break
		elseif type(val[1]) == "table" then -- Polygon format
			for _, val2 in ipairs(val) do
				stack[#stack + 1] = val2
			end
		else -- LineString format
			stack[#stack + 1] = val
		end
	end
	return stack
end

-- remove duplicated points, they may affect zoom calculation
local function setUniquePoints(t)
	-- build set of unique values
	local uniqueElements = {}
	for _, point in ipairs(t) do
		if not uniqueElements[point[1]] then
			uniqueElements[point[1]] = {}
		end
		uniqueElements[point[1]][point[2]] = true
	end
	-- convert the set
	local result = {}
	for lon, _ in pairs(uniqueElements) do
		for lat, _ in pairs(uniqueElements[lon]) do
			table.insert(result, {lon, lat})
		end
	end
	
	return result
end

local function getCoordBounds(data)
	local latN, latS = -90, 90
	local lonE, lonW = -180, 180
	for i, val in ipairs(data) do
		latN = math.max(val[2], latN)
		latS = math.min(val[2], latS)
		lonE = math.max(val[1], lonE)
		lonW = math.min(val[1], lonW)
	end
	
	return latN, latS, lonE, lonW
end

local function getCoordCenter(data)
	local latN, latS, lonE, lonW = getCoordBounds(data)
	
	local latCenter = latS + (latN - latS) / 2
	local lonCenter = lonW + (lonE - lonW) / 2
	
	return lonCenter, latCenter
end

-- meters per degree by latitude
local function mxdByLat(lat)
	local latRad = math.rad(lat)
	-- see [[Geographic coordinate system#Expressing latitude and longitude as linear units]], by CSGNetwork
	local mxdLat = 111132.92 - 559.82 * math.cos(2 * latRad) + 1.175 * math.cos(4 * latRad) - 0.023 * math.cos(6 * latRad)
	local mxdLon = 111412.84 * math.cos(latRad) - 93.5 * math.cos(3 * latRad) + 0.118 * math.cos(5 * latRad)
	return mxdLat, mxdLon
end

-- Calculate zoom to fit coordinate bounds into height and width of frame
local function getZoom(data, height, width)
	local lat1, lat2, lon1, lon2 = getCoordBounds(data)
	
	local latMid = (lat1 + lat2) / 2 -- mid latitude
	local mxdLat, mxdLon = mxdByLat(latMid)
	-- distances in meters
	local distLat = math.abs((lat1 - lat2) * mxdLat)
	local distLon = math.abs((lon1 - lon2) * mxdLon)
	
	-- margin 100px in height and width, right upper icon is about 50x50px
	local validHeight = math.max(height - 100, 100)
	local validWidth = math.max(width - 100, 100)
	
	-- maximum zoom fitting all points
	local latRad = math.rad(latMid)
	for zoom = 19, 0, -1 do
		-- see https://wiki.openstreetmap.org/wiki/Zoom_levels#Metres_per_pixel_math
		-- equatorial circumference 40 075 036 m: [[Equator#Exact length]]
		local distLatFrame = 40075036 * validHeight * math.cos(latRad) / (2 ^ (zoom + 8))
		local distLonFrame = 40075036 * validWidth * math.cos(latRad) / (2 ^ (zoom + 8))
		if distLatFrame > distLat and distLonFrame > distLon then
			return zoom
		end
	end
	
	return 0
end

-- Geotype based on coordinates format pattern
local function findGeotype(coord)
	local _, semicolons = string.gsub(coord, ';', '')
	local firstcoord = string.match(coord, "[0-9%.%-]+%s*,%s*[0-9%.%-]+")
	local lastcoord = string.match(string.reverse(coord), "[0-9%.%-]+%s*,%s*[0-9%.%-]+")
	if firstcoord == nil or lastcoord == nil then
		printError('coordinate-invalid', coord)
	else
		lastcoord = string.reverse(lastcoord)
	end
	if string.find(coord, ':') or (semicolons > 2 and firstcoord == lastcoord) then
		return 'Polygon'
	elseif semicolons > 0 then
		return 'LineString' -- or MultiPoint
	else
		return 'Point'
	end
end

local function fetchWikidata(id, snak)
	-- snak is a table like {'claims', 'P625', 1, 'mainsnak', 'datavalue', 'value'}
	local value
	id = mw.text.trim(id)
	if not string.find(id, "^Q%d+$") then
		printError('ids-invalid', id)
	else
		value = mw.wikibase.getBestStatements(id, snak[2])
		for i = 3, #snak do
			if value == nil then break end
			value = value[snak[i]]
		end
	end
	
	return value
end

-- Fetch coordinates from Wikidata for a list of comma separated ids
local function getCoordinatesById(ids)
	local function roundPrec(num, prec)
		if prec == nil or prec <= 0 then return num end
		local sig = 10^math.floor(math.log10(prec)+.5) -- significant figure from sexagesimal precision: 0.00123 -> 0.001
		return math.floor(num / sig + 0.5) * sig
	end
	
	if ids == nil then return end
	local coord = {}
	local snak = {'claims', 'P625', 1, 'mainsnak', 'datavalue', 'value'}
	for idx in mw.text.gsplit(ids, '%s*,%s*') do
		local value = fetchWikidata(idx, snak)
		if value then
			local prec = value.precision
			coord[#coord+1] = roundPrec(value.latitude, prec) .. ',' .. roundPrec(value.longitude, prec)
		end
	end
	
	return #coord > 0 and table.concat(coord, ';') or nil
end

local function getBoundsById(ids, coordInput)
	if ids == nil then return {} end
	local coord = mw.text.split(coordInput, '%s*;%s*')
	local id = mw.text.split(ids, '%s*,%s*')
	if #coord ~= #id then return {} end
	local id_parent = nil
	if #id == 1 then
		id_parent = fetchWikidata(id[1], {'claims', 'P131', 1, 'mainsnak', 'datavalue', 'value', 'id'})
		if id_parent ~= nil then
			id[2] = id_parent -- P131: located in the administrative territorial entity, last try
			coord[2] = coord[1]
		end
	end
	local bounds = {}
	-- try to fetch Wikidata in this order: area, watershed area, population, and finally by administrative entity
	local snak_area = {'claims', 'P2046', 1, 'mainsnak', 'datavalue', 'value'} -- area and unit
	local snak_warea = {'claims', 'P2053', 1, 'mainsnak', 'datavalue', 'value'} -- area and unit
	local snak_pop = {'claims', 'P1082', 1, 'mainsnak', 'datavalue', 'value'} -- population
	local convert_area = {['Q712226'] = 1000000, ['Q35852'] = 10000, ['Q232291'] = 2589988.110336, ['Q81292'] = 4046.8564224,
		['Q935614'] = 1600, ['Q857027'] = 0.09290304, ['Q21074767'] = 1138100, ['Q25343'] = 1} -- to square metres
		-- query Wikidata: http://tinyurl.com/j8aez2g
	for i = 1, #id do
		if i == 2 and id[2] == id_parent and #bounds > 0 then break end -- only if not found previously
		local amount, unit, area
		local value = fetchWikidata(id[i], snak_area) or fetchWikidata(id[i], snak_warea)
		if value then
			amount = tonumber(value.amount)
			unit = string.match(value.unit, "(Q%d+)")
			if convert_area[unit] then
				area = amount * convert_area[unit]
			end
		end
		if area == nil then
			value = fetchWikidata(id[i], snak_pop)
			if value then
				amount = tonumber(value.amount)
				-- average density estimated for populated areas: 100; see [[Population density]]
				area = amount / 100 * 1000000
			end
		end
		if area then
			local radius = math.sqrt(area / math.pi) -- approximation with a circle
			local latlon = mw.text.split(coord[i], '%s*,%s*')
			local mxdLat, mxdLon = mxdByLat(latlon[1])
			bounds[#bounds+1] = {latlon[2] + (radius / mxdLon), latlon[1] + (radius / mxdLat)} -- NE bound, geoJSON format
			bounds[#bounds+1] = {latlon[2] - (radius / mxdLon), latlon[1] - (radius / mxdLat)} -- SW bound
		end
	end
	return bounds
end

local function circleToPolygon(center, radius, edges, turn)
	-- From en:Module:Mapframe, based on https://github.com/gabzim/circle-to-polygon, ISC licence
	
	local function offset(cLat, cLon, distance, bearing)
		local lat1 = math.rad(cLat)
		local lon1 = math.rad(cLon)
		local dByR = distance / 6378137 -- distance divided by 6378137 (radius of the earth) wgs84
		local lat = math.asin(
			math.sin(lat1) * math.cos(dByR) +
			math.cos(lat1) * math.sin(dByR) * math.cos(bearing)
		)
		local lon = lon1 + math.atan2(
			math.sin(bearing) * math.sin(dByR) * math.cos(lat1),
			math.cos(dByR) - math.sin(lat1) * math.sin(lat)
		)
		return math.deg(lat) .. ',' .. math.deg(lon)
	end
	
	local coords = mw.text.split(center, ',', true)
	local lat = tonumber(coords[1])
	local long = tonumber(coords[2])
	edges = edges or 32
	local move = 2 * math.pi * (turn or 0)
	local coordinates = {}
	for i = 0, edges do
		table.insert(coordinates, offset(lat, long, radius, ((2*math.pi*-i)/edges) + move))
	end
	return table.concat(coordinates, ';')
end

local function addCategories(geotype, i)
	if not mw.title.getCurrentTitle().isContentPage then return end
	
	if i > 2 and i18n["cat-several-features"] ~= '' then
		cat["cat-several-features"] = true
	end
	if geotype == "LineString" and i18n["cat-linestring-drawn"] ~= '' then
		cat["cat-linestring-drawn"] = true
	elseif geotype == "Polygon" and i18n["cat-polygon-drawn"] ~= '' then
		cat["cat-polygon-drawn"] = true
	end
	return
end

-- Main function
function p._tag(args)
	local tagname = args.type or 'mapframe'
	if tagname ~= 'maplink' and tagname ~= 'mapframe' then printError('type-invalid', tagname) end
	
	local tagArgs = {
		text = args.text,
		lang = "ar",
		zoom = tonumber(args.zoom),
		latitude = tonumber(args.latitude),
		longitude = tonumber(args.longitude)
	}
	local defaultzoom = tonumber(args.default_zoom)
	if tagname == 'mapframe' then
		tagArgs.width = args.width or 300
		tagArgs.height = args.height or 300
		tagArgs.align = args.align or 'right'
		if args.frameless ~= nil and tagArgs.text == nil then tagArgs.frameless = true end
	else
		tagArgs.class = args.class
	end
	
	local wdid = args.item or mw.wikibase.getEntityIdForCurrentPage()
	
	if args['coordinates1'] == nil and args['geotype1'] == nil then -- single feature
		args['coordinates1'] = args['coordinates'] or args[1]
		if args['coordinates1'] == nil and args['latitude'] and args['longitude'] then
			args['coordinates1'] = args['latitude'] .. ',' .. args['longitude']
		elseif args['coordinates1'] == nil then
			args['coordinates1'] = getCoordinatesById(wdid)
		end
		local par = {'title', 'image', 'description', 'geotype', 'commons', 'radius', 'radiuskm', 'edges', 'turn'}
		for _, v in ipairs(par) do
			args[v .. '1'] = args[v .. '1'] or args[v]
		end
	end
	
	local externalData = {['geoshape'] = true, ['geomask'] = true, ['geoline'] = true, ['page'] = true, ['none'] = true}
	local featureCollection = {['Point'] = true, ['MultiPoint'] = true, ['LineString'] = true, ['Polygon'] = true, ['circle'] = true}
	local myfeatures, myexternal, allpoints = {}, {}, {}
	local i, j = 1, 1
	while args['coordinates'..i] or args['ids'..i] or externalData[args['geotype'..i]] or args['commons'..i] do
		local geotypex = args['geotype'..i] or args['geotype']
		if geotypex == nil and args['commons'..i] then
			geotypex = 'page'
		end
		if geotypex ~= nil and not (featureCollection[geotypex] or externalData[geotypex]) then
			printError('geotype-invalid', geotypex)
			break
		end
		
		if geotypex == 'none' then -- skip this object
			i = i + 1
		else
			local mystack
			if externalData[geotypex or ''] then
				mystack = myexternal
				j = #mystack + 1
				mystack[j] = {}
				mystack[j]['type'] = "ExternalData"
				mystack[j]['service'] = geotypex
				if geotypex == "page" then
					local page_name = args['commons'..i]
					if mw.ustring.find(page_name, "Data:", 1, true) == 1 then
						page_name = string.sub(page_name, 6)
					end
					if mw.ustring.find(page_name, ".map", -4, true) == nil then
						page_name = page_name .. '.map'
					end
					mystack[j]['title'] = page_name
				else
					mystack[j]['ids'] = args['ids'..i] or args['ids'] or wdid
					if mystack[j]['ids'] == nil then printError('ids-not-found'); break end
				end
				local mycoordinates = args['coordinates'..i]
				if mycoordinates == nil and (tagArgs.latitude == nil or tagArgs.longitude == nil or tagArgs.zoom == nil) then
					mycoordinates = getCoordinatesById(mystack[j]['ids'])
				end
				if mycoordinates ~= nil then
					local mypoints = getBoundsById(mystack[j]['ids'], mycoordinates)
					if #mypoints == 0 then
						mypoints = parseGeoSequence(mycoordinates, mycoordinates:find(';') and 'MultiPoint' or 'Point')
					end
					allpoints = mergePoints(allpoints, mypoints)
				end
			else
				args['coordinates'..i] = args['coordinates'..i] or getCoordinatesById(args['ids'..i])
				if geotypex == 'circle' then
					if not args['radius'..i] and args['radiuskm'..i] then
						args['radius'..i] = args['radiuskm'..i] * 1000
					end
					args['coordinates'..i] = circleToPolygon(args['coordinates'..i], args['radius'..i], args['edges'..i], args['turn'..i])
					geotypex = 'Polygon'
				end
				mystack = myfeatures
				j = #mystack + 1
				mystack[j] = {}
				mystack[j]['type'] = "Feature"
				mystack[j]['geometry'] = {}
				mystack[j]['geometry']['type'] = geotypex or findGeotype(args['coordinates'..i])
				mystack[j]['geometry']['coordinates'] = parseGeoSequence(args['coordinates'..i], mystack[j]['geometry']['type'])
				allpoints = mergePoints(allpoints, mystack[j]['geometry']['coordinates'])
				addCategories(mystack[j]['geometry']['type'], i)
			end
			mystack[j]['properties'] = {}
			mystack[j]['properties']['title'] = args['title'..i] or (geotypex and geotypex .. i) or mystack[j]['geometry']['type'] .. i
			if args['image'..i] then
				args['description'..i] = (args['description'..i] or '') .. '[[ملف:' .. args['image'..i] .. '|300px]]'
			end
			mystack[j]['properties']['description'] = args['description'..i]
			mystack[j]['properties']['marker-size'] = args['marker-size'..i] or args['marker-size']
			mystack[j]['properties']['marker-symbol'] = args['marker-symbol'..i] or args['marker-symbol']
			mystack[j]['properties']['marker-color'] = args['marker-color'..i] or args['marker-color']
			mystack[j]['properties']['stroke'] = args['stroke'..i] or args['stroke']
			mystack[j]['properties']['stroke-opacity'] = tonumber(args['stroke-opacity'..i] or args['stroke-opacity'])
			mystack[j]['properties']['stroke-width'] = tonumber(args['stroke-width'..i] or args['stroke-width'])
			mystack[j]['properties']['fill'] = args['fill'..i] or args['fill']
			mystack[j]['properties']['fill-opacity'] = tonumber(args['fill-opacity'..i] or args['fill-opacity'])
			
			i = i + 1
		end
	end
	
	-- calculate defaults for static mapframe; maplink is dynamic
	if (tagArgs.latitude == nil or tagArgs.longitude == nil) and #allpoints > 0 then
		if tagname == "mapframe" or tagArgs.text == nil then -- coordinates needed for text in maplink
			tagArgs.longitude, tagArgs.latitude = getCoordCenter(allpoints)
		end
	end
	if tagArgs.zoom == nil then
		if tagname == "mapframe" then
			local uniquepoints = setUniquePoints(allpoints)
			if #uniquepoints == 1 then
				local coordInput = uniquepoints[1][2] .. ',' .. uniquepoints[1][1]
				local mybounds = getBoundsById(wdid, coordInput) -- try to fetch by area
				uniquepoints = mergePoints(uniquepoints, mybounds)
			end
			if #uniquepoints <= 1 then
				tagArgs.zoom = defaultzoom or 9
			else
				tagArgs.zoom = getZoom(uniquepoints, tagArgs.height, tagArgs.width)
			end
		else
			tagArgs.zoom = defaultzoom
		end
	end
	
	local geojson = myexternal
	if #myfeatures > 0 then
		geojson[#geojson + 1] = {type = "FeatureCollection", features = myfeatures}
	end
	
	if args.debug ~= nil then
		local html = mw.text.tag{name = tagname, attrs = tagArgs, content = mw.text.jsonEncode(geojson, mw.text.JSON_PRETTY)}
		return 'syntaxhighlight', tostring(html) .. ' Arguments:' .. mw.text.jsonEncode(args, mw.text.JSON_PRETTY), {lang = 'json'}
	end
	
	if geojson and #geojson == 0 then
		errormessage = erromessage or '' -- previous message or void for no map data
	end
	
	return tagname, geojson and mw.text.jsonEncode(geojson) or '', tagArgs
end

function p.tag(frame)
	--if mw.title.new(frame:getParent():getTitle()).isContentPage and not mw.title.new(frame:getTitle()).isSubpage then
		-- invoked from a content page and not invoking a module subpage
	--	printError('not-from-content-page')
	--end
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame)
	local tag, geojson, tagArgs = p._tag(args)
	
	local categories = ''
	
	if errormessage then
		if errormessage == '' then -- no map data
			return
		else
			categories = mw.message.new('Kartographer-broken-category'):inLanguage(mw.language.getContentLanguage().code):plain()
			return errormessage .. '[[تصنيف:' .. categories .. ']]'
		end
	end
	
	for k, v in pairs(cat) do
		if v then
			categories = categories .. '[[تصنيف:' .. i18n[k] .. ']]'
		end
	end
	
	return frame:extensionTag(tag, geojson, tagArgs) .. categories
end

return p