Modulo:TeamBracket

Versione del 2 mag 2018 alle 05:38 di Moroboshi (discussione | contributi) (fix uso getWidth)

Questo modulo serve a realizzare dei tabelloni per i tornei sportivi.

(legenda colori)

{{#invoke:TeamBracket|teamBracket
|compact = yes/no [default: yes]
|rounds =
|maxround = deve essere inferiore a rounds
|headings = valorizzare con no per non farli apparire
|RD-shade = colore di sfondo dell'intestazione del round (se diverso da quello di default)
|RDn-RDn+1-path = valorizzare con 0 per non far apparire i collegamenti fra i due round selezionati
|byes = valorizzare con il numero di turni in cui un incontro potrebbe non verificarsi (bye, walkover, ecc.)
|seeds = valorizzare con yes per farli apparire in ogni turno
|autoseeds = yes/no [default: no]
|sets = alternativo a legs (se variano da turno a turno, separare con /)
|legs = alternativo a sets (se variano da turno a turno, separare con /)
|hideomittedscores = yes/no [default: no]
|aggregate = valorizzare con yes, sets o legs
|boldwinner = yes/no [default: no]
|seed-width = [default: 28px]
|team-width = [default: 170px]
|score-width = [default: 28px]
|RDn = intestazione del round (se diverso dal nome di default)
|RD-finalina = intestazione della finale per il 3º posto (se diverso dal nome di default)
|RDn-groupm =
|RDn-seedm = seed della squadra o del partecipante
|RDn-teamm = squadra o partecipante
|RDn-scorem = punteggio della squadra o del partecipante
|RDn-scorem-s = punteggio della squadra o del partecipante (se l'incontro prevede più di una gara o di un set)
|RDn-shadem-s = colore di sfondo del risultato (se diverso da quello di default)
|RDn-scorem-agg = sovrascrive il valore automatico
}}


--
-- This module will implement {{TeamBracket}}
--
require('Module:No globals')
local getArgs = require('Module:Arguments').getArgs

local p = {}
local yes_replies = { 'yes', 'y', 's', 'si', 'sì'}
local no_replies = { 'n', 'no'}
local debug = false

local style = {
	table = "border-style:none;font-size:90%;margin:1em 2em 1em 1em;border-collapse:separate;border-spacing:0",
	seed_cell = "background-color:#f2f2f2;border:1px solid #aaa;text-align:center;",
	team_cell = "background-color:#f2f2f2;border:1px solid #aaa;text-align:left;", --padding:0 2px
	score_cell ="background-color:#f9f9f9;border:1px solid #aaa;text-align:center;",
	header_third_place = "text-align:center;border:1px solid #aaa;background-color:#f2f2f2",
	header_cell = "text-align:center;border:1px solid #aaa;background-color:#f2f2f2",
	path_bottom = "border:0 solid black;border-bottom-width:1px;",
	path_rigth_top = "border:0 solid black;border-top-width:1px;border-right-width:2px;",
	path_right_bottom = "border:0 solid black;border-bottom-width:1px;border-right-width:2px;",
	path_top = "border:0 solid black;border-top-width:2px;",
	group = "text-align:center", 
	winner = 'font-weight:bold',
	first_place = 'background-color:gold',
	second_place = 'background-color:silver',
	third_place = 'background-color:#C96',
	row_height = "3px", 
	buffer_sx_width = "5px",
	seed_width = "25px",
	team_width = "150px",
	score_width = "25px",
	row_width = "10px"
}

if debug then
	style.group = style.group .. ";background-color:green"
	style.path_bottom = style.path_bottom .. ";background-color:#F08080"
	style.path_top= style.path_top .. ";background-color:#F08080"
	style.path_rigth_top= style.path_rigth_top .. ";background-color:#F08080"
	style.path_right_bottom= style.path_right_bottom .. ";background-color:#F08080"
end

--[[ ===============================================================================
Ritorna true se needle è lista haystack, altrimenti ritorna false
	===============================================================================]]
local function in_array( needle, haystack )
	if needle == nil then return false; end
	for n,v in ipairs( haystack ) do
		if v == needle then return true; end
	end
	return false;
end

--[[ ===============================================================================
Oggetto per generare un TeamBracket
- args: array dei parametri passati al modulo
===============================================================================]]
local TeamBracket = {}

function TeamBracket:new(args)

	local self = {}
	setmetatable(self, { __index = TeamBracket,
						 __tostring = function(t) return self:__tostring() end })

	self.args = args
	self.rounds = tonumber(self.args['rounds']) or 2
	self.teams = math.pow(2, self.rounds)
	self.compact = in_array(self.args['compact'], yes_replies)
	self.hideSeeds = in_array(self.args['seeds'], no_replies)
	self.showSeeds = in_array(self.args['seeds'], yes_replies)
	local padding = '%0' .. ((self.teams < 10) and 1 or 2) .. 'd'
	self.argname_pattern = 'RD%d-%s' .. padding
	self.scorename_pattern = 'RD%d-score' .. padding
	local autobold_par = self.args['bold_winner'] or ''
	self.bold_winner = not in_array(autobold_par, no_replies)
	self.low_winner = self.bold_winner and (autobold_par == 'low') 
	self.medals = self.bold_winner and not in_array(self.args['medals'], no_replies)
	-- load number of scores for each round
	self.scores =  {}
	local scores_raw = self.args.scores or '1'
	local max_scores = 1
	for i,score_value in ipairs(mw.text.split(scores_raw, ',')) do
		self.scores[i] = tonumber(score_value) or 1
		if self.scores[i] > max_scores then max_scores = self.scores[i] end
	end
	local last_scores = self.scores[#self.scores]
	for i =#self.scores+1, self.rounds do
		self.scores[i] = last_scores
	end
	if max_scores > 1 then
		self.scorename_pattern = self.scorename_pattern .. "-%d"
	end
	-- custom style
	style.winner = self.args.style_medal or style.winner
	style.first_place = self.args.style_first_place or style.first_place
	style.second_place = self.args.style_second_place or style.second_place
	style.third_place = self.args.style_third_place or style.third_place
	style.header_cell = self.args.style_header or style.header_cell
	style.header_third_place = self.args.style_header_third_place or style.header_third_place
	style.seed_width = self:getWidth('seed', style.seed_width)
	style.team_width = self:getWidth('team', style.team_width)
	style.score_width = self:getWidth('score', style.score_width)
	-- set default seeds for round 1
	if not(self.hideSeeds) then
		local seeds = self:getSeeds()
		local argname
		for i = 1, table.getn(seeds) do
			argname = self:getTeamArgName(1, 'seed', i)
			if not args[argname] then
				args[argname] = seeds[i]
			end
		end
	end
	self.tbl = mw.html.create('table'):cssText(style.table)
	if in_array(args['nowrap'], yes_replies) then
		self.tbl:css('white-space', 'nowrap')
	end
	self.rows = { }
	if self.compact then self.tbl:attr('cellpadding', '0') end
	self.last_element = {}
	self.rows = {}
	self.current_col = 0
	self.dump = {}
	self.not_draw_top = false
	self:renderHeading()
	self:renderTree()
	return self
end

--[[ ===============================================================================
-- collassa l'oggetto in una stringa contenente il wikicodice per generare l'albero
===============================================================================]]
function TeamBracket:__tostring()
	return tostring(self.tbl)
end

--[[ ===============================================================================
Genera i valori di default dei seeds
===============================================================================]]
function TeamBracket:getSeeds()
	local seeds = {1, 2}
	local count = 2
	local before = false
	for r = 2, self.rounds do
		local max = math.pow(2, r)
		for i = 1, count do
			local pos = i * 2
			if before then pos = pos - 1 end
			table.insert(seeds, pos, max - seeds[i * 2 - 1] + 1)
			before = not before
		end
		count = count * 2
	end
	return seeds
end

--[[ ===============================================================================
Se fino alla riga row, colonna col-1 non sono ancora stati aggiunti elementi aggiunte
una cella vuota dall'ultima colonna usta dalla riga. 
Aggiorna quindi il conteggio delle colonne usate alla posizione colonna +width 
Il valore di default di width è 0 
=============================================================================== --]]
function TeamBracket:addGap(row, col, width, debug_info)
	width = width or 1
	if self.last_element[row] + 1 < col  then
		local gap = tostring(col - self.last_element[row] - 1)
		local cell=self.rows[row]:tag('td'):css('background-color', (debug and '#F5F5DC') or ''):attr('colspan', gap)
		if debug then
			cell:wikitext(string.format('<div style="text-align:center;" title="row=%d, colonna:%d, last_col=%d, width:%d, gap:%s, last_col_set:%d, round:%d, deb:%s">info</div>',
							row, col, self.last_element[row], width, gap, col+width, self.current_round, debug_info or ''))
		end
	end
	self.last_element[row] = col + width - 1 
end

function TeamBracket:getWidth(param, default)
	local arg = self.args[param .. '-width']
	if not arg then return default end
	arg = arg .. ((tonumber(arg) and 'px') or '')
	return arg
end

function TeamBracket:getTeamArgName(round, argname, team)
	return string.format(self.argname_pattern, round, argname, team)
end

function TeamBracket:getTeamArg(round, argname, team)
	return self.args[self:getTeamArgName(round, argname, team)]
end

function TeamBracket:getScoreArg(round, team, score)
	return self.args[string.format(self.scorename_pattern, round, team, score)]
end

function TeamBracket:getRoundName(round)
	local name = self.args['RD' .. round]
	if name then return name end
	local round_names = {"Finale", "Semifinali", "Quarti", "Ottavi"}
	local roundFromLast = self.rounds - round + 1
	if roundFromLast < 5 then
		return round_names[roundFromLast]
	else
		return tostring(round) .. "° turno"
	end
end

--[[ ===============================================================================
Aggiunge le righe grafiche di congiunzione delle celle per la riga index del
turno round. top e left indicano se deve essere generata rispettivamente la riga
sinistra o quella superiore
- row1 = prima riga del match superiore da cui esce il path
- row2 = prima riga del match inferiore da cui esce il path
- row_gap = ampiezza del path
- col = colonna in cui inizia il path
row1 nullo significa che il path parte solo da row2 e viceversa. Se entrambi sono
nulli la funzione non disegna niente.
===============================================================================]]
function TeamBracket:addPath(row1, row2, row_gap, next_match)

	local col = self.current_col + self.current_width  
	local half_gap = row_gap / 2
	if not (row1 or row2) then return end
	self.render_match[self.current_round+1][next_match] = true
	-- first half
	if row1 then 
		self.rows[row1+1]:tag('td'):cssText(style.path_bottom)
		for r = row1+1, row1+3 do self.last_element[r] = col end
		for r = row1+4, row1+4+half_gap-1 do self:addGap(r, col, 1, 'ap1') end
		self.rows[row1+2]:tag('td')
			:attr('rowspan', half_gap + 2)
			:cssText(style.path_rigth_top)	
	end
	--second half
	if row2 then
		self.rows[row2+2]:tag('td'):cssText(style.path_top)
		for r = row2, row2+2 do self.last_element[r] = col end
		for r = row2-half_gap, row2-1 do self:addGap(r, col, 1, 'ap2') end
		self.rows[row2-half_gap]:tag('td')
			:attr('rowspan', half_gap+2)
			:cssText(style.path_right_bottom)
	end
	local middle_row
	middle_row  = (row1 and (row1 + 2 + half_gap)) or (row2 - half_gap -2)
	for r = middle_row-1, middle_row+2 do self:addGap(r, col+1, 1, 'ap3') end
	--self:addGap(middle_row, col+1, 1, 'ap3')
	--self:addGap(middle_row+1, col+1, 1, 'ap3')
	self.rows[middle_row]:tag('td'):cssText(style.path_bottom):attr('rowspan', '2')
	self.rows[middle_row+2]:tag('td'):cssText(style.path_top):attr('rowspan', '2')
end

--[[ ------------------------------------------------------------
Compare score values and return and return the winner of the match 
(team with majority of win, 0 mean parity, 1 fist team win, 2
second team win) and and array with winner for single scores (for
each single score 0 parity, 1 first team win, 2 second team win.
 ------------------------------------------------------------ --]]
function TeamBracket:getWinner(first_team_number)
	local victories1 = 0
	local victories2 = 0
	local score_results = {}
	for i = 1, self.scores[self.current_round] do
		local score1 = self:getScore(self.current_round, first_team_number, i)
		local score2 = self:getScore(self.current_round, first_team_number+1, i)
		if score1 and score2 then 
			if score1 ~= score2 then
				local first_winner = score1 > score2
				if self.low_winner then first_winner = not first_winner end
				if first_winner then 
					victories1 = victories1 + 1
					score_results[i] = 1
				else 
					victories2 = victories2 + 1
					score_results[i] = 2
				end
			end
		end
		score_results[i] = score_results[i] or 0
	end
	if victories1 == victories2 then return 0, score_results end
	if victories1 > victories2 then return 1, score_results end
	return 2, score_results
end

--[[ ===============================================================================
Disegna la cella per il team.
-- team_name: il nome del team
-- team_numer: il numero del team nella sequenza del round
-- row: la riga della prima cella in cui disegnare il team
-- top: true se è il primo team del match
-- winner: il vincitore del match (1: il primo, 2: il secondo, 0:pari)
-- score_results: risultato degli scores individuali (1: vinto dal primo team, 2 vinto dal secondo, 3 pari)
===============================================================================]]
function TeamBracket:renderTeam(team_name, team_number, row, show_seed, top, winner, score_results, is_bye)

	self:addGap(row, self.current_col, self.current_width, 'rt')
	self:addGap(row+1, self.current_col, self.current_width, 'rt')
	-- seed cell
	local pos = (top and 1) or 2
	local seedArg = self:getTeamArg(self.current_round, 'seed', team_number)
	if show_seed and (not self.hideSeeds) then
		local seedCell = self.rows[row]:tag('td'):attr('rowspan', '2'):cssText(style.seed_cell):wikitext(seedArg):newline()
		if self.not_draw_top then seedCell:css('border-top-width', '0' ) end
		if self.bold_winner and (winner == pos) then 
			seedCell:cssText(style.winner) 
		end
	end
	-- team cell
	local teamCell = self.rows[row]:tag('td'):attr('rowspan', '2'):cssText(style.team_cell):wikitext(team_name):newline()
	--local teamCell = self.rows[row]:tag('td'):attr('rowspan', '2'):cssText(style.team_cell):wikitext(tostring(seedArg)):newline()
	if not show_seed and (not self.hideSeeds) then
		teamCell:attr('colspan', '2')
	end
	if self.not_draw_top then
		teamCell:css('border-top-width', '0' )
	end
	if self.bold_winner and winner == pos then 
		teamCell:cssText(style.winner) 
	end
	if self.current_round == self.rounds and winner ~= 0 and self.medals then
		if winner == pos then
			if team_number < 3 then
				teamCell:cssText(style.first_place)
			else
				teamCell:cssText(style.third_place)
			end
		elseif team_number < 3 then
			teamCell:cssText(style.second_place)
		end
	end
	-- scores cells
	for i = 1, self.scores[self.current_round] do
		local scoreCell = self.rows[row]
			:tag('td')
			:attr('rowspan', '2')
			:cssText(style.score_cell)
			:wikitext(self:getScoreArg(self.current_round, team_number, i))
			:newline()
		if self.not_draw_top then scoreCell:css('border-top-width', '0') end
		if score_results[i] == pos then scoreCell:cssText(style.winner) end
	end
	self.not_draw_top = (self.current_round == 1) and (not self.not_draw_top)
end

function TeamBracket:getScore(round, team_number, i)
	local score = string.gsub(self:getScoreArg(round, team_number, i) or '', '%D', '')
	return tonumber(score)
end

--[[ ===============================================================================
Gestisce un match
-- match_number: il numero del match contando dall'alto (1, 2, 3, ...)
-- row_base: la prima riga della tabella in cui disegnare il batch
===============================================================================]]

function TeamBracket:renderMatch(match_number, row_base)
	
	-- ottiene i nomi dei team del match, verifica che non siano bye e setta self.not_draw_top
	-- per registrare se si deve disegnare o meno la riga superiore del match
	-- ritorna i nomi dei team e se è un bye (true) o no (false)
	local function getTeams(team_number1, team_number2)
		local team_name1 = self:getTeamArg(self.current_round, 'team', team_number1)
		local team_name2 = self:getTeamArg(self.current_round, 'team', team_number2)
		if team_name1 == 'bye' or team_name2 == 'bye' or not (team_name1 and team_name2) then
			self.not_draw_top = false
			return '', '', true
		end
		return team_name1, team_name2, false
	end

	local team_number2 = match_number * 2 
	local team_number1 = team_number2 - 1
	local team_name1, team_name2, is_bye = getTeams(team_number1, team_number2)
	if is_bye then
		if self.render_match[self.current_round][match_number] then
			for r = row_base, row_base+3 do 
				self:addGap(r, self.current_col, self.current_width, 'rm')
			end	
			local cell=self.rows[row_base]:tag('td')
				:attr('rowspan', '2')
				:attr('colspan', self.current_width)
				:cssText(style.path_bottom)
			if debug then cell:css('background-color', '#ADD8E6') end
			cell = self.rows[row_base+2]:tag('td')
				:attr('rowspan', '2')
				:attr('colspan', self.current_width)
				:cssText(style.path_top)
			if debug then cell:css('background-color', '#ADD8E6') end
			return row_base
		end
		return nil
	end
	if row_base	< self.min_row_used then self.min_row_used = row_base end
	if row_base + 3 > self.max_row_used then self.max_row_used = row_base + 3 end
	local showSeed = false
	local winner, score_results = 0, {}
	local seedArg1 = self:getTeamArg(self.current_round, 'seed', team_number1)
	local seedArg2 = self:getTeamArg(self.current_round, 'seed', team_number2)
	local showSeed = self.showSeeds
		or (seedArg1 and seedArg1 ~= '-')
		or (seedArg2 and seedArg2 ~= '-')
	if self.bold_winner then 
		winner, score_results = self:getWinner(team_number1)
	end
	self:renderTeam(team_name1, team_number1, row_base, showSeed, true, winner, score_results, is_bye)
	self:renderTeam(team_name2, team_number2, row_base+2, showSeed, false, winner, score_results, is_bye)
	return row_base
end

--[[ ===============================================================================
Disegna la finale per il terzo posto
--row: riga in cui iniziare a disegnare la finale
===============================================================================]]

function TeamBracket:render_final_3(row)
	local team_name1 = self:getTeamArg(self.rounds, 'team', 3)
	local team_name2 = self:getTeamArg(self.rounds, 'team', 4)
	if team_name1 == 'bye' or team_name2 == 'bye' then
		return nil
	end
	local seedArg1 = self:getTeamArg(self.rounds, 'seed', 3)
	local seedArg2 = self:getTeamArg(self.rounds, 'seed', 4)
	local showSeed = self.showSeeds
		or (seedArg1 and seedArg1 == '-')
		or (seedArg2 and seedArg2 == '-')
	local winner, score_results = 0, {}
	if self.bold_winner then 
		winner, score_results = self:getWinner(self.rounds, 3)
	end
	local seedArg1 = self:getTeamArg(self.rounds, 'seed', 3)
	local seedArg2 = self:getTeamArg(self.rounds, 'seed', 4)
	self:addGap(row, self.current_col, self.current_width, 'f3')
	if row+4 > self.max_row_used then self.max_row_used = row + 4 end
	self.rows[row]:tag('td')
		:cssText(style.header_third_place)
		:attr('colspan', self.current_width)
		:wikitext("Finale 3° posto")
		:newline()
	self:renderTeam(team_name1, 3, row+2, showSeed, true, winner, score_results)
	self:renderTeam(team_name2, 4, row+4, showSeed, false, winner, score_results)
end

function TeamBracket:AddGroup(row, group_number)
	local name = self.args[string.format('RD%d-group%d', self.current_round, group_number)]
	if name then
		local span =  self.current_col + self.current_width - 2
		self:addGap(row, self.current_col, self.current_col + span, 'g')
		self:addGap(row+1, self.current_col, self.current_col + span, 'g')
		self.rows[row]:tag('td')
			:attr('rowspan', '2')
			:attr('colspan', span)
			:cssText(style.group)
			:wikitext(name)
			:newline()
	end
end

function TeamBracket:renderTree()
	local function normal_gap(row_count, match_number)
		return (row_count + 2 - match_number * 4) / match_number
	end

	local function compact_gap(row_count, match_number)
		return (row_count - 4 * match_number) / match_number
	end

	-- create 3 or 1 rows for every team
	local row_count =  self.teams * 2 + (self.compact and 0 or (self.teams - 2))
	self.min_row_used = row_count
	self.max_row_used = 1
	local gap_function = (self.compact and compact_gap) or normal_gap
	for i = 1, row_count do
		self.rows[i] = mw.html.create('tr')
		self.rows[i]:tag('td'):css('height', style.row_height):wikitext(debug and i or '')
		self.last_element[i] = 1
	end
	self.current_col = 2
	self.render_match = {}
	self.render_match[1] = {}
	for round =1, self.rounds do
		self.current_round = round
		self.render_match[round+1] = {}
		self.current_width = (self.hideSeeds and 1 or 2) + self.scores[round]
		local match_number = math.pow(2, self.rounds - round)
		local gap = gap_function(row_count, match_number)
		local row_base = gap / 2 + (self.compact and 1 or 0)
		local group_number = 1
		for n = 1, match_number, 2 do
			local match1 = self:renderMatch(n, row_base)
			if round < self.rounds then
				local match2 = self:renderMatch(n+1, row_base + gap + 4)
				if not self.compact and round % 4 == 1 then
					self:AddGroup(row_base+4, group_number)
					group_number = group_number + 1
				end
				self:addPath(match1, match2, gap, (n+1)/2)
				row_base = row_base + 2 * gap + 8
			end
		end
		self.current_col = self.current_col + self.current_width + 2
	end
	local third_place = self:getTeamArg(self.rounds, 'team', 3)
	--if true then return third_place end
	if third_place then -- semifinale 3° e 4° posto
		self.current_col = self.current_col - self.current_width - 2
		local offset = gap_function(row_count, 1) / 2 + (self.compact and 1 or 0) + 6
		if offset+5 > row_count then
			for i=row_count+1, offset+5 do
				self.rows[i] = mw.html.create('tr')
				self.rows[i]:tag('td'):css('height', style.row_height):wikitext(debug and i or '')
				self.last_element[i] = 1
			end
		end
		self:render_final_3(offset)
	end			
	for row = self.min_row_used, self.max_row_used do
		self.tbl:node(self.rows[row])
	end
end

--[[ ===============================================================================
Disegna le righe di testata del template
===============================================================================]]
function TeamBracket:renderHeading()
	local titleRow = self.tbl:tag('tr')
	local widthRow = self.tbl:tag('tr')
	local blank_text = self.compact and '' or '&nbsp;'
	titleRow:tag('td')
	local row_count = 1
	widthRow:tag('td')
		:css('width', style.buffer_sx_width)
		:css('height', '5px')
		:wikitext(debug and row_count or '')
	row_count = row_count + 1
	for round = 1, self.rounds do
		local colspan = tostring((self.hideSeeds and 1 or 2) + self.scores[round])
		local teamCell = titleRow:tag('td')
			:cssText(style.header_cell)
			:attr('colspan', colspan)
			:wikitext(self:getRoundName(round))
			:newline()
		if not self.hideSeeds then
			widthRow:tag('td'):css('width', style.seed_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
		teamCell = widthRow:tag('td'):css('width', style.team_width):wikitext(debug and row_count or blank_text)
		row_count = row_count + 1
		for i = 1, self.scores[round] do
			widthRow:tag('td'):css('width', style.score_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
		if round < self.rounds then
			titleRow:tag('td'):attr('colspan', '2')
			widthRow:tag('td'):css('width', style.row_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
			widthRow:tag('td'):css('width', style.row_width):wikitext(debug and row_count or blank_text)
			row_count = row_count + 1
		end
	end
end

-- =================================================================
-- Funzione di interfaccia con i template
-- =================================================================

function p.teamBracket(frame)
	local args = getArgs(frame, {
		-- se l'argomento è un seed lo ritorna così com'è anche se blank, tutti gli altri argomenti sono puliti
		valueFunc = function (key, value)
			if not value then return nil end
			if key:find("^RD%d%d?-seed") then return value end
			value = mw.text.trim(value)
			if value == '' then return nil end 
			return value
		end
		}
	)
	if args['debug'] then
		debug = in_array(args['debug'], yes_replies) or debug
	end
	local team_bracket = TeamBracket:new(args)
	return tostring(team_bracket)
end

return p