Share to:

Modulo:Coord


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

require('strict')

local mWikidata = require('Modulo:Wikidata')
local cfg = mw.loadData('Modulo:Coord/Configurasion')
local errorCategory = '[[Categoria:Erori de conpletamento del modeło Coord]]'

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

-- Error handler per xpcall, formatta l'errore.
--
-- @param {string} msg
-- @return {string}
local function errhandler(msg)
	local cat = mw.title.getCurrentTitle().namespace == 0 and errorCategory or ''
	return string.format('<div style="color:red">El modeło {{Coord}} el ga catà fora dei erori' ..
						 '([[Template:Coord|istrusion]]):\n%s</div>%s', msg, cat)
end

-- Restituisce il numero arrotondato al numero di cifre decimali richiesto.
-- http://lua-users.org/wiki/SimpleRound
--
-- @param {number} num
-- @param {number} idp
-- @return {number}
local function round(num, idp)
	local mult = 10^(idp or 0)
	return math.floor(num * mult + 0.5) / mult
end

-- Restituisce la stringa "0 + numero" quando il numero è di una sola cifra, altrimenti lo stesso numero.
--
-- @param {number} num
-- @return {string}
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).
--
-- @param {number} num
-- @return {string}
local function numberToString(num)
	-- la parentesi () extra serve per non restituire anche il gsub.count
	return (string.format('%f', num):gsub('%.?0+$', ''))
end

-- Parsifica il parametro display e restituisce una table con chiavi inline, title e debug.
--
-- @param {table} args
-- @return {table}
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 getZoom( extraparams )
	local scale = extraparams:match( '%f[%w]scale: ?(%d+)' )
	if scale then
		return math.floor(math.log10( 5 / tonumber( scale ) ) * 3 + 25)
	end

	local extraType = extraparams:match( '%f[%w]type: ?(%w+)' )
	if extraType then
		local zoomType = {
			country = 5,
			state = 6,
			adm1st = 7,
			adm2nd = 8,
			city = 9,
			isle = 10,
			mountain = 10,
			waterbody = 10,
			airport = 12,
			landmark = 13,
		}
		return zoomType[ extraType ]
	end
	
	return 13
end

-- Legge i parametri passati al modulo.
--
-- @param {table} frame
-- @return {table}
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

-- =============================================================================
--                            Classe DecCoord
-- =============================================================================

-- La classe DecCoord rappresenta una coordinata (latitudine o longitudine) in gradi decimali.

local DecCoord = {}
local DmsCoord = {} -- dichiarata qui per le conversioni

-- Costruttore della classe DecCoord.
--
-- @param {string} deg - i gradi decimali, positivi o negativi, se negativi viene
--                       cambiato il segno e la direzione cardinale eventualmente invertita
-- @param {string} card - la direzione cardinale (N|S|E|W)
-- @return {table} un nuovo oggetto DecCoord
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.
--
-- @return {string}
function DecCoord:__tostring()
	return numberToString(self.deg) .. '°' .. self.card
end

-- Restituisce i gradi con segno.
--
-- @return {string}
function DecCoord:getDeg()
	local deg = self.deg * ((self.card == 'N' or self.card =='E') and 1 or -1)
	return numberToString(deg)
end

-- Restituisce un nuovo oggetto DmsCoord, convertendo in gradi/minuti/secondi.
--
-- @return {table} un nuovo oggetto DmsCoord
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

-- =============================================================================
--                            Classe DmsCoord
-- =============================================================================

-- La classe DmsCoord rappresenta una coordinata (latitudine o longitudine) in gradi/minuti/secondi.

-- Costruttore della classe DmsCoord.
--
-- @param {string} deg - i gradi
-- @param {string} min - i minuti, può essere nil
-- @param {string} sec - i secondi, può essere nil
-- @param {string} card - la direzione cardinale (N|S|E|W)
-- @return {table} un nuovo oggetto DmsCoord
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.
--
-- @return {string}
function DmsCoord:__tostring()
	return self.deg .. '°' ..
		   (self.min and (padleft0(self.min) .. '′') or '') ..
		   (self.sec and (padleft0(self.sec) .. '″') or '') ..
		   self.card
end

-- Restituisce un nuovo oggetto DecCoord, convertendo in gradi decimali.
--
-- @return {table} un nuovo oggetto DecCoord
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
-- =============================================================================

-- La classe Coord è la classe principale del modulo.
-- Al suo interno ha un riferimento alla latitudine e longitudine in ogni formato.

local Coord = {}

