Jump to content

Module:Taxon

From WhiskerWiki

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

-- ================================================================================
-- Module dependencies
-- ================================================================================
local getArgs = require('Module:Arguments').getArgs
local cargo = mw.ext.cargo -- for cargo queries if needed
local cf = require("Module:Custom_functions")
-- ========================================
-- Get taxonomic schema===================================
local ts = require("Module:Taxonomic schema")
local rank_lookup = ts.rank_lookup
local all_ranks = ts.all_ranks
local rank_dictionary = ts.rank_dictionary
local utils = require("Module:Utilities")
local tf = require("Module:Table_functions")
local ibf = require("Module:Infobox_functions")
local html = mw.html.create()



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












-- =========================================================
-- Generate taxon lists
-- =========================================================
--[[
This is a parent function that uses various helper functions to create a nested, bulleted, html list
for display on a taxon (or other) page.
--]]
function p.taxon_list_pipeline(frame)
    
	-- ========================================
	-- Arguments and settings
	-- ========================================
	local args = getArgs(frame)
    args.parent_title = mw.title.getCurrentTitle().text

	-- get focal taxon from the tile of parent page calling the module or user-provided argument
	local focal_taxon = args.taxon or args.parent_title
	
	-- Set Cargo table
	local cargo_table_name = "Species"
	
	
	
	-- ========================================
	-- Get focal Cargo row
	-- ========================================
	local cargo_focal_row = utils.get_cargo_row(cargo_table_name, focal_taxon, all_ranks)	
	
	
	
	-- ========================================
	-- Find the focal rank
	-- ========================================
	local focal_rank = utils.get_focal_rank(cargo_focal_row, focal_taxon, all_ranks)
	
	if not focal_rank then
		return "An taxon list can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
	end
	
	local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
	local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
	
	
	
	
	-- ========================================
	-- Get correct rank list for display
	-- ========================================
	-- Create a 'genealogy' for the focal taxon which is a concise list of the Linnean values moving up the hierarchy
	-- from the focal taxon for searching the correct taxon rank list
	local genealogy = utils.get_genealogy(cargo_focal_row, focal_rank, focal_taxon, all_ranks)
	
	-- Look to see if there is a custom rank list for the focal taxa and if not use the default
	local rank_list = utils.check_for_custom_ranks(genealogy, rank_dictionary)
	-- Make a string version for use as the fields of cargo.query
	local rank_list_string = utils.list_to_csv_string(rank_list)
	
	
	-- ========================================
	-- Define downstream child ranks
	-- ========================================
	-- get list of the names of the child ranks with respect to page rank
	local child_ranks = ""
	if focal_rank ~= "Species" then
		child_ranks = utils.get_downstream_items(rank_list, focal_rank)
	else
    	return "Warning: 'Module:Higher taxon content' shouldn't be used on species pages."
	end
	
	
	
	
	
	
	-- ========================================
	-- Get Cargo query for the focal rank
	-- ========================================
	-- Get all the records that correspond to the focal (page) rank
	
	local fields = rank_list_string
	local cargo_args = {
		where = string.format("%s = '%s'", focal_rank, focal_taxon)
	}
	local cargo_results = cargo.query(cargo_table_name, fields, cargo_args)
	
	
	

	-- ========================================
	-- Build a nested hierarchy of the Cargo data
	-- ========================================
	local nested_taxon_list = utils.build_nested_hierarchy(cargo_results, child_ranks)
	
	
	
	-- ========================================
	-- Format the nested list
	-- ========================================
	--  Define and apply formatting before building the html code
	
	--[[I have decided it's best to apply formatting at this stage, i.e. apply the formatting to the nested object and not to the cargo results.
	In other pipelines in the wiki, where I don't use this nested structure step, I do apply formatting results directly to the cargo structure.
	However, since the generation of these taxonomic lists requires this extra nested structure step before producing the HTML list, it's better for several reasons to 
	apply the formatting at this stage. In this way the formatting is applied right before producing the forward-facing HTML list. For one thing, that way, 
	the information of the hierarchy could be used to apply formatting rules. Otherwise I run the risk of having messy code where formatting needs to be applied twice
	--]]
	
	
	
	
	
	
	
	
	-- Function that is used by traverse_and_format to apply formatting rules based on rank
	--[[
	This helper function, used by traverse_and_format, has the primary responsibility of applying formatting rules.
	It contains the specific logic for modifying a table entry based on its field or other hierarchy-based rules (e.g., italicizing species names or adding hyperlinks).
	By isolating this logic, we keep the traversal function focused on traversal, improving readability and maintainability. 
	This function, in turn doesn't apply formatting directly it applies helper functions that have specific raw formatting tasks.
	--]]
	-- This helper function now takes a node (with .rank and .name) instead of separate "field" and "values".
	-- If the node's rank is "Species," italicize the node.name in place.
	local function apply_rank_formatting(node)
	    -- Replace underscores with spaces in the rank field
	    node.rank = node.rank:gsub("_", " ")
	    
	    if node.name ~= "No information" and node.rank ~= "Root" then
	        
	        if node.rank == "Species" or node.rank == "Genus" then
	            -- For species, italicize and create a page link
	            node.name = "[[" .. node.name .. "|" .. "''" .. node.name .. "''" .. "]]"
            
            
            
	        elseif node.rank == "Subgenus" then
	        	local taxon_only = string.match(node.name, "^(.-) %(.-%)$") or node.name
	            -- For subgenus, italicize taxon name and remove " (subgenus)" text
            	node.name = "[[" .. node.name .. "|" .. "''" .. taxon_only .. "''" .. "]]"
            
            
            
            elseif node.rank == "Species group" then
	            -- Italicize the binomial in from of species groups
	            local binomial_only = string.gsub(node.name, " group$", "")
	            node.name = "[[" .. node.name .. "|" .. "''" .. binomial_only .. "''" .. " group]]"
	            
            	            
            elseif node.rank == "Clade II" then
	            -- Get the text before the word "group"
	            local binomial_only = node.name:match("^(.-) group")
	            -- Get the text after the word "group"
	            local string_after_group = node.name:match("group (.+)") or ""
	            -- Combine with the correct italicizing
	            node.name = "[[" .. node.name .. "|" .. "''" .. binomial_only .. "''" .. " group " .. string_after_group .. "]]"
	            
	        elseif node.name == "Incertae sedis" then
	            -- For "Incertae sedis", italicize and don't create a page link
	            node.name = "''" .. node.name .. "''"
	        else
	            -- For everything else, just create a normal link
	            node.name = "[[" .. node.name .. "]]"
	        end
	        
	    end
	    
	    -- Finally remove "Species" in the rank field after processing as it's not needed for display
	    if node.rank == "Species" then
	    	node.rank = node.rank:gsub("Species", "")
	    end
	    
	end
	
	
	
	
	
	
	
	-- Format the nested taxon list
	--[[Note that this function edits the nested table in place--]]
	utils.traverse_and_format(nested_taxon_list, apply_rank_formatting)



	-- ========================================
	-- Output section 
	-- ========================================
	-- Build the html formatted list
	local taxon_list = utils.build_bulleted_taxon_list(nested_taxon_list)
	
	-- Get values for correct grammatical syntax
	local focal_taxon_name_display = rank_lookup[child_ranks[1]].plural
	
	-- Produce the display for the forward-facing page
	local output_header = tostring(mw.html.create('h2'):wikitext("Available taxon pages"))
	local output_text = tostring(mw.html.create('p'):wikitext("The following " .. focal_taxon_name_display .. " are available on WhiskerWiki for this " .. focal_rank_lowercase .. ":\n"))
	
	if taxon_list then
	    output = output_header .. output_text .. taxon_list .. "\n"
	end
    
    return output
    
