Questa pagina è semiprotetta. Può essere modificata solo da utenti registrati.

Modulo:Coord

Da Wikiversità, l'apprendimento libero.
Jump to navigation Jump to search

Modulo Lua che implementa le funzionalità del Template:Coord.

Ha una sottopagina di configurazione: Modulo:Coord/Configurazione.

Utilizzo da un altro modulo

Il modulo può essere usato anche da un altro modulo tramite "require". È sufficiente inserire nel modulo:

local mCoord = require('Modulo:Coord')

La funzione esportata è _main, con gli stessi parametri del template.

Esempio
local mCoord = require('Modulo:Coord')
local p = {}

function p.main(frame)
    local sydney, wd

    sydney = mCoord._main( { '-33.86', '151.211111', format = 'dms' } )
    wd = mCoord._main( { display = 'inline,title', format = 'dec' } )
    
    return string.format('Le coordinate dms di Sydney sono: %s. ' .. 
						 'Le coordinate dec dell\'elemento Wikidata collegato: %s.',
						 sydney, wd)
end

return p



--[[
* Modulo che implementa il template Coord.
]]

require('Modulo:No globals')
local mWikidata = require('Modulo:Wikidata')
local errorCategory = '[[Categoria:Errori di compilazione del template Coord]]'

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

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

-- Error handler per xpcall, formatta l'errore
local function errhandler(msg)
    local cat = mw.title.getCurrentTitle().namespace == 0 and errorCategory or ''
    return string.format('<span style="color:red">Il template {{Coord}} ha riscontrato degli errori ' ..
                         '([[Template:Coord|istruzioni]]):\n%s</span>%s', msg, cat)
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

-- 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

