Modul:Homokozó/JulesWinnfield-hu/Coordinate

A Wikipédiából, a szabad enciklopédiából

Homokozó/JulesWinnfield-hu/Coordinate[mi ez?] • [dokumentáció: mutat, szerkeszt] • [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 .. '&params='

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