Модуль:Wikidata/FamilyTree/doc


local p = {};

local ELEMENT_WIDTH = 5;
local ELEMENT_HEIGHT = 5;
local DEFAULT_MODE = "vertical"; -- horizontal
local DEFAULT_DECORATE = "none"; -- by-generation

local BUSY = 'X';

local EMPTY_TABLE = {};

local MISSING_LABEL_LINK = function( entityId )
	return '[[:d:' .. entityId .. '|' .. entityId .. ']]<span style="border-bottom: 1px dotted; cursor: help; white-space: nowrap" title="В Викиданных нет русской подписи к элементу. Вы можете помочь, указав русский вариант подписи.">?</span>';
end
local RED_LINK_F = function( entityId, label )
	local templateText = "{{Универсальная карточка|" .. entityId .. "}}%0A'''" .. label .. "''' — %0A%0A== Примечания ==%0A{{примечания}}%0A";
	local templateText = templateText .. "[[Категория:Викиучебник:Карточки с явно указанным элементом Викиданных|" .. entityId .. "]]";
	local preloadUrl = tostring( mw.uri.canonicalUrl( label, 'action=edit&preload=Ш:Preload/Викиданные&preloadparams[]=' .. templateText ));
	local redLink = mw.getCurrentFrame():expandTemplate{ title='цветная ссылка', args = { '#ba0000', preloadUrl, label }};
	return '<span class="plainlinks">' .. redLink .. '</span><sup>[[:d:' .. entityId .. '|[d]]]</sup>';
end

local function copyTo( obj, target, skipEmpty )
	for k, v in pairs( obj ) do
		if skipEmpty ~= true or ( v ~= nil and v ~= '' ) then
			target[k] = v;
		end
	end
	return target;
end

local function expectString( var, name )
	if (type(var) ~= 'string') then
		error("Expected string but have " .. type(var) .. " of argument " .. name);
	end
end

local function getEntityIdsFromClaims( claims )
	if ( claims == nil ) then return EMPTY_TABLE end
	local result = {};
	for _, claim in pairs( claims ) do
		local id = (((claim.mainsnak or EMPTY_TABLE).datavalue or EMPTY_TABLE).value or EMPTY_TABLE).id or nil;
		if ( id ~= nil ) then
			result[id] = id;
		end
	end
	return result;
end

local function createNode( entityId, generation )
	expectString( entityId, "entityId" );

	local node = {};
	node.entityId = entityId;
	node.generation = generation;
	return node;
end

local function populateRelation( relationType, propertyId, line, node, newGeneration )
	expectString( relationType, "relationType" );
	expectString( propertyId, "propertyId" );

	local relativesIds = getEntityIdsFromClaims( mw.wikibase.getBestStatements( node.entityId, propertyId ) );

	local propertyKey = relationType .. 'Nodes';
	for _, relativeId in pairs( relativesIds ) do
		if ( node[propertyKey] == nil ) then node[propertyKey] = {} end

		local relativeNode = createNode( relativeId, newGeneration );
		table.insert( line, relativeNode );
		table.insert( node[propertyKey], relativeNode );
	end
end

local function max( a, b )
	return a > b and a or b;
end
local function min( a, b )
	return a < b and a or b;
end

