Vai al contenuto

Modulo:Coord/sandbox

Da Wikipedia, l'enciclopedia libera.
Versione del 11 gen 2015 alle 22:43 di Rotpunkt (discussione | contributi) (uniformati apici)
--[[
* Modulo per implementare le funzionalità di Coord
*
* Traduce in lua:
* Template:Coord
* Template:Coord/input/dec
* Template:Coord/input/d
* Template:Coord/input/dm
* Template:Coord/input/dms
* Template:Coord/input/ERROR
* Template:Coord/input/error2
* Template:Coord/link
* Template:Coord/prec dec
* Template:Coord/dms2dec
* Template:Coord/dec2dms
* Template:Coord/dec2dms/d
* Template:Coord/dec2dms/d1
* Template:Coord/dec2dms/dm
* Template:Coord/dec2dms/dm1
* Template:Coord/dec2dms/dms
* Template:Coord/dec2dms/dms1
* Template:Precision1
]]

-- Variabili globali
local p = {}           -- per l'esportazione delle funzioni del modulo
local args = {}        -- argomenti passati al template
local errorTable = {}  -- table per contenere gli errori da ritornare

-- Configurazione
local cfg = mw.loadData("Modulo:Coord/Configurazione")

-------------------------------------------------------------------------------
--                      Funzioni di utilità
-------------------------------------------------------------------------------

-- Ritorna il numero arrotondato al numero di cifre decimali richiesto
local function round(num, idp)
	local mult = 10^(idp or 0)

	return math.floor(num * mult + 0.5) / mult
end

-- Ritorna la stringa "0 + numero" quando il numero è di una sola cifra, altrimenti lo stesso numero
local function padleft0(num)
	return (num < 10 and "0" or "") .. num
end

-- Converte un numero in stringa senza usare la notazione scientifica, esempio tostring(0.00001)
local function numberToString(num)
	-- la parentesi () extra serve per non ritornare anche il gsub.count
	return (string.format("%f", num):gsub("%.?0+$", ""))
end

-- Ritorna i claim con il rank richiesto
local function filterRankValue(claims, rank)
	local ret = {}
	for i, claim in pairs(claims) do
		if claim.rank == rank then
			table.insert(ret, claim)
		end
	end
	return ret
end

-- Ritorna latitudine e longitudine della [[d:property:p625]]
-- o nil nel caso non siano presenti
local function getWikidataCoordinates()
	local entity, claims, value, lat, long

	entity = mw.wikibase.getEntityObject()
	if entity and entity.claims and entity.claims.P625 and #entity.claims.P625 > 0 then
		claims = filterRankValue(entity.claims.P625, "preferred")
		if #claims == 0 then
			claims = filterRankValue(entity.claims.P625, "normal")
		end
		if #claims > 0 and claims[1].mainsnak.snaktype == "value" then
			value = claims[1].mainsnak.datavalue.value
			lat = round(value.latitude, 6)
			long = round(value.longitude, 6)
		end
	end

	return lat, long
end

-------------------------------------------------------------------------------
--                      Parsing parametri
-------------------------------------------------------------------------------

-- Error handler per xpcall, formatta l'errore
-- nel namespace principale aggiunge una categoria di warning
local function errhandler(msg)
	local cat = ""

	if mw.title.getCurrentTitle().namespace == 0 then
		cat = "[[Categoria:" .. cfg.categorie["warning"] .. "]]\n"
	end

	return string.format("%s<span style=\"color:red;\">Il template {{Coord}} ha riscontrato degli errori " ..
						 "([[Template:Coord|istruzioni]]):\n%s</span>\n", cat, msg:match(".+%d:%s(.+)$"))
end

-- Raccoglie più messaggi di errore in un'unica table prima di usare error()
local function dumpError(...)
	local arg = {...}

	table.insert(errorTable, "* ")
	for _, val in ipairs(arg) do
		table.insert(errorTable, val)
	end
	table.insert(errorTable, "\n")
end

-- Parsifica il parametro "display"
local function getDisplay()
	local display = {}
	
	display.inline = not args["display"] or args["display"] == "inline" or args["display"] == "inline,title"
	display.title = args["display"] == "title" or args["display"] == "inline,title"
	display.debug = args["display"] == "debug"
	
	return display
end

