Module:Species

From HopperWiki
Jump to navigation Jump to search

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

-- ================================================================================
-- Module dependencies
-- ================================================================================
local cargo = mw.ext.cargo
local html = mw.html.create()
local getArgs = require('Module:Arguments').getArgs -- for processing arguments
local cf = require("Module:Custom_functions")
local cf2 = require("Module:Custom_functions_new")
local ibf = require("Module:Infobox_functions")
local tf = require("Module:Table_functions")
local utils = require("Module:Utilities")



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




-- ================================================================================
-- Species pest display box
-- ================================================================================
--[[
Create a pest display table that has data from both the species and and the resources table. The point of this function
is to basically perform a work-around for Cargo's limiations with performing a JOIN operation on fields of type list. It queries both the
Species and the Resource table and maps the results from the latter onto the former.
--]]

function p.get_pest_species_table(frame)
    local args = getArgs(frame)
    local parent_args = frame:getParent().args
    local title = args.title or parent_args.title or mw.title.getCurrentTitle().text
    
    title = title:match("^%s*(.-)%s*$") -- This just removes leading/trailing whitespace as a precaution
    
    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]]."
    
    -- ========================================
    -- Query both Cargo tables independently
    -- ========================================
    
    -- Query Species table
    local species_tables = "Species"
    local species_fields = "Name, Image, Distribution, All_geography"
    local species_where = "All_geography HOLDS '" .. title .. "' AND Is_pest = 'TRUE'"
    local species_results = cargo.query(species_tables, species_fields, {where = species_where})
    
    -- Query Subresource table
    local subresource_tables = "Subresource"
    local subresource_fields = "Name, Parent_resource, Species_purview, File_name"
    local subresource_where = "All_geography HOLDS '" .. title .. "' AND Parent_resource = 'USDA ARS grasshopper species fact sheets'"

    local subresource_results = cargo.query(subresource_tables, subresource_fields, {where = subresource_where})

    -- ========================================
    -- Map one table data to the other REFACTORED
    -- ========================================
	local function map_tables(left_table_results, right_table_results, left_table_join_field, right_table_join_field, right_table_fields_to_join)
	    -- Build a map from the right table using the specified join field
	    local right_table_map = {}
	    for _, row in ipairs(right_table_results) do
	        if row[right_table_join_field] then
	            for join_value in row[right_table_join_field]:gmatch("([^,]+)") do
	                join_value = mw.text.trim(join_value)
	                -- Store only specified fields in the map
	                local mapped_data = {}
	                for _, field in ipairs(mw.text.split(right_table_fields_to_join, ",")) do
	                    field = mw.text.trim(field)
	                    mapped_data[field] = row[field]
	                end
	                right_table_map[join_value] = mapped_data
	            end
	        end
	    end
	
	    -- Merge left table results with right table mapped data
	    local merged_results = {}
	    for _, left_row in ipairs(left_table_results) do
	        local join_value = left_row[left_table_join_field]
	        local mapped_data = right_table_map[join_value] or {}
	        
	        -- Add mapped data to the left row
	        for k, v in pairs(mapped_data) do
	            left_row[k] = v
	        end
	        
	        table.insert(merged_results, left_row)
	    end
	
	    return merged_results
	end
	
	local left_table_join_field = "Name"
	local right_table_join_field = "Species_purview"
	local right_table_fields_to_join = "File_name"
	
	local merged_results = map_tables(species_results, subresource_results, left_table_join_field, right_table_join_field, right_table_fields_to_join)
	

    
    -- ========================================
    -- Format results for display
    -- ========================================
    local field_formats = {
        Name = "page",
        Official_common_name = "string",
        Image = "file",
        Distribution = "list_of_page",
        All_geography = "list_of_page",
        File_name = "file",
    }
    local formatted_results = utils.format_cargo_results(merged_results, field_formats)

    -- Display table with optional collapse
    local collapse_cutoff = tonumber(args.collapse_cutoff) or 300
    local collapse = #formatted_results > collapse_cutoff

    local display_fields = "Name, Image, Distribution, File_name"
    if #formatted_results > 0 then
        local header = ""
        return tf.generate_wiki_table(formatted_results, header, display_fields, collapse)
    else
        return no_results_message
    end
