Modulo:Coord/sandbox
Aspetto
--[[
* Modulo che implementa il template Coord.
]]
require('Modulo:No globals')
-- 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
local function getArgs(frame)
local args = {}
-- 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 args
end
-------------------------------------------------------------------------------
-- Parsing parametri
-------------------------------------------------------------------------------
-- Error handler per xpcall, formatta l'errore
-- nel namespace principale aggiunge una categoria di warning
local function errhandler(msg)
local cat = mw.title.getCurrentTitle().namespace == 0 and
'[[Categoria:' .. cfg.categorie.warning .. ']]\n' or ''
return string.format('%s<span style="color:red">Il template {{Coord}} ha riscontrato degli errori ' ..
'([[Template:Coord|istruzioni]]):\n%s</span>\n', cat, msg)
end
-- Raccoglie più messaggi di errore in un'unica table prima di usare error()
local function dumpError(t, ...)
local args = {...}
table.insert(t, '* ')
for _, val in ipairs(args) do
table.insert(t, val)
end
table.insert(t, '\n')
end
-- Parsifica il parametro 'display'
local function getDisplay(args)
return {
inline = not args.display or args.display == 'inline' or args.display == 'inline,title',
title = args.display == 'title' or args.display == 'inline,title',
debug = args.display == 'debug'
}
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(args, 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', 2)
-- {{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', 2)
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', 2)
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(args)
local reqFormat, currFormat, globe, earth, prefix, num, str
local param = {}
local errorTable = {}
-- riconoscimento tipo di richiesta
if #args < 2 then
error('* coordinate non specificate', 2)
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', 2)
end
-- valida i parametri che possono essere lasciati vuoti
reqFormat = paramsEmpty(args, 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(errorTable, prefix, ' < ', param.min)
elseif earth and math.floor(num) > param.max then
dumpError(errorTable, prefix, ' > ', param.max)
end
else
dumpError(errorTable, 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(errorTable, prefix, ' diverso da ', param.min, ' e da ', param.max)
end
end
end
end
if #errorTable > 0 then
error(table.concat(errorTable), 2)
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(args, display)
local cat, retLat, retLong
if mw.title.getCurrentTitle().namespace == 0 and display.title then
local mWikidata = require('Modulo:Wikidata')
local wdLat = mWikidata._getProperty( { 'P625', coord = 'latitude', n = 1 } )
local wdLong = mWikidata._getProperty( { 'P625', coord = 'longitude', n = 1 } )
if wdLat and wdLong then
wdLat = round(wdLat, 6)
wdLong = round(wdLong, 6)
-- 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, 1, numberToString(wdLat))
table.insert(args, 2, numberToString(wdLong))
cat = 'Coordinate lette da Wikidata'
else
-- per poterle confrontare dopo la conversione
retLat, retLong = wdLat, wdLong
end
else
cat = 'Coordinate assenti su Wikidata'
end
end
return cat and string.format('[[Categoria:%s]]', cat) or '', retLat, retLong
end
-- Crea l'HTML ritornato da Coord
local function buildHTML(args, 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') ..
'¶ms=' .. 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(' / ')
: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
-------------------------------------------------------------------------------
-- API
-------------------------------------------------------------------------------
local p = {}
-- 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.
function p._coord(args)
local decLat, decLong, dmsLat, dmsLong, wdLat, wdLong
local reqFormat, defaultFormat, geohackParams, display, wdCat, gdStr, gdRet, ret
-- parsifica il parametro 'display'
display = getDisplay(args)
-- legge [[d:property:P625]] ed eventualmente la utilizza
wdCat, wdLat, wdLong = checkWikidata(args, display)
-- parsifica i parametri posizionali
ret, reqFormat = xpcall(function() return paramsParse(args) end, 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.catuguali and wdLat and wdLong and
wdLat == round(decLat:getDeg(), 6) and
wdLong == round(decLong:getDeg(), 6) then
wdCat = wdCat .. '[[Categoria:Coordinate uguali a Wikidata]]'
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(args, 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)
return p._coord(getArgs(frame))
end
return p