-- Con le richieste "dm" e "dms" verifica se ci sono
-- parametri lasciati vuoti in modo valido.
-- Ritorna il tipo di richiesta dopo la eventuale semplificazione.
local function paramsEmpty(reqFormat)
	if reqFormat == "dms" then
		-- {{coord|1|2||N|5|6||E}} valido
		if args[3] == "" and args[7] == "" then
			table.remove(args, 7)
			table.remove(args, 3)
			reqFormat = "dm"
		-- {{coord|1|2|3|N|5|6||E}} non valido
		elseif args[3] == "" or args[7] == "" then
			error("* lat e long hanno diversa precisione")
		-- {{coord|1||3|N|5||7|E}} valido
		elseif args[2] == "" and args[6] == "" then
			args[2], args[6] = 0, 0
		-- {{coord|1|2|3|N|5||7|E}} non valido
		elseif args[2] == "" or args[6] == "" then
			error("* lat e long hanno diversa precisione")
		end
	end

	if reqFormat == "dm" then
		-- {{coord|1||N|4||E}} valido
		if args[2] == "" and args[5] == "" then
			table.remove(args, 5)
			table.remove(args, 2)
			reqFormat = "d"
		-- {{coord|1|2|N|4||E}} non valido
		elseif args[2] == "" or args[5] == "" then
			error("* lat e long hanno diversa precisione")
		end
	end

	return reqFormat
end

