Module:Wikidata

From Wikispecies
Jump to navigation Jump to search

Documentation for this module may be created at Module:Wikidata/doc

local wiki = {
	langcode = mw.language.getContentLanguage().code
}

local i18n = {
	["errors"] = {
		["property-param-not-provided"] = "Property parameter not provided.",
		["property-not-found"] = "Property not found.",
		["entity-not-found"] = "Entity not found.",
		["qualifier-not-found"] = "Qualifier not found.",
		["unknown-claim-type"] = "Unknown claim type.",
		["unknown-snak-type"] = "Unknown snak type.",
		["unknown-datavalue-type"] = "Unknown datavalue type.",
		["unknown-entity-type"] = "Unknown entity type.",
		["unknown-value-module"] = "You must set both value-module and value-function parameters.",
		["value-module-not-found"] = "The module pointed by value-module not found.",
		["value-function-not-found"] = "The function pointed by value-function not found."
	},
	["somevalue"] = "Unknown value",
	["novalue"] = "Missing value",
	["monolingualtext"] = '<span lang="%language">%text</span>'
}

-- the "qualifiers" and "snaks" field have a respective "qualifiers-order" and "snaks-order" field
-- use these as the second parameter and this function instead of the built-in "pairs" function
-- to iterate over all qualifiers and snaks in the intended order.
local function orderedpairs(array, order)
	if not order then return pairs(array) end

	-- return iterator function
	local i = 0
	return function()
		i = i + 1
		if order[i] then
			return order[i], array[order[i]]
		end
	end
end

function getEntityFromId( id )
	if id then
		return mw.wikibase.getEntityObject( id )
	end
	return mw.wikibase.getEntityObject()
end

function getEntityIdFromValue( value )
	local prefix = ''
	if value['entity-type'] == 'item' then
		prefix = 'Q'
	elseif value['entity-type'] == 'property' then
		prefix = 'P'
	else
		return formatError( 'unknown-entity-type' )
	end
	return prefix .. value['numeric-id']
end

function formatError( key )
	return '<span class="error">' .. i18n.errors[key] .. '</span>'
end

function formatStatements( options )
	if not options.property then
		return formatError( 'property-param-not-provided' )
	end

	--Get entity
	local entity = getEntityFromId( options.entityId )
	if not entity then
		return -- formatError( 'entity-not-found' )
	end

	if (entity.claims == nil) or (not entity.claims[string.upper(options.property)]) then
		return '' --TODO error?
	end

	--Format statement and concat them cleanly
	local formattedStatements = {}
	for i, statement in pairs( entity.claims[string.upper(options.property)] ) do
		table.insert( formattedStatements, formatStatement( statement, options ) )
	end
	if options.first then
		return formattedStatements[1]
	else
		return mw.text.listToText( formattedStatements, options.separator, options.conjunction )
	end
end

function formatStatement( statement, options )
	if not statement.type or statement.type ~= 'statement' then
		return formatError( 'unknown-claim-type' )
	end

	return formatSnak( statement.mainsnak, options )
end

function formatSnak( snak, options )
	if snak.snaktype == 'somevalue' then
		return i18n['somevalue']
	elseif snak.snaktype == 'novalue' then
		return i18n['novalue']
	elseif snak.snaktype == 'value' then
		return formatDatavalue( snak.datavalue, options )
	else
		return formatError( 'unknown-snak-type' )
	end
end

