Jump to content

Module:Geography

From HopperWiki

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

-- ================================================================================
-- Module dependencies
-- ================================================================================
local cargo = mw.ext.cargo
local html = mw.html.create()
local getArgs = require("Module:Arguments").getArgs -- for processing arguments

-- Utilities
local str = require("Module:String utilities")
local tbl = require("Module:Table utilities")
local tax = require("Module:Taxonomy utilities")
local out = require("Module:Wiki output utilities")
local cfmt = require("Module:Cargo format utilities")
local sc = str.sc -- Short cut for a safe Lua concatenation function

-- Galleries
local gallery = require("Module:Gallery")

-- JSON
local json = require("Module:JSON_pipeline")



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



-- ================================================================
-- Helper: Format species name with italics
--
-- If name has parentheses (e.g., "Common name (Binomial name)"):
--   Only italicize what's inside parentheses
-- If no parentheses (e.g., "Binomial name"):
--   Italicize entire name
-- ================================================================
local function format_species_name(text)
    if text:match("%((.-)%)") then
        -- Has parentheses: italicize what's inside INCLUDING closing paren
        return text:gsub("%((.-)%)", "(''%1)''")
    else
        -- No parentheses: italicize entire name
        return "''" .. text .. "''"
    end
end




-- ================================================================
-- Helper: Resolve a geography page title to its QID
--
-- Cold-start lookup used by the pure Geography/Custom_geography hierarchy
-- queries below. Returns qid, err -- err is nil on success, or a wikitext
-- error message to surface directly on failure.
-- ================================================================
local function resolve_geo_qid(title)
    local qid_records, qid_fields, _, err = json.load_dataset("JSON:Title_to_geo_QID_map")
    if err then return nil, "Error loading geography QID map: " .. err end

    local raw_qid = json.get(qid_records, title)
    if not raw_qid then return nil, "No geography QID found for '" .. title .. "'." end

    local qid_record = json.zip(qid_fields, raw_qid)
    local qid = qid_record and qid_record.QID
    if not qid or qid == "" then return nil, "No geography QID found for '" .. title .. "'." end

    return qid, nil
end




-- ================================================================
-- Helper: Look up a Region/Subregion/Intermediate_region's own hierarchy
-- record (its Subregions/Intermediate_regions/Countries children lists) by
-- QID. Unlike the old Cargo-based lookup, this is a "look up my own row"
-- query -- Region/Subregion/Intermediate_region entities have their own row
-- in JSON:Geography_hierarchy_map, keyed by their own QID.
-- Returns record, err -- err is nil on success.
-- ================================================================
local function resolve_geo_hierarchy(qid)
    local hierarchy_records, hierarchy_fields, _, err = json.load_dataset("JSON:Geography_hierarchy_map")
    if err then return nil, "Error loading geography hierarchy map: " .. err end

    local raw = json.get(hierarchy_records, qid)
    if not raw then return nil, nil end -- not every geography QID is a Region/Subregion/Intermediate_region

    return json.zip(hierarchy_fields, raw), nil
end



-- ================================================================
-- Helper: Format a flat list of page names as a sorted, deduplicated,
-- comma-separated list of wiki links -- reuses tbl.key_value_table_to_csv's
-- existing sort/link-wrap/join logic by wrapping each name in the
-- cargo-row shape that function expects.
-- ================================================================
local function format_page_list(names)
    if not names or #names == 0 then return nil end
    local rows = {}
    for _, name in ipairs(names) do
        table.insert(rows, { Name = name })
    end
    return tbl.key_value_table_to_csv(rows, "Name", "page")
end




