Module:Table des isotopes

 Documentation[créer] [purger]
require("strict")

local listeria_datas = require("Module:DonnéesListeria")
local json = require("Module:Jf-JSON")

local p = {}

-- utilities (table manipulation) ------------------------------------

-- trouver l’indice minimal dans un tableau indexé par des entiers
local function find_min_idx(tabl, cmp)
	local min_idx
	cmp = cmp or math.min
	for i,v in pairs(tabl) do
		min_idx = min_idx or i
		min_idx = cmp(i, min_idx)
	end
	return min_idx
end

local function find_max_idx(tabl, cmp)
	local max_idx
	cmp = cmp or math.max
	
	for i,v in pairs(tabl) do
		max_idx = max_idx or i
		max_idx = cmp(i, max_idx)
	end
	assert(max_idx, mw.dumpObject(tabl))
	return max_idx
end

-- like the usual "map" function except the role of the mapped function is taken by a dictionary
local function mapdic(tbl, d)
    local t = {}
    for k,v in pairs(tbl) do
        t[k] = d[v]
    end
    return t
end

table.reduce = function (list, fn, init) 
    local acc = init
    for k, v in ipairs(list) do
        if 1 == k then
            acc = v
        else
            acc = fn(acc, v)
        end 
    end 
    return acc 
end


-- time unit conversion

local function to_minutes(seconds)
	return seconds/60
end

local function from_minutes(minutes)
	return minutes*60
end

local function to_days(seconds)
	return to_minutes(seconds)/(24*60)
end

local function from_days(days)
	return days*24*3600
end

local function to_years(seconds)
	return to_days(seconds)/365.2422
end

local function from_years(years)
	return years*from_days(365.2422)
end

local function from_seconds(seconds)
	return seconds
end


--------------------------------------------------------------------------------
-- module datas
--------------------------------------------------------------------------------
p.classes_fmtdata = {
	{from_seconds(1),             "unstable_atom",
		"instable",
		{
			"background-color: #ffffff",
			"color:black"
		}, 
	},
	{from_days(1),             "between_seconds_and_days",
		"demie vie > 1 s",
		{
			"background-color: #f2f2f2",
			"color:black",
		}, 
	},
	{from_days(10),            "days_stable_atom", 
		"demie vie <10 jours",
		{
			"background-color: #993399",
			"color:white"
		},
	},
	{from_days(100),            "months_stable_atom", 
		"demie vie <100 jours",
		{
			"background-color: #6600cc",
			"color:white"
		},
		{
			"color:white",
			"text-decoration: underline"
		}
	},
	{from_years(10),           "day100_to_year10_stable_atom",
		"demie vie < 10 ans",
		{
			"background-color: #3333ff",
			"color:white"
		},
		{
			"color:white",
			"text-decoration: underline"
		}
	},
	{from_years(10000),         "year100_to_year10000_stable_atom",
		"demie vie < 10 000 ans",
		{			
			"background-color: #33ff33",
			"color:black"
		}, 
	},
	{from_years(1000000),         "year10000_to_year1000000_stable_atom",
		"demie vie < 1 000 000 ans",
		{			
			"background-color: #ffff00",
			"color:black"
		}, 
	},
	{from_years(1000000000000), "stable_radioisotope", 
		"radioisotope stable",
		{
			"background-color: #ff9900",
			"color:black"
		}, 
	},
	{nil,                     "stable_atom"  , 
		"stable",
		{
			"background-color: #A9A9A9",
			"color:black"
		},
	}
}
--------------------------------------------------------------------------------

-- data loading 

function p.data_from_listeria_json(wikipage)
	local datas = mw.title.new(wikipage):getContent()
	datas = datas and listeria_datas.extraction(datas)
	datas=json:decode(datas)
	
	return datas
end

--preprocessing : compute the cells of the htmltable content type

-- compute in which cell the isotopes go

local function insert_in_matrix(isotable, row, col, val)
	
	if not isotable.min then
		isotable.min = row
	else
		isotable.min = math.min(isotable.min, row)
	end
	
	if not isotable[row] then
		isotable[row]={}
	end
	isotable[row][col] = val
end

local Matrix = {}
Matrix.__index = Matrix
--local RevMatrix = {}
--Matrix.__index = RevMatrix

function Matrix:create()
   local matr = {m = {},max_col=0}              -- our new object
   setmetatable(matr, Matrix)   -- make Account handle lookup
--   self.__index = self
   -- matr.max_col = 0
   return matr
end

function Matrix:insert(proton, neutron, val)
	insert_in_matrix(self.m, neutron, proton, val)
	self.max_col=math.max(self.max_col, proton)
