Module:Geography
Appearance
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