Jump to content

Module:Geography

From WhiskerWiki

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

-- ================================================================================
-- Module dependencies
-- ================================================================================
local getArgs = require('Module:Arguments').getArgs -- for processing arguments
local ibf = require("Module:Infobox_functions")
local json = require("Module:JSON_pipeline")



-- ================================================================================
-- Main table
-- ================================================================================
local p = {}




-- ================================================================================
-- Resolve a County/State highlight-map filename on Commons
--
-- The naive "Map of {state} highlighting {county}.svg" guess (built straight
-- from the Census NAMELSAD-derived page title) is correct for the large
-- majority of counties, but Commons' actual file names diverge in a couple
-- of systematic, checkable ways -- confirmed against Category:Pages_with_
-- broken_file_links (2026-09-08): every failure fell into exactly these
-- buckets:
--   - Census's NAMELSAD lowercases the "city" in independent cities (e.g.
--     "Richmond city, Virginia"); Commons always capitalizes it ("...
--     highlighting Richmond City.svg"). Covers all VA independent cities
--     plus Baltimore city, MD.
--   - Commons spells "St." out as "Saint" (St. Croix/Francois/Joseph/Louis
--     counties, and combined with the city-capitalization rule for
--     St. Louis *city* -> "Saint Louis City.svg").
--   - A small number of genuine one-offs where Commons' name doesn't derive
--     from the Census name by any rule at all (e.g. Skagway Municipality,
--     Alaska -> "Skagway City and Borough.svg", matching the same kind of
--     Alaska borough-naming quirk as Yakutat from the geo QID work) --
--     these get a literal override, not a transform.
--
-- Checks candidates via mw.title.new(...).exists rather than guessing, so it
-- self-heals if Commons ever renames a file -- no periodic re-verification
-- step needed the way the R-side geo QID crosswalk needs one. Returns nil
-- (not an error, not a broken-link placeholder) if nothing matches, so the
-- caller can just skip rendering the image.
-- ================================================================================
local county_map_file_overrides = {
    ["Skagway Municipality, Alaska"] = "Map of Alaska highlighting Skagway City and Borough.svg",
}

local function resolve_county_map_file(title, state, county)
    if county_map_file_overrides[title] then
        return county_map_file_overrides[title]
    end

    if not state or not county then
        return nil
    end

    local candidates = { "Map of " .. state .. " highlighting " .. county .. ".svg" }

    local city_base = county:match("^(.+) city$")
    if city_base then
        table.insert(candidates, "Map of " .. state .. " highlighting " .. city_base .. " City.svg")
    end

    local saint_rest = county:match("^St%. (.+)$")
    if saint_rest then
        table.insert(candidates, "Map of " .. state .. " highlighting Saint " .. saint_rest .. ".svg")
    end

    -- Both transforms at once (e.g. "St. Louis city" -> "Saint Louis City")
    if city_base then
        local city_saint_rest = city_base:match("^St%. (.+)$")
        if city_saint_rest then
            table.insert(candidates, "Map of " .. state .. " highlighting Saint " .. city_saint_rest .. " City.svg")
        end
    end

    for _, candidate in ipairs(candidates) do
        local file_title = mw.title.new("File:" .. candidate)
        -- file_title.exists only checks for a LOCAL page at that title --
        -- always false for a Commons-hosted file with no local description
        -- page, regardless of whether the file itself is real. file_title.file
        -- is the Scribunto object that actually resolves through the file
        -- repo (including foreign/Commons repos) -- same accessor
        -- Module:Multiple image (standard Wikipedia core module) uses to
        -- check file existence/dimensions.
        local file_obj = file_title and file_title.file
        if file_obj and file_obj.exists then
            return candidate
        end
    end

    return nil
end




-- ================================================================================
-- County infobox
-- ================================================================================
function p.county_infobox(frame)
    local args = getArgs(frame)
    local title = mw.title.getCurrentTitle().text
    local focal_county = title


    -- Extract naked state and county names from the title
    local function split_on_comma(s)
    	local before = s:match("^(.-),") or s
    	local after  = s:match(",%s*(.+)$") or nil
    	return before, after
    end
    local county, state = split_on_comma(title)

    -- ========================================
    -- Map image management for country and administrative unit
    -- ========================================
	local image = resolve_county_map_file(title, state, county)

    
    -- ========================================
    -- UI / Theming
    -- ========================================
    local html_theme_class = "-base"  -- or "-species" if that's the standard for geography
    local subtitle = state and ("Location within " .. state) or ""

    -- ========================================
    -- Build infobox HTML
    -- ========================================
    local root = ""
    -- `county` (e.g. "Crawford County"), not the full `title` (e.g.
    -- "Crawford County, Wisconsin") -- the state already shows in `subtitle`
    -- ("Location within Wisconsin"), so using the full title here would just
    -- repeat it.
    root = ibf.create_infobox(root, county, subtitle, html_theme_class)

    -- Add image if available
    if image then
        local image_options = {
            link = state,     -- clicking the image links to the state
            caption = ""
        }
        root = ibf.add_image(root, image, html_theme_class, image_options)
    end

    return root
end









-- ================================================================================
-- State infobox
-- ================================================================================
function p.state_infobox(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	local subtitle = "Location within the USA"

    -- ========================================
    -- Cargo query
    -- ========================================    
	-- cargo query to find state metadata for focal geo 
	local tables = ""
	local fields = ""
	local cargo_args = {}
	
    -- ========================================
    -- Map image management for country and administrative unit
    -- ========================================	
	local image = title .. " in United States.svg" -- generic MediaWiki Commons formatting for State map images
	
	
    -- ========================================
    -- Create html output for infobox
    -- ========================================	
	-- Begin creating the html table infobox template
	local html_theme_class = "-species"
	local root = ""
	root = ibf.create_infobox(root, title, subtitle, html_theme_class)	
	root = ibf.add_image(root, image, html_theme_class)
	
    return root
    
end




-- get_county_list_for_state() lives further down, after resolve_geo_qid()'s
-- definition -- it depends on that local function, which must come first.












-- ================================================================================
-- Resolve a geography page title to its QID
--
-- Cold-start lookup against JSON:Title_to_geo_QID_map (see
-- ww_03d_title_to_geo_qid_map.R), used by get_species_list_for_geography()
-- below. Returns (qid, err) -- err is nil on success.
-- ================================================================================
local function resolve_geo_qid(title)
    -- load_dataset_multi() (not the simpler json.lookup()) because this
    -- dataset can be chunked via a "_index" routing page once it grows past
    -- its char_limit (build_json_pages_from_structure()'s convention) --
    -- json.lookup() only recognizes a "_meta"-suffixed routing page, a
    -- different chunking convention from a different R-side builder, and
    -- silently returns nil against this one instead of erroring. See the
    -- identical fix on JSON:Geography_to_species below -- that dataset is
    -- already chunked in production (18 pages), which is exactly how this
    -- was caught: it silently broke there first.
    local records, fields = json.load_dataset_multi("JSON:Title_to_geo_QID_map")
    local raw = records and json.get(records, title)
    local record = raw and json.zip(fields, raw)
    local qid = record and record.QID

    if not qid or qid == "" then
        return nil, "No geography QID found for '" .. title .. "'."
    end

    return qid, nil
end




-- ================================================================================
-- Species list for a State/County page, by QID
--
-- Replaces the old baked-in-wikitext species list on Data:State-*/
-- Data:County-* (see ww_03f_geo_species_json.R's header for why): resolves
-- the current page's QID, looks up its record in JSON:Geography_to_species,
-- and renders the same "===[[Rodentia]]===" / "===[[Eulipotyphla]]==="
-- bulleted-list wikitext shape the baked version used to produce, so the
-- rendered page looks the same -- only the mechanism changed.
-- ================================================================================
function p.get_species_list_for_geography(frame)
    local title = mw.title.getCurrentTitle().text

    local qid, qid_err = resolve_geo_qid(title)
    if qid_err then return "" end -- no QID yet (e.g. still-pending ledger entry) -- render nothing rather than an error on a live data page

    -- load_dataset_multi(), not json.lookup() -- see resolve_geo_qid()'s
    -- comment above. This dataset is confirmed chunked live (18 pages via
    -- JSON:Geography_to_species_index), which is exactly the case
    -- json.lookup() doesn't handle.
    local records, fields = json.load_dataset_multi("JSON:Geography_to_species")
    local raw = records and json.get(records, qid)
    local record = raw and json.zip(fields, raw)
    if not record then return "" end

    local function render_section(heading, species_list)
        if type(species_list) ~= "table" or #species_list == 0 then
            return ""
        end
        local sorted = {}
        for _, v in ipairs(species_list) do table.insert(sorted, v) end
        table.sort(sorted)

        local lines = { "===[[" .. heading .. "]]===" }
        for _, species_link in ipairs(sorted) do
            table.insert(lines, "* " .. species_link)
        end
        return table.concat(lines, "\n")
    end

    local sections = {}
    local rodentia_section = render_section("Rodentia", record.Rodentia)
    local eulipotyphla_section = render_section("Eulipotyphla", record.Eulipotyphla)
    if rodentia_section ~= "" then table.insert(sections, rodentia_section) end
    if eulipotyphla_section ~= "" then table.insert(sections, eulipotyphla_section) end

    return table.concat(sections, "\n")
end




-- ================================================================================
-- Get county lists for state, by QID
--
-- Replaces the old Cargo-table query (Template:State's #cargo_store /
-- get_county_list_for_state() querying the "State" table by State_name --
-- a name-based lookup, exactly the fragile pattern the QID rosetta stone
-- project exists to eliminate). The State Cargo table's only real consumer
-- was this function, and it's not crowd-sourced content -- it's geography
-- scaffolding the wiki's structure depends on -- so there's no reason to
-- keep a whole Cargo table/Data:State-* page around just to serve it.
-- Resolves the current page's QID and looks up County/County_state in the
-- same JSON:Geography_to_species dataset the species lists above use (see
-- ww_03f_geo_species_json.R) -- those two fields are populated for state
-- QIDs only, empty lists for county QIDs.
-- ================================================================================
function p.get_county_list_for_state(frame)
    local title = mw.title.getCurrentTitle().text

    local qid, qid_err = resolve_geo_qid(title)
    if qid_err then
        return "<noinclude><!-- No counties found for state '" .. title .. "': " .. qid_err .. " --></noinclude>"
    end

    local records, fields = json.load_dataset_multi("JSON:Geography_to_species")
    local raw = records and json.get(records, qid)
    local record = raw and json.zip(fields, raw)

    if not record or type(record.County) ~= "table" or #record.County == 0 then
        return "<noinclude><!-- County list empty for state '" .. title .. "' --></noinclude>"
    end

    local counties      = record.County
    local county_states = record.County_state or {}

    local list_items = {}

    for i, display_raw in ipairs(counties) do
        local display = mw.text.trim(display_raw or "")

        -- Strip ", Arizona" (or any ", <something>") from the display text.
        -- Example: "Maricopa County, Arizona" -> "Maricopa County"
        display = display:gsub("%s*,.*$", "")

        local target = mw.text.trim(county_states[i] or "")

        if display ~= "" then
            if target ~= "" then
                table.insert(list_items, string.format("* [[%s|%s]]", target, display))
            else
                -- Fallback if no page stored
                table.insert(list_items, string.format("* %s", display))
            end
        end
    end

    -- Leading/trailing newlines help when nested inside templates
    return "\n" .. table.concat(list_items, "\n") .. "\n"
end




return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.