end
		
function Matrix:get(proton, neutron)
	assert(proton, "the proton number is nil")
	assert(neutron, "the neutron number is nil")
	assert(self.m[neutron], "nothing at row " .. neutron)
	return self.m[neutron][proton]
end

function Matrix:min_col_idx(col)
	local min = nil
	
	if self.m[col][0] ~= nil then -- special case of Hydrogen without neutrons
		min = 0
		return min
	end
	
	for i, line in pairs(self.m) do
		if type(i) == "number" then -- big hack as the min is stored in the object at key "min" TODO : FIX
			if line[col] then 
				min = (min and math.min(i, min)) or i
			end
		end
	end
	assert(min, "column seems empty")
	return min
end

function Matrix:prot_neutron_at_index(row, col)
	return col, row
end


local RevMatrix = Matrix:create()

--function RevMatrix:create()
--   local matr = {}             -- our new object
--   setmetatable(matr,RevMatrix)  -- make Account handle lookup
--   matr.max_col = 0
--   return matr
--end

function RevMatrix:insert(proton, neutron, val)
	insert_in_matrix(self.m, proton, neutron, val)
	self.max_col=math.max(self.max_col, row)
end
		
function RevMatrix:get(proton, neutron)
	return self.m[proton][neutron]
end


function RevMatrix:prot_neutron_at_index(row, col)
	return row, col
end

-- inserts the isotopes into an integer indexed bidimensional matrix (without the headers)

local function matrix_from_datas(elements, isomatrix)
	
	for num_ato, element in pairs(elements) do
		for num_neutrons, iso in pairs(element.iso) do
			isomatrix:insert(tonumber(num_ato), tonumber(num_neutrons), iso)
		end
	end
	
	return isomatrix
end