end








-- ================================================================================
-- Species infobox
-- ================================================================================
function p.infobox_new(frame)
	
	
	
    -- ========================================
    -- Process arguments
    -- ========================================
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	title = title:match("^%s*(.-)%s*$") --This just removes leading/trailing whitespace as a precaution
	
	-- get taxon rank of parent page calling the module
	local rank = cf2.rank_finder("Species", title)
	if not rank then
	    -- Handle the error, e.g., by returning a message or logging an error
	    error(string.format("Error: The taxon '%s' could not be found in the Cargo database.", title))
	end
	
	
	local rank_lowercase = cf2.rank_lookup[rank].lowercase
	local rank_sentence_case = cf2.rank_lookup[rank].sentence_case
	local rank_cargo = cf2.rank_lookup[rank].cargo
	
	-- if rank is species than use the `Name` field to lookup the title
	if rank_cargo == "Species"
		then rank_cargo = "Name"
	end
	
	-- control loop to see if no arguments are provided from the parent page
	-- and patch with a Cargo query if they aren't
	if rank_cargo == nil then
		return("Test An infobox can't be built because the taxon for this page can't be found in the Cargo database!\n\n")
	end
	
	
	-- local Cargo query to patch values not provided as arguments
	local tables = "Species"
	local fields = "Image, Taxon_author, Official_common_name, Family, Subfamily, Genus, Species, Distribution, OSF_URL"
	local cargo_args = {
		where = string.format("%s = '%s'", rank_cargo, title)
	        	}
    local cargo_results = cargo.query( tables, fields, cargo_args )
    
    


    -- extract the cargo_results from the cargo.query output
	local image = cargo_results[1].Image or "Species icon.png"
	local taxon_author = cargo_results[1].Taxon_author
	local common_name = cargo_results[1].Official_common_name
	local family = cargo_results[1].Family
	local subfamily = cargo_results[1].Subfamily
	local genus = cargo_results[1].Genus
	local species = cargo_results[1].Species
	local distribution = cargo_results[1].Distribution
	local osf_url = cargo_results[1].OSF_URL
	local osf_url_formatted = "[" .. osf_url .. " " .. "Full taxonomy at OSF]"
	    
    
    
    
    
	
	
	-- Make redundant fields nil so they don't display in infobox for records which are higher than species
	if rank == "family" then
	  family, subfamily, genus, species = nil
	elseif rank == "subfamily" then
	  subfamily, genus, speices = nil
	elseif rank == "genus" then
	  genus, species = nil
	end
	
	-- start creating infobox
	local root = ""
	local html_theme_class = '-species'
	root = ibf.create_infobox(root, title, html_theme_class)	
	-- add common name subtitle below the title
	if common_name then
		root = ibf.add_subtitle(root, common_name, html_theme_class)	
	end
	-- add image
	root = ibf.add_image(root, image, html_theme_class)
	
	-- add Distribution section
	if distribution then 
		root = ibf.add_header(root, "Distribution", html_theme_class, 2)
		root = ibf.add_spanning_row(root, distribution, html_theme_class, "page")
	end
	
	-- add header that says taxonomic classification
    if family ~= nil then
    root = ibf.add_header(root, "Taxonomy", html_theme_class, 2)
    end
    root = ibf.add_row(root, 'Family:', family, html_theme_class, "page")
    root = ibf.add_row(root, 'Subfamily:', subfamily, html_theme_class, "page")
    root = ibf.add_row(root, 'Genus:', genus, html_theme_class, "page")
    
    if osf_url then 
		root = ibf.add_header(root, "Additional resources", html_theme_class, 2)
		root = ibf.add_spanning_row(root, osf_url_formatted, html_theme_class, "text")
    end
	
    return root
	
end