function formatGlobeCoordinate( value, options )
	if options['subvalue'] == 'latitude' then
		return value['latitude']
	elseif options['subvalue'] == 'longitude' then
		return value['longitude']
	else
		local eps = 0.0000001 -- < 1/360000
		local globe = '' -- TODO
		local lat = {}
		lat['abs'] = math.abs(value['latitude'])
		lat['ns'] = value['latitude'] >= 0 and 'N' or 'S'
		lat['d'] = math.floor(lat['abs'] + eps)
		lat['m'] = math.floor((lat['abs'] - lat['d']) * 60 + eps)
		lat['s'] = math.max(0, ((lat['abs'] - lat['d']) * 60 - lat['m']) * 60)
		local lon = {}
		lon['abs'] = math.abs(value['longitude'])
		lon['ew'] = value['longitude'] >= 0 and 'E' or 'W'
		lon['d'] = math.floor(lon['abs'] + eps)
		lon['m'] = math.floor((lon['abs'] - lon['d']) * 60 + eps)
		lon['s'] = math.max(0, ((lon['abs'] - lon['d']) * 60 - lon['m']) * 60)
		local coord = '{{coord'
		if (value['precision'] == nil) or (value['precision'] < 1/60) then -- по умолчанию с точностью до секунды
			coord = coord .. '|' .. lat['d'] .. '|' .. lat['m'] .. '|' .. lat['s'] .. '|' .. lat['ns']
			coord = coord .. '|' .. lon['d'] .. '|' .. lon['m'] .. '|' .. lon['s'] .. '|' .. lon['ew']
		elseif value['precision'] < 1 then
			coord = coord .. '|' .. lat['d'] .. '|' .. lat['m'] .. '|' .. lat['ns']
			coord = coord .. '|' .. lon['d'] .. '|' .. lon['m'] .. '|' .. lon['ew']
		else
			coord = coord .. '|' .. lat['d'] .. '|' .. lat['ns']
			coord = coord .. '|' .. lon['d'] .. '|' .. lon['ew']
		end
		coord = coord .. '|globe:' .. globe
		if options['display'] then
			coord = coord .. '|display=' .. options.display
		else
			coord = coord .. '|display=title'
		end
		coord = coord .. '}}'

		return g_frame:preprocess(coord)
	end
end

function formatDatavalue( datavalue, options )
	--Use the customize handler if provided
	if options['value-module'] or options['value-function'] then
		if not options['value-module'] or not options['value-function'] then
			return formatError( 'unknown-value-module' )
		end
		local formatter = require ('Module:' .. options['value-module'])
		if formatter == nil then
			return formatError( 'value-module-not-found' )
		end
		local fun = formatter[options['value-function']]
		if fun == nil then
			return formatError( 'value-function-not-found' )
		end
		return fun( datavalue.value, options )
	end

	--Default formatters
	if datavalue.type == 'wikibase-entityid' then
		return formatEntityId( getEntityIdFromValue( datavalue.value ), options )
	elseif datavalue.type == 'string' then
		if options.pattern and options.pattern ~= '' then
			return formatFromPattern( datavalue.value, options )
		else
			return datavalue.value
		end
	elseif datavalue.type == 'globecoordinate' then
		return formatGlobeCoordinate( datavalue.value, options )
	elseif datavalue.type == 'time' then
		local Time = require 'Module:Time'
		return Time.newFromWikidataValue( datavalue.value ):toHtml()
	else
		return formatError( 'unknown-datavalue-type' )
	end
end

function formatEntityId( entityId, options )
	if (options.format == 'id') then
		return entityId
	end

	local label = mw.wikibase.label( entityId )
	local link = mw.wikibase.sitelink( entityId )
	if link and (options.format ~= 'label') then
		if label then
			return '[[' .. link .. '|' .. label .. ']]'
		else
			return '[[' .. link .. ']]'
		end
	else
		return label --TODO what if no links and label + fallback language?
	end
end

local p = {}

function p.formatStatements( frame )
	local args = frame.args

	--If a value if already set, use it
	if args.value and args.value ~= '' then
		return args.value
	end
	return formatStatements( frame.args )
end