-- ===========================
-- Species for Geography
--
-- This function queries a set number of select images for species pertaining to a foca geographic unit. To avoid bad photos showing up
-- it only uses the main profile image of each species.
-- ===========================
function p.species_gallery_for_geography(frame)
    local args = getArgs(frame)
    local focal_geography = args.focal_geography or mw.title.getCurrentTitle().text
    local no_results_message = "No results found. If you have expertise or materials on this topic that you are willing to contribute, [[Hopperwiki:About#Who manages HopperWiki?|please get in touch with an administrator]]."
    
    local gallery_header = "= Species gallery =\n\n"
    if args.header == "false" then
    	gallery_header = ""
	end
    
    -- ═══════════════════════════════════════════════════════════
    -- Step 1: Get OTU IDs for focal geography
    -- ═══════════════════════════════════════════════════════════
    local geo_records, geo_fields, _, err = json.load_dataset("JSON:Geography_to_species")
    if err then return gallery_header .. "Error loading geography data: " .. err end

    local raw = json.get(geo_records, focal_geography)
    if not raw then return no_results_message end

    local geo_record = json.zip(geo_fields, raw)
    local otu_ids = geo_record and geo_record.OTU_ID or {}

    if #otu_ids == 0 then return gallery_header end


    -- ═══════════════════════════════════════════════════════════
    -- Step 2: Query Cargo for species images
    -- ═══════════════════════════════════════════════════════════
    local otu_set = {}
    for _, otu_id in ipairs(otu_ids) do
        otu_set[tostring(otu_id)] = true
    end
    
    local otu_id_strings = {}
    for _, otu_id in ipairs(otu_ids) do
        table.insert(otu_id_strings, mw.ustring.format("'%s'", tostring(otu_id)))
    end
    local otu_where = "OTU_ID IN (" .. table.concat(otu_id_strings, ", ") .. ")"

    local cargo_results = cargo.query(
        "Species",
        "OTU_ID, Image",
        {
            where = otu_where .. " AND Image IS NOT NULL AND Image <> ''",
            limit = 500
        }
    )

    local otu_to_image = {}
    if cargo_results then
        for _, row in ipairs(cargo_results) do
            local id = row.OTU_ID and tostring(row.OTU_ID) or nil
            if id and otu_set[id] and row.Image and row.Image ~= "" then
                otu_to_image[id] = row.Image
            end
        end
    end
    
    
    
    -- ═══════════════════════════════════════════════════════════
    -- Step 3: Load OTU→Title map
    -- ═══════════════════════════════════════════════════════════
    local title_records, title_fields, _, err2 = json.load_dataset_multi("JSON:OTU_to_title_map")
    if err2 then return gallery_header .. "Error loading title map: " .. err2 end
    
    
    


    -- ═══════════════════════════════════════════════════════════
    -- Step 4: Convert OTU IDs to page titles (with images only)
    -- ═══════════════════════════════════════════════════════════
    local species_list = {}
    for otu_id, image in pairs(otu_to_image) do
        local entry = json.get(title_records, otu_id)
        local rec = entry and json.zip(title_fields, entry)
        local title = rec and rec.Page_title and rec.Page_title
        if title and title ~= "" then
            table.insert(species_list, {
                name   = title,
                otu_id = otu_id,
                image  = image
            })
        end
    end
    
    if #species_list == 0 then return gallery_header .. no_results_message end
    
    
    
    -- ═══════════════════════════════════════════════════════════
    -- Step 5: Shuffle and format gallery
    -- ═══════════════════════════════════════════════════════════
    math.randomseed(tonumber(os.date("%Y%m%d%H")))
    for i = #species_list, 2, -1 do
        local j = math.random(i)
        species_list[i], species_list[j] = species_list[j], species_list[i]
    end

    local max_images = tonumber(args.max_images)
    if not max_images or max_images < 1 then max_images = 16 end

    local gallery_items = {}
    for _, species in ipairs(species_list) do
        if #gallery_items >= max_images then break end
        local formatted_name = format_species_name(species.name)
        table.insert(gallery_items, {
            image   = species.image,
            link    = species.name,
            caption = "[[" .. species.name .. "|" .. formatted_name .. "]]"
        })
    end

    if #gallery_items > 0 then
        local built_gallery = gallery.build_modern_gallery(gallery_items, {
            heights = "150px",
            perrow  = 4,
            caption = "Highlighted species for " .. focal_geography
        })
        return frame:preprocess(gallery_header .. built_gallery)
    else
        return gallery_header .. no_results_message
    end
end


















    
    
    
    
    
    
    
    
    
    