-- Riconosce il tipo di richiesta ("dec", "d", "dm" o "dms") e parsifica i parametri posizionali
-- Ritorna il tipo di richiesta.
local function paramsParse()
	local reqFormat, globe, earth, prefix, num, str
	local param = {}

	-- riconoscimento tipo di richiesta
	if #args < 2 then
		error("* coordinate non specificate")
	elseif #args < 4 then
		reqFormat = "dec"
	elseif #args < 6 then
		reqFormat = "d"
	elseif #args < 8 then
		reqFormat = "dm"
	elseif #args < 10 then
		reqFormat = "dms"
	else
		error("* errato numero di parametri")
	end

	-- valida i parametri che possono essere lasciati vuoti
	reqFormat = paramsEmpty(reqFormat)

	-- validazione parametri posizionali
	currFormat = cfg.params[reqFormat]
	globe = args[#args]:match("globe:(%w+)")
	earth = not globe or globe == "earth"
	for k, v in ipairs(args) do
		if currFormat[k] then
			param.type = currFormat[k][1]
			param.name = currFormat[k][2]
			param.min = currFormat[k][3]
			param.max = currFormat[k][4]
			prefix = reqFormat .. " format: " .. param.name
			-- valida un parametro di tipo numero
			if param.type == "number" then
				num = tonumber(v)
				if num then
					if earth and num < param.min then
						dumpError(prefix, " < ", param.min)
					elseif earth and math.floor(num) > param.max then
						dumpError(prefix, " > ", param.max)
					end
				else
					dumpError(prefix, " non è un numero")
				end
			-- valida un parametro di tipo stringa
			elseif param.type == "string" then
				if v ~= param.min and v ~= param.max then
					dumpError(prefix, " diverso da ", param.min, " e da ", param.max)
				end
			end
		end
	end

	if #errorTable > 0 then
		error(table.concat(errorTable))
	end
	
	return reqFormat
end

-------------------------------------------------------------------------------
--                      classi DecCoord e DmsCoord
-------------------------------------------------------------------------------

-- Rappresenta una coordinata (lat o long) in gradi decimali 
local DecCoord = {}

-- Rappresenta una coordinata (lat o long) in gradi/minuti/secondi
local DmsCoord = {}

-- Costruttore di DecCoord
-- deg: gradi decimali, positivi o negativi, se negativi viene cambiato il segno e
--      la direzione cardinale eventualmente invertita
-- card: direzione cardinale (N|S|E|W)
function DecCoord:new(deg, card)
	local self = {}

	setmetatable(self, { __index = DecCoord,
						 __tostring = function(t) return self:__tostring() end,
						 __concat = function(t, t2) return tostring(t) .. tostring(t2) end })

	self.deg = tonumber(deg)
	if self.deg < 0 then
		self.card = card == "N" and "S" or (card == "E" and "W" or card)
		self.deg = -self.deg
	else
		self.card = card
	end

	return self
end

-- Richiamata automaticamente ogni volta che è richiesto un tostring o un concatenamento
function DecCoord:__tostring()
	return numberToString(self.deg) .. "°" .. self.card
end

-- Ritorna i gradi con segno
function DecCoord:getDeg()
	local deg = self.deg * ((self.card == "N" or self.card =="E") and 1 or -1)
	
	return numberToString(deg)
end

-- Ritorna un nuovo oggetto DmsCoord, convertendo in gradi/minuti/secondi
function DecCoord:toDms()
	local deg, min, sec
	
	deg = round(self.deg * 3600, 2)
	sec = round(math.floor(deg) % 60 + deg - math.floor(deg), 2)
	deg = math.floor((deg - sec) / 60)
	min = deg % 60
	deg = math.floor((deg - min) / 60) % 360

	return DmsCoord:new(deg, min, sec, self.card)
end

-- Costruttore di DmsCoord
-- deg: gradi
-- min: minuti, può essere nil
-- sec: secondi, può essere nil
-- card: direzione cardinale (N|S|E|W)
function DmsCoord:new(deg, min, sec, card)
	local self = {}

	setmetatable (self, { __index = DmsCoord,
						  __tostring = function(t) return self:__tostring() end,
						  __concat = function(t, t2) return tostring(t) .. tostring(t2) end })

	self.deg = tonumber(deg)
	self.min = min and tonumber(min)
	self.sec = sec and tonumber(sec)
	self.card = card

	return self
end

-- Richiamata automaticamente ogni volta che è richiesto un tostring o un concatenamento
function DmsCoord:__tostring()
	return self.deg .. "°" ..
		   (self.min and (padleft0(self.min) .. "′") or "") ..
		   (self.sec and (padleft0(self.sec) .. "″") or "") ..
		   self.card
end

-- Ritorna un nuovo oggetto DecCoord, convertendo in gradi decimali
function DmsCoord:toDec()
	local deg = round((self.deg + ((self.min or 0) + (self.sec or 0) / 60) / 60), 6)
	
	return DecCoord:new(deg, self.card)
end

-------------------------------------------------------------------------------
--                                Coord
-------------------------------------------------------------------------------

-- Nel namespace principale e con display.title legge la [[d:property:p625]]
-- e la utilizza come latitudine e longitudine se non fornite dall'utente.
-- Ritorna la categoria appropriata per [[Categoria:Wikidata]], altrimenti una stringa vuota
local function checkWikidata(display)
	local wdLat, wdLong, cat

	if mw.title.getCurrentTitle().namespace == 0 and display.title then
		wdLat, wdLong = getWikidataCoordinates()
		if wdLat and wdLong then
			-- se l'utente non ha fornito lat e long usa quelli di Wikidata
			if #args == 0 or (#args == 1 and not tonumber(args[1])) then
				table.insert(args, numberToString(wdLat))
				table.insert(args, numberToString(wdLong))
				if #args == 3 then
					table.insert(args, table.remove(args, 1))
				end
				cat = "Coordinate assenti ma presenti su Wikidata"
			end
		else
			cat = "Coordinate non presenti su Wikidata"
		end
	end

	return cat and ("[[Categoria:" .. cat .. "]]") or ""
end

-- Crea l'HTML ritornato da Coord
local function buildHTML(decLat, decLong, dmsLat, dmsLong, geohackParams, defaultFormat, display)
	local root, text, url, htmlTitle, noprint

	-- geohack url e parametri
	url = cfg.geohackUrl ..
		  "&pagename=" .. mw.uri.encode(mw.title.getCurrentTitle().prefixedText, "WIKI") ..
		  "&params=" .. geohackParams ..
		  (args["name"] and ("&title=" .. mw.uri.encode(args["name"])) or "")

	root = mw.html.create("")
	root
		:tag("span")
			:addClass("plainlinks nourlexpansion")
			:wikitext("[" .. url)
			:tag("span")
				:addClass(defaultFormat == "dec" and "geo-nondefault" or "geo-default") 
				:tag("span")
					:addClass("geo-dms")
					:attr("title", "Mappe, foto aeree e altri dati per questa posizione")
					:tag("span")
						:addClass("latitude")
						:wikitext(tostring(dmsLat))
						:done()
					:wikitext(" ")
					:tag("span")
						:addClass("longitude")
						:wikitext(tostring(dmsLong))
						:done()
					:done()
				 :done()
			:tag("span")
				:addClass("geo-multi-punct")
				:wikitext("&#xfeff; / &#xfeff;")
				:done()
			:tag("span")
				:addClass(defaultFormat == "dec" and "geo-default" or "geo-nondefault")
				:wikitext(args["name"] and "<span class=\"vcard\">" or "")
				:tag("span")
					:addClass("geo-dec")
					:attr("title", "Mappe, foto aeree e altri dati per questa posizione")
					:wikitext(decLat .. " " .. decLong)
					:done()
				:tag("span")
					:attr("style", "display:none")
					:tag("span")
						:addClass("geo")
						:wikitext(decLat:getDeg() .. "; " .. decLong:getDeg())
						:done()
					:done()
				:wikitext(args["name"] and ("<span style=\"display:none\"> (<span class=\"fn org\">" ..
						  args["name"] .. "</span>)</span></span>") or "")
				:done()
			:wikitext("]")
			:done()

	-- formatta il risultato a seconda di args["display"] (nil, "inline", "title", "inline,title")
	text = tostring(root) .. (args["notes"] or "")
	-- se inline e title, in stampa deve apparire solo il primo
	noprint = display.inline and "class=\"noprint\" " or ""
	htmlTitle = "<span style=\"font-size: small;\"><span " .. noprint .. "id=\"coordinates\">[[Coordinate geografiche|Coordinate]]: "

	return (display.inline and text or "") ..
		   (display.title and (htmlTitle .. text .. "</span></span>") or "")
end

-- Dato un input fino a 9 parametri posizionali e 4 con nome, ritorna un html
-- contenente le coordinate in formato dec e dms come collegamento esterno a geohack.php.
local function coord()
	local decLat, degLong, dmsLat, dmsLong
	local reqFormat, defaultFormat, geohackParams, display, wdCat, gdStr, gdRet, ret
	
	-- parsifica il parametro "display"
	display = getDisplay()

	-- legge [[d:property:p625]] ed eventualmente la utilizza
	wdCat = checkWikidata(display)

	-- parsifica i parametri posizionali
	ret, reqFormat = xpcall(paramsParse, errhandler)
	if not ret then
		return reqFormat
	end

	if reqFormat == "dec" then
		-- {{coord|1.111|2.222}}
		decLat = DecCoord:new(args[1], "N")
		decLong = DecCoord:new(args[2], "E")
	elseif reqFormat == "d" then
		-- {{coord|1.111|N|3.333|W}}
		decLat = DecCoord:new(args[1], args[2])
		decLong = DecCoord:new(args[3], args[4])
	elseif reqFormat == "dm" then
		-- {{coord|1|2|N|4|5|W}}
		dmsLat = DmsCoord:new(args[1], args[2], nil, args[3])
		dmsLong = DmsCoord:new(args[4], args[5], nil, args[6])
	elseif reqFormat == "dms" then
		-- {{coord|1|2|3|N|5|6|7|W}}
		dmsLat = DmsCoord:new(args[1], args[2], args[3], args[4])
		dmsLong = DmsCoord:new(args[5], args[6], args[7], args[8])
	end

	-- conversioni dec <=> dms
	if reqFormat == "dec" or reqFormat == "d" then
		dmsLat = decLat:toDms()
		dmsLong = decLong:toDms()
		-- rimuove secondi e minuti se zero e presenti in lat e long
		if dmsLat.sec == 0 and dmsLong.sec == 0 then
			dmsLat.sec, dmsLong.sec = nil, nil
			if dmsLat.min == 0 and dmsLong.min == 0 then
				dmsLat.min, dmsLong.min = nil, nil
			end
		end
	elseif reqFormat == "dm" or reqFormat == "dms" then
		decLat = dmsLat:toDec()
		decLong = dmsLong:toDec()
	end

	if args["format"] then
		defaultFormat = args["format"]
	elseif reqFormat == "dec" then
		defaultFormat = "dec"
	else
		defaultFormat = "dms"
	end

	-- crea la stringa param per geohack.php
	if reqFormat == "dec" then
		geohackParams = args[1] .. "_N_" .. args[2] .. "_E" .. (args[3] and ("_" .. args[3]) or "")
	else
		-- concatena solo i posizionali
		geohackParams = table.concat(args, "_")
	end

	-- utilizza l'estensione [[:mw:Extension:GeoData]]
	gdStr = string.format("{{#coordinates:%s|%s|name=%s}}", table.concat(args, "|"),
						  (display.title and mw.title.getCurrentTitle().namespace == 0) and "primary" or "",
						  args["name"] or "")
	gdRet = mw.getCurrentFrame():preprocess(gdStr)

	return display.debug and (decLat .. " " .. decLong .. " " .. dmsLat .. " " .. dmsLong) or
		   wdCat .. buildHTML(decLat, decLong, dmsLat, dmsLong, geohackParams, defaultFormat, display) .. gdRet
end

-- Entry-point per eventuale {{dms2dec}}
function p.dms2dec(frame)
	local args = frame.args

	-- {{dms2dec|N|2|3|4}}
	return DmsCoord:new(args[2], args[3], args[4], args[1]):toDec():getDeg()
end

-- Entry-point per eventuale {{dec2dms}}
function p.dec2dms(frame)
	local args = frame.args
	
	-- {{dec2dms|1.111|N|S}}
	return DecCoord:new(args[1], tonumber(args[1]) >= 0 and args[2] or args[3]):toDms()
end

-- Entry-point per {{coord}}
function p.coord(frame)
	-- copia i parametri ricevuti, eccetto quelli con nome valorizzati a stringa vuota
	for k, v in pairs(frame:getParent().args) do
		if v ~= "" or tonumber(k) then
			args[k] = string.gsub(v, "^%s*(.-)%s*$", "%1")
		end
	end
	-- retrocompatibilità con una funzionalità nascosta del precedente template:
	-- ignorava qualunque parametro posizionale vuoto dopo longitudine e parametri geohack
	for i = #args, 1, -1 do
		if args[i] == "" then
			table.remove(args, i)
		else
			break
		end
	end
	-- rimuove i parametri posizionali vuoti front to back fermandosi al primo non vuoto
	while args[1] == "" do
		table.remove(args, 1)
	end

	return coord()
end

return p