Jump to content

Module:Sandbox/RedWolf

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by RedWolf (talk | contribs) at 01:11, 8 March 2025 (changed notes column to custom). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
--[[
This module builds a wiki table that lists information pertinent to mountains. This module accomodates up to 996 lines
of mountains where each line consists of the elevation, wiki-linked name and optional notes information. This
module uses the wiki-linked name to find its corresponding Wikidata item (eid) and then retrieve the following
information from Wikidata: elevation, prominence, isolation, range, first ascent, coordinates
]]
local p = {}
--local wd = require('Module:Wd')
local Titled_coords = require('Module:Titled_coords')

-- Wikidata properties
local WD_PROPERTIES = {
    elevation  = "P2044",
    prominence = "P2660",
    mtn_range  = "P4552",
    coords     = "P625",
    sig_event  = "P793",
    pt_in_time = "P585",
    isolation  = "P2659"
}

-- Table column titles for easier translation
local COL_TITLES = {
    rank   = "Rank",
    mtn    = "Mountain / Peak",
    elev   = "Elevation",
    prom   = "Prominence",
    isoltn = "Isolation",
    subrng = "Subrange",
    fa     = "FA",
    coords = "Coordinates",
    custom = "Notes"
}

-- ** Runtime Option names **
local OPT_NAMES = {
    RANK   = "rank",
    PROM   = "prom",
    ISOLTN = "isolation",
    RANGE  = "range",
    FA     = "fa",
    COORDS = "coords",
    TCOORDS= "tcoords",
    CUSTOM1= "custom1",
    CUSTOM2= "custom2",
    DEBUG  = "debug"
}

-- Runtime options about what information to display
local Options  = { debug = false, test = false, showFA = false, showCoord = false, showTCoord = false,
                   showRank = false, showRange = false, showIsoltn = false,
                   showCustom1 = false, showCustom2 = false, customTitle = COL_TITLES.custom }

-- Information about a mountain from Module arguments and Wikidata.
local Mountain = { rank, page, name, eid, elevation, elevation_wd = 0,
                   prominence, range = "", isolation = "", fa = "", coords, custom }

local QID_FIRST_ASCENT = "Q1194369"
local FA_UNKNOWN = "Unk"
local FA_ERROR   = "????"
local FS = "^"    -- field separator
local NBSP = " "
local COL_TITLE_SEP = ":"     -- do not use "="
local OPTIONS_SEP = ","
local NEWLINE = "\n"
local BR_NEWLINE ="<br/>"..NEWLINE
local errors, dbgout

local function debug(msg)
    mw.log(msg)
    dbgout = dbgout .. msg .. BR_NEWLINE
end

local function debugv(var, value)
    local s = var .. "=" .. value
    mw.log(s)
    dbgout = dbgout .. s .. BR_NEWLINE
end

local function addError(msg)
    errors = errors .. msg .. BR_NEWLINE
    debug(msg)
end

local function errorFont(value)
    return "<font color=red>" .. value .. "</font>"
end

-- Split a string based on a separator
local function split(istring, sep)
    -- if sep is null, use default
    if sep == nil then sep = FS end

    local t = {}
    for str in string.gmatch(istring, '([^'..sep..']+)') do
        table.insert(t, str)
    end
    return t
end

-- Strip newline character at end --
local function stripNewline(value)
    local n = string.find(value, NEWLINE)
    if n then
        return string.sub(value, 1, n-1)
    end
    return value
end

-- Extract raw elevation/prominence value
local function extractRaw(value)
    local i, i1, i2, n

    if value == nil then return -1 end
    i = string.find(value, " metre")
    if i then
        -- remove thousands separator while we are it
        return string.sub(value, 1, i-1):gsub(',', '')
    end
    i = string.find(value, "feet") 
    if i then
        return string.sub(value, 1, i-1):gsub(',', '')
    end
    return -1
end

