[edytuj] [odśwież] Dokumentacja modułu

Moduł pomocniczy do obsługi współrzędnych geograficznych.

szablon edytuj

Funkcja nie przyjmuje żadnych parametrów bezpośrednich. Interpretuje tylko parametry przekazane do szablonu (obecnie bez implementacji). Jeśli argumenty pozycyjne przekazane do szablonu są nieprawidłowe, to wynikiem jest komunikat błędu.

parametry szablonu edytuj

Pole Do czego służy? Jak wypełnić?
od 1 do 9 Parametry ze współrzędnymi geograficznymi tj. stopnie, minuty i sekundy szerokości geograficznej oraz stopnie, minuty i sekundy długości geograficznej. Ostatni opcjonalny parametr to dodatkowe parametry przekazywane w linku do geohacka.
Uwaga!
Koordynaty można również podać w jednym parametrze, w takim przypadku podany tekst będzie odpowiednio zinterpretowany. Akceptowane są dwie liczby interpretowane odpowiednio jako szerokość i długość geograficzna lub dwa zbiory do trzech nieujemnych liczb zakończonych znakiem N, S, W, E określających półkulę. Liczby mogą być ozdobione znakami stopni, minut i sekund lecz są one ignorowane.
{{...|54|22|11|N|18.913|W}}
umieść Parametr opcjonalny do pozycjonowania wyniku:
  • na górze - wyświetla współrzędne w prawym górnym rogu,
  • w tekście - wyświetla współrzędne w miejscu wywołania funkcji (to jest wartość domyślna)
  • w tekście i na górze - wyświetla w obu tych miejscach
{{...|54|22|11|N|18.913|W|umieść=na górze}}
dokładność Określa sposób formatowania wyniku:
  • 10st - tylko stopnie zaokrąglone do pełnych dziesiątek
  • st - tylko stopnie jako liczba całkowita
  • min - stopnie i minuty (obie liczby całkowite)
  • sek - stopnie, minuty i sekundy (wszystkie liczby całkowite)
  • sek+ - stopnie, minuty i sekundy z jedną cyfrą po przecinku
  • sek2...sek4 - stopnie, minuty i sekundy z określoną liczbą cyfr po przecinku
  • 1...7 - tylko stopnie z określoną liczbą cyfr po przecinku
  • dziesiętnie - automatyczna dokładność w zapisie dziesiętnym
  • kątowo - automatyczna dokładność w zapisie kątowym (wartość domyślna)
{{...|54|22|11|N|18.913|W|dokładność=min}}
nazwa Opcjonalna nazwa obiektu geograficznego. {{...|54|22|11|N|18.913|W|nazwa=Trójmiasto}}

szablon/mikro edytuj

Funkcja nie przyjmuje żadnych parametrów bezpośrednich. Interpretuje tylko parametry przekazane do szablonu (obecnie bez implementacji). Jeśli argumenty pozycyjne przekazane do szablonu są nieprawidłowe to wynikiem jest komunikat błędu.

parametry szablonu edytuj

Pole Do czego służy? Jak wypełnić?
od 1 do 9 Parametry ze współrzędnymi geograficznymi tj. stopnie, minuty i sekundy szerokości geograficznej oraz stopnie, minuty i sekundy długości geograficznej. Ostatni opcjonalny parametr to dodatkowe parametry przekazywane w linku do geohacka. {{...|54|22|11|N|18.913|W}}
nazwa Opcjonalna nazwa obiektu geograficznego. {{...|54|22|11|N|18.913|W|nazwa=Trójmiasto}}

szerokość, długość edytuj

Pomocnicze funkcje do konwersji koordynat na liczbową wartość szerokości i długości geograficznej. Głównym przeznaczeniem funkcji jest podawanie szerokości i długości geograficznej dla szablonów map w przypadku przekazywania koordynat w skompresowanej formie za pomocą jednego parametru.

W przypadku błędu funkcja zwraca pusty tekst.

parametry edytuj

Pole Do czego służy? Jak wypełnić?
od 1 do 9 Parametry ze współrzędnymi geograficznymi tj. stopnie, minuty i sekundy szerokości geograficznej oraz stopnie, minuty i sekundy długości geograficznej. Ostatni opcjonalny parametr to dodatkowe parametry przekazywane w linku do geohacka. {{...|54|22|11|N|18.913|W}}

przykłady edytuj

