# Module:Shindo

--- This is the Shindo module. Named after the Japanese term used for classifying earthquakes , this module provides utility for seismic intensity scales.  
---
-- Seismic intensity data generated on Module:Shindo/data. Messages on Module:Shindo/messages.  
-- @module shindo  
-- @alias p  
-- @require Module:Arguments  
-- @require Module:MakeInvokeFunc  
-- @require Module:Message  
-- @require Module:Yesno  
-- @release beta  
---

local p = {}  
local getArgs = require("Module:Arguments").getArgs  
local data = mw.loadData("Module:Shindo/data")  
local messages = mw.loadData("Module:Shindo/messages")  
local makeInvokeFunc = require("Module:MakeInvokeFunc")(p)  
local message = require("Module:Message")(messages)  
local yn = require("Module:Yesno")  

--- Gets the grayscale value of a specific color  
-- @function getValueOfColor  
-- @param {table} tbl table for colors  
-- @param {number} tbl[1] red value  
-- @param {number} tbl[2] green value  
-- @param {number} tbl[3] blue value  
-- @return {number} Decimal value of the color.  
local function getValueOfColor(tbl)  
return (tbl[1] + tbl[2] + tbl[3]) / 3 / 255  
end  

--- Gets the color as a comma-separated value for CSS  
-- @function rgbColor  
-- @todo compute alpha transparency for dark mode, etc.  
-- @param {table} tbl table for colors  
-- @param {number} tbl[1] red value  
-- @param {number} tbl[2] green value  
-- @param {number} tbl[3] blue value  
-- @return {string} The color in the format red, green, blue  
local function rgbColor(tbl)  
return tbl[1] .. (tbl[2] and ", " .. tbl[2] .. (tbl[3] and ", " .. tbl[3] or "") or "")  
end  