-- Legge 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

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
    -- se l'utente non ha fornito lat e long con i posizionali ma con latdec e longdec
    if (#args == 0 or (#args == 1 and not tonumber(args[1]))) and
       tonumber(args.latdec) and tonumber(args.longdec) then
        table.insert(args, 1, numberToString(args.latdec))
        table.insert(args, 2, numberToString(args.longdec))
    end

    return args
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

-------------------------------------------------------------------------------
--                           classe Coord
-------------------------------------------------------------------------------

local Coord = {}

function Coord:new(args)
    local decLat, decLong, dmsLat, dmsLong
    local self = { args = args }

    setmetatable(self, { __index = Coord })

    -- nel namespace principale e con display=title (o con il parametro "prop")
    -- legge le coordinate da P625 per utilizzarle o per confrontarle con quelle inserite
    if mw.title.getCurrentTitle().namespace == 0 and (getDisplay(self.args).title or self.args.prop) then
        self:_checkWikidata()
    end

    -- identifica il tipo di chiamata
    self:_checkRequestFormat()

    -- in base al tipo di chiamata crea gli oggetti DecCoord o DmsCoord
    if self.reqFormat == 'dec' then
        -- {{coord|1.111|2.222}}
        decLat = DecCoord:new(args[1], 'N')
        decLong = DecCoord:new(args[2], 'E')
    elseif self.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 self.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 self.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

    -- effettua le conversioni dec <=> dms
    if self.reqFormat == 'dec' or self.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 self.reqFormat == 'dm' or self.reqFormat == 'dms' then
        decLat = dmsLat:toDec()
        decLong = dmsLong:toDec()
    end
    
    -- se presente args.catuguali e non è stato usato Wikidata verifica se uguali
    if args.catuguali and self.wdLat and self.wdLong and
       self.wdCat == nil and
       self.wdLat == round(decLat:getDeg(), 6) and
       self.wdLong == round(decLong:getDeg(), 6) then
        self.wdCat = '[[Categoria:Coordinate uguali a Wikidata]]'
    end

    self.decLat = decLat
    self.decLong = decLong
    self.dmsLat = dmsLat
    self.dmsLong = dmsLong

    return self
end

-- Legge la P625 e la utilizza come latitudine e longitudine se non fornite dall'utente.
function Coord:_checkWikidata()
    if self.args.prop then
        self.wdLat = mWikidata._getQualifier({ self.args.prop, 'P625', coord = 'latitude', n = 1, nq = 1 })
        self.wdLong = mWikidata._getQualifier({ self.args.prop, 'P625', coord = 'longitude', n = 1, nq = 1 })
    else
        self.wdLat = mWikidata._getProperty({ 'P625', coord = 'latitude', n = 1 })
        self.wdLong = mWikidata._getProperty({ 'P625', coord = 'longitude', n = 1 })
    end
    if self.wdLat and self.wdLong then
        self.wdLat = round(self.wdLat, 6)
        self.wdLong = round(self.wdLong, 6)
        -- se l'utente non ha fornito lat e long usa quelli di Wikidata
        if #self.args == 0 or (#self.args == 1 and not tonumber(self.args[1])) then
            table.insert(self.args, 1, numberToString(self.wdLat))
            table.insert(self.args, 2, numberToString(self.wdLong))
            self.wdCat = '[[Categoria:Coordinate lette da Wikidata]]'
        end
    else
        self.wdCat = '[[Categoria:Coordinate assenti su Wikidata]]'
    end
end

-- Riconosce il tipo di richiesta: dec, d, dm o dms.
function Coord:_checkRequestFormat()
    local currFormat, globe, earth, prefix, num, str
    local param = {}
    local errorTable = {}

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

    -- con le richieste dm e dms verifica se ci sono parametri lasciati vuoti in modo valido.
    if self.reqFormat == 'dms' then
        -- {{coord|1|2||N|5|6||E}} valido
        if self.args[3] == '' and self.args[7] == '' then
            table.remove(self.args, 7)
            table.remove(self.args, 3)
            self.reqFormat = 'dm'
        -- {{coord|1|2|3|N|5|6||E}} non valido
        elseif self.args[3] == '' or self.args[7] == '' then
            error('* lat e long hanno diversa precisione', 4)
        -- {{coord|1||3|N|5||7|E}} valido
        elseif self.args[2] == '' and self.args[6] == '' then
            self.args[2], self.args[6] = 0, 0
        -- {{coord|1|2|3|N|5||7|E}} non valido
        elseif self.args[2] == '' or self.args[6] == '' then
            error('* lat e long hanno diversa precisione', 4)
        end
    end
    if self.reqFormat == 'dm' then
        -- {{coord|1||N|4||E}} valido
        if self.args[2] == '' and self.args[5] == '' then
            table.remove(self.args, 5)
            table.remove(self.args, 2)
            self.reqFormat = 'd'
        -- {{coord|1|2|N|4||E}} non valido
        elseif self.args[2] == '' or self.args[5] == '' then
            error('* lat e long hanno diversa precisione', 4)
        end
    end

    -- validazione parametri posizionali
    currFormat = cfg.params[self.reqFormat]
    globe = self.args[#self.args]:match('globe:(%w+)')
    earth = not globe or globe == 'earth'
    for k, v in ipairs(self.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 = self.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), 4)
    end
end

-- Utilizza l'estensione [[mw:Extension:GeoData]]
function Coord:_setGeoData(display)
    local gdStr = string.format('{{#coordinates:%s|%s|name=%s}}',
                        table.concat(self.args, '|'),
                        (display.title and mw.title.getCurrentTitle().namespace == 0) and 'primary' or '',
                        self.args.name or '')
    return mw.getCurrentFrame():preprocess(gdStr)
end

-- Funzione di debug
function Coord:getDebugCoords()
    return self.decLat .. ' ' .. self.decLong .. ' ' .. self.dmsLat .. ' ' .. self.dmsLong
end

-- Crea l'HTML contenente le coordinate in formato dec e dms come collegamento esterno a geohack.php.
function Coord:getHTML()
    local defaultFormat, geohackParams, display, root, html, url, htmlTitle
    
    -- legge il parametro display
    display = getDisplay(self.args)

    if self.args.format then
        defaultFormat = self.args.format
    elseif self.reqFormat == 'dec' then
        defaultFormat = 'dec'
    else
        defaultFormat = 'dms'
    end

    -- crea la stringa per il parametro params di geohack.php
    if self.reqFormat == 'dec' then
        geohackParams = string.format('%s_N_%s_E', self.args[1], self.args[2])
        if self.args[3] then
            geohackParams = geohackParams .. '_' .. self.args[3]
        end
    else
        -- concatena solo i posizionali
        geohackParams = table.concat(self.args, '_')
    end

    -- geohack url e parametri
    url = string.format('%s&pagename=%s&params=%s', cfg.geohackUrl,
                         mw.uri.encode(mw.title.getCurrentTitle().prefixedText, 'WIKI'), geohackParams)
    if self.args.name then
        url = url .. '&title=' .. mw.uri.encode(self.args.name)
    end

    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(self.dmsLat))
                        :done()
                    :wikitext(' ')
                    :tag('span')
                        :addClass('longitude')
                        :wikitext(tostring(self.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(self.args.name and '<span class="vcard">' or '')
                :tag('span')
                    :addClass('geo-dec')
                    :attr('title', 'Mappe, foto aeree e altri dati per questa posizione')
                    :wikitext(self.decLat .. ' ' .. self.decLong)
                    :done()
                :tag('span')
                    :attr('style', 'display:none')
                    :tag('span')
                        :addClass('geo')
                        :wikitext(self.decLat:getDeg() .. '; ' .. self.decLong:getDeg())
                        :done()
                    :done()
                :wikitext(self.args.name and ('<span style="display:none"> (<span class="fn org">' ..
                          self.args.name .. '</span>)</span></span>') or '')
                :done()
            :wikitext(']')
            :done()

    html = tostring(root) .. (self.args.notes or '')

    -- formatta il risultato a seconda di args.display (nil, 'inline', 'title', 'inline,title')
    -- se inline e title, in stampa visualizza solo il primo
    htmlTitle = string.format('<span style="font-size: small"><span %s id="coordinates">[[Coordinate geografiche|Coordinate]]: %s</span></span>',
                              display.inline and 'class="noprint"' or '', html)

    return (display.inline and html or '') ..
           (display.title and htmlTitle or '') ..
           self:_setGeoData(display) ..
           (self.wdCat or '')
end

-------------------------------------------------------------------------------
--                               API
-------------------------------------------------------------------------------

local p = {}

-- Per l'utilizzo da un altro modulo
function p._main(args)
    local coord = Coord:new(args)
    return args.display == 'debug' and coord:getDebugCoords() or coord:getHTML()
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.main(frame)
    return select(2, xpcall(function()
        return p._main(getArgs(frame))
    end, errhandler))    
end

return p