-- Costruttore della classe Coord.
--
-- @param {table} args
-- @return {table} un nuovo oggetto Coord
function Coord:new(args)
	local decLat, decLong, dmsLat, dmsLong
	local display = getDisplay(args)
	local self = { args = args }

	setmetatable(self, { __index = Coord })

	if args.from and display.title then
		error('el paramatro "from" el ze vàlido solché co display=inline', 3)
	end
	-- con display=title o con i parametri "prop" o "from"
	-- legge le coordinate da P625 per utilizzarle o per confrontarle con quelle inserite
	if getDisplay(self.args).title or self.args.prop or args.from then
		self:_checkWikidata()
		-- con "from", senza coordinate utente e su Wikidata non esegue i controlli successivi
		if self.args.from and #self.args < 2 and not tonumber(args[1]) then
			return self
		end
	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:P625 conpagna in 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()
	self.wdEntityId = self.args.from or mw.wikibase.getEntityIdForCurrentPage();
	if self.args.prop then
		self.wdLat = mWikidata._getQualifier({ self.args.prop, 'P625', coord = 'latitude', n = 1, nq = 1, from = self.wdEntityId })
		self.wdLong = mWikidata._getQualifier({ self.args.prop, 'P625', coord = 'longitude', n = 1, nq = 1, from = self.wdEntityId })
	else
		self.wdLat = mWikidata._getProperty({ 'P625', coord = 'latitude', n = 1, from = self.wdEntityId })
		self.wdLong = mWikidata._getProperty({ 'P625', coord = 'longitude', n = 1, from = self.wdEntityId })
	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:P625 lexesta da Wikidata]]'
		end
	else
		self.wdCat = '[[Categoria:P625 no ła ghe xe in Wikidata]]'
	end
end