end













-- =========================================================
-- Taxon infobox
-- =========================================================
function p.infobox(frame)
	
	
	
	-- ========================================
	-- Main settings and arguments
	-- ========================================	
	
	local args = getArgs(frame)
    local focal_taxon = mw.title.getCurrentTitle().text
    
    local placeholder_image = "No-Image-Placeholder.svg"
    
    
   	-- get taxon rank of parent page calling the module
	local focal_rank = ts.rank_finder("Species", focal_taxon)
	
	-- Control loop to avoid Lua errors if taxon doesn't exist in Cargo
	if not focal_rank then
		return "An infobox can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
	end
	
	local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
	local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
	
	
	
	-- ========================================
	-- Get images from Cargo
	-- ========================================
	local range_map = ""
	local profile_image = ""
	
	
	local tables = "Taxon_image"
	local fields = "Species, Range_map, Profile_image"
	local cargo_args = {
		where = string.format("%s = '%s'", "Species", focal_taxon)
	        	}
	        	
    local cargo_results = cargo.query( tables, fields, cargo_args )
    
	-- local function to check if a file exists. This is a patch to deal with the fact that we
	-- added a lot of file names to a Cargo table that have not been uploaded yet.
	local function file_exists(file_name)
	    local focal_taxon = mw.title.new("File:" .. file_name)
	    return focal_taxon and focal_taxon.exists
	end
	
	if #cargo_results > 0 then
	    local range_map_name = cargo_results[1].Range_map
	    if not file_exists(range_map_name) then
	        range_map_name = placeholder_image
	    end
	    range_map = range_map_name
	end
	
	
	
	-- ========================================
	-- Get main infobox information from Cargo
	-- ========================================	
	-- Get infobox information from Cargo
	local tables = "Species"
	local fields = "Order_taxon, Suborder, Infraorder, Superfamily, Family, Subfamily, Tribe, Genus, Subgenus, Clade_I, Species_group, Clade_II, Species, Common_name, Full_name, Species_author, Species_year"
	local cargo_args = {
		where = string.format("%s = '%s'", focal_rank, focal_taxon)
	}
    local cargo_results = cargo.query( tables, fields, cargo_args )
	
	-- Pull the results from the Cargo array and assign to variables
	if cargo_results[1] then
	    local fields_array = {}
	    for field in fields:gmatch("[^,]+") do  -- Split fields by comma
	        table.insert(fields_array, field:match("^%s*(.-)%s*$")) -- Trim spaces
	    end
	
	    for _, field in ipairs(fields_array) do
	    	args[field] = cargo_results[1][field]
    	end
	end


	
	-- color control for the infobox (do this before objects get set to nil below)
	local html_theme_class = "-base"
    
 
	
	
	
	if focal_rank ~= "Species" then

		-- Create a 'genealogy' for the focal taxon which is a concise list of the Linnean values moving up the hierarchy
		-- from the focal taxon for searching the correct taxon rank list
		-- Look to see if there is a custom rank list for the focal taxa and if not use the default
		local rank_list = utils.check_for_custom_ranks({focal_taxon}, rank_dictionary)
		
		local genealogy = utils.get_genealogy(cargo_results, focal_rank, focal_taxon, rank_list)
		
		-- remove the focal taxon from the genealogy as this doesn't need to display in the infobox
		table.remove(genealogy, 1)
		
		-- nullify all fields in args except those in the genealogy table
		if genealogy then
		    for key in pairs(args) do
		        local found = false
		        for _, allowed_value in pairs(genealogy) do
		            if args[key] == allowed_value then
		                found = true
		                break
		            end
		        end
		        if not found then
		            args[key] = nil
		        end
		    end
		end
		
	end
	
	
	
    -- ========================================
	-- Begin creating the infobox html
	-- ========================================
	local display_title = focal_rank_sentence_case .. " " .. focal_taxon .. "<br/>" -- this is the default focal_taxon
	local subtitle = ""
	
	-- overwrite the default title for certain groups of ranks
	
	
	if focal_rank == "Species_group" then
		local binomial_only = string.gsub(focal_taxon, " group$", "")
		display_title = "<i>" .. binomial_only .. "</i>" .. " group"
	end
	
	if focal_rank == "Clade_I" or focal_rank == "Clade_II" then
		display_title = focal_taxon
		
		if focal_rank == "Clade_I" or focal_rank == "Clade_II" then
			subtitle =  "(informal rank)"
		end
	end
	
	if focal_rank == "Genus" then
		display_title = focal_rank_sentence_case .. " <i>" .. focal_taxon .. "</i>"
	end
	
	if focal_rank == "Subgenus" then
		local taxon_only = string.match(focal_taxon, "^(.-) %(.-%)$") or focal_taxon
		display_title = focal_rank_sentence_case .. " <i>" .. taxon_only .. "</i>"
	end
	
	if focal_rank == "Species" then
		display_title = args.Common_name
		subtitle =  "(<i>" .. focal_taxon .. "</i>)"
	end


	-- after formatting species reformat the subset that are undescribed species
	-- Check if the species follows the undescribed pattern
	if focal_taxon:match("^%w+ sp%.") then
	    -- Extract genus and the remaining part (after "sp.")
	    local genus, descriptor = focal_taxon:match("^(%w+) sp%. (.+)$")
	    
	    if genus and descriptor then
	        -- Format the display title and subtitle
	        display_title = "<i>" .. genus .. "</i> sp. " .. descriptor
	        subtitle = "(undescribed)"
	    else
	        -- Handle cases where the format is invalid
	        return "Warning: Formatting is not correct for an undescribed species. Expected format: 'Genus sp. Descriptor'."
	    end
	end
	
	
	root = ibf.create_infobox(root, display_title, subtitle,  html_theme_class)
	
	
	if range_map and range_map ~= "" then
		root = ibf.add_header(root, "Range", html_theme_class)
		root = ibf.add_image(root, range_map, html_theme_class)
	end
	
	root = ibf.add_header(root, "Taxonomic classification", html_theme_class)
    
    root = ibf.add_row(root, 'Order:', args.Order_taxon, html_theme_class, "page")
    root = ibf.add_row(root, 'Suborder:', args.Suborder, html_theme_class, "page")
    root = ibf.add_row(root, 'Infraorder:', args.Infraorder, html_theme_class, "page")
    root = ibf.add_row(root, 'Superfamily:', args.Superfamily, html_theme_class, "page")
    root = ibf.add_row(root, 'Family:', args.Family, html_theme_class, "page")
    root = ibf.add_row(root, 'Subfamily:', args.Subfamily, html_theme_class, "page")
    root = ibf.add_row(root, 'Tribe:', args.Tribe, html_theme_class, "page")
    root = ibf.add_row(root, 'Genus:', args.Genus, html_theme_class, "italic page")
    root = ibf.add_row(root, 'Subgenus:', args.Subgenus, html_theme_class, "italic page")
    root = ibf.add_row(root, 'Clade I:', args.Clade_i, html_theme_class, "page")
    root = ibf.add_row(root, 'Species group:', args.Species_group, html_theme_class, "page")
    root = ibf.add_row(root, 'Clade II:', args.Clade_ii, html_theme_class, "page")
    
    if focal_rank == "Species" then
    	root = ibf.add_header(root, "Binomial details", html_theme_class, 2)
    	root = ibf.add_spanning_row(root, "<i>" .. args.Species .. "</i> " .. args.Species_author, html_theme_class, "text")
	end
    
    
    return root