local function copyRendered( src, dst, dstPosX, dstPosY, width, height )
	if ( #src < height ) then error( "Need " .. (height) .. " lines in src; have only " .. #src ); end
	if ( #dst < dstPosY + height - 1 ) then error( "Need " .. (dstPosY + height - 1) .. " lines in dst; have only " .. #dst ); end

	for y=1, height do
		local srcLine = src[ y ];
		local dstLine = dst[ dstPosY + y - 1 ];
		for x=1, width do
			local newValue = srcLine[ x ];
			if (newValue ~= nil) then
				dstLine[ dstPosX + x - 1 ] = newValue;
			end
		end
	end
end

function willOverride( src, dst, dstPosX, dstPosY, width, height )
	for y=1, height do
		local srcLine = src[ y ];
		local dstLine = dst[ dstPosY + y - 1 ];
		if ( srcLine == nil ) then error( "Missing line " .. y .. " in src" ); end
		if ( dstLine == nil ) then error( "Missing line " .. ( dstPosY + y - 1 ) .. " in dst" ); end
		for x=1, width do
			local hasInSrc = srcLine[ x ] ~= nil;
			local hasInDst = dstLine[ dstPosX + x - 1 ] ~= nil;
			if ( hasInSrc and hasInDst ) then
				return true;
			end
		end
	end
	return false;
end

function p.generateLineUp( )
	local result = '{';
	for i1, n in ipairs( {false, true} ) do
		for i2, e in ipairs( {false, true} ) do
			for i3, s in ipairs( {false, true} ) do
				for i4, w in ipairs( {false, true} ) do
					if ( n or e or s or w ) then
						local srcVarName = ( n and 'N' or '') .. ( e and 'E' or '') .. ( s and 'S' or '') .. ( w and 'W' or '');
						local dst = 'N' .. ( e and 'E' or '') .. ( s and 'S' or '') .. ( w and 'W' or '');
						result = result .. srcVarName .. '="' .. dst .. '",';
					end
				end
			end
		end
	end
	return result .. '}';
end

local APPEND_LINE_UP = {W="NW",S="NS",SW="NSW",E="NE",EW="NEW",ES="NES",ESW="NESW",N="N",NW="NW",NS="NS",NSW="NSW",NE="NE",NEW="NEW",NES="NES",NESW="NESW",};

function p.generateLineDown( )
	local result = '{';
	for i1, n in ipairs( {false, true} ) do
		for i2, e in ipairs( {false, true} ) do
			for i3, s in ipairs( {false, true} ) do
				for i4, w in ipairs( {false, true} ) do
					if ( n or e or s or w ) then
						local srcVarName = ( n and 'N' or '') .. ( e and 'E' or '') .. ( s and 'S' or '') .. ( w and 'W' or '');
						local dst = ( n and 'N' or '') .. ( e and 'E' or '') .. 'S' .. ( w and 'W' or '');
						result = result .. srcVarName .. '="' .. dst .. '",';
					end
				end
			end
		end
	end
	return result .. '}';
end

local APPEND_LINE_DOWN = {W="SW",S="S",SW="SW",E="ES",EW="ESW",ES="ES",ESW="ESW",N="NS",NW="NSW",NS="NS",NSW="NSW",NE="NES",NEW="NESW",NES="NES",NESW="NESW",};

function findFirstIndex( arr, length )
	for i=1, length do
		if ( arr[i] ~= nil ) then
			return i;
		end
	end
	error('first element not found');
end

function findLastIndex( arr, length )
	for i=length,1,-1 do
		if ( arr[i] ~= nil ) then
			return i;
		end
	end
	error('last element not found');
end

local function parentsAreSame( node1, node2 )
	if (not node1.parentNodes) then return false; end
	if (not node2.parentNodes) then return false; end

	local firstGrandParentCount = 0;
	local firstGrandParentIds = {};
	for _, grandParent in pairs( node1.parentNodes ) do
		firstGrandParentIds[ grandParent.entityId ] = true;
		firstGrandParentCount = firstGrandParentCount + 1;
	end

	if ( firstGrandParentCount ~= #node2.parentNodes ) then
		return false
	end
	for _, grandParent in pairs( node2.parentNodes ) do
		if ( not firstGrandParentIds[ grandParent.entityId ] ) then
			return false;
		end
	end

	return true;
end

local function putEntity( result, yStart, xStart, node )
	for y=yStart, yStart + ELEMENT_HEIGHT - 1 do
		for x=xStart, xStart + ELEMENT_WIDTH - 1 do
			result[y][x] = BUSY;
		end
	end
	result[yStart][xStart] = {
		entityId = node.entityId,
		generation = node.generation
	};
end

local function renderSingle( node, connectToFirstLine )
	node.resultWidth = ELEMENT_WIDTH;
	node.resultHeight = ELEMENT_HEIGHT;
	node.renderResult = {};
	for y=1,ELEMENT_HEIGHT do node.renderResult[y] = {}; end
	putEntity( node.renderResult, 1, 1, node );
	node.lineConnectionY = connectToFirstLine and 1 or ELEMENT_HEIGHT;
	node.lineConnectionX = math.ceil( ELEMENT_WIDTH / 2 );
end

local renderWithParents;
local renderInterleaveWithParents;

local function renderFakeParentsNodeWithoutChild( options, parentNodes, leftParentTailOnly, rightParentTailOnly )
	-- fake node
	local node = {};
	local lineStartConnectFrom = 99999;
	local lineEndConnectTo = 0;

	local resultWidth = 0; -- self
	local resultHeight = 0;

	local commonParentsResult = nil;
    -- we shall not interleave different incests... in can be bad, in all sences
	if ( not options.interleave and leftParentTailOnly == nil and rightParentTailOnly == nil ) then
		-- check if right corner of left parent is the same as left corner of right parent
	    if (#parentNodes == 2) then
	    	local leftRoot = parentNodes[1];
	    	local rightRoot = parentNodes[2];
	    	while (leftRoot ~= nil and rightRoot ~= nil) do
	    		if (parentsAreSame( leftRoot, rightRoot )) then
	    			leftParentTailOnly = leftRoot.entityId;
	    			rightParentTailOnly = rightRoot.entityId;
	    			commonParentsResult = renderFakeParentsNodeWithoutChild( options, leftRoot.parentNodes, nil, nil );
	    			break;
	    		end
	    		if (leftRoot.parentNodes ~= nil and #leftRoot.parentNodes ~= 0) then
	    			leftRoot = leftRoot.parentNodes[ #leftRoot.parentNodes ];
	    		else
	    			leftRoot = nil;
	    		end
	    		if (rightRoot.parentNodes ~= nil and #rightRoot.parentNodes ~= 0) then
	    			rightRoot = rightRoot.parentNodes[1];
	    		else
	    			rightRoot = nil;
	    		end
	    	end
	    end
    end

	for _, parentNode in pairs( parentNodes ) do
		if ( options.interleave ) then
			renderInterleaveWithParents( options, parentNode );
		else
			if (commonParentsResult == nil) then
				local parentLeftParentTailOnly = nil;
				local parentRightParentTailOnly = nil;
				if ( _ == 1 ) then parentLeftParentTailOnly = leftParentTailOnly; end;
				if ( _ == #parentNodes ) then parentRightParentTailOnly = rightParentTailOnly; end;
				renderWithParents( options, parentNode, parentLeftParentTailOnly, parentRightParentTailOnly );
			else
				local parentLeftParentTailOnly = nil;
				local parentRightParentTailOnly = nil;
				if ( _ == #parentNodes ) then parentLeftParentTailOnly = rightParentTailOnly; end;
				if ( _ == 1 ) then parentRightParentTailOnly = leftParentTailOnly; end;
				renderWithParents( options, parentNode, parentLeftParentTailOnly, parentRightParentTailOnly );
			end
		end
		resultHeight = max( resultHeight, parentNode.resultHeight );
	end
	resultHeight = resultHeight + 1; -- lines

	local result = {};
	for y=1,resultHeight do result[y]={} end;

	local commonParentsHorPosition = nil;
	local commonParentsVerPosition = nil;
	local verPositions = {};
	local horPositions = {};
	for _, parentNode in pairs( parentNodes ) do
		local mergeTailsHere = _ == 2 and commonParentsResult ~= nil;
		if (mergeTailsHere) then
			commonParentsHorPosition = resultWidth + 2;

			-- need to find connection points of tails in parents
			if ( parentNodes[1].resultTails == nil ) then error("Need parent tails to present in left parent result") end;
			if ( parentNodes[2].resultTails == nil ) then error("Need parent tails to present in right parent result") end;
			local tailY = parentNodes[1].resultTails[ leftParentTailOnly ].y + ( horPositions[1] - 1 );
			commonParentsVerPosition = tailY - commonParentsResult.resultHeight - 1;

			while ( commonParentsHorPosition > 4 ) do
				if ( willOverride( commonParentsResult.renderResult, result, commonParentsHorPosition - 2, commonParentsVerPosition, commonParentsResult.resultWidth, commonParentsResult.resultHeight ) ) then
					break;
				end
				commonParentsHorPosition = commonParentsHorPosition - 1;
			end

			copyRendered( commonParentsResult.renderResult, result, commonParentsHorPosition, commonParentsVerPosition, commonParentsResult.resultWidth, commonParentsResult.resultHeight );
			resultWidth = max( resultWidth, commonParentsHorPosition + commonParentsResult.resultWidth - 1 );
		end

		-- position by horizontal of current element
		local blockPosition = _ ~= 1 and resultWidth + 2 or 1;
		local parentStartLine = resultHeight - 1 - parentNode.resultHeight + 1;

		-- check if we have empty space in result to shift parent block to left
		while ( blockPosition > 2 ) do
			if ( not willOverride( parentNode.renderResult, result, blockPosition - 2, parentStartLine, parentNode.resultWidth, parentNode.resultHeight ) ) then
				blockPosition = blockPosition - 1;
			else
				break;
			end
		end

		horPositions[_] = blockPosition;
		verPositions[_] = parentStartLine;

		local parentWidth = parentNode.resultWidth;
		local parentHeight = parentNode.resultHeight;

		-- define how parent horizontal line need to be drawn
		lineStartConnectFrom = min( lineStartConnectFrom, blockPosition + parentNode.lineConnectionX - 1 )
		lineEndConnectTo = max( lineEndConnectTo, blockPosition + parentNode.lineConnectionX - 1 )

		resultWidth = max( resultWidth, blockPosition + parentNode.resultWidth - 1 );
		copyRendered( parentNode.renderResult, result, blockPosition, parentStartLine, parentNode.resultWidth, parentNode.resultHeight )

		if ( parentNode.resultTails ~= nil ) then
			if ( node.resultTails == nil) then node.resultTails = {}; end;
			for tailId, resultTail in pairs( parentNode.resultTails ) do
				node.resultTails[ tailId ] = { x = blockPosition + resultTail.x - 1, y = parentStartLine + resultTail.y - 1 };
			end
		end
	end
	node.resultWidth = resultWidth;
	node.resultHeight = resultHeight;

	-- connect tails of left and right tree with common parents part
	if ( commonParentsResult ~= nil ) then
		local x1 = parentNodes[1].resultTails[leftParentTailOnly].x + ( horPositions[1] - 1 );
		local x2 = parentNodes[2].resultTails[rightParentTailOnly].x + ( horPositions[2] - 1 );

		local y1 = parentNodes[1].resultTails[leftParentTailOnly].y + ( verPositions[1] - 1 );
		local y2 = parentNodes[2].resultTails[rightParentTailOnly].y + ( verPositions[2] - 1 );

		if ( y1 ~= y2 ) then
			mw.log( 'different vertical positions of parent tails in tree:' .. y1 .. ' vs ' .. y2 );
		else
			result[y1][x1]='ES';
			for x=(x1 + 1),(x2 - 1) do
				result[y1][x] = "EW";
			end
			result[y1][x2]='SW';
		end

		local lineConnectionX = commonParentsResult.lineConnectionX + commonParentsHorPosition - 1;
		local lineConnectionY = commonParentsResult.lineConnectionY + commonParentsVerPosition - 1;
		result[lineConnectionY][lineConnectionX] = 'ESW';

		if ( x1 > lineConnectionX or x2 < lineConnectionX ) then
			local center = x1 + math.floor((x2 - x1) / 2);
			local from = min( center, lineConnectionX );
			local to = max( center, lineConnectionX );
			result[lineConnectionY+1][from] = "E";
			for x=(from + 1),(to - 1) do
				result[lineConnectionY+1][x] = "EW";
			end
			result[lineConnectionY+1][to] = "W";

			result[lineConnectionY+0][lineConnectionX] = APPEND_LINE_DOWN[ result[lineConnectionY+0][lineConnectionX] ];
			result[lineConnectionY+1][lineConnectionX] = APPEND_LINE_UP[ result[lineConnectionY+1][lineConnectionX] ];
			result[lineConnectionY+1][center] = APPEND_LINE_DOWN[ result[lineConnectionY+1][center] ];
			result[lineConnectionY+2][center] = APPEND_LINE_UP[ result[lineConnectionY+2][center] ];
		else
			result[lineConnectionY+1][lineConnectionX] = 'NS';
			result[lineConnectionY+2][lineConnectionX] = APPEND_LINE_UP[ result[lineConnectionY+2][lineConnectionX] ];
		end
	end

	-- local center = math.ceil( resultWidth / 2 );
	local center = lineStartConnectFrom + math.floor((lineEndConnectTo - lineStartConnectFrom) / 2);
	if ( center < 3 ) then error( "center < 3" ) end

	lineStartConnectFrom = min( lineStartConnectFrom , center );
	lineEndConnectTo = max( lineEndConnectTo , center );

	-- shift child a bit to center of line
	if ( center == lineStartConnectFrom and lineEndConnectTo - lineStartConnectFrom >= 2 ) then center = center + 1; end
	if ( center == lineEndConnectTo and lineEndConnectTo - lineStartConnectFrom >= 2 ) then center = center - 1; end
	if ( center < 3 ) then error( "center < 3" ) end

	local parentConnectionLine = resultHeight;

	-- draw parent connection line
	if ( lineStartConnectFrom == lineEndConnectTo) then
		result[parentConnectionLine][lineStartConnectFrom] = "NS";
	else
		result[parentConnectionLine][lineStartConnectFrom] = "E";
		for x=(lineStartConnectFrom + 1),(lineEndConnectTo - 1) do
			result[parentConnectionLine][x] = "EW";
		end
		result[parentConnectionLine][lineEndConnectTo] = "W";
	end

	-- draw line up to parents
	for _, parentNode in pairs( parentNodes ) do
		local parentConnectionIndex = horPositions[_] - 1 + parentNode.lineConnectionX;
		result[parentConnectionLine][parentConnectionIndex] = APPEND_LINE_UP[ result[parentConnectionLine][parentConnectionIndex] ];
	end

	node.renderResult = result;
	node.lineConnectionY = resultHeight;
	node.lineConnectionX = center;
	return node;
end

renderWithParents = function( options, node, leftParentTailOnly, rightParentTailOnly )
	if ( not node.parentNodes or #node.parentNodes == 0) then
		renderSingle( node, true );
		return;
	end

	local fakeParentsNode = renderFakeParentsNodeWithoutChild( options, node.parentNodes, leftParentTailOnly, rightParentTailOnly );
	local center = fakeParentsNode.lineConnectionX;
	
	local resultWidth = fakeParentsNode.resultWidth;
	local resultHeight = fakeParentsNode.resultHeight + 2 + ELEMENT_HEIGHT;
	local result = {};
	for y=1,resultHeight do result[y]={} end;

	local parentConnectionLine = fakeParentsNode.resultHeight;

	if ( node.entityId ~= leftParentTailOnly and node.entityId ~= rightParentTailOnly ) then
		copyRendered( fakeParentsNode.renderResult, result, 1, 1, fakeParentsNode.resultWidth, fakeParentsNode.resultHeight )
		node.resultTails = fakeParentsNode.resultTails;

		-- draw line down to child
		result[parentConnectionLine][center] = APPEND_LINE_DOWN[ result[parentConnectionLine][center] ];
		result[parentConnectionLine + 1][center] = "NS";
		result[parentConnectionLine + 2][center] = "N";
	else
		if ( node.resultTails == nil ) then node.resultTails = {}; end
		node.resultTails[ node.entityId ] = { x = center, y = parentConnectionLine + 2};
	end

	node.resultWidth = resultWidth;
	node.resultHeight = resultHeight;

	-- line to child
	result[parentConnectionLine + 2][center] = APPEND_LINE_DOWN[ result[parentConnectionLine + 2][center] ] or 'S';
	putEntity( result, parentConnectionLine + 3, center - math.floor( ELEMENT_WIDTH / 2 ), node );

	node.renderResult = result;
	node.lineConnectionY = resultHeight;
	node.lineConnectionX = center;
end

renderInterleaveWithParents = function( options, node )
	local hasFathers = node.fatherNodes and #node.fatherNodes > 0;
	local hasMothers = node.motherNodes and #node.motherNodes > 0;

	if ( (not hasFathers) and (not hasMothers) ) then
		renderSingle( node, true );
		return;
	end

	local renderHeight = ELEMENT_HEIGHT;
	local renderWidth = ELEMENT_WIDTH;

	local fakeFatherNode;
	local heightWithFathers = ELEMENT_HEIGHT;
	local widthOfFathers = 0

	if ( hasFathers ) then
		fakeFatherNode = renderFakeParentsNodeWithoutChild( options, node.fatherNodes, EMPTY_TABLE );
		heightWithFathers = fakeFatherNode.resultHeight + 2;
		widthOfFathers = fakeFatherNode.resultWidth + 1;
	end

	local fakeMotherNode;
	local heightWithMothers = ELEMENT_HEIGHT;
	local widthOfMothers = 0

	if ( hasMothers ) then
		fakeMotherNode = renderFakeParentsNodeWithoutChild( options, node.motherNodes, EMPTY_TABLE );
		heightWithMothers = fakeMotherNode.resultHeight + 2;
		widthOfMothers = fakeMotherNode.resultWidth + 1;
	end

	local resultHeight = max( heightWithFathers, heightWithMothers );
	local resultWidth = widthOfFathers + ELEMENT_WIDTH + widthOfMothers;

	local result = {};
	for y=1,resultHeight do result[y]={} end;

	if ( hasFathers ) then
		local fathersX = 1;
		local fathersY = resultHeight - heightWithFathers + 1;
		copyRendered( fakeFatherNode.renderResult, result, fathersX, fathersY, fakeFatherNode.resultWidth, fakeFatherNode.resultHeight );

		-- draw line
		local lineY = fathersY + fakeFatherNode.resultHeight;
		local lineStart = fakeFatherNode.lineConnectionX;
		local lineEnd = fakeFatherNode.resultWidth + 1;
		result[lineY][lineStart] = "NE";
		for x=(lineStart+1),lineEnd do
			result[lineY][x] = "EW";
		end
		result[lineY - 1][lineStart] = APPEND_LINE_DOWN[ result[lineY - 1][lineStart] ]
	end

	local selfX = hasFathers and (widthOfFathers + 1) or 1;
	local selfY = resultHeight - ELEMENT_HEIGHT + 1;
	putEntity( result, selfY, selfX, node );

	if ( hasMothers ) then
		local mothersX = selfX + ELEMENT_WIDTH + 1;
		local mothersY = resultHeight - heightWithMothers + 1;
		copyRendered( fakeMotherNode.renderResult, result, mothersX, mothersY, fakeMotherNode.resultWidth, fakeMotherNode.resultHeight );

		-- draw line
		local lineY = mothersY + fakeMotherNode.resultHeight;
		local lineStart = mothersX - 1;
		local lineEnd = mothersX + fakeMotherNode.lineConnectionX - 1;
		result[lineY][lineEnd] = "NW";
		for x=lineStart,(lineEnd-1) do
			result[lineY][x] = "EW";
		end
		result[lineY - 1][lineEnd] = APPEND_LINE_DOWN[ result[lineY - 1][lineEnd] ]
	end

	node.renderResult = result;
	node.resultHeight = resultHeight;
	node.resultWidth = resultWidth;
	node.lineConnectionX = selfX + math.floor( ELEMENT_WIDTH / 2 );
	node.lineConnectionY = resultHeight;
end

local function renderWithChildren( options, node )
	if ( not node.childNodes or #node.childNodes == 0) then
		renderSingle( node, false );
		return;
	end

	local lineStartConnectFrom = 99999;
	local lineEndConnectTo = 0;

	local resultWidth = 0; -- self
	local resultHeight = 0;

	for _, childNode in pairs( node.childNodes ) do
		renderWithChildren( options, childNode );
		resultHeight = max( resultHeight, childNode.resultHeight );
	end
	resultHeight = resultHeight + 3 + ELEMENT_HEIGHT;

	local result = {};
	for y=1,resultHeight do result[y]={} end;

	local horPositions = {};
	for _, childNode in pairs( node.childNodes ) do
		-- position by horizontal of current element
		local blockPosition = _ ~= 1 and resultWidth + 2 or 1;

		local childWidth = childNode.resultWidth;
		local childHeight = childNode.resultHeight;

		-- check if we have empty space in result to shift child block to left
		while (blockPosition > 2) do
			if ( not willOverride( childNode.renderResult, result, blockPosition - 2, 5, childWidth, childHeight ) ) then
				blockPosition = blockPosition - 1;
			else
				break;
			end
		end
		horPositions[_] = blockPosition;

		-- define how parent horizontal line need to be drawn
		local blockLineConnectionX = blockPosition + childNode.lineConnectionX - 1;
		lineStartConnectFrom = min( lineStartConnectFrom, blockLineConnectionX )
		lineEndConnectTo = max( lineEndConnectTo, blockLineConnectionX )

		resultWidth = max(resultWidth, blockPosition + childWidth - 1);
		copyRendered( childNode.renderResult, result, blockPosition, ELEMENT_HEIGHT + 3 + 1, childNode.resultWidth, childNode.resultHeight )
	end

	node.resultWidth = resultWidth;
	node.resultHeight = resultHeight;

	-- local center = max( math.floor( (resultWidth + 1) / 2 ), 1 );
	local center = lineStartConnectFrom + math.floor( (lineEndConnectTo - lineStartConnectFrom) / 2 )
	if ( center < 3 ) then error( "center < 3" ) end
	lineStartConnectFrom = min( lineStartConnectFrom , center );
	lineEndConnectTo = max( lineEndConnectTo , center );

	if (lineStartConnectFrom > lineEndConnectTo) then
		error('lineStartConnectFrom > lineEndConnectTo');
	end

	local childConnectionLine = ELEMENT_HEIGHT + 3;
	-- draw child connection line
	if ( lineStartConnectFrom == lineEndConnectTo) then
		result[childConnectionLine][lineStartConnectFrom] = "NS";
	else
		result[childConnectionLine][lineStartConnectFrom] = "E";
		for x=(lineStartConnectFrom + 1),(lineEndConnectTo - 1) do
			result[childConnectionLine][x] = "EW";
		end
		result[childConnectionLine][lineEndConnectTo] = "W";
	end
	
	-- draw line down to children
	for _, childNode in pairs( node.childNodes ) do
		local childConnectionIndex = horPositions[_] - 1 + childNode.lineConnectionX;
		result[childConnectionLine][ childConnectionIndex ] = APPEND_LINE_DOWN[ result[childConnectionLine][ childConnectionIndex ] ];
	end

	-- draw line up to parent
	result[childConnectionLine][center] = APPEND_LINE_UP[ result[childConnectionLine][center] ];
	result[childConnectionLine-1][center] = "NS";
	result[childConnectionLine-2][center] = "NS";
	putEntity( result, 1, center - math.floor( ELEMENT_WIDTH / 2 ), node );

	node.renderResult = result;
	node.lineConnectionX = center;
	node.lineConnectionY = resultHeight;
end

local function renderRoot( options, node )
	-- render with parents first
	if ( options.interleave ) then
		renderInterleaveWithParents( options, node );
	else
		renderWithParents( options, node, nil, nil );
	end
	local withParentsResult = node.renderResult;
	local withParentsWidth = node.resultWidth;
	local withParentsHeight = node.resultHeight;
	local withParentsConnectionX = node.lineConnectionX;
	local withParentsConnectionY = node.lineConnectionY;
	
	node.renderResult = nil;
	node.resultWidth = nil;
	node.resultHeight = nil;
	node.lineConnectionX = nil;
	node.lineConnectionY = nil;
	
	-- render with children
	renderWithChildren( options, node );
	local withChildrenResult = node.renderResult;
	local withChildrenWidth = node.resultWidth;
	local withChildrenHeight = node.resultHeight;
	local withChildrenConnectionX = node.lineConnectionX;
	local withChildrenConnectionY = node.lineConnectionY;

	node.renderResult = nil;
	node.resultWidth = nil;
	node.resultHeight = nil;
	node.lineConnectionX = nil;
	node.lineConnectionY = nil;

	local shiftParents = max(withChildrenConnectionX - withParentsConnectionX, 0);
	local shiftChildren = max(withParentsConnectionX - withChildrenConnectionX, 0);

	local resultWidth = max( withParentsWidth + shiftParents, withChildrenWidth + shiftChildren );
	local resultHeight = withParentsHeight + withChildrenHeight - ELEMENT_HEIGHT;

	local result = {};
	for y=1,resultHeight do result[y]={} end;

	copyRendered( withChildrenResult, result, 1 + shiftChildren, withParentsHeight - ELEMENT_HEIGHT + 1, withChildrenWidth, withChildrenHeight )
	copyRendered( withParentsResult, result, 1 + shiftParents, 1, withParentsWidth, withParentsHeight )

	node.renderResult = result;
	node.resultWidth = resultWidth;
	node.resultHeight = resultHeight;
end

local function printEntityLabel( entityId )
	if ( type(entityId) ~= 'string' ) then error( 'Expected type of entityId to be string, but was ' .. type(entityId) ) end

	local label = mw.wikibase.getLabel( entityId );
	local link = mw.wikibase.sitelink( entityId )

	if link then
		-- link shall be prefixed with ':' to prevent category inclusion instead of link
		if label then
			return link == label and ('[[:' .. link .. '|' .. link .. ']]') or '[[:' .. link .. '|' .. label .. ']]';
		else
			return '[[:' .. link .. '|' .. link .. ']]';
		end
	end

	if label then
		-- красная ссылка
		local title = mw.title.new( label );
		if title and not title.exists then
			return RED_LINK_F( entityId, label);
		end

		-- TODO: перенести до проверки на существование статьи
		local sup = '<sup class="plainlinks noprint">[//www.wikidata.org/wiki/' .. entityId .. '?uselang=ru [d&#x5d;]</sup>'

		-- одноимённая статья уже существует - выводится текст и ссылка на ВД
		return '<span class="iw" data-title="' .. label .. '">' .. label .. sup .. '</span>'
	end

	-- сообщение об отсутвии локализованного названия
	-- not good, but better than nothing
	return MISSING_LABEL_LINK( entityId );
end

local function splitISO8601(str)
	if 'table' == type(str) then
		if str.args and str.args[1] then
			str = '' .. str.args[1]
		else
			return 'unknown argument type: ' .. type( str ) .. ': ' .. table.tostring( str )
		end
	end
	local Y, M, D = (function(str)
		local pattern = "(%-?%d+)%-(%d+)%-(%d+)T"
		local Y, M, D = mw.ustring.match( str, pattern )
		return tonumber(Y), tonumber(M), tonumber(D)
	end) (str);
	return {year=Y, month=M, day=D};
end

local function parseYearFromValue( value )
	local s = splitISO8601( value.time );
	if (not s) then return nil; end
	local precision = value.precision;

	if ( precision >= 0 and precision <= 8 ) then
		local powers = { 1000000000 , 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10 }
		local power = powers[ precision + 1 ];
		local left = s.year - ( ( s.year - 1 ) % power );
		return left, left - 1 + power / 2;
	end

	return s.year, s.year;
end

local function parseTimeValues( entityId, propertyId )
	local claims = mw.wikibase.getBestStatements( entityId, propertyId );
	if ( not claims ) then return nil; end

	local hasAnyValue = false;
	local values = {};
	for _, claim in pairs(claims) do
		local value = (((claim or EMPTY_TABLE).mainsnak or EMPTY_TABLE).datavalue or EMPTY_TABLE).value;
		if ( value ~= nil ) then
			local left, year = parseYearFromValue( value );
			if (year ~= nil) then
				values[year] = {
					precision = value.precision,
					year = year,
					left = left
				};
				hasAnyValue = true;
			end
		end
	end
	return hasAnyValue and values or nil;
end

local function getAnyValue( src )
	for _, value in pairs( src ) do
		return value;
	end
	return nil;
end

local function compareEntitiesByBirthDate( node1, node2 )
	local bYear1 = (getAnyValue( parseTimeValues( node1.entityId, 'P569' ) or EMPTY_TABLE) or EMPTY_TABLE).year or 9999;
	local bYear2 = (getAnyValue( parseTimeValues( node2.entityId, 'P569' ) or EMPTY_TABLE) or EMPTY_TABLE).year or 9999;
	return bYear1 < bYear2;
end

local function formatCentury( year )
	local moduleRoman = require( "Module:RomanNumber" )
	if ( year < 0 ) then
		local century = math.floor( (math.abs( year ) - 1) / 100 ) + 1;
        local infix = ' в ';
        if century == 2 then infix = ' во '; end;
		if ( moduleRoman ) then
			century = moduleRoman.toRomanNumber( century );
		end
		return century .. ' в. до н. э.'
	else
		local century = math.floor( ( year - 1) / 100 ) + 1;
        local infix = ' в ';
        if (century == 2) then infix = ' во ' end;
		if ( moduleRoman ) then
			century = moduleRoman.toRomanNumber( century );
		end
		return century .. ' в.'
	end
end

local function formatParsedTimeValue( parsedValue )
	if ( parsedValue == nil ) then
		return nil;
	elseif ( parsedValue.precision > 8 ) then
		if ( parsedValue.year >= 0 ) then
			return parsedValue.year;
		else
			return ( 0 - parsedValue.year ) .. ' до н.э.';
		end
		return parsedValue.year;
	elseif ( parsedValue.precision == 8) then
		if ( parsedValue.year >= 0 ) then
			return (parsedValue.left - 1) .. '-е';
		else
			return (0 - (parsedValue.left - 1) ) .. '-е до н.э.';
		end
	elseif ( parsedValue.precision == 7) then
		return formatCentury( parsedValue.year );
	else
		mw.log( 'Unsupported precision: ' .. parsedValue.precision );
		mw.log( parsedValue );
		return 'unsupported';
	end
end

local function compareAsStrings( a, b )
	return ('' .. a) < ('' .. b);
end

local function concat( parsedTimeValuesTable )
	local tmp = {};
	for _, value in pairs( parsedTimeValuesTable ) do
		table.insert( tmp , formatParsedTimeValue(value) );
	end
	table.sort( tmp, compareAsStrings );
	return table.concat( tmp, ' / ' );
end

local function printLifespan( entityId )
	local birth = parseTimeValues( entityId, 'P569' );
	local death = parseTimeValues( entityId, 'P570' );

	if (birth == nil and death == nil) then return nil end
	if (birth ~= nil and death == nil) then return 'р. ' .. concat( birth ) end
	if (birth == nil and death ~= nil) then return '? — ' .. concat( death ) end
	if (birth ~= nil and death ~= nil) then
		local bString = concat( birth );
		local dString = concat( death );
		if (bString == dString) then
			return bString;
		else
			return bString .. ' — ' .. dString;
		end
	end

	return nil;
end

local function printEntity( options, entityId )
	local html = printEntityLabel( entityId );
	if ( options.years == "label-line" ) then
		local lifespan = printLifespan( entityId );
		if ( lifespan ~= nil) then html = html .. ' <span class="years">(' .. lifespan .. '</span>)' end
	end
	if ( options.descriptions ) then
		local description = mw.wikibase.getDescription( entityId );
		if ( description ) then
			if ( options.descriptions == true or options.descriptions == "next-line" or options.descriptions == "new-line" ) then
				html = html .. '<br>'
			else
				html = html .. ', ';
			end
			html = html .. '<span class="descriptions">' .. description .. '</span>';
		end
	end
	if ( options.years == true or options.years == "next-line" or options.years == "new-line" ) then
		local lifespan = printLifespan( entityId );
		if ( lifespan ~= nil) then html = html .. '<br><div class="years">' .. lifespan .. '</div>' end
	end
	return html;
end

local function printEntityCell( options, attrs, node )
	local entityId = node.entityId;
	local generation = node.generation;
	return '<td class="Q gen' .. generation ..'" ' .. attrs .. '>'.. printEntity( options, entityId ) ..'</td>';
end

local ROTATE = {W="N",S="E",E="S",N="W"};
local INVERT_HORIZONTAL = {W="E",S="S",E="W",N="N"};

function renderHtmlHorizontal( options, node )
	local invert = options.invert;
	local resultHeight = node.resultHeight;
	local resultWidth = node.resultWidth;
	if (type(resultHeight) ~= 'number') then error("node.resultHeight expected to be number"); end
	if (type(resultWidth) ~= 'number') then error("node.resultWidth expected to be number"); end
	local renderResult = node.renderResult;

	local yStart = invert and resultHeight or 1;
	local yEnd = invert and 1 or resultHeight;
	local yStep = invert and -1 or 1;

	local html = '<div class="wikidata-familyTree wikidata-familyTree-horizontal wikidata-familyTree-decorate-' .. options.decorate .. '"><table>\n'
	for x=1,resultWidth do
		html = html .. '<tr><td></td>'

		for y=yStart,yEnd,yStep do

			local cell = renderResult[y][x];
			if (cell == nil) then

				if ( y ~= yStart and renderResult[y - yStep][x] == nil ) then
					-- skip, because handled by next block
				elseif ( renderResult[y][x] == nil ) then
					local colspan = 0;
					for yToSpan = y, yEnd, yStep do
						if ( renderResult[ yToSpan ][x] == nil ) then
							colspan = colspan + 1;
						else
							break;
						end
					end
					if (colspan == 1) then
						html = html .. '<td class=Z ></td>';
					else
						html = html .. '<td colspan=' .. colspan .. ' class=Z ></td>';
					end
				end

			elseif ( cell == BUSY ) then
				-- just skip
			elseif ( cell ~= nil and cell.entityId ) then
				html = html .. printEntityCell( options, 'colspan=' .. ELEMENT_HEIGHT .. ' rowspan=' .. ELEMENT_WIDTH, cell );
			else
				html = html .. '<td class="line ' .. cell .. '">';
				for p = 1,string.len(cell) do
					local lineClass = ROTATE[string.sub(cell, p, p)];
					if ( invert ) then lineClass = INVERT_HORIZONTAL[lineClass] end
					html = html .. '<span class=' .. lineClass .. '></span>';
				end
				html = html .. '</td>';
			end
		end
		html = html .. '<td></td></tr>\n'
	end
	html = html .. '</table></div>'
	return html;
end

function renderHtmlVertical( options, node )
	local resultHeight = node.resultHeight;
	local resultWidth = node.resultWidth;
	if (type(resultHeight) ~= 'number') then error("node.resultHeight expected to be number"); end
	if (type(resultWidth) ~= 'number') then error("node.resultWidth expected to be number"); end
	local renderResult = node.renderResult;

	local html = '<div class="wikidata-familyTree wikidata-familyTree-vertical wikidata-familyTree-decorate-' .. options.decorate .. '"><table>\n'
	for y=1,resultHeight do
		local renderLine = renderResult[y];
		html = html .. '<tr>'
		for x=1,resultWidth do
			local cell = renderLine[x];
			if (cell == nil) then
				html = html .. '<td class=Z></td>';
			elseif ( cell == BUSY ) then
				-- just skip;
			elseif ( cell ~= nil and cell.entityId ) then
				html = html .. printEntityCell( options, 'colspan=' .. ELEMENT_WIDTH .. ' rowspan=' .. ELEMENT_HEIGHT, cell );
			else
				html = html .. '<td class="line ' .. cell .. '">';
				for p = 1,string.len(cell) do
					html = html .. '<span class=' .. string.sub(cell, p, p) .. '></span>';
				end
				html = html .. '</td>';
			end
		end
		html = html .. '</tr>\n'
	end
	html = html .. '</table></div>'
	return html;
end

function p.drawTree( frame )
	local args = frame.args or EMPTY_TABLE;
	return p.drawTreeImpl( args );
end

local function toBoolean( valueToParse, defaultValue )
	if ( valueToParse ~= nil ) then
		if valueToParse == false or valueToParse == '' or valueToParse == 'false' or valueToParse == '0' then
			return false
		end
		if valueToParse == true or valueToParse == 'true' or valueToParse == '1' then
			return true
		end
	end
	return defaultValue;
end

function p.drawTreeImpl( args )
	-- проброс всех параметров из шаблона {wikidata} и параметра from откуда угодно
	local p_frame = mw.getCurrentFrame();
	while p_frame do
		if p_frame:getTitle() == mw.site.namespaces[10].name .. ':Wikidata/FamilyTree' then
			copyTo( p_frame.args, args, true );
		end
		if p_frame.args and p_frame.args.from and p_frame.args.from ~= '' then
			args.entityId = p_frame.args.from;
		end
		p_frame = p_frame:getParent();
	end

	-- p.drawTreeImpl( { entityId="Q7200" } );
	local entityId = args.entityId;
	if (entityId == "" or entityId == nil) then entityId = mw.wikibase.getEntityIdForCurrentPage(); end
	if (entityId == "" or entityId == nil) then return "Не задан идентификатор сущности entityId (страница не связана с Викиданными)"; end

	local options = {
		mode = args.mode or DEFAULT_MODE,
		invert = toBoolean( args.invert, false ),
		interleave = toBoolean( args.interleave, false ),
		ancestors = args.ancestors or ( ( args.mode or DEFAULT_MODE ) == "horizontal" and 2 or 3),
		descendants = args.descendants or ( ( args.mode or DEFAULT_MODE ) == "horizontal" and 2 or 3),
		compactParents = args.compactParents or ( ( args.mode or DEFAULT_MODE ) == "horizontal"),
		compactChildren = args.compactChildren or true,
		years = toBoolean( args.years, args.years ),
		descriptions = toBoolean( args.descriptions, args.descriptions ),
		decorate = args.decorate or DEFAULT_DECORATE,
	}

	local lines = {};
	lines[0] = {};

	local root = createNode(entityId, 0);
	table.insert( lines[0], root );

	for i=1,options.ancestors do
		lines[0 - i] = {};
		for _, node in pairs( lines[0 - i + 1] ) do
			if ( options.interleave ) then
				populateRelation( 'father', 'P22', lines[0 - i], node, node.generation - 1);
				populateRelation( 'mother', 'P25', lines[0 - i], node, node.generation - 1);
			else
				populateRelation( 'parent', 'P22', lines[0 - i], node, node.generation - 1);
				populateRelation( 'parent', 'P25', lines[0 - i], node, node.generation - 1);
			end
		end
	end

	for i=1,options.descendants do
		lines[0 + i] = {};
		for _, node in pairs( lines[0 + i - 1] ) do
			populateRelation( 'child', 'P40', lines[0 + i], node, node.generation + 1);
			if ( node.childNodes and #node.childNodes > 1 ) then
				table.sort( node.childNodes, compareEntitiesByBirthDate );
			end
		end
	end

	renderRoot( options, root );

	local html;
	if (options.mode == "horizontal") then
		html = renderHtmlHorizontal( options, root );
	else 
		html = renderHtmlVertical( options, root );
	end
	return html;
end

return p