Mòdul:Map/utilities

De la Viquipèdia, l'enciclopèdia lliure
-- Versió incial en proves

local p = {}

-- 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
		end
		table.insert(coordsGeo, coordsLine)
	end
	
	if geotype == 'Point' then
		coordsGeo = coordsGeo[1][1]
	elseif geotype == "LineString" or geotype == "MultiPoint" then
		coordsGeo = coordsGeo[1]
	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

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

local function fetchWikidata(id, snak)
	-- snak is a table like {'claims', 'P625', 1, 'mainsnak', 'datavalue', 'value'}
	-- see function ViewSomething on Module:Wikidata
	local value
	id = mw.text.trim(id)
	value = mw.wikibase.getEntityObject(id)
	for i in ipairs(snak) do
		if value == nil then break end
		value = value[snak[i]]
	end
	
	return value
end

-- Fetch coordinates from Wikidata for a list of comma separated ids
local function getCoordinatesById(ids)
	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
			coord[#coord+1] = value.latitude .. ',' .. value.longitude
		end
	end
	
	return #coord > 0 and table.concat(coord, ';') or nil
end


function p.item2geojson(frame)
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame)
	local tagname = args.type or 'mapframe'
	
	local geojson
	local tagArgs = {
		text = args.text,
		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 myfeatures, allpoints = {}, {}
	local i, j = 1, 1
	while args[i] do
		j = #myfeatures + 1
		myfeatures[j] = {}
		myfeatures[j]['type'] = "Feature"
		myfeatures[j]['geometry'] = {}
		myfeatures[j]['geometry']['type'] = 'Point'
		myfeatures[j]['geometry']['coordinates'] = parseGeoSequence(getCoordinatesById(args[i]), 'Point')
		allpoints = mergePoints(allpoints, myfeatures[j]['geometry']['coordinates'])
		myfeatures[j]['properties'] = {}
		myfeatures[j]['properties']['title'] = mw.wikibase.getLabel(args[i])
		myfeatures[j]['properties']['marker-size'] = args['marker-size'..i] or args['marker-size']
		myfeatures[j]['properties']['marker-symbol'] = args['marker-symbol'..i] or args['marker-symbol']
		myfeatures[j]['properties']['marker-color'] = args['marker-color'..i] or args['marker-color']
		
		i = i + 1
	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
			if #allpoints <= 1 then
				tagArgs.zoom = defaultzoom or 9
			else
				tagArgs.zoom = getZoom(allpoints, tagArgs.height, tagArgs.width)
			end
		else
			tagArgs.zoom = defaultzoom
		end
	end
	
	local geojson = {}
	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 frame:extensionTag('syntaxhighlight', tostring(html), {lang = 'json'})
	end
	return frame:extensionTag(tagname, mw.text.jsonEncode(geojson), tagArgs)
end

return p