-- Call {{elevation_cells} to format the values
local function getElevationCells(frame, elev, unit) --23
    if frame.expandTemplate then
        return frame:expandTemplate{title='elevation_cells', args= { elev, unit}}
    end

    return "{{elevation_cells|" .. elev .. "|" .. unit .. "}}"
end

local function stripBrackets(value)
    local stripped = value;

    -- strip leading brackets if found
    local n = string.find(stripped, "%[%[")
    if n then
        stripped = string.sub(stripped, 3)
    end

    -- strip trailing brackets if found
    n = string.find(stripped, "%]%]")
    if n then
        stripped = string.sub(stripped,1,n-1)
    end

    if stripped then return stripped
    else return value end
end

local function getPage(name)
    local parts = split(name,"|")
    local page = parts[1]
    local n = string.find(page, "%[%[")
    if n then
        page = string.sub(page,3)
    end
    n = string.find(page, "%]%]")
    if n then
        page = string.sub(page,1,n-1)
    end
    return page
end

local function getPageTitle(name)
    local title

    local parts = split(name,"|")
    if parts[2] then
        title = stripBrackets(parts[2])
    else
        title = stripBrackets(parts[1])
    end
    mw.log("name="..name..";title="..title)
    return title
end

--[[
    Retrieve a entity's property value from Wikidata. Unfortunately
    the interface only supports one property at a time.
]]
local function getWD(frame, eid, name, flag)
    if not frame.preprocess then
        return "{{Wikidata|property|" .. name .. "|eid=" .. eid .. "}}"
    end

    local args = "property|"
    if flag then args = args .. flag .. "|" end
    args = args .. name .. "|eid=" .. eid 
    local invoke = "{{#invoke:Wd|" .. args .. "}}"
    local value = frame:preprocess(invoke)
    mw.log(invoke .. " => " .. value)
    return value
end

-- Get a wiki linked property value
local function getWDLinked(frame, eid, name)
    return getWD(frame, eid, name, "linked")
end

-- Get the raw value of a property value
local function getWDRaw(frame, eid, name)
    return getWD(frame, eid, name, "raw")
end

-- Get an entity's property from Wikidata
local function getWDProperty(frame, eid, name, linked)
    debug("eid=" .. eid .. " name=" .. name)

--[[    local stmts = mw.wikibase.getBestStatements(eid, name)
    if stmts ~= nil then
        mw.logObject(stmts)
        for i=1,#stmts do
            mw.log("stmts["..i.."]="..stmts[i]);
        end
    end ]]

    -- call Module:Wd using template syntax
    if frame.preprocess then
        local args = "property|"
        if linked then args = args .. "linked|" end
        args = args .. name .. "|eid=" .. eid 
        local invoke = "{{#invoke:Wd|" .. args .. "}}"
        local value = frame:preprocess(invoke)
        debug(invoke .. " => " .. value)
        return value
    end
    return "{{Wikidata|property|" .. name .. "|eid=" .. eid .. "}}"
end
--    if frame.expandTemplate then
--        local args = { ['1'] = name, ['page'] = page }
--        return wd._property({eid, args})
--        local args = { ['1'] = 'property', ['2'] = name, ['page'] = page }
--        return frame:expandTemplate{title='Wikidata', args= args}

-- get location coordinates
local function getCoords(frame, eid)
    local wdCoords = getWDLinked(frame, eid, WD_PROPERTIES.coords)
debugv("wdCoords", wdCoords)
    if Options.test then
        local stripped = string.gsub(wdCoords, "`UNIQ%-%-templatestyles%-%d+%-QINU`", "")
        debugv("stripped", stripped)
        return stripped
    end
    return wdCoords
end

-- Call Module:Titled_coords to get titled coordinates
local function getTitledCoords(frame, eid, page)
    local title = getPageTitle(page)
    local raw_coords = getWDRaw(frame, eid, WD_PROPERTIES.coords)
    local fmt_coords = Titled_coords.build(raw_coords, title, "")
    debug(fmt_coords .. " => " .. fmt_coords)
    return fmt_coords
end

local function getElevation(frame, eid)
    value = getWD(frame, eid, WD_PROPERTIES.elevation, nil)
    return extractRaw(value)
end

local function getFirstAscent(frame, eid)
    if frame.preprocess == nil then return FA_ERROR end

    local names = WD_PROPERTIES.sig_event .. "|" .. QID_FIRST_ASCENT .. "|" .. WD_PROPERTIES.pt_in_time
    local value = frame:preprocess("{{#invoke:Wd|property|qualifier|" .. names .. "|eid=" .. eid .. "}}")
    debugv("FA value", value)
    if value == "" then
        debugv("No FA found for eid", eid)
        return FA_UNKNOWN
    end

    -- FA deliberately set to unknown value
    if string.find(value, "(unknown)") then
        debugv("FA unknown for eid", eid)
        return FA_UNKNOWN
    end

    -- Find date such as: 10 July 1913
    local i1, i2 = string.find(value, "%(%d+%s%a*%s%d%d%d%d%)")
    if i1 then
        --mw.log("i1="..i1)
        local date = string.sub(value, i1+1, i2-1)
        debugv("FA date", date)
        local len  = string.len(date)
        local year = string.sub(date, len-4, len)
        return year
    end

    -- Find date with just the year; e.g. (2025)
    i1, i2 = string.find(value, "%(%d+%)")
    if i1 then
        local year = string.sub(value, i1+1, i2-1)
        return year
    end
    addError("Unknown FA date format for eid " .. eid .. ": " .. value)
    return FA_ERROR
end

-- get prominence from Wikidata
local function getProminence(frame, eid)
    local prom = getWD(frame, eid, WD_PROPERTIES.prominence, nil)
    return extractRaw(prom)
end

-- get mountain range from Wikidata. We want it wiki-linked for
-- the first occurrence of it in the output.
local function getRange(frame, eid)
    return getWDLinked(frame, eid, WD_PROPERTIES.mtn_range)
end

-- generate table header
local function header(options, unit)
    local unit_1, unit_2

    if unit == nil or unit == "" then
        unit = 'm'
    end

    if unit == 'm' then unit_1 = 'm'; unit_2 = 'ft'
    else unit_1 = 'ft'; unit_2 = 'm'
    end

    local s = '{| class="wikitable sortable"\n!'

    if options.showRank  then s = s .. " align=\"left\" rowspan=2|" ..COL_TITLES.rank .. "||" end
    s = s .. "rowspan=2|" .. COL_TITLES.mtn .. "||colspan=2|" .. COL_TITLES.elev .. "||colspan=2| " .. COL_TITLES.prom
    if options.showRange then s = s .. "||rowspan=2|" .. COL_TITLES.subrng end
    if options.showFA    then s = s .. "||rowspan=2|" .. COL_TITLES.fa     end
    if options.showCoord or options.showTCoord then s = s .. "||rowspan=2|" .. COL_TITLES.coords end
    if options.showCustom2 then s = s .. "||rowspan=2| " .. options.customTitle end

    s = s .. "\n|-\n"
    s = s .. '!' .. unit_1 .. '||' .. unit_2 .. '||' .. unit_1 .. '||' .. unit_2 .. "\n"

    return s
end

local function finish()
    return "|}"
end

local function handleCustomOption(n, options, columnTitle)
    if columnTitle then
        options.customTitle = columnTitle
        debugv("customTitle", options.customTitle)
     end
     if n == 1 then options.showCustom1 = true else options.showCustom2 = true end
end

--[[ Process run options
     rank    - show ranking
     elev    - show elevation
     prom    - show prominence
     range   - show mountain range or subrange from WD
     fa      - show first ascent (year only) from WD
     coords  - show coordinates from WD
     tcoords - show titled coordinates from WD
     notes   - show notes
     debug   - generate debug information
]]
local function processOptions(runOptions)
    local o = Options
    if runOptions == nil then return o end
    runOptions = stripNewline(runOptions)
    debugv("runOptions", runOptions)

    local parts = split(runOptions, OPTIONS_SEP)
    for i=1,#parts do
        option = parts[i]
        debug("option:" .. option)
        local colTitle
        local n = string.find(option, COL_TITLE_SEP)
        if n then
            colTitle = string.sub(option, n+1)
            debugv("colTitle", colTitle)
            option = string.sub(option, 1, n-1)
         end

        local valid_option = true
        if option     == OPT_NAMES.RANK    then o.showRank   = true
        elseif option == OPT_NAMES.FA      then o.showFA     = true
        elseif option == OPT_NAMES.PROM    then o.showProm   = true
        elseif option == OPT_NAMES.ISOLTN  then o.showIsoltn = true
        elseif option == OPT_NAMES.COORDS  then o.showCoord  = true
        elseif option == OPT_NAMES.TCOORDS then o.showTCoord = true
        elseif option == OPT_NAMES.RANGE   then o.showRange  = true
        elseif option == OPT_NAMES.CUSTOM1 then
            handleCustomOption(1, o, colTitle)
        elseif option == OPT_NAMES.CUSTOM2 then
            handleCustomOption(2, o, colTitle)
        elseif option == OPT_NAMES.DEBUG   then o.debug = true
        else
            addError("Unknown option: " .. option)
            valid_option = false
        end
    end

    return o
end

local function processLine(frame, options, line)
    local parts, n, name, elev, page, notes, has_notes

    -- argument contains elevation, page link and notes
    parts = split(line, FS)
    if #parts == 2 then
        has_notes = false
    elseif #parts < 3 then
        local m = "<br/>Bad format on argument (<nowiki>" .. line .. "</nowiki>) -- skipped"
        errors = errors .. m
        mw.log(m)
        return nil
    else
        has_notes = true
    end

    local mtn = Mountain

    elev = parts[1]       -- "m" or "ft"
    name = parts[2]       -- wiki-linked name
    if name then
        page = getPage(name)
        debug("name = " .. name .. ";page = " .. page)
    else
        debug("name is null")
        return nil
    end

    mtn.name = name
    mtn.page = page
    mtn.eid  = nil
    mtn.elevation = elev;
    mtn.elevation_wd = 0
    mtn.prominence = nil
    mtn.range = ""
    mtn.fa = ""

    if options.showCustom1 or options.showCustom2 then
        if has_notes then
            notes = stripNewline(parts[3])
            debugv("notes", notes)
        else
             notes = NBSP
        end
        mtn.custom = notes
    end

    local eid = mw.wikibase.getEntityIdForTitle(page)
    if not eid then
        debug("Cannot find entity id for page " .. page)
        mtn.name = mtn.name .. BR_NEWLINE .. errorFont("Cannot find entity id")
        return mtn
    end
    debug("page="..page .. ",eid=" .. eid)
    mtn.eid = eid

    mtn.elevation_wd = getElevation(frame, eid)
    mtn.prominence   = getProminence(frame, eid)

    if options.showRange then
        mtn.range = getRange(frame, eid)
    end

    if options.showFA then
        mtn.fa    = getFirstAscent(frame, eid)
    end

    if options.showCoord then
        mtn.coords = getCoords(frame, eid)
    end

    if options.showTCoord then
        mtn.coords = getTitledCoords(frame, eid, mtn.name)
    end

    return mtn
end

-- Process a mountain range. Only display the linked range once.
local function processRange(ranges, mtn)
    local found = false;
    local range = mtn.range
    for k,v in pairs(ranges) do
        if v == range then
            found = true; break
         end
    end

    if not found then
        debug("Adding range " .. range)
        table.insert(ranges, range)
    else
        local i1, i2 = string.find(range, "|")
        if i1 then
            local ei = string.len(range) - 2  -- strip ending brackets
            name = string.sub(range, i1+1, ei)
        else
            local len = string.len(range)
            name = string.sub(range, 3, len-2)
        end
        mtn.range = name
    end
end

function p.list(frame)
    local debug_on = false
    local rank_number = 0
    local last_elev = ""
    local same_elev = 0

    errors = ""; dbgout = ""
    --debugv("args[1]", frame.args[1])
    --debugv("args[2]", frame.args[2])
    --debugv("args[3]", frame.args[3])
    local unit  = frame.args[1]
    local options = processOptions(frame.args[2])
    if options.showCoord and options.showTCoord then
        return "<p>" .. errorFont("Can only specify one of coords or tcoords") .. "</p>"
    end

    local output = header(options, unit)
    local ranges = {}

    for i=3,999,1 do
        local prom_cells
        local line = frame.args[i]
        if line == nil then break end

        debugv("line", line)
        local mtn = processLine(frame, options, line)
        if mtn then   -- only do if no error
            if options.showRank then
                if last_elev ~= mtn.elevation then
                    rank_number = rank_number + 1 + same_elev
                    mtn.rank = rank_number
                    last_elev = mtn.elevation
                    same_elev = 0
                else
                    same_elev = same_elev + 1
                end
            end

            if options.showRange and mtn.eid then
                processRange(ranges, mtn)
            end

            local elev_cells = getElevationCells(frame, mtn.elevation, unit)
            local prom = mtn.prominence
            if prom ~= nil and prom ~= -1 and prom ~= "" then
                prom_cells = getElevationCells(frame, prom, unit)
            else
                prom_cells = "&nbsp;||&nbsp;"
            end

            debug("elev=" .. mtn.elevation ..";elev_wd="..mtn.elevation_wd)
            if options.showNotes and mtn.elevation_wd ~= 0 and mtn.elevation ~= mtn.elevation_wd then
                local mm = "<br/><font color=green>Local/WD elevations mismatch: " .. "\"" .. mtn.elevation .. "\"".. "/\"" .. mtn.elevation_wd.. "\"</font>"
                mtn.notes = mtn.notes .. mm
            end

            local s = "|-\n|"
            if options.showRank then s = s .. "align=center|" .. mtn.rank .. "||" end
            s = s .. mtn.name .. '\n|' .. elev_cells .. '\n|' .. prom_cells .. '\n'
            if options.showRange then s = s .. '|' .. mtn.range  .. NEWLINE end
            if options.showFA    then s = s .. '|' .. mtn.fa     .. NEWLINE end
            if options.showCustom2 then s = s .. '|' .. mtn.custom  .. NEWLINE end
            if options.showCoord or options.showTCoord then s = s .. '|' .. mtn.coords .. NEWLINE end
            output = output .. s
         end
    end -- for

    output = output .. finish()
    if debug_on then
        output = "</br><nowiki>"..output.."</nowiki>"..NEWLINE..output
    end

    if string.len(errors) > 0 then
        output = output .. "<font color=red>" .. errors .. "</font>"
    end
    if options.debug and string.len(dbgout) > 0 then
        output = output .. "<br/><font color=green>Debug output<br/>\n" .. dbgout .. "</font>"
    end

    return output
end

--[[
Test via Preview Window Debug console
  print(p.test())
]]
function p.test()
    local frame = mw.getCurrentFrame()
--    if frame then mw.logObject(frame) end
--    local test_data = { {["e"]="3954",["p"]="[[Mount Robson]]",
--                                ["n"]="Highest point in the Canadian Rockies<ref name=robson/>"},
--                        {["e"]="3747",["p"]="[[Mount Columbia (Canada)|Mount Columbia]]",
--                                ["n"]="Highest point in [[Alberta]]<ref name=columbia/>"}
--                    }
    frame.args = {}
    frame.args[1]  = 'm'
    frame.args[2]  = "debug,rank,range,fa,tcoords,custom2:Additional info"

    frame.args[3]  = "3954"..FS.."[[Mount Robson]]"..FS.."Highest point in the Canadian Rockies<ref name=robson/>"
    frame.args[4]  = "3310"..FS.."[[Mount Vaux]]"..FS.."Ottertail Range"
    frame.args[5]  = "3204"..FS.."[[Ghost Mountain (Chaba Icefield)|Ghost Mountain]]"..FS.."<ref name=ghost/>"
    frame.args[6]  = "2433"..FS.."[[Saddle Mountain (Alberta)|Saddle Mountain]]"..FS
--[===[
    frame.args[3]  = "3954"..FS.."[[Mount Robson]]"..FS.."Highest point in the Canadian Rockies<ref name=robson/>"
    frame.args[4]  = "3747"..FS.."[[Mount Columbia (Canada)|Mount Columbia]]"..FS.."Highest point in [[Alberta]]<ref name=columbia/>"
    frame.args[5]  = "3731"..FS.."[[North Twin Peak]]"..FS.."Highest peak of The Twins Massif"
    frame.args[6]  = "3648"..FS.."[[Mount Clemenceau]]"..FS.."Named for [[Georges Clemenceau]], premier of France during WWI"
    frame.args[7]  = "3619"..FS.."[[Mount Alberta]]"..FS.."Most difficult +11,000 climbing objective<ref name=alberta/>"
    frame.args[8]  = "3618"..FS.."[[Mount Assiniboine]]"..FS.."Highest point in the Southern Rockies<ref name=assiniboine/>"
    frame.args[9]  = "3612"..FS.."[[Mount Forbes]]"..FS.."Highest point within the confines of [[Banff National Park|Banff Nat'l Park]]<ref name=forbes/>"
    frame.args[10] = "3567"..FS.."[[Mount Goodsir]]"..FS.."Two major summits: South Tower and North Tower (lowest)"
    frame.args[11] = "3556"..FS.."[[South Twin Peak]]"..FS.."Lowest peak of The Twins Massif"
    frame.args[12] = "3543"..FS.."[[Mount Temple (Alberta)|Mount Temple]]"..FS.."Highest point near [[Lake Louise, Alberta|Lake Louise]]<ref name=temple/>"
    frame.args[13] = "3425"..FS.."[[Resplendent Mountain]]"..FS.."tbd"
    frame.args[14] = "3204"..FS.."[[Ghost Mountain (Park Ranges)|Ghost Mountain]]"..FS.."<ref name=ghost/>"
--]===]
--    frame.args[7] = ""
--    frame.args[8] = "&nbsp;"
--[[
    local fi = 3
    for i=1,#test_data do
        frame.args[fi] = test_data[i]["e"] .. FS .. test_data[i]["l"] .. FS .. test_data[i]["n"]
        fi = fi + 1
    end ]]
    Options.test = true
    return p.list(frame)
end

function p.test2()
    local s = '<a href="/wiki/Rainbow_Range_(Rocky_Mountains)" title="Rainbow Range (Rocky Mountains)">Rainbow Range</a>'
    --local i1,i2 = string.find(s,"%>(.*)%<%/a%>")
    local i1,i2 = string.find(s,"%b><")
    if i1 then 
        mw.log("i1=" .. i1 .. " i2=" .. i2)
        mw.log(string.sub(s,i1+1,i2-1))
    else
        mw.log("not found")
    end
end

function p.test3()
    mw.log(stripBrackets("[[abc]]"))
    mw.log(stripBrackets("def"))
    mw.log(stripBrackets("[[Mount Victoria (Bow Range)|Mount Victoria]]"))
    mw.log("title=" .. getPageTitle("[[Mount Victoria (Bow Range)|Mount Victoria]]"))
end

function p.testR()
    local frame = mw.getCurrentFrame()
    frame.args = {}
    frame.args[1]  = 'm'
    frame.args[2]  = "debug,fa,tcoords,custom2:Easiest route"

    frame.args[3]  = "3543"..FS.."[[Mount Temple (Alberta)|Mount Temple]]"..FS.."Moderate scramble on SW face"
    frame.args[4]  = "3492"..FS.."[[Mount Hungabee|Hungabee Mountain]]"..FS.."[[International Climbing and Mountaineering Federation|UIAA]] III 5.4 on West ridge"
    frame.args[5]  = "3464"..FS.."[[Mount Victoria (Bow Range)|Mount Victoria]]"..FS.."UIAA II on SE ridge, South Summitend"

    Options.test = true
    return p.list(frame)
end

return p

--[[
{| class="wikitable sortable"
|- bgcolor="#ffffcc"
! align="left" rowspan=2|Rank||rowspan=2|Mountain/Peak  ||colspan=2|Elevation ||colspan=2| Prominence ||rowspan=2| Subrange
!rowspan=2| FA ||rowspan=2| Notes ||rowspan=2| References
|-
!m || ft || m || ft
|-
|align=center|1||Mount Robson
|{{elevation_cells|3,959|m}}|{{elevation_cells|2829|m}}||Rainbow Range
|1913||Highest point in the Canadian Rockies|| <ref name=robson/>
]]