function p.infobox(frame)
	
	
	
    -- ========================================
    -- Process arguments
    -- ========================================
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	title = title:match("^%s*(.-)%s*$") --This just removes leading/trailing whitespace as a precaution
	
	-- get taxon rank of parent page calling the module
	local rank = cf2.rank_finder("Species", title)
	local rank_lowercase = cf2.rank_lookup[rank].lowercase
	local rank_sentence_case = cf2.rank_lookup[rank].sentence_case
	local rank_cargo = cf2.rank_lookup[rank].cargo
	
	
	local name = args.Name
	local image = args.Image or "Species icon.png"
	
	local class = args.Class
	local family = args.Family
	local subfamily = args.Subfamily
	local genus = args.Genus
	local species = args.Species
	local taxon_author = args.Taxon_author
	local common_name = args.Official_common_name
	local distribution = args.Distribution
	local osf_url = args.OSF_URL
	local osf_url_formatted = "[" .. osf_url .. " " .. "Full taxonomy at OSF]"
	
	-- control loop to see if no arguments are provided from the parent page
	-- and patch with a Cargo query if they aren't
	if rank_cargo == nil then
		return("Test An infobox can't be built because the taxon for this page can't be found in the Cargo database!\n\n")
	end
	
	if not (family or subfamily or genus or species) then
		-- local Cargo query to patch values not provided as arguments
		local tables = "Species"
		local fields = "Family, Subfamily, Genus, Species"
		local cargo_args = {
			where = string.format("%s = '%s'", rank_cargo, title)
		        	}
	    local cargo_results = cargo.query( tables, fields, cargo_args )
	
	    -- extract the cargo_results from the cargo.query output
	    family = cargo_results[1].Family
	    subfamily = cargo_results[1].Subfamily
	    genus = cargo_results[1].Genus
	    species = cargo_results[1].Species
	end
	
	-- Make redundant fields nil so they don't display in infobox for records which are higher than species
	if rank == "family" then
	  family, subfamily, genus, species = nil
	elseif rank == "subfamily" then
	  subfamily, genus, speices = nil
	elseif rank == "genus" then
	  genus, species = nil
	end
	
	-- start creating infobox
	local root = ""
	local html_theme_class = '-species'
	root = ibf.create_infobox(root, title, html_theme_class)	
	-- add common name subtitle below the title
	if common_name then
		root = ibf.add_subtitle(root, common_name, html_theme_class)	
	end
	-- add image
	root = ibf.add_image(root, image, html_theme_class)
	
	-- add Distribution section
	if distribution then 
		root = ibf.add_header(root, "Distribution", html_theme_class, 2)
		root = ibf.add_spanning_row(root, distribution, html_theme_class, "page")
	end
	
	-- add header that says taxonomic classification
    if family ~= nil then
    root = ibf.add_header(root, "Taxonomy", html_theme_class, 2)
    end
    root = ibf.add_row(root, 'Family:', family, html_theme_class, "page")
    root = ibf.add_row(root, 'Subfamily:', subfamily, html_theme_class, "page")
    root = ibf.add_row(root, 'Genus:', genus, html_theme_class, "page")
    
    if osf_url then 
		root = ibf.add_header(root, "Additional resources", html_theme_class, 2)
		root = ibf.add_spanning_row(root, osf_url_formatted, html_theme_class, "text")
    end
	
    return root
	
end





-- ================================================================================
-- Species for geography gallery
-- ================================================================================
--[[In theory the logic of this function is very similar to the "get X for Y" style Cargo query templates that can
just directly query the Module:Cargo_query functions but gallery producing function for species needs it's own function
as it requires a little prefilter logic.
--]]
function p.get_species_gallery_for_geographic_unit(frame)
	-- ========================================
    -- Process and prioritize arguments
    -- ========================================
    local args = getArgs(frame)
    local parent_args = frame:getParent().args
    
    local title = mw.title.getCurrentTitle().text
    title = title:match("^%s*(.-)%s*$") -- This just removes leading/trailing whitespace as a precaution
    
    local cargo_table = "Species"
    local cargo_focal_field = "All_geography"
    local cargo_fields = "Species, All_geography, Image"
    
    local filter_values = args.filter_value or args.filter_values or parent_args.filter_value or parent_args.filter_values or title
    local not_values = args.not_value or args.not_values or parent_args.not_value or parent_args.not_values
	
    -- placeholder image for when there are no images
    local placeholder_image = args.placeholder_image or 'No image available.svg'
    -- set gallery mode
    local gallery_mode = args.gallery_mode or "packed"
    
    
    
    -- ========================================
    -- Run Cargo query and format
    -- ========================================
    local where_condition = string.format("%s HOLDS '%s'", cargo_focal_field, filter_values)
    local cargo_args = {where = where_condition}
   
   
    local cargo_results = cargo.query(cargo_table, cargo_fields, cargo_args)
    
    -- Get results from Cargo table
	local gallery_wikitext = cf.create_cargo_gallery(cargo_results, "Species", "Image", placeholder_image, name_array, gallery_mode)
    local header = "<h1> Species of locust and grasshoppers in " .. filter_values .. " represented in HopperWiki</h1>"
    local gallery = frame:preprocess(gallery_wikitext)
    
    return header .. gallery
    
    
