Модуль:Wikidata/FamilyTree
Перейти к навигации
Перейти к поиску
Для документации этого модуля может быть создана страница Модуль: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]]</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 role="presentation">\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