Module:Coordinates: Difference between revisions

From TEPwiki, Urth's Encyclopedia
Jump to navigation Jump to search
Content added Content deleted
(copy from test2wiki. Tried to import, but that doesn't presently work for Modules)
 
No edit summary
Line 4: Line 4:
-- Attempt is not really to write a nice and proper module from scratch :D
-- Attempt is not really to write a nice and proper module from scratch :D
local coordinates = {
local coordinates = {
prec_dec = require "Module:Coord_prec_dec",
mod_math = require "Module:Math",
precision = require "Module:Precision",
wikitext = require "Module:Wikitext"
wikitext = require "Module:Wikitext"
}
}

Revision as of 21:19, 4 March 2013

Documentation for this module may be created at Module:Coordinates/doc

-- A module that mimics the functionality of Template:Coord and its sub templates
-- The attempt is to actually mimic a conversion of an often used en.wp template in the way
-- that most templates will actually be converted by the wiki users. 
-- Attempt is not really to write a nice and proper module from scratch :D
local coordinates = {
    mod_math = require "Module:Math",
    wikitext = require "Module:Wikitext"
}
globalFrame = nil

--- Replacement for {{coord/display/title}}
function displaytitle (s)
    local l = "[[Geographic coordinate system|Coordinates]]: " .. s
    local co = mw.text.tag({name="span",contents=l,params={id="coordinates"}})
    local p = {}
    p["font-size"] = "small"
    return mw.text.tag({name="span",contents=co,params=p})
end

--- Replacement for {{coord/display/inline}}
function displayinline (s)
    return s    
end

--- Test if the arguments imply that DMS might be in use
local dmsTest = function(first, second)
    local concatenated = first .. second;
    
    if concatenated == "NE" or concatenated == "NW" or concatenated == "SE" or concatenated == "SW" then
        return true;
    end
    return false;
end

--- Parse the frame assuming that it is in dec format.
-- @frame
-- @returns a table with all information needed to display coordinates
function parseDec(args)
    local coordinateSpec = {}
    local errors = {}
    
    if args[2] == "" or args[2] == nil then
        return nil, {{"parseDec", "Missing longitude"}}
    end
    
    coordinateSpec["dec-lat"]  = args[1]
    coordinateSpec["dec-long"] = args[2]

    local precision = coordinates.prec_dec.max_precision(args[1], args[2])
    coordinateSpec["dms-lat"]  = convert_dec2dms(args[1], "N", "S", precision)  -- {{coord/dec2dms|{{{1}}}|N|S|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
    coordinateSpec["dms-long"] = convert_dec2dms(args[2], "E", "W", precision)  -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
    coordinateSpec.param    = args[1] .."_N_" .. args[2] .. "_E_" .. args[3]

    if args["format"] ~= "" then
        coordinateSpec.default = args["format"]
    else
        coordinateSpec.default = "dec"
    end
    coordinateSpec.name     = args["name"]
        
    -- TODO refactor the validations into separate functions
    if (tonumber(args[1]) or 0) > 90 then
        table.insert(errors, {"parseDec","latd>90"})
    end
    if (tonumber(args[1]) or 0) < -90 then
        table.insert(errors, {"parseDec", "latd<-90"})
    end
    if (tonumber(args[2]) or 0) >= 360 then
        table.insert(errors, {"parseDec", "longd>=360"})
    end
    if (tonumber(args[2]) or 0) <= -360 then
        table.insert(errors, {"parseDec", "longd<=-360"})
    end
    
    return coordinateSpec, errors
end

function optionalArg(arg, suplement)
    if arg ~= nil and arg ~= "" then
        return arg .. suplement
    end
    return ""
end

function isEmpty(arg)
  if arg == nil or arg == "" then
    return true
  end
  return false
end

function parseDMS(args)
    local coordinateSpec = {}
    local errors = {}
    
    coordinateSpec["dms-lat"]  = args[1].."°"..optionalArg(args[2],"′") .. optionalArg(args[3],"″") .. args[4]
    coordinateSpec["dms-long"] = args[5].."°"..optionalArg(args[6],"′") .. optionalArg(args[7],"″") .. args[8]
    coordinateSpec["dec-lat"]  = convert_dms2dec(args[4],args[1],args[2],args[3]) -- {{coord/dms2dec|{{{4}}}|{{{1}}}|0{{{2}}}|0{{{3}}}}}
    coordinateSpec["dec-long"] = convert_dms2dec(args[8],args[5],args[6],args[7]) -- {{coord/dms2dec|{{{8}}}|{{{5}}}|0{{{6}}}|0{{{7}}}}}
    -- TODO Use loop when we know it won't break everything
    coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5] .. "_" .. args[6] .. "_" .. args[7] .. "_" .. args[8] .. "_" .. args[9]
    if args["format"] ~= "" then
        coordinateSpec.default = args["format"]
    else
        coordinateSpec.default = "dms"
    end
    coordinateSpec.name     = args["name"]

    -- Error reporting
    if isEmpty(args[5]) then
        table.insert(errors, {"parseDM", "Missing longitude" })
    end

    if not isEmpty(args[10])  then
        table.insert(errors, {"parseDM", "Unexpected extra parameters"})
    end
    
    if (tonumber(args[1]) or 0) > 90 then
        table.insert(errors, {"parseDMS", "latd>90"})
    end
    if (tonumber(args[1]) or 0) < -90 then
        table.insert(errors, {"parseDMS", "latd<-90"})
    end
    if (tonumber(args[2]) or 0) >= 60 then
        table.insert(errors, {"parseDMS", "latm>=60"})
    end
    if (tonumber(args[2]) or 0) < 0 then
        table.insert(errors, {"parseDMS", "latm<0"})
    end
    if (tonumber(args[3]) or 0) >= 60 then
        table.insert(errors, {"parseDMS", "lats>=60"})
    end
    if (tonumber(args[3]) or 0) < 0 then
        table.insert(errors, {"parseDMS", "lats<0"})
    end
    if (tonumber(args[5]) or 0) >= 360 then
        table.insert(errors, {"parseDMS", "longd>=360"})
    end
    if (tonumber(args[5]) or 0) <= -360 then
        table.insert(errors, {"parseDMS", "longd<=-360"})
    end
    if (tonumber(args[6]) or 0) >= 60 then
        table.insert(errors, {"parseDMS", "longm>=60"})
    end
    if (tonumber(args[6]) or 0) < 0 then
        table.insert(errors, {"parseDMS", "longm<0"})
    end
    if (tonumber(args[7]) or 0) >= 60 then
        table.insert(errors, {"parseDMS", "longs>=60"})
    end
    if (tonumber(args[7]) or 0) < 0 then
        table.insert(errors, {"parseDMS", "longs<0"})
    end

    return coordinateSpec, errors
end

function parseDM(args)
    local coordinateSpec = {}
    local errors = {}

    coordinateSpec["dms-lat"]  = args[1].."°"..optionalArg(args[2],"′") .. args[3]
    coordinateSpec["dms-long"] = args[4].."°"..optionalArg(args[5],"′") .. args[6]
    coordinateSpec["dec-lat"]  = convert_dms2dec(args[3],args[1],args[2]) -- {{coord/dms2dec|{{{3}}}|{{{1}}}|0{{{2}}}}}
    coordinateSpec["dec-long"] = convert_dms2dec(args[6],args[4],args[5]) -- {{coord/dms2dec|{{{6}}}|{{{4}}}|0{{{5}}}}}
    -- TODO Use loop when we know it won't break everything
    coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5] .. "_" .. args[6] .. "_" .. args[7]
    if args["format"] ~= "" then
        coordinateSpec.default = args["format"]
    else
        coordinateSpec.default = "dms"
    end
    coordinateSpec.name = args["name"]
    
    -- Error reporting
    if isEmpty(args[4]) then
        table.insert(errors, {"parseDM", "Missing longitude" })
    end

    if not (isEmpty(args[8]) and isEmpty(args[9]) and isEmpty(args[10])) then
        table.insert(errors, {"parseDM", "Unexpected extra parameters"})
    end

    if (tonumber(args[1]) or 0) > 90 then
        table.insert(errors, {"parseDM", "latd>90"})
    end
    if (tonumber(args[1]) or 0) < -90 then
        table.insert(errors, {"parseDM", "latd<-90"})
    end
    if (tonumber(args[2]) or 0) >= 60 then
        table.insert(errors, {"parseDM", "latm>=60"})
    end
    if (tonumber(args[2]) or 0) < 0 then
        table.insert(errors, {"parseDM", "latm<0"})
    end
    if (tonumber(args[4]) or 0) >= 360 then
        table.insert(errors, {"parseDM", "longd>=360"})
    end
    if (tonumber(args[4]) or 0) <= -360 then
        table.insert(errors, {"parseDM", "longd<=-360"})
    end
    if (tonumber(args[5]) or 0) >= 60 then
        table.insert(errors, {"parseDM", "longm>=60"})
    end
    if (tonumber(args[5]) or 0) < 0 then
        table.insert(errors, {"parseDM", "longm<0"})
    end
    
    return coordinateSpec, errors
end

function parseD(args)
    local coordinateSpec = {}
    local errors = {}

    coordinateSpec["dec-lat"]  = args[1]
    if args[2] =="S" then
        coordinateSpec["dec-lat"] = "-" .. coordinateSpec["dec-lat"]
    end
    coordinateSpec["dec-long"]  = args[4]
    if args[4] =="W" then
        coordinateSpec["dec-long"] = "-" .. coordinateSpec["dec-long"]
    end
        
    coordinateSpec["dec-lat-display"] = args[1] .. "°" .. args[2]
    coordinateSpec["dec-long-display"] = args[3] .. "°" .. args[4]
        
    local function postfixInverter(NE, latlong)
        if NE == "N" and latlong == "lat" then
            return "S"
        elseif NE == "E" and latlong == "long" then
            return "W"
        elseif latlong == "lat" then
            return "N"
        else
            return "E"
        end
    end

    local precision = coordinates.prec_dec.max_precision(args[1], args[3])
    coordinateSpec["dms-lat"] = convert_dec2dms(args[1], args[2], postfixInverter(args[2],"lat"), precision) -- {{coord/dec2dms|{{{1}}}|{{{2}}}|{{#ifeq:{{{2}}}|N|S|N}}|{{coord/prec dec|{{{1}}}|{{{3}}}}}}}
    coordinateSpec["dms-long"] = convert_dec2dms(args[3], args[4], postfixInverter(args[4],"long"), precision) -- {{coord/dec2dms|{{{3}}}|{{{4}}}|{{#ifeq:{{{4}}}|E|W|E}}|{{coord/prec dec|{{{1}}}|{{{3}}}}}}}

    -- TODO Use loop when we know it won't break everything
    coordinateSpec.param = args[1] .. "_" .. args[2] .. "_" .. args[3] .. "_" .. args[4] .. "_".. args[5]
        
    if args["format"] ~= "" then      
        coordinateSpec.default = args["format"]
    else
        -- {{#ifeq:{{coord/prec dec|{{{1}}}|{{{3}}}}}|d|dms|dec}}
        if precision == "d" then
            coordinateSpec.default = "dms"
        else
            coordinateSpec.default = "dec"
        end
    end
    coordinateSpec.name     = args["name"]
    
    -- Error reporting
    if isEmpty(args[3]) then
        table.insert(errors, {"parseD", "Missing longitude" })
        args[3] = 0 -- to avoid error in tonumber() later on
    end

    if not (isEmpty(args[6]) and isEmpty(args[7]) and isEmpty(args[8]) and isEmpty(args[9]) and isEmpty(args[10])) then
        table.insert(errors, {"parseD", "Unexpected extra parameters"})
    end

    if (tonumber(args[1]) or 0) > 90 then
        table.insert(errors, {"parseD", "latd>90"})
    end
    if (tonumber(args[1]) or 0) < -90 then
        table.insert(errors, {"parseD", "latd<-90"})
    end
    if (tonumber(args[3]) or 0) >= 360 then
        table.insert(errors, {"parseD", "longd>=360"})
    end
    if (tonumber(args[3]) or 0) <= -360 then
        table.insert(errors, {"parseD", "longd<=-360"})
    end

    return coordinateSpec, errors
end

--- BAD BAD URL escape
-- replace this later with the actual helper template
function urlEscape(arg)
    return arg:gsub("%s+", '%%20'):gsub("%<", "%%3C"):gsub("%>", "%%3E")
end

--- A function that prints a table of coordinate specifications to HTML
function specPrinter(args, coordinateSpec)
    local uriComponents = coordinateSpec["param"]
    if uriComponents == "" then
        -- RETURN error, should never be empty or nil
        return "ERROR param was empty"
    end
    if args["name"] ~= "" and args["name"] ~= nil then
        uriComponents = uriComponents .. "&title=" .. urlEscape(coordinateSpec["name"])
    end
    
    -- TODO i18n
    local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
             .. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
             .. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
             .. '</span>'

    local geodeclat = coordinateSpec["dec-lat-display"]
    if geodeclat == nil then
        local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
        if lat < 0 then
            -- FIXME this breaks the pre-existing precision
            geodeclat = coordinateSpec["dec-lat"]:sub(2) .. "°S"
        else
            geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
        end
    end
    
    -- FIXME ugly code duplication, but lazy :D
    local geodeclong = coordinateSpec["dec-long-display"]
    if geodeclong == nil then
        local long = tonumber( coordinateSpec["dec-long"] ) or 0
        if long < 0 then
            -- FIXME does not handle unicode minus
            geodeclong = coordinateSpec["dec-long"]:sub(2) .. "°W"
        else
            geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
        end
    end
    
    -- TODO requires DEC formatting
    -- TODO requires vcard
    local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
             .. '<span class="latitude">' .. geodeclat .. '</span> '
             .. '<span class="longitude">' .. geodeclong .. '</span>'
             .. '</span>'
             
    local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
            .. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
            .. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">' .. geodechtml .. '</span>'
    
    return '<span class="plainlinks nourlexpansion">' .. globalFrame:preprocess('[http://toolserver.org/~geohack/geohack.php?pagename={{FULLPAGENAMEE}}&params=' .. uriComponents .. ' ' .. inner .. ']') .. '</span>'
end

function errorPrinter(errors)
    local result = ""
    for i,v in ipairs(errors) do
        local errorHTML = '<strong class="error">' .. v[2] .. ' in Module:Coordinates.' .. v[1] .."()" .. '</strong>'
        result = result .. errorHTML .. "<br />"
    end
    return result
end

--- Determine the required CSS class to display coordinates
-- Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
-- default is the mode as specificied by the user when calling the {{coord}} template
-- mode is the display mode (dec or dms) that we will need to determine the css class for 
function displayDefault(default, mode)
    if default == "" then
        default = "dec"
    end
    
    if default == mode then
        return "geo-default"
    else
        return "geo-nondefault"
    end
end

--- Check the arguments to determine what type of coordinates has been input
function formatTest(args)
    if args[1] == "" then
        -- no lat logic
        return errorPrinter( {{"formatTest", "Missing latitude"}} )
    elseif args[4] == "" and args[5] == "" and args[6] == "" then
        -- dec logic
        local decResult, errors = parseDec(args)
        return specPrinter(args,decResult) .. " " .. errorPrinter(errors)
    elseif dmsTest(args[4], args[8]) then
        -- dms logic
        local dmsResult, errors = parseDMS(args)
        return specPrinter(args, dmsResult ) .. " " .. errorPrinter(errors)
        -- return "dms"
    elseif dmsTest(args[3], args[6]) then
        -- dm logic
        local dmResult, errors = parseDM(args)
        return specPrinter(args, dmResult ) .. " " .. errorPrinter(errors)
        -- return "dm"
    elseif dmsTest(args[2], args[4]) then
        -- d logic
        local dResult, errors = parseD(args)
        return specPrinter(args, dResult ) .. " " .. errorPrinter(errors)
        -- return "d"
    end
    -- Error
    return errorPrinter( {{"formatTest", "Unknown argument format"}} )
end

function convert_dec2dms_d(coordinate)
    local d = coordinates.precision.round( coordinate, 0) .. "°"
    return d .. ""
end

function convert_dec2dms_dm(coordinate)
    -- {{#expr:{{{1}}} mod 360}}°{{padleft:{{#expr:({{{1}}} * 600 round 0) mod 600 / 10 round 0}}|2|0}}′
    local d = math.floor(coordinate % 360) .."°"
    local m = string.format( "%02d′", coordinates.precision.round( coordinates.precision.round(coordinate * 600, 0) % 600 / 10, 0) )
    
    return d .. m
end

function convert_dec2dms_dms(coordinate)
    --{{#expr:(((({{{1|0}}}) * 3600) round 0) / 3600) mod 360}}°{{padleft:{{#expr:(((3600 * ({{{1|0}}})) round 0) / 60) mod 60}}|2|0}}′{{padleft:{{#expr:((360000 * ({{{1|0}}})) round -2) mod 6000 div 100}}|2|0}}″
    local d = math.floor(coordinate % 360) .. "°"
    local m = string.format( "%02d′", math.floor(60 * coordinate) % 60 )
    local s = string.format( "%02d″", (coordinates.precision.round(360000 * coordinate, -2) % 6000) / 100 )

    return d .. m .. s
end

--- Convert a latitude or longitude to the DMS format
function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
    -- {{Coord/dec2dms/{{{4}}}|{{#ifexpr:{{{1}}} >= 0||-}}{{{1}}}}}{{#ifexpr:{{{1}}} >= 0|{{{2}}}|{{{3}}}}}
    local coord = tonumber(coordinate) or 0
    local postfix
    if coord >= 0 then
        postfix = firstPostfix
    else
        postfix = secondPostfix
    end

    if precision == "dms" then
        return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
    elseif precision == "dm" then
        return convert_dec2dms_dm( math.abs( coord ) ) .. postfix;
    elseif precision == "d" then
        return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
    end
    
    -- return "" .. globalFrame:expandTemplate{ title = 'coord/dec2dms', args = {coordinate, firstPostfix, secondPostfix, precision}}
end

--- Convert DMS into a N or E decimal coordinate
-- @param coordinate direction. either "N" "S" "E" or "W"
-- @param degrees: string or number
-- @param minutes: string or number
-- @param seconds: string or number
-- @return a number with the N or E normalized decimal coordinate of the input
function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
    local degrees = tonumber(degrees_str) or 0
    local minutes = tonumber(minutes_str) or 0
    local seconds = tonumber(seconds_str) or 0
    -- {{#expr:{{#switch:{{{1}}}|N|E=1|S|W=-1}}*({{{2|0}}}+({{{3|0}}}+{{{4|0}}}/60)/60) round {{{precdec|{{#if:{{{4|}}}|5|{{#if:{{{3|}}}|3|0}}}}+{{precision1|{{{4|{{{3|{{{2}}}}}}}}}}}}}}}} 
    
    local factor
    if direction == "N" or direction == "E" then
        factor = 1
    else
        factor = -1
    end
    
    local precision = 0
    if not isEmpty(seconds_str) then
        precision = 5 + coordinates.precision.prec1(seconds_str)
    elseif not isEmpty(minutes_str) then
        precision = 3 + coordinates.precision.prec1(minutes_str)
    else
        precision = coordinates.precision.prec1(degrees_str)
    end
    
    -- nil -> 0
    local decimal = factor * (degrees+(minutes+seconds/60)/60) 
    return string.format( "%." .. precision .. "f", decimal ) -- not tonumber since this whole thing is string based.
    
    --return "" .. globalFrame:expandTemplate{ title = 'coord/dms2dec', args = {direction, degrees, minutes, seconds}}
end

--- TODO not yet in use
function validateDegreesLatitude(degrees)
    if 0+tonumber(degrees) > 90 then
        return "latd>90"
    end
    if 0+tonumber(degrees) < -90 then
        return "latd<-90"
    end
    return true
end

--- TODO not yet in use
function validateDegreesLongtitude(degrees)
    if 0+tonumber(degrees) >= 360 then
        return "longd>=360"
    end
    if 0+tonumber(degrees) <= -360 then
        return "longd<=-360"
    end
    return true
end

--- TODO not yet in use
function validateMinutes(minutes)
    if 0+tonumber(minutes) >= 60 then
        return "m>=60"
    end
    if 0+tonumber(minutes) < 0 then
        return "m<0"
    end
    return true
end

--- TODO not yet in use
function validateSeconds(seconds)
    if 0+tonumber(seconds) >= 60 then
        return "s>=60"
    end
    if 0+tonumber(seconds) < 0 then
        return "s<0"
    end
    return true
end

--- The display function we exposed to Module:Coordinates
function coordinates.input(frame)
    globalFrame = frame;
    return formatTest(frame.args)
end

--- The dec2dms function exposed to Module:Coordinates
function coordinates.dec2dms(frame)
    globalFrame = frame
    local coordinate = frame.args[1]
    local firstPostfix = frame.args[2]
    local secondPostfix = frame.args[3]
    local precision = frame.args[4]

    return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
end

--- The dec2dms function exposed to Module:Coordinates
function coordinates.dms2dec(frame)
    globalFrame = frame
    local direction = frame.args[1]
    local degrees = frame.args[2]
    local minutes = frame.args[3]
    local seconds = frame.args[4]

    return convert_dms2dec(direction, degrees, minutes, seconds)
end

--- This is used by {{coord}}.
function coordinates.coord(frame)
    globalFrame = frame
    local pframe = frame:getParent()
    local args = pframe.args
    local config = frame.args
    for i=1,10 do 
        if ( nil == args[i] ) then args[i] = "" end 
    end
    local contents = formatTest(args)
    local Notes = args.notes or ""
    local Display = args.display or "inline"
    local text
    if ( "title" ~= Display ) then
        text = displayinline(contents)
    else
        text = displaytitle(contents)
    end
    return text .. Notes
end

return coordinates