end






















-- ================================================================================
-- Content for higher taxon pages
-- ================================================================================
function p.get_taxon_list(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text

	-- get taxon rank of parent page calling the module
	local rank_table = cf.rank_finder(title)
	local rank = rank_table.lowercase
	local rank_cargo = rank_table.cargo_field
	local rank_title = rank_table.title
	
	-- get downstream child ranks with respect to page rank
	local child_ranks = ""
	if rank ~= "species" then
    	child_ranks = cf.get_child_ranks(rank)
	else
    	return "Warning: 'Module:Higher taxon content' shouldn't be used on species pages."
	end

	
	
	-- check and give error if Cargo arguments aren't present
	if rank_cargo == nil then
			return("Content can't be built because the taxon for this page can't be found in the Cargo database!\n\n")
	end
	
	
	-- cargo query
	local fields = "Family, Subfamily, Genus, Species"
	local tables = "Species"
	local cargo_args = {
		where = string.format("%s = '%s'", rank_cargo, title)
	}
	local cargo_results = cargo.query(tables, fields, cargo_args)
	

	-- output section where output structure is built based on page type
	local field_name = child_ranks["cargo_field"][1] -- Specify the field name
	local taxon_list = cf.bulleted_list(cargo_results, field_name)
	
	-- produce display for forward-facing page
	local output = tostring(mw.html.create('h2'):wikitext("The following " .. child_ranks["plural"][1] .. " are available on HopperWiki for " .. title))
	
	-- site 1
	if taxon_list then
	    output = output .. "\n" .. taxon_list .. "\n"
	end

    
    return output
end










-- ================================================================================
-- Species title italicizer
-- ================================================================================
-- A function to handle italicizing species names based on the page title format
function p.species_title_italicizer(frame)
    local args = getArgs(frame)
    local title = mw.title.getCurrentTitle().text

    if string.find(title, "%(") then
        -- Expand the Italic dab template
        return frame:expandTemplate{ title = 'Italic dab' }
    else
        -- Expand the Italic title template
        return frame:expandTemplate{ title = 'Italic title' }
    end
end












-- ================================================================================
-- Orthoptera Species File connecting information
-- ================================================================================
function p.get_osf(frame)
	
	local args = getArgs(frame)
	local title = mw.title.getCurrentTitle().text
	local species = cf.title_to_sci(title)
    local taxon_author = args.taxon_author
    local output = ""
    
    -- check that sci_name_author manual override arguments haven't been provided
    if not taxon_author then
        -- run cargo query to find the full species name (with author) in the Cargo table
        local tables = "Species"
        local fields = "Species, Taxon_author, OSF_URL"
        local cargo_args = {
            where = "Species = " .. "'" .. species .. "'"
        }
        
        local cargo_results = cargo.query(tables, fields, cargo_args)

        -- get only the first row of cargo_results
        local cargo_row = cargo_results[1]
        
        -- Italicize only the first part of the Taxon_author field
        local binomial_formatted = "<i>" .. cargo_row.Species .. "</i> "
        local taxon_author = cargo_row.Taxon_author:gsub("(%a+ %a+)", "<i>%1</i>")
		local osf_url = cargo_row.OSF_URL
		local osf_link = "[" .. osf_url .. " Orthoptera Species File]"
		
		output = binomial_formatted .. " " .. taxon_author .. ". For full nomenclature, see this taxon's page on " .. osf_link
    else
        output = output .. "<i>" .. cargo_row.Species .. "</i> " .. cargo_row.Taxon_author .. ". For full nomenclature, see this taxon's page on " .. " Orthoptera Species File.]"
    end

    return output

end






return p