Модуль:Генеалогия

Материал из ВикиФур
Перейти к: навигация, поиск
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) :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 border_width=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