-- computes in which row of the html table the atomic number and the atom symbol should go
local function compute_rows_of_column_headers(isotopematrix, elements, rows_matrix, headers_function)
	local prev = 1
	
	for col_idx=1, isotopematrix.max_col do
		local element = elements[tostring(col_idx)]
		
		local header_row_base = isotopematrix:min_col_idx(col_idx)
		
		assert(header_row_base, "nothing in col " .. col_idx .. "/" .. isotopematrix.max_col)
		
		-- décallage négatif si on est dans un trou
		if elements[tostring(col_idx+1)] and header_row_base > 2 then
			if (elements[tostring(col_idx+1)].iso[tostring(header_row_base-1)]) 
			then
				header_row_base = prev
			end
		end
		
		local headers = headers_function(elements, col_idx)
		for n, header in ipairs(headers) do
			insert_in_matrix(rows_matrix, header_row_base - #headers + n  - 1, col_idx, header)
		end
		
		prev=header_row_base
	end
	return rows_matrix
end


local function isotope_cell_content(iso, element, num_nucleons)
	local label = "<small><sup>" .. num_nucleons .. "</sup></small>" .. element.s
	if iso.lien then
		return "[["..iso.lien .. "|" .. label.. "]]"
	end
	return label
end

-- table formatting

local function class_by_halflife(dv, range_map)
	if not(dv) then 
		return range_map[1]["class"]
	end
	for i, bound_data in ipairs(range_map) do
		local bound = bound_data["bound"]
		if not(bound) or dv < bound then
			return bound_data["class"]
		end
	end
	return range_map[#bound_data]["class"]
end

local function is_stable(isotope_variant)
	return isotope_variant and isotope_variant.stable
end

-- compare isotopes according to their half life
local function is_stabler_or_equal(iso1, iso2)
	if is_stable(iso1) and is_stable(iso2) then
		return iso1.init_key > iso2.init_key
	elseif is_stable(iso1) then
		return true
	elseif is_stable(iso2) then
		return false
	else
		if iso1.dv and not iso2.dv then
			return true
		elseif iso2.dv and not iso1.dv then
			return false
		elseif iso1.dv ~= nil and iso2.dv ~= nil then
			return iso1.dv > iso2.dv
		end
	end
	return iso1.init_key < iso2.init_key
end


-- computes the css classes relevant for some isotope
local function isotope_cell_classes(fmt_datas, isotopes_in_cell, classes)
	local sort = true
	
	-- force a total ordering for indishinguishible elements
	for i = 1, #isotopes_in_cell do
		isotopes_in_cell[i].init_key = i 
	end
	
	-- tri des variants isotopiques par stabilité
	table.sort(isotopes_in_cell, is_stabler_or_equal)
	
	local classes = {}
	local last
	
	for i, iso in ipairs(isotopes_in_cell) do
		local class 
		if is_stable(iso) then 
			class="stable_atom"
		else
			class=class_by_halflife(iso.dv, fmt_datas.bounds)
		end

		if class ~= last then
			classes[#classes+1] = class 
			last = class
		end
	end
	return classes
end


local function html_isotope_cell(fmt_datas, html_line, isotopematrix, elements, row_num, col_num)
	local isos = isotopematrix:get(row_num, col_num)
	
	local atomic_num, neutron_num = isotopematrix:prot_neutron_at_index(row_num, col_num)
	
	if not(isos) then
		html_line:tag("td"):done()
	else
		local isotope_sym = isotope_cell_content(isos[1], elements[tostring(neutron_num)], atomic_num+neutron_num)
		local classes = isotope_cell_classes(fmt_datas, isos)
		local html_cell = 
			html_line
				:tag("td")
		-- help to debug
		local hls = {}
		for x, iso in pairs(isos) do
			if iso.dv then 
				hls[#hls+1] = tostring(iso.dv) .. " s"
			end
		end

		local heading = table.concat(hls, " ; ")
		
		
		if #isos == 1 then
			heading = heading .. " "
		else
			heading = heading .. tostring(" " .. #isos .. " variantes: ")
		end
		
		if #isos == 1 or #classes == 1 then
		    html_cell:addClass(classes[1])
		    		 :attr("title", heading .. fmt_datas.messages[classes[1]])
		    		 :wikitext(isotope_sym)
		else
			html_cell:addClass(classes[2])
					 :tag("div")
						:addClass(classes[1])
						:attr("title", heading .. table.concat(mapdic(classes, fmt_datas.messages), " ; " ))
						:wikitext(isotope_sym)
					 :done()
		end
		html_line:done()
	end
end

local function insert_needed_colspan(html, last, current)
	if current > last + 1 then
		html:tag("td")
	      :attr("colspan", current - last - 1)
	      :addClass("empty_cell")
	      :done()
	end
	return html
end

local function wikilink(title, text)
	return "[[" .. title .. "|" .. text .. "]]"
end


local function empty_cell(hrow, size)
	if size > 0 then
		hrow
			:tag("td")
				:attr("colspan", size)
				:addClass("empty_cell")
				:done()
	end
end

local function wikitext_element(element)
	if element.lien then
		return wikilink(element.lien, element.s)
	else
		return element.s
	end
end


local function element_headers_cells(elements, num_ato)
	local element = elements[tostring(num_ato)]
	local element_number_string = num_ato
	if element and element.isolist then
		element_number_string = wikilink(element.isolist, num_ato)
	end
	return {
		mw.html.create("th")
			:wikitext(num_ato)
			:done(),
		mw.html.create("th")
			:wikitext(wikitext_element(element))
			:done()
	}
end

local function neutron_header_cells(neutron_num)
	return {
		mw.html.create("th")
			:wikitext(neutron_num)
			:done()
	}
end

local function insert_headers_in_row(hrow, headers, col_decay, first)
	if first then
		hrow:node(first)
		empty_cell(hrow, col_decay - 1)
	else
		empty_cell(hrow, col_decay)
	end
	if headers then
		for _, header in ipairs(headers) do
			hrow:node(header)
		end
	end
end


-- génère un tableau html d’isotopes, à partir 
---- fmt_datas : les informations de style
---- isomatrix : la table des isotopes
---- headermatrix : les en-tête 

local function to_html_table(fmt_datas, isomatrix, headermatrix, elements)
	local htable = mw.html.create("table")
	htable:addClass("wikitable")
	
	local row_ref = headermatrix.min
	
	local headers_ref = neutron_header_cells(1)
	
	local col_decay = #headers_ref  
	
	local row_header_title = mw.html.create("th"):wikitext(wikilink("Proton" ,"p")):done()
	local col_header_title = mw.html.create("th"):wikitext(wikilink("Neutron" ,"n")):done()

	local hrow = htable:tag("tr")
	insert_headers_in_row(hrow, headermatrix[row_ref], col_decay, row_header_title)
	
	hrow:done()
	row_ref = row_ref + 1
	
	-- find and store the table data first row number
	local min_row_idx = 1
	if isomatrix.m[0] then
		min_row_idx = 0
	end
	
	-- fill the space between the highest column header and the first row with the needed headers
	while(row_ref < min_row_idx ) do
		local hrow = htable:tag("tr")
		local first = nil
		if row_ref == min_row_idx - 1 then
			first = col_header_title
		end
		insert_headers_in_row(hrow, headermatrix[row_ref], col_decay, first)
		htable:done()
		row_ref = row_ref +1
	end

	for row_num = min_row_idx, #isomatrix.m do
		local hrow = mw.html.create("tr") 
		local data_row = isomatrix.m[row_num]
		
		local minidx = math.max(find_min_idx(data_row), 1)
		local maxidx = find_max_idx(data_row)
	
		local headers_in_row = headermatrix[row_num]
		local headers = neutron_header_cells(row_num)
		
		
		empty_cell(hrow, - col_decay + minidx)
		for _, header in ipairs(headers) do
			hrow:node(header)
		end
		assert (next(data_row), "data row is empty for row" .. row_num)
	
		-- populate table rows
		for col_num = minidx, maxidx do
			html_isotope_cell(
								fmt_datas, hrow, isomatrix, elements, 
								col_num, row_num
							)
		end
		
		local last_full_cell_index = maxidx
		if headers_in_row then
			local str=tostring(maxidx)
			for i,x in ipairs(headers_in_row) do
				str = str .. " " .. i
			end
			mw.log(str)
		end
		if headers_in_row then
			assert (next(headers_in_row), "header row is empty for row " .. row_num)
			for column = maxidx, find_max_idx(headers_in_row) do
				local cell_content = headers_in_row[column]
				if cell_content then
					insert_needed_colspan(hrow, last_full_cell_index, column)
					
					-- adding columns headers (element symbol, element atomic 
					-- number and links to the element and its isotope articles)
					
					hrow:node(cell_content)
					last_full_cell_index = column
				end
			end
		end
		
		htable:node(
			hrow
		)
		--end --
	end
	return htable
end

function p.preprocess_fmtdatas(fmt_datatable)
	local fmt_datas = {classes = {}, bounds={}, messages={}, styles={}, linkstyles={}}
	for i, class_datas in pairs (p.classes_fmtdata) do
		local class_lbl = class_datas[2]
		fmt_datas.classes[i] = class_lbl
		fmt_datas.bounds[i] = { bound = class_datas[1], class=class_lbl }
		fmt_datas.messages[class_lbl] = class_datas[3]
		fmt_datas.styles[class_lbl] = class_datas[4] 
		fmt_datas.linkstyles[class_lbl] = class_datas[5] 
	end
	return fmt_datas
end

local function logmatrix(datamatrix)
	for x, row in ipairs(isomatrix.m) do
		local str=""
		for y, c in pairs(row) do
			str = str.. "(" .. x .. ",".. y .. ")"
		end
		mw.log(str)
	end
end

function p.tableau(frame)
	local elements = p.data_from_listeria_json("Modèle:Table des isotopes/données")
	
	local fmt_datas = p.preprocess_fmtdatas(p.classes_fmtdata)
	local isomatrix = matrix_from_datas(elements, Matrix:create())

	local headermatrix = compute_rows_of_column_headers(isomatrix, elements, Matrix:create(), element_headers_cells)
	local htmltable = to_html_table(fmt_datas, isomatrix, headermatrix, elements)
	
	return p.legend(frame) .. tostring(htmltable) 
end

function p.legend(frame)
	local fmt_datas = p.preprocess_fmtdatas(p.classes_fmtdata)
	local htable = mw.html.create("table")
	htable:addClass("wikitable")
	htable:tag("caption"):wikitext("légende"):done()
	
	
	for i, cell in pairs(fmt_datas.bounds) do
		htable:tag("tr")
				:tag("td")
					:addClass(cell.class)
					:wikitext("isotope")
				:done()
				:tag("td")
					:wikitext(fmt_datas.messages[cell.class])
				:done()
			:done()
	end
	return tostring(htable:done())
end

local function css_class(classes, props, selector_prefix, selector_suffix)
	local res = "."
	selector_prefix = selector_prefix or ""
	selector_suffix = selector_suffix or ""
	
	return selector_prefix .. "." .. table.concat(classes," .") .. selector_suffix .. "{\n\t" .. table.concat(props, ";\n\t") .. ";\n}\n" 
end


-- utilisation pour générer la CSS, dans la console lua d’édition de modules : 
-- taper « = p.gen_css(p.classes_fmtdata) »

p.gen_css = function(fmtdatas)
	fmtdatas = p.preprocess_fmtdatas(fmtdatas)
	local styles = fmtdatas.styles
	local linkstyles = fmtdatas.linkstyles
	local classes = fmtdatas.classes

	for i, class in ipairs(classes) do
		mw.log(css_class({class}, styles[class]))
		if linkstyles[class] then
			mw.log(css_class({class},linkstyles[class], "td", ">a"))
			mw.log(css_class({class},linkstyles[class], "td>div" ,">a"))
		end
	end
end

return p