end
































-- =========================================================
-- General species data based on literary references
-- =========================================================
function p.get_species_refs(frame)
	
	local title = mw.title.getCurrentTitle().text
	local args = getArgs(frame)
	
	-- override title with user defined species entry
	if args.species then
		title = args.species
	end
	
	-- set default for collapse cutoff in case user does not through the template
	local collapse_cutoff = 100
	if args.collapse_cutoff then 
	    collapse_cutoff = tonumber(args.collapse_cutoff)
	end
	
	-- get information from cargo	
    local tables = "Species_reference"
    local fields = "Part_of_range, Reference, Total_length, Tail_length, Hindfoot_length, Ear_length, Mass"

    local cargo_args = {
		where = string.format("%s = '%s'", "Species", title)
	        	}
    local cargo_results = cargo.query(tables, fields, cargo_args)
    
    
   
	-- Remove exact duplicate rows from cargo_results
	local unique_cargo_results = {}
	local seen = {}
	
	for _, row in ipairs(cargo_results) do
		-- Build a unique string key for the entire row (order of fields matters)
		local row_key = (row.Part_of_range or "")
			.. "|" .. (row.Reference or "")
			.. "|" .. (row.Total_length or "")
			.. "|" .. (row.Tail_length or "")
			.. "|" .. (row.Hindfoot_length or "")
			.. "|" .. (row.Ear_length or "")
			.. "|" .. (row.Mass or "")

		if not seen[row_key] then
			table.insert(unique_cargo_results, row)
			seen[row_key] = true
		end
	end
	
	-- Replace original cargo_results with the deduplicated version
	cargo_results = unique_cargo_results
	
    
    
    
    
    -- format cargo information in place
	local field_formats = {
	    Species = "page",
	    Part_of_range = "string",
	    Reference = "string",
	}
	local cargo_results_formatted = tf.format_cargo_results(cargo_results, field_formats)

    -- control row cutoff for collapsing display
    local collapse_cutoff = collapse_cutoff
    
    
    -- return args.collapse_cutoff
	local collapse = false
	if #cargo_results_formatted > collapse_cutoff then
		collapse = true
	end
	
	-- Final formatting of output
    local header = ""
    local description = "Length measurements are in millimeters (mm) and weight measurements are in grams (g), unless stated otherwise. If available, the sample size (n=) is provided. If a range is not provided and n= is not given, then the listed measurement represents an average."
    local display_fields = fields
    
    local output = ""
    
    if #cargo_results_formatted > 0 then
    	local output_table = tf.generate_wiki_table(cargo_results_formatted, header, display_fields, collapse)
        output = header .. "\n" .. description .. "\n" .. output_table
    else
        output = "No results found."
    end
    
    return output
    