local function formatDatavalueCoordinate(data, parameter)
	-- data fields: latitude [double], longitude [double], altitude [double], precision [double], globe [wikidata URI, usually http://www.wikidata.org/entity/Q2 [earth]]
	if parameter then
		if parameter == "globe" then data.globe = mw.ustring.match(data.globe, "Q%d+") end -- extract entity id from the globe URI
		return data[parameter]
	else
		return data.latitude .. "/" .. data.longitude -- combine latitude and longitude, which can be decomposed using the #titleparts wiki function
	end
end

local function formatDatavalueQuantity(data, parameter)
	-- data fields: amount [number], unit [string], upperBound [number], lowerBound [number]
	if parameter then
		return data[paramater]
	else
		return tonumber(data.amount)
	end
end

local function formatDatavalueEntity(data, parameter)
	-- data fields: entity-type [string], numeric-id [int, Wikidata id]
	local id = "Q" .. data["numeric-id"]
	if parameter then
		if parameter == "link" then
			return "[[" .. (mw.wikibase.sitelink(id) or (":d:" .. id))  .. "|" ..  (mw.wikibase.label(id) or id)  .. "]]"
		else
			return data[parameter]
		end
	else
		if data["entity-type"] == "item" then return mw.wikibase.label("Q" .. data["numeric-id"]) or id else formatError("unknown-entity-type") end
	end
end

local function formatDatavalueMonolingualText(data, parameter)
	-- data fields: language [string], text [string]
	if parameter then
		return data[parameter]
	else
		return mw.ustring.gsub(mw.ustring.gsub(i18n.monolingualtext, "%%language", data["language"]), "%%text", data["text"])
	end
end

function findClaims(entity, property)
	if not property or not entity or not entity.claims then return end

	if mw.ustring.match(property, "^P%d+$") then
		-- if the property is given by an id (P..) access the claim list by this id
		return entity.claims[property]
	else
		property = mw.wikibase.resolvePropertyId(property)
		if not property then return end

		return entity.claims[property]
	end
end

function getSnakValue(snak, parameter)
	-- snaks have three types: "novalue" for null/nil, "somevalue" for not null/not nil, or "value" for actual data
	if snak.snaktype == "novalue" then
		return i18n["novalue"]
	elseif snak.snaktype == "somevalue" then
		return i18n["somevalue"]
	elseif snak.snaktype ~= "value" then
		return nil, formatError("unknown-snak-type")
	end

	-- call the respective snak parser
	if snak.datavalue.type == "string" then return snak.datavalue.value
	elseif snak.datavalue.type == "globecoordinate" then return formatDatavalueCoordinate(snak.datavalue.value, parameter)
	elseif snak.datavalue.type == "quantity" then return formatDatavalueQuantity(snak.datavalue.value, parameter)
	elseif snak.datavalue.type == "wikibase-entityid" then return formatDatavalueEntity(snak.datavalue.value, parameter)
	elseif snak.datavalue.type == "monolingualtext" then return formatDatavalueMonolingualText(snak.datavalue.value, parameter)
	else return nil, formatError("unknown-datavalue-type")
	end
end

function getQualifierSnak(claim, qualifierId)
	-- a "snak" is Wikidata terminology for a typed key/value pair
	-- a claim consists of a main snak holding the main information of this claim,
	-- as well as a list of attribute snaks and a list of references snaks
	if qualifierId then
		-- search the attribute snak with the given qualifier as key
		if claim.qualifiers then
			local qualifier = claim.qualifiers[qualifierId]
			if qualifier then return qualifier[1] end
		end
		return nil, formatError("qualifier-not-found")
	else
		-- otherwise return the main snak
		return claim.mainsnak
	end
end

function getValueOfClaim(claim, qualifierId, parameter)
	local error
	local snak
	snak, error = getQualifierSnak(claim, qualifierId)
	if snak then
		return getSnakValue(snak, parameter)
	else
		return nil, error
	end
end

function p.claim(frame)
	local property = frame.args[1] or ""
	local id = frame.args["id"]	-- "id" must be nil, as access to other Wikidata objects is disabled in Mediawiki configuration
	local qualifierId = frame.args["qualifier"]
	local parameter = frame.args["parameter"]
	local list = frame.args["list"]
	local references = frame.args["references"]
	local showerrors = frame.args["showerrors"]
	local default = frame.args["default"]
	if default then showerrors = nil end

	-- get wikidata entity
	local entity = mw.wikibase.getEntityObject(id)
	if not entity then
		if showerrors then return formatError("entity-not-found") else return default end
	end
	-- fetch the first claim of satisfying the given property
	local claims = findClaims(entity, property)
	if not claims or not claims[1] then
		if showerrors then return formatError("property-not-found") else return default end
	end

	-- get initial sort indices
	local sortindices = {}
	for idx in pairs(claims) do
		sortindices[#sortindices + 1] = idx
	end
	-- sort by claim rank
	local comparator = function(a, b)
		local rankmap = { deprecated = 2, normal = 1, preferred = 0 }
		local ranka = rankmap[claims[a].rank or "normal"] ..  string.format("%08d", a)
		local rankb = rankmap[claims[b].rank or "normal"] ..  string.format("%08d", b)
		return ranka < rankb
	end
	table.sort(sortindices, comparator)

	local result
	local error
	if list then
		local value
		-- iterate over all elements and return their value (if existing)
		result = {}
		for idx in pairs(claims) do
			local claim = claims[sortindices[idx]]
			value, error = getValueOfClaim(claim, qualifierId, parameter)
			if not value and showerrors then value = error end
			if value and references then value = value .. getReferences(frame, claim) end
			result[#result + 1] = value
		end
		result = table.concat(result, list)
	else
		-- return first element
		local claim = claims[sortindices[1]]
		result, error = getValueOfClaim(claim, qualifierId, parameter)
		if result and references then result = result .. getReferences(frame, claim) end
	end

	if result then return result else
		if showerrors then return error else return default end
	end
end

return p