Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:ObliqueDanishAirPhoto

From Wikipedia, the free encyclopedia

local p = {}

-- Ellipsoid constants (WGS84 / ETRS89)
local a = 6378137.0
local f = 1 / 298.257223563
local b = a * (1 - f)
local e2 = (a^2 - b^2) / a^2
local e_prime2 = (a^2 - b^2) / b^2
local k0 = 0.9996
local FALSE_EASTING = 500000.0

local function rad(deg) return deg * math.pi / 180 end

--- Forward Transverse Mercator: (Lat, Lon, Central Meridian) -> (Easting, Northing)
local function latLonToUtm(latDeg, lonDeg, centralMeridianDeg)
    local phi = rad(latDeg)
    local lambda = rad(lonDeg)
    local lambda0 = rad(centralMeridianDeg)

    local sin_phi = math.sin(phi)
    local cos_phi = math.cos(phi)
    local tan_phi = math.tan(phi)

    local N = a / math.sqrt(1 - e2 * sin_phi^2)
    local T = tan_phi^2
    local C = e_prime2 * cos_phi^2
    local A = (lambda - lambda0) * cos_phi

    local M = a * (
        (1 - e2/4 - 3*e2^2/64 - 5*e2^3/256) * phi
        - (3*e2/8 + 3*e2^2/32 + 45*e2^3/1024) * math.sin(2*phi)
        + (15*e2^2/256 + 45*e2^3/1024) * math.sin(4*phi)
        - (35*e2^3/3072) * math.sin(6*phi)
    )

    local easting = FALSE_EASTING + k0 * N * (
        A 
        + ((1 - T + C) * (A^3 / 6))
        + ((5 - 18*T + T^2 + 72*C - 58*e_prime2) * (A^5 / 120))
    )

    local northing = k0 * (
        M + N * tan_phi * (
            (A^2 / 2)
            + ((5 - T + 9*C + 4*C^2) * (A^4 / 24))
            + ((61 - 58*T + T^2 + 600*C - 330*e_prime2) * (A^6 / 720))
        )
    )

    return easting, northing
end

--- Geographic extent this link is meaningful for.
-- skraafoto only serves Danish imagery, so anything outside Denmark would
-- produce a valid-looking but useless link. The eastern bound includes
-- Bornholm (~15.2E), which EPSG:25832 covers even though it lies in zone 33.
local MIN_LAT, MAX_LAT = 54.4, 57.9
local MIN_LON, MAX_LON = 7.8, 15.3

local EARTH_ITEM = "Q2"

local function errorSpan(msg)
    return '<span class="error">Error: ' .. msg .. '</span>'
end

--- Is this coordinate value on Earth? A missing globe means Earth by default.
local function isOnEarth(val)
    if not val.globe then
        return true
    end
    return string.match(val.globe, "(Q%d+)$") == EARTH_ITEM
end

--- Get Lat/Lon from Wikidata (best-ranked P625 statement on current page)
local function getWikidataCoordinates()
    -- Wikibase client is not enabled on every wiki.
    if not mw.wikibase then
        return nil, nil
    end

    local entityId = mw.wikibase.getEntityIdForCurrentPage()
    if not entityId then
        return nil, nil
    end

    -- getBestStatements honours statement rank and, unlike getEntity, does not
    -- load the whole item (which counts against the expensive parser limit).
    local statements = mw.wikibase.getBestStatements(entityId, "P625")
    for _, statement in ipairs(statements or {}) do
        local snak = statement.mainsnak
        if snak and snak.datavalue and snak.datavalue.value then
            local val = snak.datavalue.value
            if val.latitude and val.longitude and isOnEarth(val) then
                return val.latitude, val.longitude
            end
        end
    end

    return nil, nil
end

--- Entry point for template/invoke
function p.zone32NFromPage(frame)
    local args = frame.args
    if not args[1] and not args.lat then
        -- getParent returns nil when #invoke is called from a page directly
        -- rather than through a template wrapper.
        local parent = frame:getParent()
        args = parent and parent.args or args
    end

    local lat = tonumber(args.lat or args[1])
    local lon = tonumber(args.lon or args[2])

    -- Fetch from Wikidata if no explicit parameters are passed
    if not lat or not lon then
        lat, lon = getWikidataCoordinates()
    end

    if not lat or not lon then
        return errorSpan("No coordinates found on page or Wikidata.")
    end

    if lat < MIN_LAT or lat > MAX_LAT or lon < MIN_LON or lon > MAX_LON then
        return errorSpan(string.format(
            "Coordinates (%.5f, %.5f) are outside the covered area.", lat, lon))
    end

    -- Convert Geographic (Lat/Lon) to UTM Zone 32N (Central Meridian = 9°E)
    local e32, n32 = latLonToUtm(lat, lon, 9.0)

    -- Format link: https://skraafoto.dataforsyningen.dk/?center=easting,northing
    local url = string.format("https://skraafoto.dataforsyningen.dk/?center=%.2f,%.2f", e32, n32)

    return string.format("[%s Oblique air photography]", url)
end

return p