end











-- =========================================================
-- Sorex character data querying
-- =========================================================
function p.get_sorex_chars(frame)
	
	local title = mw.title.getCurrentTitle().text
	local args = getArgs(frame)
	
	-- override title with user defined species entry
	if args.species then title = args.species end
	
	
	-- get information from cargo	
    local tables = "Sorex_character"
    local fields = "Summer_dorsum_color, Winter_dorsum_color, Condylobasal_length," .. 
    				"Shape_medial_edge_incisors, Tines_upper_incisors, Tine_size, Tine_position," ..
    				"Number_unicuspids, Unicuspid_notes, Postmandibular_canal, Skull_shape," ..
    				"Dental_formula, Dental_characters, Other_characters, Tail_bicolored, Paired_toe_pads"
    local cargo_args = {where = string.format("%s = '%s'", "Species", title)}
    
    if title == "all" then cargo_args = {} end -- select all records if user enters "all" for species
	        	
    local cargo_results = cargo.query(tables, fields, cargo_args)
    
    -- format cargo information in place
	local field_formats = {
	    Species = "page",
	    Part_of_range = "string",
	    Reference = "string",
	}
	local cargo_results_formatted = tf.format_cargo_results(cargo_results, field_formats)
    
	
	-- Final formatting of output
    local html_theme_class = "-base"
    if #cargo_results_formatted > 0 then
    	
    -- Begin creating the html table infobox template
	local display_title = "<i>" .. title .. "</i>" .. " skull characters"
	root = ibf.create_infobox_small(root, display_title, html_theme_class)
    root = ibf.add_spanning_row(root, "<i>units in mm</i>", html_theme_class, "text")
    root = ibf.add_header(root, "Skull", html_theme_class)
    root = ibf.add_row(root, 'Condylobasal length:', cargo_results[1].Condylobasal_length, html_theme_class, "text")
    root = ibf.add_row(root, 'Postmandibular canal:', cargo_results[1].Postmandibular_canal, html_theme_class, "text")
    root = ibf.add_row(root, 'Shape:', cargo_results[1].Skull_shape, html_theme_class, "text")
    root = ibf.add_header(root, "Dental", html_theme_class)
    root = ibf.add_row(root, 'Upper unicuspids:', cargo_results[1].Number_unicuspids, html_theme_class, "text")
    root = ibf.add_row(root, 'Unicuspid notes:', cargo_results[1].Unicuspid_notes, html_theme_class, "text")
    root = ibf.add_row(root, 'Tines present:', cargo_results[1].Tines_upper_incisors, html_theme_class, "text")
    root = ibf.add_row(root, 'Tine size:', cargo_results[1].Tine_size, html_theme_class, "text")
    root = ibf.add_row(root, 'Tine position:', cargo_results[1].Tine_position, html_theme_class, "text")
    root = ibf.add_row(root, 'Shape upper incisors:', cargo_results[1].Shape_medial_edge_incisors, html_theme_class, "text")
    root = ibf.add_row(root, 'Dental characters:', cargo_results[1].Dental_characters, html_theme_class, "text")
    
    return root
        
    else
        return "No results found."
    end

