Moduł:Wikidane/Tree

Z Wikibooks, biblioteki wolnych podręczników.
 Dokumentacja modułu [stwórz] [odśwież]
--------------------------------------------------------------------------------
-- NODE of data
--------------------------------------------------------------------------------

local nodeMetatable = {}
local nodeMethodtable = {}

nodeMetatable.__index = nodeMethodtable

nodeMethodtable.getLevel = function(me)
	local result = 0
	local parent = me.parent
	while parent do
		result = result + 1
		parent = parent.parent
	end
	
	return result
end

nodeMethodtable.normalize = function(me)
	local nodes = me.nodes
	local i = 1
	if #nodes <= 0 then
		-- there are no children
		return 0
	end
	
	local node = nodes[i]
	i = i + 1
	local deep = node:normalize()
	while i <= #nodes do
		local node2 = nodes[i]
		i = i + 1
		local deep2 = node2:normalize()
		if deep < deep2 then
			-- take longer path
			node = node2
			deep = deep2
		end
	end

	-- use single parent
	me.nodes = { node }
	return 1 + deep
end

nodeMethodtable.getPath = function(me, accept, useroot)
	local result = {}
	if me.nodes[1] then
		local node = useroot and me or me.nodes[1]
		while node do
			local name = node.name
			if not accept(name) then
				-- terminate sequence
				break
			end
			
			table.insert(result, name)
			node = node.nodes[1]
		end
		
		-- reverse result
		local i = 1
		local j = #result
		while i < j do
			local l = result[i]
			local r = result[j]
			result[i] = r
			result[j] = l
			i = i + 1
			j = j - 1
		end
	end
	
	return result
end

nodeMethodtable.add = function(me, node)
	node.parent = me
	table.insert(me.nodes, node)
end

nodeMethodtable.remove = function(me, node)
	local nodes = {}
	for i = 1, #me.nodes do
		local item = me.nodes[i]
		if item.name ~= node.name then
			table.insert(nodes, item)
		else
			node.parent = nil
		end
	end

	me.nodes = nodes
end

local function newNode(name)
	local me = {}
	me.name = name
	me.nodes = {}
	me.parent = nil
	setmetatable(me, nodeMetatable)
	return me
end

--------------------------------------------------------------------------------
-- TOOLS
--------------------------------------------------------------------------------

