Module:Wikidata art

From Wikisource
Jump to navigation Jump to search
--[[  
  __  __           _       _      __        ___ _    _     _       _                     _   
 |  \/  | ___   __| |_   _| | ___ \ \      / (_) | _(_) __| | __ _| |_ __ _    __ _ _ __| |_ 
 | |\/| |/ _ \ / _` | | | | |/ _ (_) \ /\ / /| | |/ / |/ _` |/ _` | __/ _` |  / _` | '__| __|
 | |  | | (_) | (_| | |_| | |  __/_ \ V  V / | |   <| | (_| | (_| | || (_| | | (_| | |  | |_ 
 |_|  |_|\___/ \__,_|\__,_|_|\___(_) \_/\_/  |_|_|\_\_|\__,_|\__,_|\__\__,_|  \__,_|_|   \__|
                                                                                             
This module is intended to provide localized text for different infobox fields.
At the moment we have:
|====================|===========================|=====================|
| Infobox Field      | Property                  | Template            |
|====================|===========================|=====================|
| object history     | commissioned by (P88)     | {{ProvenanceEvent}} |
|                    | owned by (P127)           |                     |
|                    | significant event (P793)  |                     |
| exhibition history | exhibition history (P608) | none                |
| inscriptions       | inscription (P1684)       | {{inscription}}     |
| medium             | material used (P186)      | {{Technique}}       |
| work location      | work location (P937)      | none                |
| creator            | creator(P170), author(P50)|                     |
|                    | architect (P84)           | {{Creator}}         |
| institution        | inventory number (P217)   | {{Institution}}     |
|                    | collection (P195)         |                     |
|                    | location (P276)           |                     |
| accession number   | inventory number (P217)   |  none               |
|====================|===========================|=====================|


Please do not modify this code without applying the changes first at 
"Module:Artwork/sandbox" and testing at "Module:Artwork/testcases".

Authors and maintainers:
* User:Jarekt - original version 
]]
local getLabel         = require("Module:Wikidata label")._getLabel            -- used for creation of name based on wikidata
local getDate          = require("Module:Wikidata date")._date                 -- used for processing of date properties
local qualifierDate    = require("Module:Wikidata date")._qualifierDate        -- used for processing of date qualifiers
local creator          = require("Module:Creator")._creator                    -- render creator templates
local institution      = require("Module:Institution")._institution            -- render institution templates
local material_LUT     = require('Module:Artwork/Technique LUT')
local id_properties_LUT = require('Module:Artwork/Artwork ID properties LUT')
local TagQS            = require('Module:TagQS')
local core             = require('Module:Core')

-- ==================================================
-- === Internal functions ===========================
-- ==================================================

local function length(T)
  local count = 0
  for _ in pairs(T) do count = count + 1 end
  return count
end

-------------------------------------------------------------------------------
local function getProperty(entity, prop)
	return (core.parseStatements(entity:getBestStatements( prop ), nil) or {nil})[1]
end

-------------------------------------------------------------------------------
local function getBestProperties(entity, prop)
	return core.parseStatements(entity:getBestStatements( prop ), nil)
end

-------------------------------------------------------------------------------
local function getAllProperties(entity, prop)
	return core.parseStatements(entity:getAllStatements( prop ), nil)
end


-------------------------------------------------------------------------------
local function getItemProperty(item, prop)
	return (core.parseStatements(mw.wikibase.getBestStatements( item, prop ), nil) or {nil})[1]
end

-------------------------------------------------------------------------------
local function getAllItemProperties(item, prop)
	return core.parseStatements(mw.wikibase.getAllStatements( item, prop ), nil)
end

-------------------------------------------------------------------------------
local function getPropertyQual(entity, prop, qualifiers, lang, offset)
	local Res = {}
	if entity.claims and entity.claims[prop] then
		for k, statement in ipairs( entity:getBestStatements( prop )) do
			if (statement.mainsnak.snaktype == "value") then 
				local res = {} -- table with fields: key, value, P... (qualifiers)
				local jdn = k + (offset or 0) -- "Julian day number" will be used as a key for sorting events; initialize
				local val = statement.mainsnak.datavalue.value
				if val.id then 
					res.value_id = val.id
					val = core.getLabel(val.id, lang)
				elseif val.text then
					res.value_lang = val.language
					val = val.text
				end
				res.value = val
				for iQual, qual in ipairs( qualifiers ) do
					if statement.qualifiers and statement.qualifiers[qual] then
						local snak = statement.qualifiers[qual][1]
						if (snak.snaktype == "value" and snak.datatype == 'wikibase-item') then 
							val = core.getLabel(snak.datavalue.value.id, lang)
							res[qual ..'_id'] = snak.datavalue.value.id
						elseif (snak.snaktype == "value" and snak.datatype == 'string') then 
							val = snak.datavalue.value
						elseif (snak.snaktype == "value" and snak.datatype == 'monolingualtext') then 
							val = snak.datavalue.value.text
							res[qual.."_lang"] = snak.datavalue.value.language
						elseif (snak.snaktype == "value" and snak.datatype == 'time') then
							val = qualifierDate(snak, lang)
							if iQual==1 then -- first qualifier in the qualifiers list will be used as a sorting value
								jdn = val.jdn
							end
							val = val.str
						else
							val = nil
						end
						res[qual] = val
					end
				end
				res.key = jdn
				table.insert(Res, res)
			end
		end
	end
	local tableComp = function (rec1, rec2) return rec1.key<rec2.key end
	table.sort(Res, tableComp)
	return Res
end

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

-- ===========================================================================
function p.get_object_history(entity, lang)
	-- Provenance look up table converting items IDs to template inputs
	local ProvenanceLUT = {
		Q22340494  = "acquisition", --acquisition
		Q707482    = "gift", --gift
		Q211557    = "bequest", --bequest
		Q194189    = "sale", --sales
		Q184303    = "gift", --gift
		Q177923    = "auction", --auction
		Q5260774   = "deposit", --deposit
		Q1124860   = "gift", --donation
		Q1362753   = "acquisition", --acquisition
		Q1756454   = "theft", --art theft
		Q17781833  = "destruction", --destruction
		Q217102    = "restored", --conservation
		Q2727213   = "theft", --theft
		Q6498684   = "in collection", --ownership
		Q753297    = "discovery", --discovery
		Q14903979  = "conveyance", --change of ownership
		Q105334701 = "loan", --loan
		Q601401    = "exchange", --trade
		Q6160      = "damaged", --vandalism
		Q19880899  = "theft", --Isabella Stewart Gardner Museum theft
		Q1156800   = "restitution", --restitution
		Q1369832   = "purchase", --purchasing
		Q851304    = "theft", --Looted art
		Q3030513   = "missing", --disappearance
		Q21745157  = "destruction", --destroyed artwork
		Q53706     = "theft", --robbery
		Q328376    = "theft", --Nazi plunder
		Q420708    = "acquisition", --Acquisition
		Q760089    = "commission", --commission
		Q200303    = "inheritance", --inheritance
		Q3196      = "burnt", --fire
		Q192623    = "theft", --looting
	}
	--{{ProvenanceEvent|time=1950-03-01|type=discovery|newowner=Elias Cohen|place=The Hague}}
	local frame = mw.getCurrentFrame()
	local EventList = {}

	-- discovery statements
	local discoveror     =  getProperty(entity, 'P61')   -- discoverer or inventor (P61) 
	local discoveryPlace =  getProperty(entity, 'P189')   -- location of discovery (P189) 
	local d = getDate(entity, 'P575' , lang)              -- discovery date
	local discoveryTime = d.str
	local event = {}
	if discoveror or discoveryPlace or discoveryTime then
		event.str = frame:expandTemplate{ title = 'ProvenanceEvent', args = { type='discovery', time=discoveryTime, discoveror=discoveror, place=discoveryPlace, inline=1, lang=lang } } 
		event.key = 0; -- keys are usually dates passed from getPropertyQual but place this one at the front of the queue
		table.insert(EventList, event)
	end
	
	-- from   commissioned by (P88) / point in time (P585) (time property)
	local eIcon = core.editAtWikidata(entity.id, 'P88', lang)
	local provEvents = getPropertyQual(entity, 'P88', {'P585'}, lang) -- 0 is where the numbering of undated events will start
	for _, event in ipairs( provEvents) do
		if event.P585 then
			event.str = frame:expandTemplate{ title = 'ProvenanceEvent', args = { type='commissioned', time=event.P585, newowner=event.value, inline=1, lang=lang } } .. eIcon
			table.insert(EventList, event)
		end
	end
	-- from  owned by (P127) / P580 (time property)
	eIcon = core.editAtWikidata(entity.id, 'P127', lang)
	provEvents = getPropertyQual(entity, 'P127', {'P580'}, lang, 100) -- 100 is where the numbering of undated events will start
	for _, event in ipairs( provEvents) do
		if event.P580 then
			event.str = frame:expandTemplate{ title = 'ProvenanceEvent', args = { type='acquisition', time=event.P580, newowner=event.value, inline=1, lang=lang } } .. eIcon
			table.insert(EventList, event)
		end
	end
	-- from significant event P793 property with point in time (P585) qualifier
	eIcon = core.editAtWikidata(entity.id, 'P793', lang)
	provEvents = getPropertyQual(entity, 'P793', {'P585'}, lang, 200) -- 200 is where the numbering of undated events will start
	for _, event in ipairs( provEvents) do
		local eventType = ProvenanceLUT[event.value_id] -- look up event type based on stored item ID
		if event.P585 and eventType then
			event.str = frame:expandTemplate{ title = 'ProvenanceEvent', args = { type=eventType, time=event.P585, inline=1, lang=lang } } .. eIcon
			table.insert(EventList, event)
		elseif event.P585  then
			event.str = event.P585 .. ": unknown event: "..event.value .. eIcon.."<br/>"
			table.insert(EventList, event)
		end
	end
	if #EventList>0 then -- if any events
		local tableComp = function (rec1, rec2) return rec1.key<rec2.key end
		table.sort(EventList, tableComp) -- sort them by the date using sort key
		local X, event = {}, {}
		for _, event in ipairs(EventList) do -- collect just text of the template
			table.insert(X, event.str)
		end
		return '* ' .. table.concat(X,"\n* ")
	end
	return nil
end

-- ===========================================================================
function p.get_exhibition_history(entity, lang)	
	local comma = mw.message.new( "comma-separator"):inLanguage(lang):plain()

--  exhibition history (P608)  (item property) /  'P580', 'P582' (time properties) 
	local prop = getPropertyQual(entity, 'P608', {'P580', 'P582', 'P585', 'P276'}, lang)
	local rows, cells = {}, {}
	for i, p in ipairs(prop) do
		local places, dates_flag, row, cell
		
		if p.P580 or p.P585 then 
			-- likely the item is for a GLAM institution or a city
			cells = { p.value }
			if p.P276 then
				table.insert(cells, p.P276 )
			end
			if p.P580 then
				table.insert(cells, mw.ustring.format("%s - %s",  p.P580, p.P582 or '') )
			else
				table.insert(cells, p.P585 )
			end
			row = table.concat(cells, comma) 
			if i==1 then 
				row = row .. core.editAtWikidata(p.value_id, 'P608', lang)
			end
		else -- likely the item is for the exhibit where dates and location will be a property of the exhibit
			cells = { "<i>" .. p.value .. "</i>" }
			local exh_entity = mw.wikibase.getEntity(p.value_id)
			places = getPropertyQual(exh_entity, 'P276', {'P580', 'P582'}, lang)
			for _, pl in ipairs(places) do
				dates_flag = pl.P580 or pl.P585
				if pl.P580 then
					cell = mw.ustring.format("%s (%s - %s)", pl.value, pl.P580, pl.P582 or '') 
				elseif pl.P585 then
					cell = mw.ustring.format("%s (%s)", pl.value, pl.P585) 				
				else
					cell = pl.value
				end
			end
			table.insert(cells, cell)
			local start = getDate(exh_entity, 'P580', lang).str
			local stop  = getDate(exh_entity, 'P582', lang).str 
			if start and not dates_flag then
				table.insert(cells, mw.ustring.format("%s - %s", start, stop or '') )
			end
			row = table.concat(cells, comma) .. core.editAtWikidata(p.value_id, '', lang)
		end
		table.insert(rows, row)
	end
	if length(rows)>0 then
		return "\n*" .. table.concat(rows,"\n*")
	end
	return nil
end

-- ===========================================================================
function p.get_medium(entity, lang)
	-- material used (P186) (item property) /  applies to part (P518) (item property) 
	local prop = getPropertyQual(entity, 'P186', {'P518'}, lang)
	if not prop or length(prop)==0 then
		return nil -- if no P186 statements than exit
	end
	local temp_args = {} -- technique template arguments
	local And = {'1', 'and', 'and2', 'and3', 'and4'} -- field names to use
	temp_args.lang = lang
	local material = {}
	local n = 0;
	local ok = true;
	for _, p in ipairs( prop) do
		table.insert(material, p.value)
		local mat = material_LUT[p.value_id] -- use lookup table to convert item IDs to terms used by the template
		if not mat then
			ok = false;                      -- unrecognized material
		end
		if p.P518_id=='Q861259' then         -- applies to part: painting surface
			temp_args.on = mat
		elseif p.P518_id=='Q107105674' then  -- applies to part: mount
			temp_args.mounted = mat
		elseif n<4 then
			n = n + 1;
			temp_args[And[n]] = mat
		end
	end
	local medium
	if length(material)==2 and temp_args["1"]=='oil' and temp_args.on=='canvas' then
		local LUT = require("Module:i18n/oil on canvas")  -- oil on canvas can be done in LUA
		medium = core.langSwitch(LUT, lang)
	elseif ok then -- if 
		local frame = mw.getCurrentFrame()
		medium = frame:expandTemplate{ title = 'technique', args=temp_args }
	else 
		medium = table.concat(material,", ")
	end
	if not medium then
		local id = getProperty(entity, 'P2079')
		if id then
			medium = core.getLabel(id, lang) .. core.editAtWikidata(entity.id, 'P608', lang)
		end
	else
		medium = medium .. core.editAtWikidata(entity.id, 'P186', lang)
	end
	return medium
end

-- ===========================================================================
function p.get_inscription(entity, lang)
--[[
 Wikidata
	 inscription (P1684) - Monolingual text
	 applies to part (P518) - item property
	 instance of (P31) - item property
 Commons template:
	{{inscription |1= |full form= |type= |side= |position= |description= |comment= |ID= |language= |translation= |en= |de= |medium= }}
]]
	local LUT = {
	  -- positions stored in "applies to part (P518)" qualifier
		Q15332388 = "bottom",
		Q17525439 = "bottom",
		Q11812678 = "bottom",
		Q15332375 = "top",
		Q17525438 = "top",
		Q23595    = "center",
		Q13196750 = "left",
		Q17525441 = "left",
		Q14565199 = "right",
		Q17525442 = "right",
		Q27956549 = "top left",
		Q27956533 = "top right",
		Q27956553 = "bottom left",
		Q27956561 = "bottom right",
		Q82383    = "on the base",
		Q11193    = "on the base",
		Q860792   = "on the frame",
		
		-- if 2 positions are provided they can be combined into a single entry 
		-- supported by the template
		bottom_left   = "bottom left",
		bottom_right  = "bottom right",
		bottom_center = "bottom center",
		center_left   = "center left",
		center_right  = "center right",
		left_top      = "top left",
		right_top     = "top right",
		center_top    = "top center",

		-- sides in "applies to part (P518)" qualifier
		Q9305022  = "recto",
		Q257418   = "recto", -- obverse
		Q9368452  = "verso",
		Q1542661  = "verso", -- reverse
		Q32198402 = "verso", -- reverse
		Q16938807 = "verso", -- reverse
		
		-- inscription type stored in "object has role (P3831)" qualifier
		Q188675   = "signature",   -- signature
		Q1373131  = "signature",   -- signature 
		Q205892   = "date",        -- calendar date 
		Q1898184  = "dedication",  -- dedication
		Q168346   = "monogram",    -- monogram
		Q2221906  = "place",       -- geographic location 
		Q783521   = "title",       -- title
		Q206287   = "quotation",   -- quotation
		Q644099   = "stamp",       -- rubber stamp 
		Q162919   = "seal",        -- seal
		Q1417099  = "accession number", -- accession number
		Q319608   = "artist's address", -- postal address 
		Q14659    = "coat of arms",     -- coat of arms
		Q42470    = "motto",            -- motto
		Q1772     = "epitaph",          -- epitaph
		Q43065    = "watermark",        -- watermark
		Q827198   = "speech balloon",   -- speech balloon
		Q98877418 = "reign mark",       -- reign mark on Chinese ceramics
		Q18585177 = "caption",          -- caption 
		Q2374398  = "cartouche",        -- cartouche
		Q112110   = "emblem",           -- emblem
		Q3509975  = "in memoriam",      -- in memoriam
		Q1272809  = "kalos inscription",-- kalos inscription
		Q15873403 = "publisher's mark", -- publisher's mark
		Q105844397 = "initials",
		
		-- if 2 inscription types are provided they can be combined into a 
		-- single entry supported by the template
		date_signature = "signature and date",
		date_monogram  = "monogram and date"
	}
	local max_insc = 20
	local frame = mw.getCurrentFrame()
	local AllInsc = {}
	local addIconFlag = true
	for iInsc, statement in ipairs( entity:getBestStatements( 'P1684' )) do -- "inscription (P1684)"
		if (statement.mainsnak.snaktype == "value") then 
			local val = statement.mainsnak.datavalue.value
			local temp_args, position, iType = {}, {}, {}
			temp_args['1']     = val.text -- text
			temp_args.language = val.language -- language of the text
			temp_args.lang     = lang -- language of the reader
			temp_args.nocat    = '1'  -- no inscription categories (They are very slow)

			if statement.qualifiers then
				if statement.qualifiers.P3831 then -- object has role (P3831)
					for _, snak in ipairs( statement.qualifiers.P3831) do
						table.insert(iType, LUT[snak.datavalue.value.id])
					end
					if length(iType)==2 then -- two iType values can be combined into a single value used by the template
						table.sort(iType) -- order them alphabetically
						val = LUT[table.concat(iType, '_')]
						if val ~= nil then
							iType = { [1]=val } -- example: signature and date
						end
					end
					temp_args.type = table.concat(iType, '/')
				end				
				if statement.qualifiers.P518 then -- applies to part (P518) used for locattion
					for _, snak in ipairs( statement.qualifiers.P518) do
						local part = LUT[snak.datavalue.value.id]
						if (part=="recto" or part=="verso") then
							temp_args.side = part
						else
							table.insert(position, part)
						end
					end
				end
				if statement.qualifiers.P2441 then --  literal translation (P2441) 
					local snak = statement.qualifiers.P2441[1]
					val = snak.datavalue.value
					temp_args[val.language]  = val.text
				end
 				if statement.qualifiers.P7008 then --  unabbreviated text (P7008) 
					local snak = statement.qualifiers.P7008[1]
					val = snak.datavalue.value
					temp_args['full form']  = val.text
				end 
				if length(position)==1 then
					temp_args.position = position[1]
				elseif length(position)==2 then -- two position values can be combined into a single value used by the template
					table.sort(position) -- order them alphabetically
					temp_args.position = LUT[table.concat(position, '_')]
				end
			end
			if 	addIconFlag and iInsc==1 then
				temp_args['1'] = temp_args['1'].. core.editAtWikidata(entity.id, 'P1684', lang)		
				addIconFlag = false				
			end
			if 	iInsc<max_insc then		
				val = frame:expandTemplate{ title = 'inscription', args=temp_args }
				table.insert(AllInsc, val)
			end
		end
	end
	if length(AllInsc)>0 then
		return "\n*" .. table.concat(AllInsc,"\n*")
	end
	return nil
end

-- ===========================================================================
function p.get_work_location(entity, lang)
	-- work_location (P937) /  'P580', 'P582' (time properties) 
	local prop = getPropertyQual(entity, 'P937', {'P580', 'P582', 'P585'}, lang)
	local X={}
	for _, p in ipairs(prop) do
		local str = p.value
		if p.P580 or p.P582 then
			str = mw.ustring.format("%s (%s - %s)", p.value, p.P580 or '', p.P582 or '')
		elseif p.P585 then
			str = mw.ustring.format("%s (%s)", p.value, p.P585)
		else
			str = p.value
		end
		table.insert(X, str)
	end
	if length(X)>0 then
		return table.concat(X,"; ") .. core.editAtWikidata(entity.id, 'P937', lang)
	end
	return nil
end

-- ===========================================================================
function p.get_depicted(entity, lang)

	local maxDepict = 50 -- maximum number of Depict statements to check, to prevent running out of memory for items with 100's of them
	local X, Y, Done = {{},{}}, {}, {}
	local ID_LUT = { 
		Q5        = 1, -- human  
		Q21070568 = 1, -- human who may be fictional
		Q95074    = 1, -- fictional character 
		Q4271324  = 1, -- mythical character 
		Q18563360 = 1, -- Quranic character 
		Q20643955 = 1, -- human biblical figure 
		Q22813672 = 1, -- two biblical humans
		Q22813674 = 1, -- group of biblical humans
		Q235113   = 1, -- angel  
		Q178342   = 1, -- archangel 
		Q581450   = 1, -- fallen angel  
	    Q178885   = 1, -- deity 
		Q979507   = 1, -- Hindu deity 
		Q22989102 = 1, -- Greek deity 
		Q11688446 = 1, -- Roman deity  
		Q16513881 = 1, -- Norse deity  
		
		Q22704077 = 2, -- biblical episode 
		Q1406161  = 2, -- artistic theme
		Q46999986 = 2  -- martyrdom
	}

	local props = {P921='main subject', P180='depicts'}
	for prop, _ in pairs(props) do
		local iCounter = {0, 0} -- two counters
		local items = getBestProperties(entity, prop)
		for iDepict, pid in ipairs(items or {}) do
			if iDepict<maxDepict then 
				local P31s = getAllItemProperties(pid, 'P31')
				for _, p31 in ipairs(P31s or {}) do
					local dType = ID_LUT[p31] -- type of depicts: 1 or 2					
					if dType and (not Done[pid]) then -- instance of "human", etc. was found 
						local text = core.getLabel(pid, lang)
						iCounter[dType] = iCounter[dType] + 1 -- how many of that type
						if iCounter[dType]==1 then
							text = text .. core.editAtWikidata(entity.id, prop, lang) 
						end
						table.insert(X[dType], text)
						Done[pid] = true
						break
					end					
				end
			end
		end
	end
	for i = 1,2 do
		local n = #X[i]
		if n==0 then
			X[i] = nil
		elseif n==1 then
			X[i] = X[i][1]
		else
			X[i] = '* ' .. table.concat(X[i],"\n* ")
		end
	end
	return X 
end

-- ===========================================================================
function p.get_depicted_people(entity, lang)
	return p.get_depicted(entity, lang)[1]
end

-- ===========================================================================
function p.get_accession_number (entity, lang)
	local Res = {} -- initialize final output

	-- harvest data from inventory number (P217) property with qualifiers:  collection (P195) and end time (P582)
	local Y = {}  -- Y is a structure where we have a table of IDs for each collection
	local prop = getPropertyQual(entity, 'P217', {'P195', 'P582'}, lang)
	for k, p in ipairs(prop) do                      -- loop over all IDs found
		if not p.P582 then                             -- skip if there is an "end date"
			local key = p.P195_id or k
			if not Y[key] then Y[key]={} end -- initialize if it does not exist
			table.insert(Y[key], p.value)          -- group IDs by collection
			Res.id = p.value -- return one of the pure ID strings, to be used as category sortkey
		end
	end
	
	--assemble the wikitext of the accession_number field
	local X = {}                 -- table with wikitext strings for each "collection"
	for key, id in pairs(Y) do          -- loop over institutions
		local id=mw.text.listToText(id)   -- convert all the IDs into a single string (in most cases there will be only one)
		if type(key)=='string' then       -- if "collection" qualifier is used than add it to the ID
			table.insert(X, mw.ustring.format( "%s <small>(%s)</small>", id , core.getLabel(key, lang) ) )
		else
			table.insert(X, id)     -- if no "collection" is mentioned than just return ID
		end
	end
	
	-- assemble final output structure
	if length(X)==1 then     -- single ID case
		Res.str = X[1] .. core.editAtWikidata(entity.id, 'P217', lang)  -- just return the string
	elseif length(X)>1 then  -- if more than one than return bulleted list
		X[1] = X[1] .. core.editAtWikidata(entity.id, 'P217', lang)
		Res.str = "* " .. table.concat(X, "\n* ")
	end
	return Res
end

-- ===========================================================================
local function renderInstitution(entity, lang)
-- local function to create wikitext for a single institution template or {{Private collection}} template
-- once we have entity check if Institution template exist and call it or assemble one based on Wikidata
	local frame = mw.getCurrentFrame()
	local inst
	
	-- first check for few special cases which will result in {{Private collection}} template
	if entity.id == 'Q768717' then   -- render {{Private collection}} template
		return frame:expandTemplate{ title ='Private collection'} .. '<br/>'
	end
	local P31 = getBestProperties(entity, 'P31') -- look up "instance of" property for "Institution" entity
	for _, p in ipairs(P31 or {}) do
		if p=='Q5' then                             -- if "Institution" entity is a person than render {{Private collection}} template
			return frame:expandTemplate{ title ='Private collection', args={ owner = getLabel(entity, lang)}} .. '<br/>'
		elseif p=='Q768717' then                    -- if "Institution" is an instance of "Private collection" than render {{Private collection}} template
			return frame:expandTemplate{ title ='Private collection'} .. '<br/>'
		end
	end
	
	-- render Institution template
	local P1612 = getProperty(entity, 'P1612')                                  -- look up "Commons Institution page" property
	-- make sure second argument is string, never nil (it returns nil for the empty string, but throws error for nil)
	local templateName = mw.title.makeTitle( 'Institution', P1612 or '' )
	if templateName and templateName.exists then
		return frame:expandTemplate{ title ='Institution:' .. P1612, args={'collapse'} } -- use existing template
	else
		local inst,_ = institution({wikidata=entity.id, lang=lang, collapse=1})          -- create institution based on item id 
		return inst
	end
end

-- ===========================================================================
local function isPrivateCollection(entity)
	-- test if collection is a private_collection
	-- see https://www.wikidata.org/wiki/Wikidata:WikiProject_sum_of_all_paintings/Private_collection
	local private_collection = 'Q768717'
	if entity.claims and entity.claims.P195 then
		for _, statement in ipairs( entity:getBestStatements( 'P195' )) do
			if (statement.mainsnak.snaktype == "somevalue") then 
				local quals = statement.qualifiers
				if quals and not quals.P582 and quals.P3831 then -- object has role (P3831) and no end time (P582)
					for _, qual in ipairs( quals.P3831 ) do
						local role0 = qual.datavalue.value.id    -- specify the role of "collection"
						if role0 == private_collection then
							return true
						end
					end
				end
			end
		end
	end
	return false
end

-- ===========================================================================
function p.get_institution(entity, lang)
  local collection, location = {}, {} -- relevant data is stored in collection (P195) and location (P276) properties

	--  harvest data from inventory number (P217) property with qualifiers:  collection (P195), and end time (P582)
	local prop = getPropertyQual(entity, 'P217', {'P580', 'P582', 'P195'}, lang) -- P580 if present is used for sorting
	for _, p in ipairs(prop) do
		if not p.P582 and p.P195_id then  -- skip if there is an "end date"
			collection[p.P195_id] = 1       -- store collection item ID
		end
	end
	
	-- harvest data from collection (P195) / start time (P580) + end time (P582)
	local prop = getPropertyQual(entity, 'P195', {'P580', 'P582'}, lang) -- P580 if present is used for sorting
	for _, p in ipairs(prop) do
		if  p.P582 then                -- skip if there is an "end date"
			collection[p.value_id] = nil -- and delete from Collection list
		else
			collection[p.value_id] = 1   -- otherwise  collection item ID to the list
		end
	end
	if isPrivateCollection(entity) then
		collection.Q768717 = 1
	end
	
	-- harvest data from location (P276) / start time (P580) + end time (P582)
	local prop = getPropertyQual(entity, 'P276', {'P580', 'P582'}, lang)
	for _, p in ipairs(prop) do 
		if not p.P582 and not collection[p.value_id] then -- skip if there is an "end date" or the value is in collection table
			location[p.value_id] = 1     -- store location item ID
		end
	end 
	
	-- initialize output structure
	local Res = {}
	Res.institution = nil
	Res.location    = nil
	Res.id          = nil
	
	-- first try usual cases of single collection item 
	if length(collection)==1  then                       -- only a single collection item
		local cId, _  = next(collection, nil)            -- collection item ID
		local cEntity = mw.wikibase.getEntity(cId)       -- collection entity
		local cParent = getProperty(cEntity, 'P361')     -- collection parent object of which collection item is part of (P361) 
		if cParent == 'Q19675' or cParent == 'Q1075988' then    -- special case where collection is part of Louvre Museum
			local frame = mw.getCurrentFrame()
			Res.institution = frame:expandTemplate{ title ='Institution:Louvre', args={'collapse'} } -- render existing {{Institution:Louvre}} template
			Res.id          = 'Q1075988'
			Res.location    = getLabel(cEntity, lang)           -- use collection and location tables to populate location/department field  
			if length(location)>0 then
				local lId, _  = next(location, nil)             -- Location item ID
				Res.location = Res.location .. '<br/>\n' .. core.getLabel(lId, lang)	
			end
			return Res
		end
		
		if cId=='Q812285' and length(location)>0 then           -- if collection is Bavarian State Painting Collections (Q812285) 
			collection = location                                 -- use location instead collection
		else
			Res.institution = renderInstitution(cEntity, lang)    -- use collection entity to render Institution template
			Res.id          = cEntity.id
			if length(location)>0 then                            -- single collection and at least one location
				local lId, _  = next(location, nil)               -- location item ID
				local lParent = getItemProperty(lId, 'P361')         -- location parent object of which location item is part of (P361) 
				if lParent == cId then                            -- location is part of the collection listed above
					Res.location = getLabel(cEntity, lang)	        -- use location entity as location/department field                                            
				end                                               -- if collection and locations are not related so ignore location(s)
			end
			return Res
		end
	end
	
	-- If the case is not usual try generic approach
	if length(collection)==0 and length(location)>0 then    -- no collections but we have some locations
		collection = location                                 -- use location instead collection
	end
	if length(collection)>0 then                            -- collections or locations only or locations same as collections
		local X = {}                                          -- table with wikitext of all the institution templates
		for cId, _ in pairs(collection) do                    -- render all collections
			local inst = renderInstitution(mw.wikibase.getEntity(cId), lang)
			table.insert(X, inst )
		end
		Res.institution = table.concat(X, '\n')
	end
	return Res
end

-- ===========================================================================
function p.get_creator(entity, prop, lang)

	-- harvest the data
	local IDs = {}
	local qualifiers = {P1773='attributed to', P1774='workshop of', P1775='follower of', P1776='circle of', 
	                    P1777='manner of', P1779='possibly', P1780='school of', P1877='after'};
	local LUT = {Q18122778='presumably', Q30230067='possibly', Q56644435='probably', 
	             Q50137645='attributed to', Q230768='attributed to'}
	local anonymous = 'Q4233718'
	if entity.claims and entity.claims[prop] then
		for _, statement in ipairs( entity:getBestStatements( prop )) do
			local option, itemID1, itemID2, role, quals
			if (statement.mainsnak.snaktype == "value") then 
				itemID1 = statement.mainsnak.datavalue.value.id
			else -- if (statement.mainsnak.snaktype == "somevalue")
				itemID1 = nil
			end
			-- look for role of "creator" like: bookbinding, lithography, etc.
			quals = statement.qualifiers
			if quals and quals.P518 then                    -- applies to part (P518) 
				role = quals.P518[1].datavalue.value.id     -- specify the role of "creator"
			end
			if quals and quals.P3831 then                   -- object has role (P3831)
				for _, qual in ipairs( quals.P3831 ) do
					local role0 = qual.datavalue.value.id    -- specify the role of "creator"
					if itemID1 == nil and role0 == anonymous then
						itemID1 = anonymous                  -- user is anonymous
					else
						role = role0
					end
				end
			end
			-- look for "options" related to certainty ('presumably', 'possibly', 'probably', etc.)
			if quals and (quals.P5102 or quals.P1480) then  -- sourcing circumstances (P1480) and  nature of statement (P5102) 
				local q = quals.P5102 or quals.P1480		-- values used by P1480 were moved to P5102
				option = LUT[q[1].datavalue.value.id]       -- add certainty qualifiers
			end				
			-- look for qualifiers thet provide new creator ID  ('school of', 'after', etc.)
			for qual, opt in pairs( qualifiers ) do
				if quals and quals[qual] then
					itemID2 = quals[qual][1].datavalue.value.id 
					table.insert(IDs, {itemID=itemID2, option=opt, role=role});
					break
				end
			end
			-- add new creator, except for the case when they are anonymous and we have secondary ID 
			if not ((itemID1==anonymous or itemID1==nil) and itemID2) then 
				table.insert(IDs, {itemID=itemID1, option=option, role=role});
			end
		end
	end

	--sort the table
	local tableComp = function (rec1, rec2) return (rec1.itemID or 'ZZZ')<(rec2.itemID or 'ZZZ') end
	table.sort(IDs, tableComp)
	
	-- IDs table cleanup
	-- "workshop of", "circle of", "school of", "studio of", "or follower", "or workshop", "and workshop", "attributed to", "after", "formerly attributed to", "follower of", "manner of", "namepiece", "possibly", "probably".
	for k = 2, #IDs do
		if IDs[k-1].itemID==IDs[k].itemID then
			local val = (IDs[k-1].option or '') .. (IDs[k].option or '')
			if val=='workshop of' then
				IDs[k  ].option = "and workshop"
				IDs[k-1].option = "delete"
			elseif val=="follower of" then
				IDs[k  ].option = "or follower"
				IDs[k-1].option = "delete"
			end	
		end
	end

	-- render the output template(s)
	local Creators = {}
	local frame = mw.getCurrentFrame()
	for k =1, #IDs do
		local val, _
		local itemID = IDs[k].itemID
		local option = IDs[k].option
		local role   = IDs[k].role
		if itemID == nil then  -- render {{Unknown|author}} template
			val = frame:expandTemplate{ title ='Unknown', args={'author'}}
			table.insert(Creators, val)
		elseif itemID == anonymous then -- render anonymous label
			val = core.getLabel(itemID, lang)
			table.insert(Creators, val)
		elseif option ~= "delete" then
			local P1472 = getItemProperty(itemID, 'P1472') -- look up "Commons Creator page" property
			local templateName = mw.title.makeTitle('Creator',P1472 or '' )
			if P1472 and templateName.exists then
				if option then option=option..'/collapse' else option='collapse' end
				val = frame:expandTemplate{ title ='Creator:' .. P1472, args = {option} } -- use existing template 
			else
				val, _ = creator({wikidata=itemID, lang=lang, option=option, collapse=1})-- create creator based on item id 
			end
			if role then
				local tag = TagQS.createTag('creator_role', nil, role)
				val = "'''" .. core.getLabel(role, lang) .. "''': " .. tag .. val
			end
			table.insert(Creators, val)
		end
	end  -- for

	-- gather the output structure
	local Res = {}
	Res.str = nil
	Res.id  = nil                            -- if only one creator and no "option" modifier than return ID
	Res.IDs = IDs                            -- raw data used to render the template(s)
	if #Creators>0 then
		Res.str = string.format('<table cellpadding=0 cellspacing=0><tr><td>%s</td><td style="vertical-align:top">%s</td></tr></table>', 
			table.concat(Creators, '\n'),
			core.editAtWikidata(entity.id, prop, lang))
	end
	if #IDs==1 and not IDs[1].option then
		Res.id = IDs[1].itemID
	end
	return Res
end

-- ===========================================================================
function p.get_references(entity, lang)
	local Res -- initialize final output
	
	local wordsep   = mw.message.new( "Word-separator" ):inLanguage(lang):plain()
	local colon     = mw.message.new( "Colon-separator" ):inLanguage(lang):plain() .. wordsep
	local comma     = mw.message.new( "Comma-separator" ):inLanguage(lang):plain() .. wordsep

	-- harvest data from catalog code (P528) property with qualifiers:  catalog (P972)
	local strTable = {}                 -- table with wikitext strings for each "reference"
	local prop = getPropertyQual(entity, 'P528', {'P972'}, lang)
	local str
	local addIconFlag = true
	for _, p in ipairs(prop) do                      -- loop over all IDs found
		if p.P972 then
--[=[
		-- commented out to not use label cf. https://commons.wikimedia.org/wiki/Module_talk:Wikidata_art#Catalogues
			local catalog = core.getLabel('Q2352616', lang); -- get translation of word "catalog"
			str = catalog .. colon ..  "''" .. p.P972 .. "''" .. comma .. p.value
--]=]
			str = "''" .. p.P972 .. "''" .. comma .. p.value
			if addIconFlag then 
				str = str .. core.editAtWikidata(entity.id, 'P528', lang)
				addIconFlag = false
			end
			table.insert(strTable, str)          -- group IDs by collection
		end
	end

	-- harvest data from "described at URL" (P973) property with qualifier: language (P407), title (P1476), publisher (P123) and author (P50) 
	local label
	prop = getPropertyQual(entity, 'P973', {'P407', 'P1476', 'P123', 'P50'}, lang)
	for k, p in ipairs(prop) do
--[=[
		-- commented out to not use label cf. https://commons.wikimedia.org/wiki/Module_talk:Wikidata_art#Catalogues
		if not label then
			label  = getLabel('P973', lang) -- get translation of phrase "described at URL"
		end
		str = string.format("%s%s [%s ''%s'']", label, colon, p.value, mw.uri.decode( p.value ) )
--]=]
		str = string.format("[%s ''%s'']", p.value, mw.uri.decode( p.value ) )
		if p.P1476 then -- display title if available rather than raw URL
			local linkTitle = mw.ustring.gsub(p.P1476, '%]', '&#93;')
			-- escape closing square brackets that close links in MediaWiki
--[=[
			-- commented out to not use label cf. https://commons.wikimedia.org/wiki/Module_talk:Wikidata_art#Catalogues
			str = string.format("%s%s [%s ''%s'']", label, colon, p.value, linkTitle)
--]=]
			str = string.format("[%s ''%s'']", p.value, linkTitle)
		end
		if p.P50 then -- add author
			str = str .. ", " .. p.P50
		end
		if p.P123 then -- add publisher
			str = str .. ", " .. p.P123
		end
		if p.P407 then -- add language
			str = str .. " (" .. p.P407 .. ")"
		end
		if k==1 then 
			str = str .. core.editAtWikidata(entity.id, 'P973', lang)
		end
		table.insert(strTable, str)          -- group IDs by collection
	end
	
	-- Support for direct IDs to museum databases: table of 
	--     a) Wikidata properties of the museum catalog ID and
	--     b) names of the corresponding template on Wikimedia Commons
	-- e.g. the Museum of Modern Art online collection has a
	-- "Museum of Modern Art work ID" (P2014) on Wikidata
	-- and Template:Moma_online on Wikimedia Commons.
	-- we can add everything from https://commons.wikimedia.org/wiki/Category:Museum_database_templates
	local commons_templates_for_database = {
--[=[
		["P347"]  = "Joconde",        -- Joconde database (French Republic)
		["P4659"] = "Orsay online",   -- Orsay database (Musée d'Orsay)
		["P4157"] = "MEG online",     -- MEG database (Musée d'Ethnographie de Genève)
		["P1679"] = "Art UK",         -- identifier for artworks (publicly owned oil paintings in the UK)
		["P2014"] = "Moma online",    -- identifier for a Museum of Modern Art artwork
		["P2092"] = "Bildindex",      -- Bildindex der Kunst und Architektur ID
		["P2108"] = "Kunstindeks",    -- Kunstindeks Danmark artwork ID
		["P4611"] = "LACMA online",   -- Los Angeles County Museum of Art website
		["P350"]  = "RKDimages"       -- RDK (Netherlands Institute for Art History)
--]=]
	}
--[=[
	for property, template_name in pairs(commons_templates_for_database) do
		local database_id = getProperty(entity, property)
		if database_id then
			local frame = mw.getCurrentFrame()
			local eIcon = core.editAtWikidata(entity.id, property, lang)
			str = frame:expandTemplate{ title = template_name, args = { database_id } }
			str = mw.text.trim(str) .. eIcon
			table.insert(strTable, str)
		end
	end

	-- harvest more direct IDs to museum databases from Wikidata and display
	-- them independently of Commons templates
--]=]
	id_properties_LUT['P1212'] = nil -- remove retired Atlas ID
	for prop, property_urls in pairs(id_properties_LUT) do
		if not commons_templates_for_database[prop] then
			local formatter_url = core.langSwitch(property_urls, lang)
			if not formatter_url then
				for _,url in pairs(property_urls) do
					-- use any URL if none found with langSwitch
					formatter_url = url
				end
			end
			local eIcon = core.editAtWikidata(entity.id, prop, lang)
			for i, database_id in ipairs(getBestProperties(entity, prop) or {}) do
				local str = (getLabel(prop, lang) .. ': ['
					.. string.gsub(formatter_url, '$1', database_id) .. ' '
					.. database_id .. ']' .. eIcon)
				table.insert(strTable, str)
			end
	    end
	end

	-- harvest data from described by source (P1343) property with qualifiers:  pages (P304), publication date (P577), 
	--  section, verse, or paragraph (P958),  volume (P478),  reference URL (P854),  title (P1476),  statement is subject of (P805)  
	prop = getPropertyQual(entity, 'P1343', {'P304', 'P958', 'P478', 'P854', 'P1476', 'P805', 'P577'}, lang)
	label = nil
	for k, p in ipairs(prop) do 
--[=[
		-- commented out to not use label cf. https://commons.wikimedia.org/wiki/Module_talk:Wikidata_art#Catalogues
		if not label then
			label  = getLabel('P1343', lang) -- get translation of word ""
		end
--]=]
		local frame = mw.getCurrentFrame()
		local cite_arg = {} 
		cite_arg.title   = p.value	    -- described by source (P1343)
		cite_arg.url     = p.P854 or ''	-- reference URL (P854)
		cite_arg.volume  = p.P478 or ''	-- volume (P478)
		cite_arg.pages   = p.P304 or ''	-- pages (P304)
		-- differentiate between "pages" and "page"
		if not string.find(cite_arg.pages, '%p') then
			cite_arg.page = cite_arg.pages
			cite_arg.pages = nil
		end
		cite_arg.chapter = p.P958 or ''	-- section, verse, or paragraph (P958)
		cite_arg.series  = p.P805 or ''	-- statement is subject of (P805)
		cite_arg.date    = p.P577 or ''	-- statement is subject of (P805)
		str = frame:expandTemplate{ title ='Cite_book', args = cite_arg }
		if k==1 then 
			str = str .. core.editAtWikidata(entity.id, 'P1343', lang)
		end
--[=[
		-- commented out to not use label cf. https://commons.wikimedia.org/wiki/Module_talk:Wikidata_art#Catalogues
		table.insert(strTable, label .. colon .. str) 
--]=]
		table.insert(strTable, str) 
	end
	-- assemble final output structure
	if #strTable==1 then     -- single ID case
		Res = strTable[1]  -- just return the string
	elseif #strTable>1 then  -- if more than one than return bulleted list
		Res = "* " .. table.concat(strTable, "\n* ")
	end
	return Res
end

-- ===========================================================================
function p.get_other_versions(entity, pagename, lang)
	local property = {P10='video', P18='image', P996='scan', P3451='nighttime view', 
		P4896='model3D', P7420='framed_image', P7417='image of backside', 
		P7457="creator's signature", P10093='image with color chart'
	}  
	local files = {}
	for prop, field in pairs( property ) do
		for __, file in ipairs( getAllProperties(entity, prop) or {} ) do
			if file ~= pagename then
				local label = getLabel(prop, lang, '-')
				local eIcon = core.editAtWikidata(entity.id, prop, lang)
				table.insert(files, file .. "|" .. label .. eIcon)
			end
		end		
	end
	if #files<=1 then -- #files==1 means gallery with a single image (which is also displayed above)
		return ''
	end
	files = '\n' .. table.concat(files, '\n')
	local frame = mw.getCurrentFrame()
	return frame:extensionTag{ name = 'gallery', content = files, args = {mode = 'packed-hover'}}
end

-- ===========================================================================
function p.debug(frame)
	local field  = frame.args.field
	local lang   = frame.args.lang
	local entity = mw.wikibase.getEntity(frame.args.wikidata)
	local str, X
	if field=='object_history' then
		return p.get_object_history(entity, lang)     -- object history
	elseif field=='exhibition_history' then
		return p.get_exhibition_history(entity, lang) -- exhibition history
	elseif field=='inscription' then
		return p.get_inscription(entity, lang)
	elseif field=='medium' then
		return p.get_medium(entity, lang)
	elseif field=='work_location' then
		return p.get_work_location(entity, lang)	
	elseif field=='institution' then
		X = p.get_institution(entity, lang)	
		return (X.institution or '') .. '\n' .. (X.location or '')
	elseif field=='accession_number' then
		local res = p.get_accession_number(entity, lang)
		return res.str or ''
	elseif field=='creator' then
		local res = p.get_creator(entity, 'P170', lang)
		return res.str or '';
	elseif field=='references' then
		return p.get_references(entity, lang) or ''
	elseif field=='depicted_people' then
		return p.get_depicted(entity, lang)[1] or ''		
	elseif field=='depicted_themes' then
		return p.get_depicted(entity, lang)[2] or ''		
	elseif field=='other_versions' then
		return p.get_other_versions(entity, '', lang) or ''	
	end
	return ''
end

return p