Modul:Homokozó/JulesWinnfield-hu/Coordinate
Megjelenés
Homokozó/JulesWinnfield-hu/Coordinate[mi ez?] • [dokumentáció: mutat, ] • [tesztek: sikeres: 2, sikertelen: 0, kihagyva: 2 (részletek)]
-------------------------------------------------------------------------------
-- Coordinate processing functions
-------------------------------------------------------------------------------
local lang = mw.getContentLanguage()
local current_page = mw.title.getCurrentTitle()
local page_name = mw.uri.encode(current_page.prefixedText, 'WIKI')
local coord_link = '//tools.wmflabs.org/geohack/geohack.php?language=hu&pagename=' .. page_name .. '¶ms='
-- The class used to internally represent a single coordinate
local Coordinate = {}
--Internal functions
--[[
Normalize cardinal direction
@param string cardDir Cardinal direction, English or Hungarian
@return string
--]]
local function normCardDir(cardDir)
if lang:uc(cardDir) == 'É' then return 'N' end
if cardDir:upper() == 'D' then return 'S' end
if cardDir:upper() == 'K' then return 'E' end
if cardDir:upper() == 'NY' then return 'W' end
return cardDir
end
--[[ Helper function, used in detecting DMS formatting ]]
local dmsTest = function(first, second)
first = normCardDir(first or '')
second = normCardDir(second or '')
local concatenated = first:upper() .. second:upper()
if concatenated == "NE" or concatenated == "NW" or concatenated == "SE" or concatenated == "SW" or
concatenated == "EN" or concatenated == "WN" or concatenated == "ES" or concatenated == "WS" then
return true
end
return false
end
--[[
Transform degrees, minutes, seconds format latitude and longitude
into the a structure to be used in displaying coordinates
--]]
function parseDMS(lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f)
lat_f = normCardDir(lat_f):upper()
long_f = normCardDir(long_f):upper()
-- Check if specified backward
if lat_f == 'E' or lat_f == 'W' then
local t_d, t_m, t_s, t_f
t_d = lat_d
t_m = lat_m
t_s = lat_s
t_f = lat_f
lat_d = long_d
lat_m = long_m
lat_s = long_s
lat_f = long_f
long_d = t_d
long_m = t_m
long_s = t_s
long_f = t_f
end
if long_d == nil or long_d == "" then
return nil, {{"parseDMS", "Missing longitude"}}
end
local lat = (lat_f:upper() == 'S' and -1 or 1) * ((tonumber(lat_d) or 0) + (tonumber(lat_m) or 0) / 60 + (tonumber(lat_s) or 0) / 3600)
local long = (long_f:upper() == 'W' and -1 or 1) * ((tonumber(long_d) or 0) + (tonumber(long_m) or 0) / 60 + (tonumber(long_s) or 0) / 3600)
local prec = math.min(detectPrecisionForFloat(lat_d), detectPrecisionForFloat(long_d))
if lat_m ~= nil or long_m ~= nil then
if prec < 1 then
return nil, {{'parseDMS', 'Nem értelmezhető adatok'}}
else
prec = math.min(detectPrecisionForFloat(lat_m), detectPrecisionForFloat(long_m))
if prec < 0.0001 then
prec = 1e-6
elseif prec < 1 then
prec = prec / 100
else
prec = Coordinate.PRECISION.M
end
end
end
if lat_s ~= nil or long_s ~= nil then
if prec == Coordinate.PRECISION.M then
prec = math.max(math.min(detectPrecisionForFloat(lat_s), detectPrecisionForFloat(long_s)), 0.001) * Coordinate.PRECISION.S
else
return nil, {{'parseDMS', 'Nem értelmezhető adatok'}}
end
end
local coord = Coordinate:new{latitude = lat, longitude = long, precision = prec}
return coord, coord and {} or {{'parseDMS', 'Nem értelmezhető adatok'}}
end
--[[
Format any error messages generated for display
--]]
function errorPrinter(errors)
return nil, nil, nil, errors
--[[
local result = ""
for _, v in ipairs(errors) do
local errorHTML = '<strong class="error">Coordinates: ' .. v[2] .. '</strong>'
result = result .. errorHTML .. '<br>'
end
return result
--]]
end
Coordinate.PRECISION = {
MS = 1 / 3600 / 1000, -- to 1/1000 of an arcsecond
D000001 = 1e-6, -- ±0.000001°
MS10 = 1 / 3600 / 100, -- to 1/100 of an arcsecond
D00001 = 1e-5, -- ±0.00001°
MS100 = 1 / 3600 / 10, -- to 1/10 of an arcsecond
D0001 = 0.0001, -- ±0.0001°
S = 1 / 3600, -- to an arcsecond
D001 = 0.001, -- ±0.001°
D01 = 0.01, -- ±0.01°
M = 1 / 60, -- to an arcminute
D1 = 0.1, -- ±0.1°
D = 1, -- to a degree
D10 = 10 -- ±10°
}
local orderedPrecisions = {
Coordinate.PRECISION.MS10,
Coordinate.PRECISION.MS100,
Coordinate.PRECISION.S,
Coordinate.PRECISION.M,
Coordinate.PRECISION.D
}
local function detectWikidataPrecision(float)
local precision
for _, v in ipairs(orderedPrecisions) do
local m = float / v
if math.abs(Coordinate.mfloor(m + 0.5) - m) < 1e-6 / v + 1e-12 then
precision = v
end
end
return precision or detectPrecisionForFloat(float)
end
--[[
Check the input arguments for coord to determine the kind of data being provided
and then make the necessary processing.
--]]
function formatTest(args)
local result, format, coordParams
local errors = {}
if args.wikidata == 'primary' and current_page.namespace == 0 then
local entity = mw.wikibase.getEntity()
if entity and entity.claims and entity.claims.p625 then
local value = entity.claims.p625[0].mainsnak.datavalue.value
value.precision = math.min(detectWikidataPrecision(value.latitude), detectWikidataPrecision(value.longitude))
result = Coordinate:new(value)
if result == nil then
return errorPrinter{{'formatTest', 'Wikidata hiba'}}
end
return result, 'dms', args[9], errors
end
end
if not args[1] then
-- no lat logic
return errorPrinter{{"formatTest", "Missing latitude"}}
elseif not args[4] and not args[5] and not args[6] then
-- dec logic
local prec = math.min(detectPrecisionForFloat(args[1]), detectPrecisionForFloat(args[2]))
result = Coordinate:new{latitude = tonumber(args[1]), longitude = tonumber(args[2]), precision = prec}
format = 'dec'
coordParams = args[3]
if result == nil then
return errorPrinter{{'formatTest', 'Nem értelmezhető adatok'}}
end
elseif dmsTest(args[4], args[8]) then
-- dms logic
result, errors = parseDMS(args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8])
format = 'dms'
coordParams = args[9]
if args[10] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
elseif dmsTest(args[3], args[6]) then
-- dm logic
result, errors = parseDMS(args[1], args[2], nil, args[3], args[4], args[5], nil, args[6])
format = 'dms'
coordParams = args[7]
if args[8] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
elseif dmsTest(args[2], args[4]) then
-- d logic
result, errors = parseDMS(args[1], nil, nil, args[2], args[3], nil, nil, args[4])
format = result.precision < 1 and 'dec' or 'dms'
coordParams = args[5]
if args[6] then
table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
end
else
-- Error
return errorPrinter{{"formatTest", "Unknown argument format"}}
end
return result, format, coordParams, errors
end
--[[
Validate a Coordinate defintion
@param table definition data
@return boolean
--]]
function validate(definition)
--Validate precision
if not validatePrecision(definition.precision) then
return false
end
--Validate latitude and longitude
if not validateNumberInRange(definition.latitude, -180, 360) or not validateNumberInRange(definition.longitude, -180, 360) then
return false
end
return true
end
--[[
Check if a value is a number in the given range
@param mixed value
@param number min
@param number max
@return boolean
--]]
function validateNumberInRange(value, min, max)
return type(value) == 'number' and value >= min and value <= max
end
--[[
Validate precision
--]]
function validatePrecision(precision)
for _, v in pairs(Coordinate.PRECISION) do
if v == precision then
return true
end
end
return false
end
--[[
Try to find the relevant precision for a GlobeCoordinate definition
@param table GlobeCoordinate definition
@return number the precision
--]]
function guessPrecision(definition)
return math.min(detectPrecisionForFloat(definition.latitude), detectPrecisionForFloat(definition.longitude))
end
--[[
Try to find the relevant precision for a latitude or longitude as float
@param float float
@return number the precision
--]]
function detectPrecisionForFloat(float)
local parts = mw.text.split(tostring(float), '%.')
if parts[2] then
return math.pow(10, -1 * math.min(#parts[2], 6))
else
return 1
end
end
-------------------------------------------------------------------------------
-- Creates a new Coordinate
-- @param float latitude Latitude ("vertical" position) as a signed floating-point value (North is positive, South is negative)
-- @param float longitude Longitude ("horizontal" position) as a signed floating-point value (East is positive, West is negative)
-- @example Coordinate.create(12.3456, -98.7654)
--
function Coordinate.create(latitude, longitude)
local coord = {}
setmetatable(coord, Coordinate)
coord.latitude = latitude
coord.longitude = longitude
return coord
end
-------------------------------------------------------------------------------
-- Build a new Coordinate
-- @param table definition Definition of the coordinate
-- @return Coordinate|nil
-- @example Coordinate:new{latitude = 12.3456, longitude = -98.7654}
--
function Coordinate:new(definition)
--Default values
if definition.precision == nil then
definition.precision = guessPrecision(definition)
else
for _, v in pairs(Coordinate.PRECISION) do
if math.abs(definition.precision - v) < 1e-12 then
definition.precision = v
end
end
end
if not validate(definition) then
return nil
end
local coord = {
latitude = definition.latitude,
longitude = definition.longitude,
precision = definition.precision or 0
}
setmetatable(coord, self)
self.__index = self
return coord
end
-------------------------------------------------------------------------------
-- == operator
-- (note that this is a naive implementation which requires exact equality if floating-point values;
-- this probably does not work very well in practice)
function Coordinate.__eq(coord1, coord2)
return math.abs(coord1.latitude - coord2.latitude) < 1e-6 and math.abs(coord1.longitude - coord2.longitude) < 1e-6
end
-------------------------------------------------------------------------------
-- Transform coordinate to string
-- @param string format
-- Special characters in the format string:
-- %L latitude as a signed float
-- %U latitude as an unsigned float
-- %D degree part of latitude (i.e. floor(latitude))
-- %M minute part of latitude
-- %S second part of latitude (including fractional part)
-- %C cardinal direction for latitude as shortcut (N/S)
-- %I internationalized cardinal direction for latitude as shortcut (currently always in in Hungarian: É/D)
-- ... same with lowercase for longitude
function Coordinate:format(format)
local d, rem = math.modf(self.latitude) -- splits number into integer and fractional part
local m, rem = math.modf(rem * 60)
local s = math.floor(rem * 60 * 100 + 0.5) / 100
format = format:gsub('%%L', lang:formatNum(self.latitude))
format = format:gsub('%%U', lang:formatNum(math.abs(self.latitude)))
format = format:gsub('%%D', d)
format = format:gsub('%%M', m)
format = format:gsub('%%S', s)
format = format:gsub('%%C', (self.latitude >= 0) and 'N' or 'S')
format = format:gsub('%%I', (self.latitude >= 0) and 'É' or 'D')
local d, rem = math.modf(self.longitude) -- splits number into integer and fractional part
local m, rem = math.modf(rem * 60)
local s = math.floor(rem * 60 * 100 + 0.5) / 100
format = format:gsub('%%l', lang:formatNum(self.longitude))
format = format:gsub('%%u', lang:formatNum(math.abs(self.longitude)))
format = format:gsub('%%d', d)
format = format:gsub('%%m', m)
format = format:gsub('%%s', s)
format = format:gsub('%%c', (self.longitude >= 0) and 'E' or 'W')
format = format:gsub('%%i', (self.longitude >= 0) and 'K' or 'Ny')
return format
end
-------------------------------------------------------------------------------
-- These elements can be used in stringPatterns between $ marks
-- E.g. "$int$° $int$′ $int$″"
local patternElements = {
uint = "[0-9]+",
int = "[-+]?[0-9]+",
ufloat = "[0-9]*[.,]?[0-9]+", -- english or hungarian separator notation
float = "[-+]?[0-9]*[.,]?[0-9]+", -- english or hungarian separator notation
cd = "[NSEWÉDK][Yy]?" -- cardinal directions in english or hungarian
}
-------------------------------------------------------------------------------
-- FIXME move this to an intl module
-- string to number, handle english and hungarian separator
local function num(s)
if type(s) == 'string' then
s = s:gsub(",", ".")
return tonumber(s)
else
return s
end
end
-------------------------------------------------------------------------------
-- cardinal direction to sign of coordinate (+1/-1), handles english and hungarian shortcuts
local directionMap = {N = 1, S = -1, E = 1, W = -1, ["É"] = 1, D = -1, K = 1, Ny = -1, NY = -1}
local function dirsign(s) return directionMap[s] end
-------------------------------------------------------------------------------
-- Contains regexp - callback pairs. The regexp describes a possible human-readable representation of a coordinate,
-- the callback receives the match results and transforms them into a latitude-longitude pair (a pair of signed floats).
-- Can use patternElement keys for syntatic sugar.
local stringPatterns = {
{"($float$), ($float$)", function(lat, long) return num(lat), num(long) end}, -- 12.3456, -98.7654
{"($cd$) ($float$), ($cd$) ($float$)",
function(lath, lat, longh, long)
return dirsign(lath) * num(lat), dirsign(longh) * num(long)
end}, -- É 48,621667, K 16,871528
{"($int$)° ($int$)['′] ($float$)[\"″] ($cd$), ($int$)° ($int$)['′] ($float$)[\"″] ($cd$)",
function(latd, latm, lats, lath, longd, longm, longs, longh)
local lat = dirsign(lath) * (num(latd) + num(latm) / 60 + num(lats) / 3600)
local long = dirsign(longh) * (num(longd) + num(longm) / 60 + num(longs) / 3600)
return lat, long
end}, -- 12° 20' 44" N, 98° 45' 55" W
{"($cd$) ($int$)° ($int$)['′] ($float$)[\"″], ($cd$) ($int$)° ($int$)['′] ($float$)[\"″]",
function(lath, latd, latm, lats, longh, longd, longm, longs)
local lat = dirsign(lath) * (num(latd) + num(latm) / 60 + num(lats) / 3600)
local long = dirsign(longh) * (num(longd) + num(longm) / 60 + num(longs) / 3600)
return lat, long
end}, -- N 12° 20' 44", W 98° 45' 55"
}
local stringPatternsOld, stringPatterns = stringPatterns, {}
for i, pair in ipairs(stringPatternsOld) do
local pattern, callback = pair[1], pair[2]
for key, value in pairs(patternElements) do
pattern = pattern:gsub('%$' .. key .. '%$', value)
end
table.insert(stringPatterns, {pattern, callback})
end
-------------------------------------------------------------------------------
-- Creates a Coordinate object from a human-readable string representation.
-- @param string s
-- @return Coordinate|nil
-- @example
--
function Coordinate.fromString(s)
for i, pair in ipairs(stringPatterns) do
local pattern, callback = pair[1], pair[2]
if mw.ustring.match(s, pattern) then
lat, long = callback(mw.ustring.match(s, pattern))
return Coordinate:new{latitude = lat, longitude = long}
end
end
return nil
end
-------------------------------------------------------------------------------
-- Returns coordinate in standard text format - two signed floats (12.3456, -98.7654)
-- @return string
--
function Coordinate:__tostring()
return self:format('%L, %l')
end
--[[
Build params uri component and link text for GeoHack link
@param string format dec|dms
@return string, string|nil, nil
@example coord:toGeoHack('dms')
--]]
function Coordinate:toGeoHack(format)
if format ~= 'dec' and format ~= 'dms' then return nil, nil end
local params = ''
local text = ''
local logPrec = -1 * math.log10(self.precision)
local decimalPrecision = logPrec == math.floor(logPrec)
if decimalPrecision then
params = math.floor(self.latitude * 1e+6 + 0.5) / 1e+6 .. ';' .. math.floor(self.longitude * 1e+6 + 0.5) / 1e+6
end
if format == 'dec' then
local decimals = math.floor(logPrec)
if decimals < 1 then decimals = 0 end
text = mw.text.tag('span', {style = 'white-space:nowrap;'}, (self.latitude >= 0 and 'é. sz.' or 'd. sz.') .. ' ' ..
string.format('%.' .. decimals .. 'f', math.abs(self.latitude)):gsub('%.', ',') .. '°') .. ', ' ..
mw.text.tag('span', {style = 'white-space:nowrap;'}, (self.longitude >= 0 and 'k. h.' or 'ny. h.') .. ' ' ..
string.format('%.' .. decimals .. 'f', math.abs(self.longitude)):gsub('%.', ',') .. '°')
end
if format == 'dms' or not decimalPrecision then
local d, m, s, ctext, decimals
if decimalPrecision then
local float = math.abs(self.latitude)
d = Coordinate.mfloor(float)
m = Coordinate.mfloor(float * 60 - d * 60)
decimals = math.floor(logPrec) - 3
if decimals < 0 then decimals = 0 end
s = Coordinate.mfloor((float * 3600 - d * 3600 - m * 60) * 10^decimals + 0.5) / 10^decimals
if s == 60 then s = 0; m = m + 1 end
if m == 60 then m = 0; d = d + 1 end
else
local intToPrecision = Coordinate.mfloor(math.abs(self.latitude) / self.precision + 0.5)
d = Coordinate.mfloor(intToPrecision * self.precision)
m = Coordinate.mfloor(intToPrecision * (self.precision * 60) - d * 60)
decimals = math.floor(-1 * math.log10(self.precision * 3600))
if decimals < 1 then decimals = 0 end
s = Coordinate.mfloor(intToPrecision - d / self.precision - m / (self.precision * 60) + 0.5) * (self.precision * 3600)
end
if not decimalPrecision then params = params .. d end
if format == 'dms' then ctext = (self.latitude >= 0 and 'é. sz.' or 'd. sz.') .. ' ' .. d .. '°' end
if self.precision < Coordinate.PRECISION.D then
if not decimalPrecision then params = params .. '_' .. m end
if format == 'dms' then ctext = ctext .. ' ' .. string.format('%02d′', m) end
if self.precision < Coordinate.PRECISION.M then
if not decimalPrecision then params = params .. '_' .. s end
if format == 'dms' then ctext = ctext .. ' ' .. (s < 10 and '0' or '') .. string.format('%.' .. decimals .. 'f', s):gsub('%.', ',') .. '″' end
end
end
if not decimalPrecision then params = params .. '_' .. (self.latitude >= 0 and 'N' or 'S') .. '_' end
if format == 'dms' then text = text .. mw.text.tag('span', {style = 'white-space:nowrap;'}, ctext) .. ', ' end
local d, m, s, ctext, decimals
if decimalPrecision then
local float = math.abs(self.longitude)
d = Coordinate.mfloor(float)
m = Coordinate.mfloor(float * 60 - d * 60)
decimals = math.floor(logPrec) - 3
if decimals < 0 then decimals = 0 end
s = Coordinate.mfloor((float * 3600 - d * 3600 - m * 60) * 10^decimals + 0.5) / 10^decimals
if s == 60 then s = 0; m = m + 1 end
if m == 60 then m = 0; d = d + 1 end
else
local intToPrecision = Coordinate.mfloor(math.abs(self.longitude) / self.precision + 0.5)
d = Coordinate.mfloor(intToPrecision * self.precision)
m = Coordinate.mfloor(intToPrecision * (self.precision * 60) - d * 60)
decimals = math.floor(-1 * math.log10(self.precision * 3600))
if decimals < 1 then decimals = 0 end
s = Coordinate.mfloor(intToPrecision - d / self.precision - m / (self.precision * 60) + 0.5) * (self.precision * 3600)
end
if not decimalPrecision then params = params .. d end
if format == 'dms' then ctext = (self.longitude >= 0 and 'k. h.' or 'ny. h.') .. ' ' .. d .. '°' end
if self.precision < Coordinate.PRECISION.D then
if not decimalPrecision then params = params .. '_' .. m end
if format == 'dms' then ctext = ctext .. ' ' .. string.format('%02d′', m) end
if self.precision < Coordinate.PRECISION.M then
if not decimalPrecision then params = params .. '_' .. s end
if format == 'dms' then ctext = ctext .. ' ' .. (s < 10 and '0' or '') .. string.format('%.' .. decimals .. 'f', s):gsub('%.', ',') .. '″' end
end
end
if not decimalPrecision then params = params .. '_' .. (self.longitude >= 0 and 'E' or 'W') end
if format == 'dms' then text = text .. mw.text.tag('span', {style = 'white-space:nowrap;'}, ctext) end
end
return params, text
end
--[[
Return a GlobeCoordinate in HTMl (with a <GlobeCoordinate> node)
@param mw.language|string|nil language to use. By default the content language.
@param table|nil attributes table of attributes to add to the <GlobeCoordinate> node.
@return string
--]]
function Coordinate.coord(frame)
local args = {}
if frame == mw.getCurrentFrame() then
for k, v in pairs(frame:getParent().args) do
if type(k) == 'number' then v = v:match('^%s*(.-)%s*$') end -- remove whitespace
if v ~= '' then args[k] = v end
end
else
args = frame
end
local coord, inputFormat, coordParams, errors = formatTest(args)
if #errors > 0 then
local result = ""
for _, v in ipairs(errors) do
local errorHTML = '<strong class="error">Coordinate: ' .. v[2] .. '</strong>'
result = result .. errorHTML .. '<br>'
end
return result .. '[[Kategória:Hibás koordináták]]'
end
local format = args.format or inputFormat
local params, linkText = coord:toGeoHack(format)
if coordParams then params = params .. '_' .. coordParams end
local title = args.name and '&title=' .. mw.uri.encode(args.name) or ''
local inlineLink = mw.text.tag(
'span', {
class = 'plainlinks nourlexpansion'
},
'[' .. coord_link .. params .. title .. ' ' .. linkText .. ']' ..
mw.text.tag(
'span', {
["class"] = "h-geo geo",
["style"] = "display:none;"
},
mw.text.tag( 'span', {
["class"] = "p-latitude latitude"
},
coord.latitude
) ..
', ' ..
mw.text.tag( 'span', {
["class"] = "p-longitude longitude"
},
coord.longitude
)
)
) .. (args.notes or '')
local display = args.display and args.display:lower() or 'inline'
local text = ''
if string.find(display, 'inline') ~= nil or display == 'i' or display == 'it' or display == 'ti' then
text = inlineLink
end
if string.find(display, 'title') ~= nil or display == 't' or display == 'it' or display == 'ti' then
text = text .. mw.text.tag('span', {style = 'font-size:small;'},
mw.text.tag('span', {id = 'coordinates'},
'[[Földrajzi koordináta-rendszer|Koordináták]]: ' .. inlineLink
)
)
end
return text
end
function Coordinate.mfloor(float)
local result = math.floor(float)
return result + 1 < float + 1e-12 and result + 1 or result
end
return Coordinate