dane koordynat {{#invoke:koordynaty|szerokość|...}} {{#invoke:koordynaty|długość|...}}
57|18|22|N|4|27|32|W 57.306111111111 -4.4588888888889
57 18 22 N 4 27 32 W 57.306111111111 -4.4588888888889
44.112|N|87.913|W 44.112 -87.913
44.112N 87.913W 44.112 -87.913
54|18|type:city 54 18
54 18|type:city 54 18
44.112|N|4|27|32|W 44.112 -4.4588888888889
44.112°N 4°27'32W 44.112 -4.4588888888889
bzdury|60|80
bzdury 60 80
dokładność=3|60|80 60 80
dokładność=3|60 80 60 80
dokładność=sek+|60|80 60 80
dokładność=sek+|60 80 60 80
60.000000|80 60 80
60.000000 80 60 80
54|22|00.00|18|38|00 54.366666666667 18.633333333333
54 22 00.00 18 38 00
57|18|22.8|N|4|27|32.2|W 57.306333333333 -4.4589444444444
57°18'22,8″N 4°27'32,2″W 57.306333333333 -4.4589444444444
57|18|22.812378|N|4|27|32.24569|W 57.306336771667 -4.4589571361111
97|18|22.8|N|4|27|32.2|W
97 18 22.8 N 4 27 32.2 W
57|18|22.8|N|184|27|32.2|W 57.306333333333 -184.45894444444
57|18|22.8|N|184|27|32.2|W 57.306333333333 -184.45894444444
57|18|-22.8|N|4|27|32.2|W
57 18 -22.8 N 4 27 32.2 W
local m = {}

local geoformatdata = {
    supportedFormats = {
        { prec = "10st", precision = 10.00000000000000000000, dms = false, secondsFormat = nil,      format = "%0.0f%s" },
        { prec = "st",   precision =  1.00000000000000000000, dms = false, secondsFormat = nil,      format = "%0.0f%s" },
        { prec = "1",    precision =  0.10000000000000000000, dms = false, secondsFormat = nil,      format = "%0.1f%s" },
        { prec = "min",  precision =  0.01666666666666670000, dms = true,  secondsFormat = "%02.0f", format = "%0.0f%s%02.0f%s" },
        { prec = "2",    precision =  0.01000000000000000000, dms = false, secondsFormat = nil,      format = "%0.2f%s" },
        { prec = "3",    precision =  0.00100000000000000000, dms = false, secondsFormat = nil,      format = "%0.3f%s" },
        { prec = "sek",  precision =  0.00027777777777777800, dms = true,  secondsFormat = "%02.0f", format = "%0.0f%s%02.0f%s%02.0f%s" },
        { prec = "4",    precision =  0.00010000000000000000, dms = false, secondsFormat = nil,      format = "%0.4f%s" },
        { prec = "sek+", precision =  0.00002777777777777780, dms = true,  secondsFormat = "%04.1f", format = "%0.0f%s%02.0f%s%04.1f%s" },
        { prec = "5",    precision =  0.00001000000000000000, dms = false, secondsFormat = nil,      format = "%0.5f%s" },
        { prec = "sek2", precision =  0.00000277777777777778, dms = true,  secondsFormat = "%05.2f", format = "%0.0f%s%02.0f%s%05.2f%s" },
        { prec = "6",    precision =  0.00000100000000000000, dms = false, secondsFormat = nil,      format = "%0.6f%s" },
        { prec = "sek3", precision =  0.00000027777777777778, dms = true,  secondsFormat = "%06.3f", format = "%0.0f%s%02.0f%s%06.3f%s" },
        { prec = "7",    precision =  0.00000010000000000000, dms = false, secondsFormat = nil,      format = "%0.7f%s" },
        { prec = "sek4", precision =  0.00000002777777777778, dms = true,  secondsFormat = "%07.4f", format = "%0.0f%s%02.0f%s%07.4f%s" },
    },

    displayGlobes = {
        earth     = "EW",
        moon      = "EW",
        mercury   = "W",
        mars      = "W",
        phobos    = "W",
        deimos    = "W",
        ganymede  = "W",
        callisto  = "W",
        io        = "W",
        europa    = "W",
        mimas     = "W",
        enceladus = "W",
        tethys    = "W",
        dione     = "W",
        rhea      = "W",
        titan     = "W",
        lapetus   = "W",
        phoebe    = "W",
        venus     = "E",
        ceres     = "E",
        vesta     = "E",
        miranda   = "E",
        ariel     = "E",
        umbriel   = "E",
        titania   = "E",
        oberon    = "E",
        triton    = "E",
        pluto     = "E",
    },

    latitudeLinkMarkers   = { degree="_", minute="_", second="_", positivePrefix="", positiveSuffix="N", negativePrefix="", negativeSuffix="S", },
    longitudeLinkMarkers  = { degree="_", minute="_", second="_", positivePrefix="", positiveSuffix="E", negativePrefix="", negativeSuffix="W", },
    latitudeGlobeMarkers  = { degree="°", minute="′", second="″", positivePrefix="", positiveSuffix="N", negativePrefix="", negativeSuffix="S", },
    longitudeGlobeMarkers = { degree="°", minute="′", second="″", positivePrefix="", positiveSuffix="E", negativePrefix="", negativeSuffix="W", },

    displayDecimalSeparator = ",",
    coordinatesSeparator = "\194\160",
    topPrefix = "Na mapach: ",
    documentationSubpage = "opis",

    geohack_link = "//tools.wmflabs.org/geohack/geohack.php?language=pl&pagename=%s&params=%s",
    geohack_hint = "Mapy, zdjęcia satelitarne i inne informacje dotyczące miejsca o współrzędnych geograficznych %s %s",

    -- template API data
    apiTemplateName = "szablon",
    apiMicroName = "mikro",
    apiAutoName = "auto",
    apiLatitude = "szerokość",
    apiLongitude = "długość",
    
    argLocation = "umieść",
    valLocationTop = "na górze",
    valLocationInline = "w tekście",
    valLocationTopAndInline = "w tekście i na górze",
    
    argPrecision = "dokładność",
    valPrecisionDecimal = { "1", "2", "3", "4", "5", "6", "7", },
    valPrecisionDMS = { "st", "min", "sek", "sek+", },
    valPrecisionAutoDecimal = "dziesiętnie",
    valPrecisionAutoDMS = "kątowo",
    
    argLink = "linkuj",
    valLinkYes = "tak",
    valLinkNo = "nie",
    
    argName = "nazwa",

    -- categories
    errorCategory = "[[Kategoria:Złe użycie szablonu]]",

    -- error messages
    errorTooManyPositionalArguments = "Za dużo parametrów", 
    errorExpectedIntegerDegree = "Oczekiwana liczba stopni bez kropki dziesiętnej jeśli podawane są minuty (%s°%s')",
    errorInvalidMinutes = "Wartość minut jest nieprawidłowa (%s°%s')",
    errorExpectedIntegerMinutes = "Oczekiwana liczba minut bez kropki dziesiętnej jeśli podawane są sekundy (%s°%s'%s″)",
    errorInvalidSeconds = "Wartość sekund jest nieprawidłowa (%s°%s'%s″)",
    errorInvalidPositionalArguments = "Nieprawidłowe parametry",
    errorExpectedNonNegativeLatitude = "Oczekiwana nieujemna wartość szerokości geograficznej: %f",
    errorLatitudeOutOfRange = "Przekroczony zakres szerokości geograficznej (%f)",
    errorExpectedNonNegativeLongitude = "Oczekiwana nieujemna wartość długości geograficznej: %f",
    errorLongitudeOutOfRange = "Przekroczony zakres długości geograficznej (%f)",
    errorUnrecognizedLinkOption = "Niedozwolona wartość parametru ''link'': %s",
    errorUnrecognizedLocationOption = "Niedozwolona wartość parametru ''umieść'': %s",
}

local function create()
    
    -- initialize default data
    local result = {
        latitude  = 0,
        longitude = 0,
        precision = 1,
        params    = nil,
        inline    = false,
        top       = false,
        link      = true,
    }

    function result:parseCoordinates(args)

        local function isInt(s)
            -- up to 3 digits is enough for coordinates
            return s:match"^-?%d%d?%d?$"
        end

        local lang = mw.getContentLanguage()

        local function parseTypes()
            local types = {}
            for i = 1, 9 do
                local arg = mw.text.trim(args[i] or "")
                if #arg==0 then
                    table.insert(types, "_")
                elseif arg == "N" or arg=="E" or arg=="S" or arg=="W" then
                    table.insert(types, arg)
                elseif lang:parseFormattedNumber(arg) then
                    local scientific = arg:match"[eE]"
                    table.insert(types, scientific and "X" or "#")
                else
                    table.insert(types, "X")
                end
            end
 
            return table.concat(types, "")
        end
 
        local function calculateDecimalPrecision(s)
            local s1 = string.gsub(s,"%d","0")
            local s2 = string.gsub(s1,"^-","0")
            local s3 = string.gsub(s2,"0$","1")
            local result = lang:parseFormattedNumber(s3)
            return result > 0 and result or 1.0
        end

        local function selectAutoPrecision(p1, p2)
            local dms = nil
            if (args[geoformatdata.argPrecision] == geoformatdata.valPrecisionAutoDecimal)  then
                dms = false
            elseif not args[geoformatdata.argPrecision] or (args[geoformatdata.argPrecision] == geoformatdata.valPrecisionAutoDMS) then
                dms = true
            else
            	-- precision is selected explicit in the parameter
                return
            end

            -- select automatic precision
            local precision = p1 < p2 and p1 or p2
 
            -- find best DMS or decimal precision
            if precision < 1 then
                local eps = precision / 1024
                for i,v in ipairs(geoformatdata.supportedFormats) do
                    if (v.dms == dms) and ((v.precision - precision) < eps) then
                        precision = v.precision
                        break
                    end
                end
            end

            self.precision = precision
        end
        
        local function parseAngle(index, extra)
 
            local degree = mw.text.trim(args[index])
            local result = lang:parseFormattedNumber(degree)
            if extra == 0 then
                return true, result, calculateDecimalPrecision(degree)
            end
 
            local minutes = mw.text.trim(args[index+1])
            if not isInt(degree) then
                return false, string.format(geoformatdata.errorExpectedIntegerDegree, degree, minutes)
            end
 
            local precision = isInt(minutes) and 0.01666666666666670000 or 0.00027777777777777800
 
            local value = lang:parseFormattedNumber(minutes)
            if value < 0 or value >= 60 then
                return false, string.format(geoformatdata.errorInvalidMinutes, degree, minutes)
            end
 
            if result < 0 then
                result = result * 60 - value
            else
                result = result * 60 + value
            end
 
            if extra == 1 then
                return true, result / 60, precision
            end
 
            local seconds = mw.text.trim(args[index+2])
            if not isInt(minutes) then
                return false, string.format(geoformatdata.errorExpectedIntegerMinutes, degree, minutes, seconds)
            end
 
            precision = 0.00027777777777777800 * calculateDecimalPrecision(seconds)
            local value = lang:parseFormattedNumber(seconds)
            if value < 0 or value >= 60 then
                return false, string.format(geoformatdata.errorInvalidSeconds, degree, minutes, seconds)
            end
 
            if result < 0 then
                result = result * 60 - value
            else
                result = result * 60 + value
            end
 
            return true, result / 3600, precision
        end
        
        local function analyzeAngle(degree, minutes, seconds)
            local result = lang:parseFormattedNumber(degree)
            if not result then
                return false, geoformatdata.errorInvalidPositionalArguments
            end
 
            if not string.match(degree, "^%d+$") then
                if (#minutes > 0) or (#seconds > 0) then
                    -- expected empty minutes and empty seconds if float degree is given
                    return false, geoformatdata.errorInvalidPositionalArguments
                end
 
                return true, result, calculateDecimalPrecision(degree)
            end
 
            if #minutes == 0 then
                if #seconds > 0 then
                    -- expected empty seconds if minute is not given
                    return false, geoformatdata.errorInvalidPositionalArguments
                end
            
                return true, result, calculateDecimalPrecision(degree)
            end
 
            local minute = lang:parseFormattedNumber(minutes)
            if not minute or (minute >= 60) then
                return false, string.format(geoformatdata.errorInvalidMinutes, degree, minutes)
            end
 
            result = result * 60 + minute
            if not string.match(minutes, "^%d+$") then
                if #seconds > 0 then
                    return false, string.format(geoformatdata.errorExpectedIntegerMinutes, degree, minutes, seconds)
                end

                return true, result/60, 0.00027777777777777800
            end
 
            if #seconds == 0 then
                return true, result/60, 0.01666666666666670000
            end
 
            local second = lang:parseFormattedNumber(seconds)
            if not second or (second >= 60) then
                return false, string.format(geoformatdata.errorInvalidSeconds, degree, minutes, seconds)
            end

            result = result*60 + second
            return true, result/3600, calculateDecimalPrecision(seconds)*0.00027777777777777800
        end
 
        assert(args, "Missing template arguments")
        if not args[1] then
            -- display nothing if no positional arguments are provided
            return false, nil
        end
 
        if args[10] then
            return false, geoformatdata.errorTooManyPositionalArguments
        end

        local types = parseTypes()

        local function parseSimpleText()
            local arg = mw.text.trim(args[1])
            if types == "XX_______" then
                self.params = mw.text.trim(args[2])
            end

            local d1, m1, s1, h1, d2, m2, s2, h2 = mw.ustring.match(arg, "^([0-9,.]+)[°_]?%s*([0-9,.]*)['′_]?%s*([0-9,.]*)[\"″_]?%s*([NSEW])[,;]?%s+([0-9,.]+)[°_]?%s*([0-9,.]*)['′_]?%s*([0-9,.]*)[\"″_]?%s*([EWNS])$")
            if d1 then
                if (((h1 == "N") or (h1 == "S")) and ((h2 == "N") or (h2 == "S"))) or (((h1 == "E") or (h1 == "W")) and ((h2 == "E") or (h2 == "W"))) then
                    return geoformatdata.errorInvalidPositionalArguments
                end

                local status1, v1, p1 = analyzeAngle(d1, m1, s1)
                if not status1 then
                    return v1
                end

                local status2, v2, p2 = analyzeAngle(d2, m2, s2)
                if not status2 then
                    return v2
                end

                if (h1 == "S") or (h1 == "W") then
                    v1 = -v1;
                end
                if (h2 == "S") or (h2 == "W") then
                    v2 = -v2;
                end

                self.latitude  = ((h1 == "N") or (h1 == "S")) and v1 or v2
                self.longitude = ((h1 == "E") or (h1 == "W")) and v1 or v2
                selectAutoPrecision(p1, p2)
                return nil
            end

            local lat, lon = string.match(arg, "^(-?[0-9.,]+)%s+(-?[0-9.,]+)$")
            if lat then
                local latitude = lang:parseFormattedNumber(lat)
                local longitude = lang:parseFormattedNumber(lon)
                if latitude and longitude then
                    self.latitude = latitude
                    self.longitude = longitude
                    selectAutoPrecision(calculateDecimalPrecision(lat), calculateDecimalPrecision(lon))
                    return nil
                end
            end

            return geoformatdata.errorInvalidPositionalArguments
        end

        if (types == "X________") or (types == "XX_______") then
            local errorMessage = parseSimpleText()
            if errorMessage then
                return false, errorMessage
            end
        else
            local mapping = mw.loadData("Module:koordynaty/parserData")[types]
            if not mapping then
                return false, geoformatdata.errorInvalidPositionalArguments
            end
     
            if mapping[7] ~= 0 then
                self.params = mw.text.trim(args[mapping[7]])
            end
            
            local status1, latitude, latPrecision = parseAngle(mapping[1], mapping[2])
            if not status1 then
                return false, latitude
            end
     
            if mapping[3] ~= 0 then
                assert(mapping[3] == 1 or mapping[3] == -1, "Invalid adjust mode: " .. mapping[3]);
                if latitude < 0 then
                    return false, string.format(geoformatdata.errorExpectedNonNegativeLatitude, latitude)
                end
                latitude = mapping[3] * latitude
            end
     
            local status2, longitude, lonPrecision = parseAngle(mapping[4], mapping[5])
            if not status2 then
                return false, longitude
            end
     
            if mapping[6] ~= 0 then
                assert(mapping[6] == 1 or mapping[6] == -1, "Invalid adjust mode: " .. mapping[6]);
                if longitude < 0 then
                    return false, string.format(geoformatdata.errorExpectedNonNegativeLongitude, longitude)
                end
                longitude = mapping[6] * longitude
            end
            
            self.latitude  = latitude
            self.longitude = longitude
            selectAutoPrecision(latPrecision, lonPrecision)
        end
    
        if self.latitude < -90 or self.latitude > 90 then
            return false, string.format(geoformatdata.errorLatitudeOutOfRange, self.latitude)
        end
 
        if self.longitude < -360 or self.longitude > 360 then
            return false, string.format(geoformatdata.errorLongitudeOutOfRange, self.longitude)
        end
        
        return true, nil
    end
    
    function result:normalize()
        assert(self,"Did you use '.' instead of ':' while calling the function?")
        local mode = false
        if self.params then
            for i, v in ipairs(mw.text.split( self.params, '_', true )) do
                if mode then
                    -- more than one globe, display as given
                    return
                end
                
                local globe = string.match(v, "^globe:(%a+)$")
                if globe then
                    mode = geoformatdata.displayGlobes[string.lower(globe)]
                    if not mode then
                        -- unrecognized display as given
                        return
                    end
                end
            end
        end
    
        if mode == "?" then
            -- unrecognized left as given
        elseif mode == "W" then
            if self.longitude > 0 then
                self.longitude = self.longitude - 360
            end
        elseif mode == "E" then
            if self.longitude < 0 then
                self.longitude = self.longitude + 360
            end
        elseif self.longitude < -180 then
            self.longitude = self.longitude + 360
        elseif self.longitude > 180 then
            self.longitude = self.longitude - 360
        end
    end
    
    function result:parseOptions(args)
        -- TODO process notation in conjuction with precision
        local precision = args[geoformatdata.argPrecision]
        if precision and (precision ~= geoformatdata.valPrecisionAutoDecimal) and (precision ~= geoformatdata.valPrecisionAutoDMS) then
            self.precision = precision
        end
        
        self.name = args[geoformatdata.argName]
       
        local link = args[geoformatdata.argLink]
        if link == geoformatdata.valLinkYes then
            self.link = true
        elseif link == geoformatdata.valLinkNo then
            self.link = false
        elseif link then
            return false, string.format(geoformatdata.errorUnrecognizedLinkOption, link)
        else -- default is yes
            self.link = true
        end

        local location = args[geoformatdata.argLocation]
        if location == geoformatdata.valLocationTop then
            self.top = true
            self.inline = false
        elseif location == geoformatdata.valLocationInline then
            self.top = false
            self.inline = true
        elseif location == geoformatdata.valLocationTopAndInline then
            self.top = true
            self.inline = true
        elseif location then
            return false, string.format(geoformatdata.errorUnrecognizedLocationOption, location)
        elseif mw.title.getCurrentTitle().isTalkPage then
            -- an exception for talk pages
            self.top = false
            self.inline = true
        else -- default if not given
            self.top = true
            self.inline = false
        end

        return true, nil
    end
 
    function result:display(inlinePrefix)
        
        local function selectFormat(precision)
            local supportedFormats = geoformatdata.supportedFormats
            local precisionType = type(precision)
            if precisionType == "string" then
                -- find wikipedia template precision
                for i, v in ipairs(supportedFormats) do
                    if (precision == v.prec) then
                        return true, v
                    end
                end
            elseif precisionType == "number" then
                -- find wikidata precision
                for i, v in ipairs(supportedFormats) do
                    local prec = v.precision
                    local eps = prec / 64
                    local minPrec = prec - eps
                    local maxPrec = prec + eps
                    if (minPrec < precision) and (precision < maxPrec) then
                        return true, v
                    end
                end
            end

            -- use the last one with highest precision
            return false, supportedFormats[#supportedFormats]
        end
    
        local function formatAngle(value, format, markers, decimalSeparator)
            assert(type(value) == "number")
            local prefix = value < 0 and markers.negativePrefix or markers.positivePrefix
            local suffix = value < 0 and markers.negativeSuffix or markers.positiveSuffix
        
            value = math.abs(value)
        
            local result = nil
        
            if not format.dms then
                -- format decimal value
                if format.precision > 1 then
                    -- round the value
                    value = math.floor(value / format.precision) * format.precision
                end
        
                result = string.format(format.format, value, markers.degree)
            else
                -- format dms value
                local angle   = math.floor(value)
                local minutes = math.floor((value - angle) * 60)
                local seconds = tonumber(string.format(format.secondsFormat, (value - angle) * 3600 - minutes * 60))
                
                -- fix rounded seconds
                if seconds == 60 then
                    minutes = minutes + 1
                    seconds = 0
                    if minutes == 60 then
                        angle = angle + 1
                        minutes = 0
                    end
                end
        
                if format.precision > 0.01 then
                    -- round the value
                    if seconds >= 30 then
                        minutes = minutes + 1
                    end
                    seconds = 0
                    if minutes == 60 then
                        angle = angle + 1
                        minutes = 0
                    end
                end
        
                result = string.format(format.format, angle, markers.degree, minutes, markers.minute, seconds, markers.second)
            end
        
            if decimalSeparator then
                result = string.gsub(result, "%.", decimalSeparator)
            end
        
            return prefix .. result .. suffix
        end
        
        local function formatDegree(value, decimalSeparator)
            local result = string.format("%f", value)
        
            if decimalSeparator then
                result = string.gsub(result, "%.", decimalSeparator)
            end
        
            return result
        end
    
        local function fullpagenamee()
            local title = mw.title.getCurrentTitle()
            return title.namespace == 0
                and title:partialUrl()
                or  title.nsText .. ":" .. title:partialUrl()
        end
    
        local status, format = selectFormat(self.precision)
        assert(format)
    
        local prettyLatitude  = formatAngle(self.latitude,  format, geoformatdata.latitudeGlobeMarkers,  geoformatdata.displayDecimalSeparator)
        local prettyLongitude = formatAngle(self.longitude, format, geoformatdata.longitudeGlobeMarkers, geoformatdata.displayDecimalSeparator)
    
        if not self.link then
            return mw.text.nowiki(prettyLatitude .. geoformatdata.coordinatesSeparator .. prettyLongitude)
        end
    
        local params = {
            formatAngle(self.latitude,  format, geoformatdata.latitudeLinkMarkers),
            formatAngle(self.longitude, format, geoformatdata.longitudeLinkMarkers),
        }
    
        if self.params then
            table.insert(params, self.params)
        end
    
        local degreeLatitude  = formatDegree(self.latitude,  geoformatdata.displayDecimalSeparator)
        local degreeLongitude = formatDegree(self.longitude, geoformatdata.displayDecimalSeparator)
    
        local geohack_link = string.format(geoformatdata.geohack_link, fullpagenamee(), table.concat(params,"_"))
        if self.name then
            geohack_link = geohack_link .. "&title=" .. mw.uri.encode(self.name)
        end
    
        local pretty_hint = string.format(geoformatdata.geohack_hint, prettyLatitude, prettyLongitude)
        local degree_hint = string.format(geoformatdata.geohack_hint, degreeLatitude, degreeLongitude)
        local separator = mw.text.nowiki(geoformatdata.coordinatesSeparator)
    
        local result = {
            "[", geohack_link, " ",
            "<span class=\"geo-default\">",
                "<span class=\"geo-dms\" title=\"", mw.text.nowiki(pretty_hint), "\">",
                    "<span class=\"latitude\">", mw.text.nowiki(prettyLatitude), "</span>",
                    separator,
                    "<span class=\"longitude\">", mw.text.nowiki(prettyLongitude), "</span>",
                "</span>",
            "</span>",
            "<span class=\"geo-multi-punct\">/</span>",
            "<span class=\"geo-nondefault\">",
                "<span class=\"geo-dms\" title=\"", mw.text.nowiki(degree_hint), "\">",
                    "<span class=\"latitude\">", mw.text.nowiki(degreeLatitude), "</span>",
                    separator,
                    "<span class=\"longitude\">", mw.text.nowiki(degreeLongitude), "</span>",
                "</span>",
            "</span>",
            "]"
        }
    
        local text = table.concat(result, "")
    
        if not self.inline and not self.top then
            return text
        end
        
        result = {}
    
        if self.inline then
            if inlinePrefix then
                table.insert(result, inlinePrefix)
            end
            if self.top then
                table.insert(result, "<span class=\"coordinates inline inline-and-top plainlinks\">")
            else
                table.insert(result, "<span class=\"coordinates inline plainlinks\">")
            end
            table.insert(result, text)
            table.insert(result, "</span>")
        end
    
        if self.top then
            table.insert(result, "<span id=\"coordinates\" class=\"coordinates put-in-header plainlinks\">")
            table.insert(result, geoformatdata.topPrefix)
            table.insert(result, text)
            table.insert(result, "</span>")
        end
    
        return table.concat(result, "")
    end

    function result:extensionGeoData(frame)
        local params = {}
 
        local title = mw.title.getCurrentTitle()
        if self.top and not title.isTalkPage and (title.subpageText ~= geoformatdata.documentationSubpage) then
            table.insert(params, "primary")
        end

        if self.latitude >= 0 then
            table.insert(params, string.format("%f", self.latitude))
            table.insert(params, "N")
        else
            table.insert(params, string.format("%f", -self.latitude))
            table.insert(params, "S")
        end

        if mode == "W" then
            if self.longitude > 0 then
                table.insert(params, string.format("%f", 360-self.longitude))
            else
                table.insert(params, string.format("%f", -self.longitude))
            end
            table.insert(params, "W")
        elseif mode == "E" then
            if self.longitude >= 0 then
                table.insert(params, string.format("%f", self.longitude))
            else
                table.insert(params, string.format("%f", 360+self.longitude))
            end
            table.insert(params, "E")
        elseif self.longitude >= 0 then
            table.insert(params, string.format("%f", self.longitude))
            table.insert(params, "E")
        else
            table.insert(params, string.format("%f", -self.longitude))
            table.insert(params, "W")
        end

        if self.params then
            table.insert(params, self.params)
        end

        if self.name then
            params.name = self.name
        end

        --  https://bugzilla.wikimedia.org/show_bug.cgi?id=50863 RESOLVED
        return frame:callParserFunction("#coordinates", params) or ""
    end

    return result;
end

local function showError(message, args)
    if not message then
        return geoformatdata.errorCategory
    end
    
    local result = {}
    table.insert(result, "<span style=\"color:red\">")
    assert(type(message) == "string", "Expected string message")
    table.insert(result, message)
    local i = 1
    while args[i] do
        if i == 1 then
            table.insert(result, ": {")
        else
            table.insert(result, "&#x7C;")
        end
        
        table.insert(result, args[i])
        i = i + 1
    end
    if i > 1 then
        table.insert(result, "}")
    end
    
    table.insert(result, "</span>")
    
    if mw.title.getCurrentTitle().namespace == 0 then
        table.insert(result, geoformatdata.errorCategory)
    end
    
    return table.concat(result, "")
end

local function parse(frame, link)
    local coordinates = create()
    local args = frame.args
    local status, errorMessage = coordinates:parseCoordinates(args)
    if not status then
        return showError(errorMessage, args)
    end

    local status, errorMessage = coordinates:parseOptions(args)
    if not status then
        return showError(errorMessage, args)
    end

    coordinates.link = link
    coordinates:normalize()
    return coordinates:display() .. coordinates:extensionGeoData(frame)
end

function m.format(frame)
    return parse(frame, false)
end

function m.link(frame)
    return parse(frame, true)
end

m[geoformatdata.apiTemplateName] = function (frame)
    local coordinates = create()
    local args = frame:getParent().args
    local status, errorMessage = coordinates:parseCoordinates(args)
    if not status then
        return showError(errorMessage, args)
    end

    local status, errorMessage = coordinates:parseOptions(args)
    if not status then
        return showError(errorMessage, args)
    end
    
    coordinates:normalize()
    return coordinates:display() .. coordinates:extensionGeoData(frame)
end

m[geoformatdata.apiMicroName] = function (frame)
    local coordinates = create()
    local args = frame:getParent().args
    local status, errorMessage = coordinates:parseCoordinates(args)
    if not status then
        return showError(errorMessage, args)
    end

    -- the only available option
    coordinates.name = args[geoformatdata.argName]

    -- options are implied in micro variant
    if coordinates.precision > 0.00027777777777777800 then
        coordinates.precision = 0.00027777777777777800 -- seconds
    elseif coordinates.precision < 0.00002777777777777780 then
        coordinates.precision = 0.00002777777777777780 -- seconds with one decimal digit
    end
    
    if not coordinates.params then
        coordinates.params    = "scale:5000" -- bonus
    end
    
    coordinates.inline    = true
    coordinates.top       = false
    coordinates.link      = true

    -- simple link without geodata extension
    coordinates:normalize()
    return coordinates:display()
end

m[geoformatdata.apiAutoName] = function(frame)
    
    local entity = mw.wikibase.getEntityObject() if not entity then return nil end -- missing entity
    local claims = entity.claims if not claims then return nil end -- missing claims
    
    local function selectProperty(pid)
        local prop = claims[pid] if not prop then return false end -- missing property
 
        -- load preferred statements
        local result = {}
        for _, v in ipairs(prop) do
            if v.rank == "preferred" then
                table.insert(result, v)
            end
        end
 
        if #result ~= 0 then return true, prop end
 
        for _, v in  ipairs(prop) do
            if v.rank == "normal" then
                table.insert(result, v)
            end
        end
 
        if #result ~= 0 then return true, prop end
 
        return false -- empty property table
    end
 
    local function selectValue(prop, expectedType)
        if not prop then return false end
        if prop.type ~= "statement" then return false end
        local snak = prop.mainsnak
        if not snak or snak.snaktype ~= "value" then return false end
        local datavalue = snak.datavalue
        if not datavalue or datavalue.type ~= expectedType then return false end
        local value = datavalue.value
        if not value then return false end
        return true, value
    end
 
    function selectGlobe(globe)
        local globes = {
            unknownGlobe = { symbol="", link=false },
            ["http://www.wikidata.org/entity/Q2"]   = { symbol="[[Plik:Geographylogo.svg|20px|alt=Ziemia|link=Ziemia]] ", link="" },
            ["http://www.wikidata.org/entity/Q405"] = { symbol="[[Plik:Nuvola apps kmoon left.png|15px|alt=Księżyc|link=Księżyc]] ", link="globe:Moon" },
            ["http://www.wikidata.org/entity/Q111"] = { symbol="[[Plik:Blue Mars symbol.svg|15px|alt=Mars|link=Mars]] ", link="globe:Mars" },
            ["http://www.wikidata.org/entity/Q308"] = { symbol="[[Plik:Blue Mercury symbol.svg|12px|alt=Merkury|link=Merkury]] ", link="globe:Mercury" },
            ["http://www.wikidata.org/entity/Q313"] = { symbol="[[Plik:Symbol venus blue.svg|12px|alt=Wenus|link=Wenus]] ", link="globe:Venus" },
        }
 
        return globes[globe or "http://www.wikidata.org/entity/Q2"] or globes.unknownGlobe
    end
 
    function selectType()
        local types = {
            unknownType = "type:city",
            [515]  = "type:city",
            [6256] = "type:country",
            [5107] = "type:satellite",
            [165]  = "type:satellite",
        }
 
        local status, classes = selectProperty("P31") if not status then return types.unknownType end
        for _, p in ipairs(classes) do
            local status2, v = selectValue(p, "wikibase-entityid")
            if status2 and v["entity-type"] == "item" then
                local result = types[v["numeric-id"]]
                if result then return result end
            end
        end
 
        return types.unknownType
    end

    local status1, coordinates = selectProperty("P" .. (frame.args[1] or "625")) if not status1 then return nil end
    local status2, autocoords = selectValue(coordinates[1], "globecoordinate") if not status2 then return nil end
    local globe = selectGlobe(autocoords.globe)
    if not globe.link then return nil end -- not supported globe
 
    local params = {
        selectType(),
    }
    if #globe.link > 0 then
        table.insert(params, globe.link)
    end
    
    local coords = create()
    coords:parseOptions(frame.args)
    coords.latitude = autocoords.latitude
    coords.longitude = autocoords.longitude
    coords.precision = autocoords.precision or 1
    coords.params = table.concat(params,"_")
 
    coords:normalize()
    return coords:display(globe.symbol)
end

m[geoformatdata.apiLatitude] = function (frame)
    local coordinates = create()
    local status = coordinates:parseCoordinates(frame.args)
    return status and coordinates.latitude or ""
end

m[geoformatdata.apiLongitude] = function (frame)
    local coordinates = create()
    local status = coordinates:parseCoordinates(frame.args)
    return status and coordinates.longitude or ""
end

return m