--- Gets the average of multiple colors  
-- @function averageColor  
-- @param {table} tbl table of multiple colors  
-- @return {number} Decimal value of the color.  
local function averageColor(tbl)  
local colors = {}  
for k,v in pairs(tbl) do  
colors[k] = v  
end  
local avg = {0, 0, 0}  
for _,color in pairs(colors) do  
avg[1] = avg[1] + color[1]  
avg[2] = avg[2] + color[2]  
avg[3] = avg[3] + color[3]  
end  
avg[1] = math.ceil(avg[1] / #colors) - avg[1] / #colors > 0.5 and math.floor(avg[1] / #colors) or math.ceil(avg[1] / #colors)  
avg[2] = math.ceil(avg[2] / #colors) - avg[2] / #colors > 0.5 and math.floor(avg[2] / #colors) or math.ceil(avg[2] / #colors)  
avg[3] = math.ceil(avg[3] / #colors) - avg[3] / #colors > 0.5 and math.floor(avg[3] / #colors) or math.ceil(avg[3] / #colors)  
return avg  
end  

--- Gets all the intensity values from a scale  
-- @function listIntensitiesFromScale  
-- @param {string} scale First scale name  
-- @param {string} intensities Table of intensity values  
-- @param {boolean} labelScale Whether to label a scale  
-- @param {string} scale Second scale name  
-- @return {string} List of intensities  
local function listIntensitiesFromScale(scale, intensities, labelScale, scale2)  
local out = ''  
if yn(labelScale) then  
out = out .. data[scale].short .. ' '  
end  
local categoriesAreSame = true  
local category = ""  
for k,v in pairs(intensities) do  
if data[scale].ranks[v] == nil then error(message("invalidIntensity", {v, scale})) end  
out = out .. data[scale].ranks[v].label  
if k ~= #intensities then  
if #intensities == 2 then  
out = out .. '–'  
else  
out = out .. '/'  
end  
end  
if category == "" and categoriesAreSame then  
category = data[scale].ranks[v].category or ""  
end  
categoriesAreSame = categoriesAreSame and category ~= "" and (data[scale].ranks[v].category and data[scale].ranks[v].category == category or false)  
end  
if categoriesAreSame and category ~= "" and not scale2 then  
out = out .. " (*" .. category .. "*)"  
end  
return out  
end  

--- Gets the inline CSS for a particular scale's intensity color  
-- @function p._color  
-- @param {table} args arguments  
-- @param {string} args.scale The name of the scale  
-- @param {string} args.intensity The intensity of the scale  
-- @return {string} Preprocessed text output  
p._color = function(args)  
local scale = string.lower(args.scale or args[1] or error(message("noScaleShortCode")))  
local intensity = string.upper(args.intensity or args[2] or error(message("noIntensity")))  
if data[scale] == nil then error(message("invalidScale", {scale})) end  
if data[scale].ranks[intensity] == nil then error(message("invalidIntensity", {intensity, scale})) end  
local order = data[scale].ranks[intensity].order  
local color = data[scale].colors[order]  
local colorIntensity = getValueOfColor(color)  
return 'background-color:rgb(' .. rgbColor(color) .. '); color:' .. (colorIntensity < 0.5 and "white" or "black") .. ";"  
end  

--- Gets the format of the scale as a wikitable  
-- @function p._formatInWikitable  
-- @param {table} args arguments  
-- @param {string} args.scale The name of the scale  
-- @param {string} args.intensity The intensity of the scale  
-- @param {string} args.tagProps additional properties for the wikitable row  
-- @param {boolean} args.header if the formatting is done as a header  
-- @param {boolean} args.link whether to link to the scale page  
-- @param {boolean} args.labelScale whether to include the name of the scale  
-- @return {string} Preprocessed text output  
p._formatInWikitable = function(args)  
local scale = string.lower(args.scale or args[1] or error(message("noScaleShortCode")))  
local link = args.link ~= nil and args.link or true  
local labelScale = args.labelScale ~= nil and args.labelScale or true  
local doColor = args.color ~= nil and args.color or true  
local intensity = string.upper(args.intensity or args[2] or error(message("noIntensity")))  
local intensities = mw.text.split(intensity, "/") or { intensity }  
if data[scale] == nil then error(message("invalidScale", {scale})) end  
local colors = {}  
for k,v in pairs(intensities) do  
local order = data[scale].ranks[v].order  
colors[k] = data[scale].colors[order]  
end  
local color = averageColor(colors)  
local colorIntensity = getValueOfColor(color)  
local out = ""  
if yn(args.header or false) then  
out = out .. "! "  
else  
out = out .. "| "  
end  
out = out .. (args.tagProps ~= nil and args.tagProps or "")  
if (yn(doColor)) then  
out = out .. 'white" or "black") .. ';' .. (args.style or "") .. '" | '  
elseif (args.style) then  
out = out .. '") .. '" | '  
end  
if yn(link) then  
out = out .. '[[' .. data[scale].name .. "#" .. data[scale].id_prefix .. data[scale].ranks[intensity].id .. "|"  
end  
if yn(doColor) then  
out = out .. '<span style=\"color:' .. (colorIntensity < 0.5 and "white" or "black") .. ';">'  
end  
out = out .. listIntensitiesFromScale(scale, intensities, labelScale, false)  
if yn(doColor) then  
out = out .. '</span>'  
end  
if yn(link) then  
out = out .. "]]"  
end  
return out  
end  

--- Gets the format of the scale as a tag  
-- @function p._formatTag  
-- @param {table} args arguments  
-- @param {string} args.scale The name of the scale  
-- @param {string} args.scale2 The name of a second scale  
-- @param {string} args.intensity The intensity of the scale  
-- @param {string} args.intensity2 The intensity for the second scale  
-- @param {string} args.tag Tag name  
-- @param {string} args.style styling for the tag  
-- @param {string} args.tagProps additional properties for the tag  
-- @param {boolean} args.color Whether to color the entry  
-- @param {boolean} args.link whether to link to the scale page  
-- @return {string} Preprocessed text output  
p._formatTag = function(args)  
local scale = string.lower(args.scale or args[1] or error(message("noScaleShortCode")))  
local scale2 = args.scale2 or args[3] or nil  
if scale2 ~= nil then scale2 = string.lower(scale2) end  
local link = args.link ~= nil and args.link or true  
local labelScale = args.labelScale ~= nil and args.labelScale or true  
local doColor = args.color ~= nil and args.color or true  
local intensity = string.upper(args.intensity or args[2] or error(message("noIntensity")))  
local intensities = mw.text.split(intensity, "/") or { intensity }  
local intensity2 = args.intensity2 or args[4] or (scale2 and error(message("noIntensity"))) or nil  
local intensities2 = {}  
if intensity2 ~= nil then  
intensity2 = string.upper(intensity2)  
intensities2 = mw.text.split(intensity2, "/") or { intensity2 }  
end  
if data[scale] == nil then error(message("invalidScale", {scale})) end  
if scale2 and data[scale2] == nil then error(message("invalidScale", {scale2})) end  
local colors = {}  
for k,v in pairs(intensities) do  
local order = data[scale].ranks[v].order  
colors[k] = data[scale].colors[order]  
end  
local color = averageColor(colors)  
local colorIntensity = getValueOfColor(color)  
local out = ''  
if yn(doColor) then  
out = out .. '<' .. (args.tag or "span") .. 'white" or "black") .. '; '  
elseif args.style then  
out = out .. '<' .. (args.tag or "span") .. 'span")  
end  
if args.style then  
out = out .. args.style  
out = out .. '" '  
elseif yn(doColor) then  
out = out .. '" '  
end  
out = out .. (args.tagProps ~= nil and args.tagProps or "")  
out = out .. ">"  
if yn(link) then  
out = out .. '[[' .. data[scale].name .. "#" .. data[scale].id_prefix .. data[scale].ranks[intensities[1]].id .. "|"  
end  
if yn(doColor) then  
out = out .. '<span style=\"color:' .. (colorIntensity < 0.5 and "white" or "black") .. ';">'  
end  
out = out .. listIntensitiesFromScale(scale, intensities, labelScale, scale2)  
if yn(doColor) then  
out = out .. '</span>'  
end  
if yn(link) then  
out = out .. "]]"  
end  
if (scale2) then  
out = out .. " ("  
if yn(link) then  
out = out .. '[[' .. data[scale2].name .. "#" .. data[scale2].id_prefix .. data[scale2].ranks[intensities2[1]].id .. "|"  
end  
if yn(doColor) then  
out = out .. '<span style=\"color:' .. (colorIntensity < 0.5 and "white" or "black") .. ';">'  
end  
out = out .. listIntensitiesFromScale(scale2, intensities2, true, true)  
if yn(doColor) then  
out = out .. '</span>'  
end  
if yn(link) then  
out = out .. "]]"  
end  
out = out .. ")"  
end  
out = out .. '</' .. (args.tag or "span") .. '>'  
return mw.getCurrentFrame:preprocess(out)  
end  

p._format = function(args)  
if args["format"] == "wikitable" then  
return p.formatInWikitable(args)  
else  
return p.formatTag(args)  
end  
end  

p._getScaleName = function(args)  
local scale = string.lower(args.scale or args[1] or error(message("noScaleShortCode")))  
if data[scale] == nil then error(message("invalidScale", {scale})) end  
local out = ''  
if yn(args.link or true) then  
out = out .. '[[' .. data[scale].name .. '|'  
end  
out = out .. data[scale].name  
if yn(args.link or true) then  
out = out .. ']]'  
end  
return out  
end  

-- uses binary search to convert a peak ground acceleration to a seismic intensity  
function convert(pga, ranks, ranksSorted, , )  
= ~= nil and or 0  
= ~= nil and or #ranksSorted  
index = math.floor(( + ) / 2)  
local lower = ranks[ranksSorted[index + 1]].pga  
local upper = ranksSorted[index + 2] and ranks[ranksSorted[index + 2]].pga or math.huge  
if (pga >= upper) then  
return convert(pga, ranks, ranksSorted, index, )  
elseif (pga < lower) then  
return convert(pga, ranks, ranksSorted, , index)  
else  
return ranksSorted[index + 1]  
end  
end  

p._convert = function(args)  
local scale = string.lower(args.scale or args[1] or error(message("noScaleShortCode")))  
if data[scale] == nil then error(message("invalidScale", {scale})) end  
local ranksSorted = {}  
for k,v in ipairs(data["mmi"].ranksSorted) do ranksSorted[k] = v end  
return convert(tonumber(args.pga or args[2]), data[scale].ranks, ranksSorted)  
end  

p.convert1 = convert --debugging  

p._list = function(args)  
local out = mw.html.create('ul')  
for k,v in pairs(data) do  
local list = out:tag('li')  
list:wikitext('<code>' .. k .. '</code>: ' .. v.name .. '')  
local tb = list:tag('table'):addClass("wikitable")  
tb  
:tag("tr")  
:tag("th"):wikitext("Seismic intensity"):done  
:tag("th"):wikitext("Display"):done  
:done  
for l,w in pairs(v.order) do  
tb:tag('tr'):wikitext('<td><code>' .. w .. '</code></td> ' .. p.format({k, w, tag = "td"})):done  
end  
list:done  
end  
out:allDone  
return tostring(out)  
end  

-- make each scale invokable  
for k,v in pairs(data) do  
if p["_" .. k] ~= nil then error(message("scaleNameInvalid", k)) end  
p["_" .. k] = function(args)  
args["scale"] = k  
args["intensity"] = args["intensity"] or args[1] or nil  
args["scale2"] = args["scale2"] or args[2] or nil  
args["intensity2"] = args["intensity2"] or args[3] or nil  
if args["format"] == "wikitable" then  
return p.formatInWikitable(args)  
else  
return p.formatTag(args)  
end  
end  
end  

-- make all functions that begin with _ invokable  
local q = mw.clone(p)  
for k,v in pairs(q) do  
if mw.ustring.sub(k, 1, 1) == "_" then  
p[mw.ustring.sub(k, 2, #k)] = makeInvokeFunc(k)  
end  
end  

return p