function p.get_species(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	local no_results_message = "No results found. If you have expertise or materials on this topic that you are willing to contribute, [[Hopperwiki:About#Who manages HopperWiki?|please get in touch with an administrator]]."
	
	
	local collapse_cutoff = args.collapse_cutoff
	-- local cargo_table = "Species"
	-- local fields = "Species"
	-- local cargo_args = {where = "All_geography HOLDS '" .. title .. "'"}
	-- local cargo_results = cargo.query(cargo_table, fields, cargo_args)
 
	-- New inserting JSON query
	local cargo_results = json.find_species_by_geography("JSON:Species to geography", title)
    
    local output = out.bulleted_list(cargo_results, "Species", { Species = { type = "page", italicize = true } })
    
    local output = ""
    for _, row in ipairs(cargo_results) do
        output = output .. "* [[" .. row.Species .. "]]\n"
    end
    
    if #cargo_results > 0 then
        local header = "= Species of locust and grasshoppers in " .. title .. " represented in HopperWiki =\n"
        output = header .. output
        return output
    else
        return "= Species of locust and grasshoppers in " .. title .. " represented in the HopperWiki =\n\n" .. no_results_message
    end
end


















-- ================================================================================
-- Get lists of items at each geographic level for CUSTOM geography
-- ================================================================================
function p.get_children_for_custom_geography(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text

    local qid, qid_err = resolve_geo_qid(title)
    if qid_err then return qid_err end

    local collapse_cutoff = args.collapse_cutoff

    local header = ""
    local output = ""

    -- Get the Cargo row that matches the wiki page the template is on
    local cargo_table = "Custom_geography"
    local fields = "Name, Geography_type, Geographic_children"
    local cargo_args = {
        where = string.format("QID = '%s'", qid)
    }
    local cargo_results = cargo.query(cargo_table, fields, cargo_args)
    
    
    -- Get length of elements for display in the header
    local output_len = str.count_comma_elements(cargo_results[1].Geographic_children)
    
    -- Get correct grammatical preposition for header depending on custom gepography type
    local preposition = ""
    local geo_type = cargo_results[1].Geography_type
    
    if geo_type == "intersecting countries" then
    	preposition = "that intesect with"
	
	elseif geo_type == "superset of countries" or geo_type == "superset of administrative units" then
    	preposition = "within"
	end
    
    
    if #cargo_results > 0 then
    	
    	output = out.display_field_in_bulleted_list{
		    cargo_results = cargo_results,
		    field_name = "Geographic_children",
		    sort_alphabetically = true,
		    page_links = true,
		    delimiter = "~~"
		}
		
        local header = string.format("= Geographic units " .. preposition .. " " .. title .. " (" .. output_len .. ")=\n\n")
        
        if output then
        	return header .. output
    	end
    else
        return "= Geographic units within " .. title .. " =\n\nNo results found."
    end
end







-- ================================================================================
-- Get organizations for geography
-- ================================================================================
function p.get_organizations(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	local no_results_message = "No results found. If you have expertise or materials on this topic that you are willing to contribute, [[Hopperwiki:About#Who manages HopperWiki?|please get in touch with an administrator]]."
	
	
	-- get information from cargo	
    local cargo_table = "Organization"
    local fields = "Organization_name=Name, Acronym, Type, Focus, Focus_keywords, Species_purview"
    local cargo_args = {where = "All_geography HOLDS '" .. title .. "'"}
    local cargo_results = cargo.query(cargo_table, fields, cargo_args)
    
    -- format cargo information in place
	local field_formats = {
	    Name = "page",
	    Acronym = "string",
	    Type = "string",
	    Focus = "list_of_string",
	    Focus_keywords = "list_of_string",
	    Species_purview = "list_of_page",
	}
	local cargo_results_formatted = cfmt.format_cargo_results(cargo_results, field_formats)

    -- control row cutoff for collapsing display
    local collapse_cutoff = tonumber(args.collapse_cutoff)
	local collapse = false
	if #cargo_results_formatted > collapse_cutoff then
		collapse = true
	end


    local display_fields = "Name, Acronym, Type, Focus, Focus_keywords, Species_purview"
    if #cargo_results_formatted > 0 then
    	local header = "= Organizations (" .. #cargo_results_formatted .. ")=\n"
        return out.generate_wiki_table(cargo_results_formatted, header, display_fields, collapse)
    else
        return "= Organizations involved with locust or grasshopper management in " .. title .. " =\n\n" .. no_results_message
    end
end






function p.get_subregions(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	
	-- get args from parent frame
	local rank = tax.geo_field_finder(title, { return_on_missing = "Administrative_unit" }) -- find rank
	local rank_display = string.gsub(rank, "_", " ")
	local rank_display = string.lower(rank_display)
	
    local collapse_cutoff = args.collapse_cutoff
    local header = ""

    if rank == "Region" then
        -- Region/Subregion/Intermediate_region have their own row in JSON:Geography_hierarchy_map
        -- (keyed by their own QID), so this is a "look up my own row" query.
        local qid, qid_err = resolve_geo_qid(title)
        if qid_err then return qid_err end

        local record, hierarchy_err = resolve_geo_hierarchy(qid)
        if hierarchy_err then return hierarchy_err end

        local output = record and format_page_list(record.Subregions)

        if output then
            local output_len = str.count_comma_elements(output)
            header = string.format("= Subregions in this %s (" .. output_len .. ")=\n\n", rank_display)
            return header .. output
        else
            header = "= Subregions in this " .. rank_display .. " =\n\nNo results found."
        end
    else
    return nil
    end
end



function p.get_intermediate_regions(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	
	-- get args from parent frame
	local rank = tax.geo_field_finder(title, { return_on_missing = "Administrative_unit" })
	
	if rank == administrative_unit then return rank end
	
	
	local collapse_cutoff = args.collapse_cutoff
	local header = ""

    local rank = tax.geo_field_finder(title, { return_on_missing = "Administrative_unit" }) -- find rank
	local rank_display = string.gsub(rank, "_", " ")
	local rank_display = string.lower(rank_display)

    if rank == "Region" or rank == "Subregion" then
    	-- Region/Subregion have their own row in JSON:Geography_hierarchy_map (keyed by their own
    	-- QID) -- see comment in get_subregions() above.
    	local qid, qid_err = resolve_geo_qid(title)
    	if qid_err then return qid_err end

    	local record, hierarchy_err = resolve_geo_hierarchy(qid)
    	if hierarchy_err then return hierarchy_err end

    	local output = record and format_page_list(record.Intermediate_regions)

        if output then
            local output_len = str.count_comma_elements(output)
            header = string.format("= Intermediate regions in this %s (" .. output_len .. ")=\n\n", rank_display)
            return header .. output
        else
            header = "= Intermediate regions in this " .. rank_display .. " =\n\nNo results found."
        end
    else
        return nil
    end
end



function p.get_countries(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	
	-- get args from parent frame
	local rank = tax.geo_field_finder(title, { return_on_missing = "Administrative_unit" })
	if rank == administrative_unit then return rank end
	
    local collapse_cutoff = args.collapse_cutoff
    local header = ""

    local rank = tax.geo_field_finder(title, { return_on_missing = "Administrative_unit" }) -- find rank
	local rank_display = string.gsub(rank, "_", " ")
	local rank_display = string.lower(rank_display)

    if rank ~= "Country" then
        -- Region/Subregion/Intermediate_region have their own row in JSON:Geography_hierarchy_map
        -- (keyed by their own QID) -- see comment in get_subregions() above.
        local qid, qid_err = resolve_geo_qid(title)
        if qid_err then return qid_err end

        local record, hierarchy_err = resolve_geo_hierarchy(qid)
        if hierarchy_err then return hierarchy_err end

        local output = record and format_page_list(record.Countries)

        if output then
        	local output_len = str.count_comma_elements(output)
            header = string.format("= Countries in this %s (" .. output_len .. ")=\n\n", rank_display)
            return header .. output
        else
            return "= Countries in this " .. rank_display .. " =\n\nNo results found."
        end
    else
        return nil
    end
end







function p.get_resources(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	local no_results_message = "No results found. If you have expertise or materials on this topic that you are willing to contribute, [[Hopperwiki:About#Who manages HopperWiki?|please get in touch with an administrator]]."
	
	
	local cargo_table = "Resource"
    local fields = "_pageName=Title, Resource_link, Category, Descriptive_keyword, Author, Language, Species_purview"
    local cargo_args = {where = "All_geography HOLDS '" .. title .. "'"}
    local cargo_results = cargo.query(cargo_table, fields, cargo_args)
    
    
    -- format cargo information in place
	local field_formats = {
	    Title = "page",
	    Resource_link = "url",
	    Category = "string",
	    Descriptive_keyword = "list_of_string",
	    Author = "list_of_string",
	    Language = "list_of_string",
	    Species_purview = "list_of_page",
	}
	local cargo_results_formatted = cfmt.format_cargo_results(cargo_results, field_formats)
    
    -- control row cutoff for collapsing display
    local collapse_cutoff = tonumber(args.collapse_cutoff)
	local collapse = false
	if #cargo_results_formatted > collapse_cutoff then
		collapse = true
	end
	
	local display_fields = "Title, Resource_link, Category, Descriptive_keyword, Author, Language, Species_purview"
    if #cargo_results_formatted > 0 then
    	local header = "= Resources (" .. #cargo_results_formatted .. ")=\n"
        return out.generate_wiki_table(cargo_results_formatted, header, display_fields, collapse, table_width)
    else
        return "= Resources =\n\n" .. no_results_message
    end
end
















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