end




-- =========================================================
-- Taxon name processor
-- =========================================================
function p.taxon_name_processor(frame)
	
	-- process arguments
	local args = getArgs(frame)
	
	local taxon_name = args[1]
	
	local focal_field = ts.rank_finder("Species", taxon_name)
	
    -- get information from cargo	
	local tables = "Species"
	local fields = "Species, Common_name"
	local where_clause = {
		where = string.format("%s = '%s'", "Species", taxon_name)
	}
	        	
	local cargo_results = cargo.query(tables, fields, where_clause)
    
	-- Check if there are any results
	if #cargo_results == 0 then
	    return "No common name found for " .. taxon_name
	end

	local common_name = cargo_results[1].Common_name

	local output = common_name .. " (<i>[[" .. taxon_name .. "]]</i>)"
	return output
end




-- =========================================================
-- Get taxon wiki category tag
-- =========================================================
function p.get_taxon_category_tag(frame)
	
	local args = getArgs(frame)
    local title = mw.title.getCurrentTitle().text
    
    
    -- get taxon rank of parent page calling the module
    local focal_rank = ts.rank_finder("Species", title)
    
    if focal_rank then
    
	    local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
		
		
		
		local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
		
		local category_tag = "[[Category:" .. focal_rank_sentence_case .. "]]"
		return category_tag
	
	else return "A taxon category tag can't be returned because this taxon can't be found in the WhiskerWiki database!\n\n"
	
	end
    