local getRequiredEntityId = function(frame, Q)
	if Q then
		return Q
	end
	
	local id = frame.args[1]
	if id and (#id > 0) then
		return id
	end
	
	local entity = mw.wikibase.getEntity()
	if entity then
		return entity.id
	end
end

local wikidataLinkItem = function(q)
	return "<span class=\"plainlinks wdlink\" title=\"edytuj dane z infoboxu w Wikidanych\">[https://www.wikidata.org/wiki/"..q.." •]</span>" 
end

local defaultFormatItem = function(q)
	local label = mw.wikibase.label(q)
	local site = mw.wikibase.sitelink(q)
	if site and label then
		return site == label and ("[["..site.."]]") or ("[["..site.."|"..label.."]]")
	elseif site then
		return "[["..site.."]]"
	elseif label then
		return label
	else
		return q
	end
end

local getBestStatements = function(entity, property)
	if entity and entity.claims and entity.claims[property] then
		local preferred = {}
		local normal = {}
		
		for _, v in ipairs(entity.claims[property]) do
			if v.rank == "normal" then
				table.insert(normal, v)
			elseif v.rank == "preferred" then
				table.insert(preferred, v)
			end
		end
		
		if #preferred > 0 then
			return preferred
		elseif #normal > 0 then
			return normal
		end
	end
end

local function isValueType(entity, type)
	mw.logObject({ entity.id, type }, "isValueType")
	local passed = {}
	local statements = {}
	
	local function loadBestStatements(e)
		local best = getBestStatements(e, "P279") or {}
		for _, v in ipairs(best) do
			table.insert(statements, v)
		end
	end
	
	passed[entity.id] = true
	loadBestStatements(entity)
	local i = 1
	while i <= #statements do
		local v = statements[i]
		if v.mainsnak.datavalue then
			local id = "Q"..v.mainsnak.datavalue.value["numeric-id"]
			mw.logObject(id, "id")
			if id == type then
				mw.logObject(true, "return isValueType")
				return true
			end
			
			if not passed[id] then
				passed[id] = true
				loadBestStatements(mw.wikibase.getEntity(id))
			end
		end
	
		i = i + 1
	end
	
	mw.logObject(false, "return isValueType")
	return false
end

--------------------------------------------------------------------------------
-- LIST FORMATTER
--------------------------------------------------------------------------------

local listFormatterMetatable = {}
local listFormatterMethodtable = {}

listFormatterMetatable.__index = listFormatterMethodtable
listFormatterMetatable.__tostring = function(me)
	return me.builder and tostring(me.builder) or nil
end

listFormatterMethodtable.append = function(me, item)
	me.builder = me.builder or mw.html.create("ul")
	me.builder:tag("li"):wikitext(me.format(item))
	return me
end

local listFormatter = function(formatItem)
	me = {
		format = formatItem or defaultFormatItem,
	}
	
	setmetatable(me, listFormatterMetatable)
	return me
end

--------------------------------------------------------------------------------
-- TAXONOMY FORMATTER
--------------------------------------------------------------------------------

local taxonomyFormatterMetatable = {}
local taxonomyFormatterMethodtable = {}

taxonomyFormatterMetatable.__index = taxonomyFormatterMethodtable
taxonomyFormatterMetatable.__tostring = function(me)
	return me.builder and table.concat(me.builder, "") or nil
end

taxonomyFormatterMethodtable.append = function(me, item)
	if #me.builder == 0 then
		table.insert(me.builder, "|-\n")
	end
	
	local formatDescription = function(site, label, latin)
		if site and label and latin then
			return "[["..site.."|"..label.."]] (''"..latin.."'')"
		elseif site and latin then
			return "[["..site.."|''"..latin.."'']]"
		elseif site and label then
			return "[["..site.."|"..label.."]]"
		elseif site then
			return "[["..site.."]]"
		elseif label and latin then
			return label.." (''"..latin.."'')"
		elseif latin then
			return "''"..latin.."''"
		elseif label then
			return label
		end
	end

	
	local entity = mw.wikibase.getEntity(item)
	local label = entity:getLabel()
	local site = entity:getSitelink()
	local ranks = getBestStatements(entity, "P105")
	local names = getBestStatements(entity, "P225")
	local taxonomicName = false
	table.insert(me.builder, "!")
	if ranks then
		for _, rank in ipairs(ranks) do
			if rank.mainsnak.snaktype == "value" then
				if (rank.mainsnak.datatype == "wikibase-item")and (rank.mainsnak.datavalue.type == "wikibase-entityid") and (rank.mainsnak.datavalue.value["entity-type"] == "item") then
					local rankid = "Q"..rank.mainsnak.datavalue.value["numeric-id"]
					table.insert(me.builder, wikidataLinkItem(rankid))
					local rankEntity = mw.wikibase.getEntity(rankid)
					local rankSite = rankEntity:getSitelink()
					local rankLabel = rankEntity:getLabel('pl')
					local rankLatin = rankEntity:getLabel('la')
					local ranktext = formatDescription(rankSite, rankLabel, rankLatin) or ""
					table.insert(me.builder, ranktext)
					break
				end
			end
		end
	end
	table.insert(me.builder, "\n|")
	if names then
		for _, name in ipairs(names) do
			if name.mainsnak.snaktype == "value" then
				if (name.mainsnak.datatype == "string") and (name.mainsnak.datavalue.type == "string") then
					taxonomicName = name.mainsnak.datavalue.value
					break
				end
			end
		end
	end

	if label == taxonomicName then
		label = false
	end
	
	table.insert(me.builder, wikidataLinkItem(entity.id))
	table.insert(me.builder, formatDescription(site, label, taxonomicName) or item)
	table.insert(me.builder, "\n|-\n")
	return me
end

local taxonomyFormatter = function()
	me = {
		builder = {}
	}
	
	setmetatable(me, taxonomyFormatterMetatable)
	return me
end

--------------------------------------------------------------------------------
-- ADMINISTRATIVE DIVISION FORMATTER
--------------------------------------------------------------------------------

local administrativeDivisionFormatterMetatable = {}
local administrativeDivisionFormatterMethodtable = {}

administrativeDivisionFormatterMetatable.__index = administrativeDivisionFormatterMethodtable
administrativeDivisionFormatterMetatable.__tostring = function(me)
	if me.usetop then
		local suffix = #me.builder == 0 and "\n|-\n" or "\n"
		table.insert(me.builder, 1, "|-\n!Państwo\n|"..defaultFormatItem(me.top[1])..suffix)
	end
	
	if me.bottom then
		me:append(me.bottom)
	end
	
	return table.concat(me.builder, "")
end

local function acceptAdministrativeDivisionEntity(entity)
	return isValueType(entity, "Q56061")    -- administrative territorial entity
		or isValueType(entity, "Q15617994") -- designation for an administrative territorial entity
		or (entity.id == "Q1078001")        -- territorial dispute
end

administrativeDivisionFormatterMethodtable.append = function(me, item)

	if #me.builder == 0 then
		table.insert(me.builder, "|-\n")
	end
	
	if item == me.bottom then
		me.bottom = false;
	end
	
	if item == "Q1078001" then
		table.insert(me.builder, "!")
		table.insert(me.builder, wikidataLinkItem(item))
		table.insert(me.builder, defaultFormatItem(item))
		for i, v in ipairs(me.top) do
			table.insert(me.builder, i > 1 and ", " or "\n|")
			table.insert(me.builder, wikidataLinkItem(v))
			table.insert(me.builder, defaultFormatItem(v))
		end
		table.insert(me.builder, "\n|-\n")
		return me
	end
	
	if (#me.top == 1) and (item == me.top[1]) then
		table.insert(me.builder, "!Państwo")
		me.usetop = false
	else
		local entity = mw.wikibase.getEntity(item)
		local itis = getBestStatements(entity, "P31")
		table.insert(me.builder, "!")
		if itis then
			local more = false
			for _, v in ipairs(itis) do
				if v.mainsnak.snaktype == "value" then
					if (v.mainsnak.datatype == "wikibase-item")and (v.mainsnak.datavalue.type == "wikibase-entityid") and (v.mainsnak.datavalue.value["entity-type"] == "item") then
						local id = "Q"..v.mainsnak.datavalue.value["numeric-id"]
						local e = mw.wikibase.getEntity(id)
						if acceptAdministrativeDivisionEntity(e) then
							if more then
								table.insert(me.builder, ", ")
							end
							
							table.insert(me.builder, wikidataLinkItem(id))
							table.insert(me.builder, defaultFormatItem(id))
							more = true
						end
					end
				end
			end
		end
	end

	table.insert(me.builder, "\n|")
	table.insert(me.builder, wikidataLinkItem(item))
	table.insert(me.builder, defaultFormatItem(item))
	table.insert(me.builder, "\n|-\n")
	return me
end

local administrativeDivisionFormatter = function(top, bottom)
	me = {
		usetop = #top == 1,
		top = top,
		bottom = bottom,
		builder = {}
	}
	
	setmetatable(me, administrativeDivisionFormatterMetatable)
	return me
end

--------------------------------------------------------------------------------
-- GENERATOR
--------------------------------------------------------------------------------

local generatorMetatable = {}
local generatorMethodtable = {}

generatorMetatable.__index = generatorMethodtable

generatorMethodtable.run = function(me, wgWikibaseItemId, builder)
	me.types = {}
	me.subclasses = {}
	mw.log("LOADING DATA "..wgWikibaseItemId)
	me:loadData(wgWikibaseItemId)
	mw.log("BUILDING TREE "..wgWikibaseItemId)
	local dictionary = {}
	local root = newNode(wgWikibaseItemId)
	dictionary[wgWikibaseItemId] = root
	me:buildingTree(dictionary, root)
	mw.log("LOOKING FOR PATH "..wgWikibaseItemId)
	local deep = root:normalize()
	local accept = function(item)
		return not me.T.terminators[item];
	end
	local path = root:getPath(accept, me.T.showLeaf);
	if #path then
		mw.log("PREPARING LINKS AND LABELS "..wgWikibaseItemId)
		for i = 1, #path do
			if (i ~= 1) or not me.T.hideTerminators or not me.T.terminators[path[i]] then
				builder:append(path[i])
			end
		end
		local result = tostring(builder)
		--mw.logObject(result, "showTree")
		return result
	end
end

generatorMethodtable.loadData = function(me, q)
	if me.T[q] then
		mw.log("loadData: cut searching "..q)
		-- cut searching
		return
	end
	
	local data = mw.wikibase.getEntity(q)
	if not me.types[q] then
		local ids = me:parseItemClaims(data.claims.P31, me.T.accept)
		if ids then
			me.types[q] = ids
			for i = 1, #me.T.parentSources do
				local p = me.T.parentSources[i]
				me:LoadParentItems(q, mw.wikibase.getEntity(q))
			end
		end
	end
end

generatorMethodtable.buildingTree = function(me, nodes, node)
	local subclasses = me.subclasses[node.name]
	if subclasses then
		for i = 1, #subclasses do
			local subclass = subclasses[i]
			-- find parent
			local n = node
			local parent = false
			while n do
				if n.name == subclass then
					parent = n
					break
				end
				
				n = n.parent
			end

			if parent then
				mw.log("there is cycle in the graph")
			else
				local available = nodes[subclass]
				if available then
					local alevel = available:getLevel()
					local nlevel = node:getLevel()
					if alevel <= nlevel then
						-- the available node level path is shorter
						-- move it to this location
						available.parent:remove(available)
						node:add(available)
					else
						-- the available node level path is same size or longer
						-- as this is further class, it is ignored
					end
				else
					local newnode = newNode(subclass)
					nodes[subclass] = newnode
					node:add(newnode)
					me:buildingTree(nodes, newnode)
				end
			end
		end
	end
end

generatorMethodtable.LoadParentItems = function(me, q, data)
	local accept = function() return true end
	for i = 1, #me.T.parentSources do
		local p = me.T.parentSources[i]
		local ids = me:parseItemClaims(data.claims[p], accept)
		if ids then
			me.subclasses[q] = ids
			for j = 1, #ids do
				me:loadData(ids[j])
			end
		end
	end
end

generatorMethodtable.parseItemClaims = function(me, items, check)
	if not items or (#items <= 0) then
		mw.log("parseItemClaims: no items")
		return {}
	end
	
	mw.logObject(items, "parseItemClaims")
	if #items > 1 then
		local recent = false
		local date = false
		for _, v in ipairs(items) do
			if v.qualifiers and v.qualifiers.P580 then
				for _, d in ipairs(v.qualifiers.P580) do
					if d.snaktype == "value" then
						if not date or (date < d.datavalue.value.time) then
							date = d.datavalue.value.time
							recent = v
						end
					end
				end
			end
		end
		
		if date and recent then
			mw.log(recemt, "recent")
			items = { recent, }
		end
	end

	local ids = {}
	local j = 1
	for i = 1, #items do
		if items[i].mainsnak.datavalue then
			local id = "Q"..items[i].mainsnak.datavalue.value["numeric-id"]
			if check(id) then
				ids[j] = id
				j = j + 1
			end
		end
	end
	
	return #ids > 0 and ids or nil
end

local generator = function(T)
	local me = {}
	me.T = T;
	setmetatable(me, generatorMetatable)
	return me
end

return {
	taxonomy = function(frame, Q)
		local entityId = getRequiredEntityId(frame, Q)
		if not entityId then
			return
		end
		
		local acceptableTypes = {
			Q16521 = true,
			Q310890 = true,
			Q713623 = true,
			Q23038290 = true,
		}
		
		local params = {
			parentSources = {
				"P171",
			},
			accept = function(id)
				return acceptableTypes[id]
			end,
			terminators = {
				Q2382443 = true,
			},
			showLeaf = true,
		}
		return generator(params):run(entityId, taxonomyFormatter())
	end,
	
	administrativeDivision = function(frame, Q)
		local entityId = getRequiredEntityId(frame, Q)
		if not entityId then
			return
		end
		
		local entity = mw.wikibase.getEntity(entityId)
		local tops = getBestStatements(entity, "P17");
		if tops then
			local params = {
				parentSources = {
					"P131",
				},
				accept = function(id)
					local entity = mw.wikibase.getEntity(id)
					return entity and acceptAdministrativeDivisionEntity(entity)
				end,
				terminators = {},
				showLeaf = true,
			}
			for _, v in ipairs(tops) do
				if (v.mainsnak.snaktype == "value") and (v.mainsnak.datatype == "wikibase-item") and (v.mainsnak.datavalue.type == "wikibase-entityid") and (v.mainsnak.datavalue.value["entity-type"] == "item") then
					local top = "Q"..v.mainsnak.datavalue.value["numeric-id"]
					table.insert(params.terminators, top)
					mw.logObject(params, top)
				end
			end
		
			return generator(params):run(entityId, administrativeDivisionFormatter(params.terminators, entityId))
		end

	end,
	
	classTree = function(frame, Q)
		local entityId = getRequiredEntityId(frame, Q)
		if not entityId then
			return
		end
	
		local params = {
			parentSources = mw.text.split( frame.args.parentSources, '%s' ),
			terminators = {},
			showLeaf = frame.args.showLeaf and true or false
		}
		local acceptableTypes = frame.args.acceptableTypes
		if acceptableTypes and (#acceptableTypes > 0) and (acceptableTypes ~= "-") then
			local accept = {}
			for _, v in ipairs(mw.text.split( frame.args.acceptableTypes, '%s' )) do
				accept[v] = true
			end

			params.accept = function(id) mw.logObject(accept[id], "classTree.accept["..id.."]") return accept[id] end
		else
			params.accept = function(id) return true end
		end

		local terminators = frame.args.terminators
		if terminators and (#terminators > 0) then
			for _, v in ipairs(mw.text.split( frame.args.terminators, '%s' )) do
				params.terminators[v] = true
			end
		end
		
		return generator(params):run(entityId, listFormatter())
	end,
}