Модуль:Генеалогия
Материал из ВикиФур
local p = {}
p.characters={}
p.characters_ordered={}
p.connections_ordered={}
p.connections={}
p.default_space_width=300
p.default_space_height=200
p.default_char_height=20
p.default_char_width=50
p.character_spacing=30
p.line_spacing=30
p.default_border_width=1
p.default_padding=0
p.max_autoplace_depth=50
p.lines={}
function p.family_tree(frame)
if frame.args['ширина'] then p.space_width=frame.args['ширина'] else p.space_width=p.default_space_width end
if frame.args['высота'] then p.space_height=frame.args['высота'] else p.space_height=p.default_space_height end
if frame.args['стиль'] then p.style=frame.args['стиль'] end
local info={character={}, union={}, child={}, siblings={} }
local temp
for i,v in ipairs(frame.args) do
temp=mw.text.split(mw.text.trim(v), '%s*,%s*')
if temp[1] and info[temp[1]] then table.insert(info[temp[1]], temp) end
end
-- сначала обрабатываются персонажи, потом союзы, потом дети
for i, o in ipairs({'character', 'union', 'child', 'siblings'}) do
for i, inf in ipairs(info[o]) do
p.parse_info(inf)
end
end
p.autoplace_characters(1) -- фиксирует горизонтальные координаты всех персонажей.
return p.build_tree()
end
p['семейное древо']=p.family_tree;
function p.parse_info(data)
local key='parse_'..data[1]
if not p[key] then return end
p[key](data)
end
function p.parse_character(data)
local character={}
character.key=data[2]
character.desc=data[3]
character.line=tonumber(data[4]) -- линия хранится в виде числа потому, что данные в p.lines пока что содержат только высоту.
character.x=data[5] -- может быть не числом, а, к примеру, '10%' или именем персонажа, с которым надо центрировать.
if data[6] and data[6]~='' then character.width=tonumber(data[6]) else character.width=p.default_char_width end
if data[7] and data[7]~='' then character.height=tonumber(data[7]) else character.height=p.default_char_height end
character.style=data[8]
character.unions={}
character.children={}
p.characters[character.key]=character
table.insert(p.characters_ordered, character)
-- Lua не поддерживает сохранения порядка именованных ключей, поэтому для всех упорядоченных циклов нужно использовать эту таблицу.
p.add_character_to_line(character, character.line)
end
function p.add_character_to_line(character, line_ord)
if #p.lines<line_ord then
for x=#p.lines+1, line_ord do
p.lines[x]={height=p.default_char_height} -- нельзя использовать таблицу из переменной потому, что тогда все линии будут указывать на один объект.
end
end
if p.lines[line_ord].height<character.height then p.lines[line_ord].height=character.height end
end
function p.parse_union(data)
if not p.characters[data[2]] then return end
if not p.characters[data[3]] then return end
local connection={}
connection.type='union'
connection.key=p.make_union_key(data[2], data[3])
connection.chars={p.characters[data[2]], p.characters[data[3]]}
connection.style=data[4]
table.insert(connection.chars[1].unions, connection)
table.insert(connection.chars[2].unions, connection)
connection.children={}
p.connections[connection.key]=connection
table.insert(p.connections_ordered, connection)
-- Lua не поддерживает сохранения порядка именованных ключей, поэтому для всех упорядоченных циклов нужно использовать эту таблицу.
end
function p.make_union_key(char1_key, char2_key)
return char1_key..'+'..char2_key
end
function p.parse_child(data)
if not p.characters[data[2]] then return end
if not p.characters[data[4]] then return end
if p.characters[data[4]].line<=p.characters[data[2]].line then return end -- потомок может находить только ниже родителя.
local connection={}
connection.type='child'
connection.key='>'..data[4]
connection.parents={p.characters[data[2]]}
table.insert(connection.parents[1].children, connection)
if data[3]~='' then
if not p.characters[data[3]] then return end
if p.characters[data[4]].line<=p.characters[data[3]].line then return end -- потомок может находиться только ниже родителя.
local union_key=p.make_union_key(data[2], data[3])
if not p.connections[union_key] then return end
table.insert(connection.parents, p.characters[data[3]])
table.insert(connection.parents[2].children, connection)
connection.union=p.connections[union_key]
table.insert(connection.union.children, connection)
end
connection.child=p.characters[data[4]]
connection.child.parents=connection
connection.style=data[5]
p.connections[connection.key]=connection
table.insert(p.connections_ordered, connection)
-- Lua не поддерживает сохранения порядка именованных ключей, поэтому для всех упорядоченных циклов нужно использовать эту таблицу.
end
-- для братьев и сестёр, чьи родители неизвестны и не хочется добавлять их в схему.
function p.parse_siblings(data)
local siblings_count=#data-2 -- первые два аргумента - тип связи и стиль
if siblings_count<2 then return end
local siblings={}
for i,char in ipairs(data) do
if i>=3 and p.characters[char] then table.insert(siblings, p.characters[char]) end
end
if #siblings<2 then return end
local key=''
for i,char in ipairs(siblings) do
if i>1 then key=key..',' end
key=key..char.key
end
local connection={}
connection.type='siblings'
connection.key=key
connection.chars=siblings
connection.style=data[2]
p.connections[connection.key]=connection
table.insert(p.connections_ordered, connection)
end
-- эта функция фисирует горизональное положение всех персонажей, превращая его из различных форм записи в число (координату)
-- персонажи должны подаваться модулю в порядке взаимосвязи, чтобы персонаж, чьё положение зависит от другого, не стоял раньше другого.
function p.autoplace_characters(depth)
if depth>p.max_autoplace_depth then
-- if depth>0 then -- запасная строчка для отладки
p.autoplace_characters_fallback()
return
end
local groups={}
local group_count=0
local not_placed=0
for i, character in ipairs(p.characters_ordered) do
repeat -- в отсутствие оператора continue
if type(character.x)=='number' then break end
if mw.ustring.match(character.x, '^%d+$') then -- числовая координата
character.x=tonumber(character.x)
break
end
local m=mw.ustring.match(character.x, '^(%d+)%%$') -- процентная координата
if m then
character.x=math.floor(character.width/2)+(p.space_width-character.width)*tonumber(m)/100
break
end
local m,n=mw.ustring.match(character.x, '^(.-)(++)$') -- формы типа "Симба++", то есть справа от персонажа с заданным промежутком.
if m then
local reference=p.characters[m]
if type(reference.x)~='number' then not_placed=not_placed+1; break end
local space=string.len(n)
character.x=reference.x+math.floor(reference.width/2)+p.character_spacing*space+math.floor(character.width/2)
break -- на случай других проверок
end
local n,m=mw.ustring.match(character.x, '^(++)(.+)$') -- формы типа "++Симба", то есть слева от персонажа с заданным промежутком.
if m then
local reference=p.characters[m]
if type(reference.x)~='number' then not_placed=not_placed+1; break end
local space=string.len(n)
character.x=reference.x-math.floor(reference.width/2)-p.character_spacing*space-math.floor(character.width/2)
break -- на случай других проверок
end
local anchor
if p.characters[character.x] then -- если ориентация относительно персонажа
anchor=p.characters[character.x]
elseif p.connections[character.x] and p.connections[character.x].type=='union' then -- если ориентация относительно союза
if table.getn(p.connections[character.x].chars)==1 then
anchor=p.connections[character.x].chars[1]
else
anchor=p.connections[character.x]
end
elseif character.x=='__parent' and character.parents then -- если ориентация относительно родителей, автоматически
if table.getn(character.parents.chars)==1 then
anchor=character.parents.chars[1]
else
anchor=character.parents
end
end
if anchor then
if not groups[anchor.key] then groups[anchor.key]={} end
-- строковый ключ линии нужен потому, что Lua не приемлет пропусков в числовых ключах таблицы, останавливает итератор ipairs при первом же отсутствующем ключе.
if not groups[anchor.key]['l'..character.line] then groups[anchor.key]['l'..character.line]={} end
table.insert(groups[anchor.key]['l'..character.line], character)
group_count=group_count+1
break
end
local char1, char2=mw.ustring.match(character.x, '^(.-)%+(.+)$')
if char1 and char2 and p.characters[char1] and p.characters[char2] then
char1=p.characters[char1]
char2=p.characters[char2]
if type(char1.x)~='number' or type(char2.x)~='number' then
not_placed=not_placed+1
else
character.x=math.floor((char1.x+char2.x)/2)
end
break
end
until true
end
if group_count>0 then
-- персонажи, ориентирующиеся относительно другого персонажа или союза и находящиеся на одной линии, располагаются равномерно под центром "якоря".
local order={}
for i,character in ipairs(p.characters_ordered) do
if groups[character.key] then table.insert(order, character.key) end
end
for key,group in pairs(groups) do
if not p.characters[key] then table.insert(order, key) end
end
for i, key in ipairs(order) do
repeat -- в отсутствие continue
local anchor
if p.characters[key] then anchor=p.characters[key] else anchor=p.connections[key] end
for line_key, group in pairs(groups[key]) do
if not p.get_anchor_x(anchor) then not_placed=not_placed+#group; break end -- вычисляется здесь для того, чтобы правильно записать подсчёт not_placed
-- хоть точное значение not_placed не используется для вывода, она может понадобиться для отладки.
local width=0
for i, subchar in ipairs(group) do
if width>0 then width=width+p.character_spacing end
width=width+subchar.width
end
for i, subchar in ipairs(group) do
if i==1 then
subchar.x=p.get_anchor_x(anchor)-math.floor(width/2)+math.floor(subchar.width/2)
else
subchar.x=group[i-1].x+math.floor(group[i-1].width/2)+p.character_spacing+math.floor(subchar.width/2)
end
end
end
until true
end
end
if not_placed>0 then
p.autoplace_characters(depth+1)
end
end
function p.autoplace_characters_fallback()
local per_line={}
local line_key
for key, character in pairs(p.characters) do
if type(character.x)~='number' then
line_key='l'..character.line
if not per_line[line_key] then per_line[line_key]=0 end
character.x=per_line[line_key]+math.floor(character.width/2)
per_line[line_key]=per_line[line_key]+character.width+p.character_spacing
end
end
end
function p.build_tree()
local tree=p.build_tree_frame()
for i, character in ipairs(p.characters_ordered) do
tree
:node(p.build_character(character))
end
for i, connection in ipairs(p.connections_ordered) do
tree
:node(p['build_'..connection.type..'_connection'](connection))
end
return tree
end
function p.build_tree_frame()
local element=mw.html.create('div')
element
:addClass('family_tree')
:cssText(p.style)
:css('width', p.space_width..'px')
:css('height', p.space_height..'px')
return element
end
function p.build_union_connection(data)
if data.element then return data.element end
local element=mw.html.create('div')
local x, y, length
element
:addClass('family_tree_connection')
data.element=element
local left_char, right_char
if data.chars[1].x<data.chars[2].x then
left_char, right_char=data.chars[1], data.chars[2]
else
right_char, left_char=data.chars[1], data.chars[2]
end
if left_char.line==right_char.line then
-- одноуровневый союз
x=p.get_char_element_x(left_char)+left_char.width
y=p.get_char_element_y(left_char)+math.floor(left_char.height/2)
length=p.get_char_element_x(right_char)-x
element
:node(p.build_horizontal_line(x, y, length, data.style))
else
local upper_char, lower_char
if left_char.line>right_char.line then
upper_char, lower_char=left_char, right_char
else
lower_char, upper_char=left_char, right_char
end
-- разноуровневый союз
-- горизонтальная линия не от рамки двух персонажей, а от центров, поскольку так проще и предполагается, что непрозрачный фон будет закрывать лишнее.
x=p.get_char_element_x(left_char)+math.floor(left_char.width/2)
y=p.get_char_element_y(lower_char)+math.floor(lower_char.height/2)
length=p.get_char_element_x(right_char)+math.floor(right_char.width/2)-x
element
:node(p.build_horizontal_line(x, y, length, data.style))
x=p.get_char_element_x(upper_char)+math.floor(upper_char.width/2)
y=p.get_char_element_y(upper_char)+upper_char.height
length=p.get_char_element_y(lower_char)+math.floor(lower_char.height/2)-x
element
:node(p.build_vertical_line(x, y, length, data.style))
end
return element
end
function p.build_child_connection(data)
if data.element then return data.element end
local anchor, lower_parent, x, y, length
if data.union then
anchor=data.union
if anchor.chars[1].line>anchor.chars[2].line then lower_parent=anchor.chars[1] else lower_parent=anchor.chars[2] end
else
anchor=data.parents[1]
lower_parent=anchor
end
local element=mw.html.create('div')
element
:addClass('family_tree_connection')
data.element=element
x=p.get_anchor_x(anchor)
y=p.get_anchor_y(anchor)
if data.child.x==x then -- потомок непосредственно под якорем
length=p.get_char_element_y(data.child)-y
element
:node(p.build_vertical_line(x, y, length, data.style))
else
length=p.get_char_element_y(data.child)-math.floor(p.line_spacing/2)-y
element
:node(p.build_vertical_line(x, y, length, data.style))
y=y+length
if data.child.x<x then -- потомок слева от якоря
length=x-data.child.x
x=data.child.x
element
:node(p.build_horizontal_line(x, y, length, data.style))
else -- потомок справа от якоря
length=data.child.x-x
element
:node(p.build_horizontal_line(x, y, length, data.style))
x=x+length
end
length=p.get_char_element_y(data.child)-y
element
:node(p.build_vertical_line(x, y, length, data.style))
end
return element
end
function p.build_siblings_connection(data)
if data.element then return data.element end
local sort_by_x=function(char1, char2)
return char1.x<char2.x
end
table.sort(data.chars, sort_by_x)
local leftmost_char=data.chars[1]
local rightmost_char=data.chars[table.getn(data.chars)]
local top_line=data.chars[1].line
for i,char in ipairs(data.chars) do
if i>1 and char.line<top_line then top_line=char.line end
end
local element=mw.html.create('div')
element
:addClass('family_tree_connection')
data.element=element
local x,y,length
x=leftmost_char.x
y=p.get_char_element_y(leftmost_char)-math.floor(p.line_spacing/2)
length=rightmost_char.x-x
element
:node(p.build_horizontal_line(x, y, length, data.style))
for i,char in ipairs(data.chars) do
x=char.x
length=p.get_char_element_y(char)-y
element
:node(p.build_vertical_line(x, y, length, data.style))
end
return element
end
function p.build_horizontal_line(x, y, length, style)
local horizontal=mw.html.create('div')
horizontal
:addClass('family_tree_horizontal_line')
:cssText(style)
:css('left', x..'px')
:css('top', y..'px')
:css('width', length..'px')
return horizontal
end
function p.build_vertical_line(x, y, length, style)
local vertical=mw.html.create('div')
vertical
:addClass('family_tree_vertical_line')
:cssText(style)
:css('left', x..'px')
:css('top', y..'px')
:css('height', length..'px')
return vertical
end
function p.build_character(data)
if data.element then return data.element end
local element=mw.html.create('div')
element
:addClass('family_tree_character')
:wikitext(data.desc)
:cssText(data.style)
:css('left', p.get_char_element_x(data)..'px')
:css('top', p.get_char_element_y(data)..'px')
:css('width', (data.width-p.extract_char_widening(data))..'px')
:css('height', (data.height-p.extract_char_heightening(data))..'px')
data.element=element
return element
end
function p.extract_char_widening(data)
if not data.style then return p.default_border_width*2+p.default_padding*2 end
-- не учитывает все случаи задания границ, рамки или других способов утолщения элемента, но для простых случаев хватит.
local border_width=data.style:match('border[^:]-:%s*(%d+)px')
if not border_width then border_width=p.default_border_width end
local padding=data.style:match('padding:%s(%d+)px')
if not padding then padding=p.default_padding end
return border_width*2+padding*2
end
function p.extract_char_heightening(data)
return p.extract_char_widening(data)
-- пока не позволяет делать границы и рамки иные по горизонтали, чем по вертикали.
end
function p.get_char_element_x(data)
return math.floor(data.x-data.width/2)
end
function p.get_char_element_y(data)
local y=p.line_spacing
for line_ord, line in ipairs(p.lines) do
if line_ord==data.line then break end
y=y+p.default_char_height+p.line_spacing
end
return y
end
function p.get_anchor_x(data)
if data.type and data.type=='union' then
if #data.chars==2 then
if type(data.chars[1].x)~='number' then return end
if type(data.chars[2].x)~='number' then return end
local left_parent, right_parent
if data.chars[1].x<data.chars[2].x then
left_parent, right_parent=data.chars[1], data.chars[2]
else
right_parent, left_parent=data.chars[1], data.chars[2]
end
return (left_parent.x+math.floor(left_parent.width/2)+right_parent.x-math.floor(right_parent.width/2))/2
else
return p.get_anchor_x(data.chars[1])
end
else
if type(data.x)~='number' then return end
return data.x
end
end
function p.get_anchor_y(data)
if data.type and data.type=='union' then
if #data.chars==2 then
local lower_parent
if data.chars[1].line>data.chars[2].line then lower_parent=data.chars[1] else lower_parent=data.chars[2] end
return p.get_char_element_y(lower_parent)+math.floor(lower_parent.height/2)
else
return p.get_anchor_y(data.chars[1])
end
else
return p.get_char_element_y(data)+data.height
end
end
return p