end



-- =========================================================
-- Get the approriate italics formatting for a title based on rank
-- =========================================================
function p.italic_title_for_taxon(frame)
	local args = getArgs(frame)
    local title = mw.title.getCurrentTitle().text
    
    -- get taxon rank of parent page calling the module
	local focal_rank = ts.rank_finder("Species", title)
	
	-- Control loop to avoid Lua errors if taxon doesn't exist in Cargo
	if not focal_rank then
		return "An italic title template can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
	end
	
	local focal_rank = rank_lookup[focal_rank].lowercase
	
	local italic_title_template = ""
	
	if focal_rank == "species" or focal_rank == "genus" then
		-- check if the binomial contans "sp."
		local contains_sp = title:find("sp%.") ~= nil
		if contains_sp then
			local substring = title:match("^%S+") -- this should extract the genus only and not "sp."
			italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
		else
			-- otherwise italicize everything
			italic_title_template = "{{Italic title}}"
		end
	end
	
	
	if focal_rank == "subgenus" then
		local substring = title:gsub("%b()", ""):gsub("^%s*(.-)%s*$", "%1") or title
		
		italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
	end
	
	if focal_rank == "species group" then
		local substring = title:match("^%S+%s+%S+")
		italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
	end
	
	
	-- Logic for Sorex-specific clades
    if focal_rank == "clade I" then
        local substring = title:match("^%S+") -- this should extract the genus only and not "sp."
		italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
    elseif focal_rank == "clade II" then
        local substring = title:match("^%S+%s+%S+")
        italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
    end
	
	
	
	 -- Use frame:preprocess to ensure the template is interpreted as wikitext
    return frame:preprocess(italic_title_template)
	
	
	
end






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