-- Riconosce il tipo di richiesta: dec, d, dm o dms.
function Coord:_checkRequestFormat()
	local errorTable = {}

	-- riconoscimento tipo di richiesta
	if #self.args < 2 then
		error('* coordenae no spesifegae', 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 i ga na precizion difarente', 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 i ga na precizion difarente', 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 i ga na precizion difarente', 4)
		end
	end

	-- validazione parametri posizionali
	local currFormat = cfg.params[self.reqFormat]
	local globe = self.args[#self.args]:match('globe:(%w+)')
	self.isEarth = not globe or globe == 'earth'
	for k, v in ipairs(self.args) do
		if currFormat[k] then
			local err
			local parType = currFormat[k][1]
			local parName = currFormat[k][2]
			local parMin = currFormat[k][3]
			local parMax = currFormat[k][4]
			-- valida un parametro di tipo numero
			if parType == 'number' then
				local num = tonumber(v)
				if num then
					if self.isEarth and num < parMin then
						err = string.format('* %s format: %s < %s', self.reqFormat, parName, parMin)
					elseif self.isEarth and math.floor(num) > parMax then
						err = string.format('* %s format: %s > %s', self.reqFormat, parName, parMax)
					end
				else
					err = string.format('* %s format: %s no el ze un nùmaro', self.reqFormat, parName)
				end
			-- valida un parametro di tipo stringa
			elseif parType == 'string' then
				if v ~= parMin and v ~= parMax then
					err = string.format('* %s format: %s difarente da %s e da %s',
										self.reqFormat, parName, parMin, parMax)
				end
			end
			if err then
				table.insert(errorTable, err)
			end
		end
	end

	if #errorTable > 0 then
		error(table.concat(errorTable, '\n'), 4)
	end
end

-- Utilizza l'estensione [[mw:Extension:GeoData]].
--
-- @param {table} display
-- @return {string}
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, restituisce latitudine e longitudine in entrambi i formati.
--
-- @return {string}
function Coord:getDebugCoords()
	-- con args.from restitusce una stringa vuota se non c'è nessun dato
	if self.args.from and #self.args < 2 and not tonumber(self.args[1]) then
		return ''
	end
	return self.decLat .. ' ' .. self.decLong .. ' ' .. self.dmsLat .. ' ' .. self.dmsLong
end

-- Restituisce l'HTML di un elemento <maplink> di Kartographer per le coordinate
--
-- @return {string}
function Coord:_buildMaplinkHTML()
	local defaultFormat
	if self.args.format then
		defaultFormat = self.args.format
	elseif self.reqFormat == 'dec' then
		defaultFormat = 'dec'
	else
		defaultFormat = 'dms'
	end

	local linkText;
	if defaultFormat == 'dec' then
		linkText = self.decLat .. ' ' .. self.decLong
	else
		linkText = tostring(self.dmsLat) .. ' ' .. tostring(self.dmsLong)
	end

	local jsonParams = {
		type = 'Feature',
		geometry = {
			type ='Point',
			coordinates = {
				round( self.decLong:getDeg(), 6 ), -- max precision in GeoJSON format
				round( self.decLat:getDeg(), 6 )
			}
		},
		properties = {
			['marker-color'] = "228b22",
		}
	}

	if self.wdEntityId then
		-- geoshape da Wikidata
		jsonParams = {
			jsonParams,
			{
				type = 'ExternalData',
				service = 'geoshape',
				ids = self.wdEntityId,
				properties = {
					['fill-opacity'] = 0.2
				}
			}
		}
	end

	return mw.getCurrentFrame():extensionTag{
		name = 'maplink',
		content = mw.text.jsonEncode( jsonParams ),
		args = {
			text = linkText,
			zoom = getZoom( self.args[#self.args] or '' ),
			latitude = self.decLat:getDeg(),
			longitude = self.decLong:getDeg(),
		}
	}
end


-- Restituisce l'HTML di un link a geohack con le coordinate (usato solo quando
--  non è possibile usare maplink).
--
-- @return {string}
function Coord:_buildGeohackLink()
	-- crea la stringa per il parametro params di geohack.php
	local geohackParams

	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
	local 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
	
	local linkNode = mw.html.create('span')
		:addClass('plainlinks nourlexpansion')
		:wikitext('[' .. url .. ' ' .. tostring(self.dmsLat) .. ' ' .. tostring(self.dmsLong) .. ']')
		:done()
	return tostring(linkNode)
end

-- Restituisce l'HTML per il microformat Geo.
-- FIXME Serve ancora nel 2024? La documentazione in proposito è scarsissima, e tutti
--   i link a strumenti che lo usano non vanno più. Inoltre, qui usiamo un elemento nascosto;
--   va bene lo stesso? Come verificarlo? Le specifiche non ne parlano.
--
-- @return {string}
function Coord:_buildGeoMarkup()
	return mw.html.create('')
		:wikitext(self.args.name and '<span class="vcard">' or '')
		:tag('span')
			:attr('style', 'display:none')
			:addClass('geo')
			:tag('span')
				:addClass('latitude')
				:wikitext(tostring(self.dmsLat))
				:done()
			:wikitext( ', ' )
			:tag('span')
				:addClass('longitude')
				:wikitext(tostring(self.dmsLong))
				:done()
		:wikitext(self.args.name and ('<span style="display:none"> (<span class="fn org">' ..
			  self.args.name .. '</span>)</span></span>') or '')
			:done()
end

-- Restituisce l'HTML contenente le coordinate con link che utilizza <maplink> (Kartographer)
--  se possibile, e geohack altrimenti.
--
-- @return {string}
function Coord:getHTML()
	-- con args.from restitusce una stringa vuota se non c'è nessun dato
	if self.args.from and #self.args < 2 and not tonumber(self.args[1]) then
		return ''
	elseif self.args.display == 'debug' then
		return self:getDebugCoords()
	end

	local display = getDisplay(self.args)

	local coordLink
	if self.isEarth then
		-- Usa maplink se possibile. A maggio 2024 supporta solo coordinate terrestri,
		-- vedi T151138.
		coordLink = self:_buildMaplinkHTML()
	else
		coordLink = self:_buildGeohackLink()
	end

	local geo = self:_buildGeoMarkup();

	local html = coordLink .. tostring(geo) .. (self.args.notes or '')

	local frame = mw.getCurrentFrame()
	
	local ret = frame:extensionTag('templatestyles', '', {src = 'Modulo:Coord/styles.css'}) ..
		   (display.inline and html or '');

	if display.title then
		-- formatta il risultato a seconda di args.display (nil, 'inline', 'title', 'inline,title')
		-- se inline e title, in stampa visualizza solo il primo
		local htmlTitle = string.format(
			'<div style="font-size: small"><span %s id="coordinates">[[Coordinae ziogràfeghe|Coordenae]]: %s</span></div>',
			display.inline and 'class="noprint"' or '',
			html
		)
		ret = ret .. frame:extensionTag( 'indicator', htmlTitle, { name = 'coordinates' } )
	end

	return ret ..
		self:_setGeoData(display) .. 
		(mw.title.getCurrentTitle().namespace == 0 and self.wdCat or '')
end

-- =============================================================================
--                            Funzioni esportate
-- =============================================================================

local p = {}

-- Funzione importata da https://en.wikipedia.org/w/index.php?title=Module:Coordinates&oldid=789126031
-- per estrarre lat, long, type, scale, dim, region, globe, source, dal link a geohack.php generato da coord.
function p.coord2text(frame)
	if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
	frame.args[2] = mw.text.trim(frame.args[2])
	if frame.args[2] == 'lat' or frame.args[2] == 'long' then
		local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ')
		if frame.args[2] == 'lat' then
			result, negative = result[1], 'S'
		else
			result, negative = result[2], 'W'
		end
		result = mw.text.split(result, '°')
		if result[2] == negative then result[1] = '-'..result[1] end
		return result[1]
	else
		return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
	end
end

-- Funzione per eventuale template {{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

-- Funzione per eventuale template {{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

-- Funzione per l'utilizzo da un altro modulo.
function p._main(args)
	return Coord:new(args):getHTML()
end

-- Funzione per il template {{Coord}}.
function p.main(frame)
	return select(2, xpcall(function()
		return p._main(getArgs(frame))
	end, errhandler))
end

return p

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.
Prefix: a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9

Portal di Ensiklopedia Dunia

Kembali kehalaman sebelumnya