Module:Gratisdata Infobox

local p = {} require('Module:No globals') local GratisdataIB = require("Module:GratisdataIB") local i18n = require( 'Module:Gratisdata Infobox/i18n' ).i18n local getBestStatements = mw.wikibase.getBestStatements local frame = mw.getCurrentFrame

local config = { -- toggle/customize infobox features: defaultsort = true, interwiki = true, autocat = true, trackingcats = true, uploadlink = true, sitelinks = true, authoritycontrol = true, helperlinks = true, coordtemplate = 1, -- 0 = none, 1 = Geohack, 2 = Coord mapwidth = 250, mapheight = 250, imagesize = '230x500px',

-- parameters for GratisdataIB: spf = '',       -- suppressfields fgd = 'ALL',    -- fetchgratisdata osd = 'no',     -- onlysourced noicon = 'yes', -- pencil icon gdlinks = 'id', -- add links to Gratisdata if no label found collapse = 10,  -- collapse list of values if too many values maxvals = 30,   -- stop fetching Gratisdata after this number of values }

-- variables set by main: local ITEM           -- mw.wikibase.entity table local QID            -- qid of ITEM, e.g. 'Q42' local CLAIMS         -- ITEM.claims local ISTAXON        -- whether ITEM is a biological taxon local INSTANCEOF = {} -- Hash set of ITEM's best "instance of" values local MYLANG         -- user's languge code local LANG           -- language object of user's language local FALLBACKLANGS  -- list containing MYLANG and its fallback languages

-- Can't have more than one, so keep track of count local primary_coordinates = 0

--- Returns label of given Gratisdata entity in user's language. --- If label doesn't exist, returns the id as link to Gratisdata. --- @param id string --- @param nolink? boolean: Whether to return link to Gratisdata if no label found local function getLabel( id, nolink ) local label = mw.wikibase.getLabel( id ) if label then return mw.text.nowiki( label ) -- nowiki to prevent wikitext injection elseif nolink then return id	else return  .. id ..  end end

--- Query Gratisdata entity for the first best value of property _pid_. --- Returns nil if first best value is novalue or somevalue. --- Returns nil if entityOrId is neither table nor string. --- @param entityOrId table|string: getEntity or qid. --- @param pid string --- @return unknown|nil local function getSingleValue( entityOrId, pid ) local claim if type( entityOrId ) == 'table' then claim = entityOrId:getBestStatements( pid )[1] elseif type( entityOrId ) == 'string' then claim = getBestStatements( entityOrId, pid )[1] end return claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value end

--- Iterator function over a list of Gratisdata claims/statements --- @param t table as returned by wikibase.getBestStatements local function iclaims( t ) local i = 1 return function while i <= #t do			local dv = t[i].mainsnak.datavalue local v = dv and dv.value i = i + 1 if v then return v end end end end

--- Returns Commons sitelink (full page title), preferably to category --- @param qid string --- @return string|nil local function getCommonsLink( qid ) local sitelink = mw.wikibase.getSitelink( qid, 'gpcommonswiki' ) if sitelink and sitelink:sub(1,9) == 'Category:' then return sitelink -- sitelink to category page end

local maincat = getSingleValue( qid, 'P162' ) -- topic's main category if maincat and maincat.id then local sl = mw.wikibase.getSitelink( maincat.id, 'gpcommonswiki' ) if sl then return sl end end

local listcat = getSingleValue( qid, 'P163' ) -- category related to list if listcat and listcat.id then local sl = mw.wikibase.getSitelink( listcat.id, 'gpcommonswiki' ) if sl then return sl end end

local P164 = getSingleValue( qid, 'P164' ) -- Commons category if P164 then return 'Category:' .. P164 end

return sitelink -- sitelink to gallery page end

local getSitelink = (mw.wikibase.getGlobalSiteId == 'gpcommonswiki') and getCommonsLink or mw.wikibase.getSitelink

--- Returns sitelink to Commons as wikilink or the label of the given Q-item --- @param qid string local function getLinkOrLabel( qid ) local sitelink = getSitelink( qid ) if sitelink then return "" .. getLabel( qid, true ) .. "" else return getLabel( qid ) end end

--- Renders snak as rich wikitext. Returns nil if snak is nil or false. --- @param snak table: claim.mainsnak or claim.qualifiers[pid] local function renderSnak( snak ) if not snak then return end local snaktype = snak.snaktype if snaktype == 'value' then local datatype = snak.datatype local value = snak.datavalue.value if datatype == 'wikibase-item' then return getLinkOrLabel( value.id ) else return mw.wikibase.formatValue( snak ) end elseif snaktype == 'somevalue' then local label = mw.message.new('Wikibase-snakview-variations-somevalue-label'):inLanguage(MYLANG):plain return ''..label..'' end end

--- Returns claim whose "language of work or name" (P35) qualifier matches --- langcode, or nil if none matches. --- @param claims table as returned by getBestStatements --- @param langcode string, e.g. "en" --- @return unknown|nil local function getClaimByLang( claims, langcode ) for _, claim in ipairs( claims or {} ) do		for _, qual in ipairs( claim.qualifiers and claim.qualifiers['P35'] or {} ) do			if getSingleValue( qual.datavalue.value.id, 'P159' ) == langcode then return claim end end end end

--- If the given snaks of datatype monolingualtext contain a string in one of --- the user's fallback languages, the string is returned; otherwise a random --- string is retuned. The second return value indicates whether finding a --- string in one of the user's fallback languages was successful. --- @param snaks table, e.g. claims.qualifiers['P130'] --- @return string?, boolean? success local function extractMonolingualText( snaks ) if not snaks or snaks == {} then return end

-- collect strings into hash table with langcodes as keys local monotext = {} for _, snak in ipairs( snaks ) do		local ms = snak.mainsnak or snak local v = ms and ms.datavalue and ms.datavalue.value if v then monotext[v.language] = v.text end end

for _, lang in ipairs( FALLBACKLANGS ) do		if monotext[lang] then return monotext[lang], true end end

-- return random string local _, v = next( monotext ) return v, false end

--- Parses a string in WikiHiero syntax local function expandhiero( hiero ) return frame:callParserFunction{ name = '#tag:hiero', args = {hiero} } end

--- Returns a string containing two table rows local function format2rowline( header, content ) return ' '..header..'  '..content..'  ' end

--- Returns a string containing a single table row local function format1rowline( trqid, header, content ) return ''..header..' '..content..' ' end

--- Returns a string containing the HTML markup for an infobox row. --- Returns nil if content is empty. --- @param eid string: ID of Gratisdata entity whose label shall be used as heading --- @param content string|nil --- @param mobile? boolean: Set to true to show on devices with narrow screens local function formatLine( eid, content, mobile ) if not content or content == '' then return end local row = mw.html.create( 'tr' ) if not mobile then row:addClass( 'gdinfo_nomobile' ) -- Template:Gratisdata_Infobox/styles.css end row:tag( 'th' ) :addClass( 'gratisdatainfobox-lcell' ) :node( LANG:ucfirst( getLabel(eid) ) ) row:tag( 'td' ) :node( content ) return tostring( row ) end

--- Returns unbulleted HTML list if given a sequence table. --- @param list string[] local function ubl( list ) if #list == 0 then return end local out = table.concat( list, '' ) return ' '..out..' ' end

--- Given a language code, returns its databaseId (as used by Gratisdata sitelinks). --- All databaseIds that a wiki knows are stored in its mw:Manual:sites table. --- @param langcode string local function databaseId( langcode ) local exceptions = { ['be-tarask'] = 'be_x_old',    -- Belarusian (Taraškievica orthography) ['bho']      = 'bh',           -- Bhojpuri ['cbk-zam']  = 'cbk_zam',      -- Chavacano de Zamboanga ['gsw']      = 'als',          -- Alemannic ['ike']      = 'iu',           -- Inuktitut ['lzh']      = 'zh_classical', -- Classical Chinese ['map-bms']  = 'map_bms',      -- Basa Banyumasan ['nan']      = 'zh_min_nan',   -- Min Nan Chinese ['nb']       = 'no',           -- Norwegian Bokmål ['nds-nl']   = 'nds_nl',       -- Low Saxon ['mo']       = 'ro',           -- Moldaawisk ['roa-tara'] = 'roa_tara',     -- Tarantino ['rup']      = 'roa_rup',      -- Aromanian ['sgs']      = 'bat_smg',      -- Samogitian ['vro']      = 'fiu_vro',      -- Võro ['yue']      = 'zh_yue',       -- Cantonese -- I did my best to make this list as comprehensive as possible. -- Useful pages for finding exceptions: -- mw:Manual:$wgExtraLanguageCodes -- Special_language codes -- 		-- meta:Template:N en/list -- meta:Template:Wikilangcode }

local exception = exceptions[langcode] if exception then return exception end

return langcode:gsub("-.*", "") -- delete everything after hyphen end

--- Wrapper around GratisdataIB. Returns nil if the item has no _pid_ statement. --- @param pid string: Gratisdata property id --- @param args? table: arguments for GratisdataIB --- @return string|nil local function getValue( pid, args ) args = args or {}

-- linking many values harms performance if the value items are big and the sitelink needs to be taken from P162, P163 or P164 local collapse = args.collapse or config.collapse local linked if pid ~= 'P736' and pid ~= 'P179' and pid ~= 'P737' then linked = #getBestStatements(args.qid or QID, pid) <= collapse end

return GratisdataIB._getValue{ pid, name = pid, qid = args.qid or QID, linked = args.linked or linked, gdlinks = args.gdlinks or config.gdlinks, prefix = args.prefix, postfix = args.postfix, linkprefix = ':', -- suppress categorization qlinkprefix = ':', -- suppress categorization sorted = args.sorted, qual = args.qual or 'MOST', qualsonly = args.qualsonly, maxvals = args.maxvals or config.maxvals, postmaxvals = '…', collapse = collapse, spf = args.spf or config.spf, fgd = args.fgd or config.fgd, osd = args.osd or config.osd, rank = 'best', noicon = args.noicon or config.noicon, list = args.list or 'Unbulleted list', sep = args.sep, unitabbr = args.unitabbr, df = args.df, -- date format plaindate = args.plaindate, lang = args.lang, gendered = args.gendered, } end

--- Used if no custom logic was specified for pid. local function defaultFunc( pid, args ) return formatLine( pid, getValue(pid, args) ) end

local function defaultFuncMobile( pid, args ) return formatLine( pid, getValue(pid, args), true ) end local function defaultFuncMobileGendered( pid ) return formatLine( pid, getValue(pid, {gendered=true}), true ) end

local function getAudio( pid ) local audiofile = getSingleValue( ITEM, pid ) return audiofile and formatLine( pid, '' ) end

local function getAudioByLang( pid ) local claims = ITEM:getBestStatements( pid ) local claim = claims[1] for i = 1, #FALLBACKLANGS do		local c = getClaimByLang( claims, FALLBACKLANGS[i] ) if c then claim = c			break end end local audiofile = claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value return audiofile and formatLine( pid, '' ) end

-- Example at local function getHieroglyphs local rows = {} for _, v in ipairs( ITEM:getBestStatements('P495') ) do -- name in hiero markup local idv = v.mainsnak.datavalue.value if v.qualifiers and v.qualifiers['P154'] then for _, w in ipairs( v.qualifiers['P154'] ) do				if w.datavalue then local label = getLabel( w.datavalue.value.id ) rows[#rows+1] = format2rowline( label, expandhiero(idv) ) end end else rows[#rows+1] = format2rowline( getLabel('Q146', true), expandhiero(idv) ) end end return table.concat( rows ) end

--- GratisdataIB arguments for birth and death related properties local birthdeath_args = { list = '', quals = table.concat({	'P498', -- refine date	'P584',   -- statement is subject of	'P738',  -- object stated as	'P82',  -- subject named as	'P497',  -- nature of statement	'P160',  -- sourcing circumstances	'P312',   -- determination method	'P201',  -- criterion used	'P678',  -- present in work	'P739', -- applies to work }, ',') }

local function getBirth( pid ) local out = {} out[#out+1] = getValue( pid, birthdeath_args )                    -- date out[#out+1] = CLAIMS['P27'] and getValue( 'P27', birthdeath_args ) -- place out[#out+1] = extractMonolingualText( ITEM:getBestStatements('P131') ) -- name return formatLine( pid, table.concat(out, ' ') ) end

local function getDeath( pid ) local out = {} out[#out+1] = getValue( pid, birthdeath_args )                    -- date out[#out+1] = CLAIMS['P516'] and getValue( 'P516', birthdeath_args ) -- place return formatLine( pid, table.concat(out, ' ') ) end

local function getWebsite( pid ) for _, claim in ipairs( ITEM:getBestStatements(pid) ) do		local quals = claim.qualifiers local url = claim.mainsnak.datavalue and claim.mainsnak.datavalue.value if url and not (quals and quals['P175']) then -- no "end time" qualifier return ' ['..url..' '..getLabel(pid)..'] ' end end end

local function getSignature( pid ) local img = getSingleValue( ITEM, pid ) if img then local alt = LANG:ucfirst( getLabel(pid, true) ) return '   ' -- equivalent to 	end end

--- If ITEM has a pid statement, this behaves exactly like defaultFunc. Otherwise --- figures out the region that ITEM is in and queries the region item for pid. --- It finds the region by first checking if ITEM has a regionPid value. --- Otherwise it takes the region from the P583 (category combines topics) --- statement of ITEM's main category. --- Examples at Cat:Health_in_Gabon, Cat:Economy_of_Germany --- @param pid string, e.g. "P679" for life expectancy --- @param regionPid string: usually "P14" (country) or "P64" (location) --- @param collapse number: argument for GratisdataIB local function getByRegion( pid, regionPid, collapse ) local region = getSingleValue( ITEM, regionPid ) if CLAIMS[pid] then region = QID elseif region then region = region.id	else local maincat = getSingleValue( ITEM, 'P162' ) -- topic's main category if maincat then for topic in iclaims( getBestStatements(maincat.id, 'P583') ) do				local id = topic.id				if id ~= 'Q3510' and id ~= 'Q3203' and id ~= 'Q3217' then -- assume id is QID of a region if it's not the QID for "health", "education", or "economy" region = id				end end end end return region and defaultFunc( pid, {		qid = region,		collapse = collapse,	}) end local function getByCountry( pid ) return getByRegion( pid, 'P14', 10 ) end local function getByLocation( pid ) return getByRegion( pid, 'P64', 10 ) end local function getByLocationCollapse4( pid ) return getByRegion( pid, 'P64', 4 ) end

local function getPrimeFactors local out = {} for _, claim in ipairs( ITEM:getBestStatements('P681') ) do		local quals = claim.qualifiers and claim.qualifiers['P181'] local quantity = quals and quals[1].datavalue.value.amount if quantity then quantity = quantity:sub(2) -- strip plus sign out[#out+1] = renderSnak(claim.mainsnak) .. ..quantity.. else out[#out+1] = renderSnak(claim.mainsnak) end end return formatLine( 'Q3511', table.concat(out, ' × ') ) end

local function getUnicodeChars( pid ) local rows = {} for _, v in ipairs( ITEM:getBestStatements(pid) ) do		local idv = v.mainsnak.datavalue.value for _, w in ipairs( v.qualifiers and v.qualifiers['P154'] or {} ) do			if w.datavalue then local qualid = w.datavalue.value.id				rows[#rows+1] = format1rowline( qualid, getLabel(qualid), idv ) end end end return table.concat( rows ) end

local function getCodes( pid ) local rows = {} for _, v in ipairs( ITEM:getBestStatements(pid) ) do		local idv = v.mainsnak.datavalue.value for _, w in ipairs( v.qualifiers and v.qualifiers['P585'] or {} ) do			if w.datavalue then local qualid = w.datavalue.value.id				if qualid == "Q2993" then idv = expandhiero( idv ) end rows[#rows+1] = format1rowline( qualid, getLinkOrLabel(qualid), idv ) end end end return table.concat( rows ) end

local function getCodeImages( pid ) local rows = {} for _, v in ipairs( ITEM:getBestStatements(pid) ) do		local idv = v.mainsnak.datavalue.value for _, w in ipairs( v.qualifiers and v.qualifiers['P585'] or {} ) do			if w.datavalue then local qualid = w.datavalue.value.id				local img = '' rows[#rows+1] = format1rowline( qualid, getLabel(qualid), img ) end end end return table.concat( rows ) end

local function getLocation local function fallback local set = {} -- locations as keys local out = {} -- locations as values for _, pid in ipairs{ 'P587', 'P64', 'P177', 'P14' } do			for _, claim in ipairs( ITEM:getBestStatements(pid) ) do				local location if pid == 'P14' then -- don't link to countries local dv = claim.mainsnak.datavalue location = dv and getLabel( dv.value.id ) else location = renderSnak( claim.mainsnak ) end if location and not set[location] then local n = #out + 1 set[location] = true    -- we don't want duplicate values out[n]       = location -- we want to preserve the order

if n > config.maxvals then out[n] = '…' -- postmaxvals return formatLine( 'P64', ubl(out) ) end end end end return formatLine( 'P64', ubl(out) ) end

local P177,P64,P587 = CLAIMS['P177'] or {}, CLAIMS['P64'] or {}, CLAIMS['P587'] or {} if (#P177 < 2) and (#P64 < 2) and (#P587 < 2) then return formatLine( 'P64', GratisdataIB.location{ args={QID} } ) or fallback else return fallback end end

local function getAuthors if CLAIMS['P127'] or CLAIMS['P484'] then local args = { list='', sep='</li>', collapse=0, maxvals=10, qual='P483,P178,P497,P154' } local authors = getValue( 'P127', args ) or '' local namestrings = getValue( 'P484', args ) return formatLine( 'P127', ubl{authors, namestrings} ) end end

local function getDifferentFrom local out = {} local i = 0 for different in iclaims( ITEM:getBestStatements('P56') ) do		i = i + 1 if i > config.maxvals then break end local href = getSitelink( different.id ) or ( 'd:'..different.id ) local label = getLabel( different.id, true )

local class = getSingleValue( different.id, 'P3' ) local isdab = class and class.id == 'Q510' local icon = isdab and ' '

local desc = mw.wikibase.getDescription( different.id ) if desc then label = '<span title="'..mw.text.nowiki(desc)..'">'..label..' ' end

out[#out+1] = string.format( '%s%s', href, label, icon or '' )

end return formatLine( 'P56', ubl(out) ) end

--- Returns common taxon name using Module:Gratisdata4Bio local function getVernacularName if ISTAXON then local vn = frame:expandTemplate{ title = 'VNNoDisplay', args = { useGratisdata = QID }}		if vn:sub(3,10) ~= 'Category' and not vn:match('class="error') then			-- we found at least one common name and there are no errors			local label = LANG:ucfirst( getLabel('Q3012') )			return ' '		end	end end

local function getTaxontree local content = require('Module:Taxontree').show{ args = { qid = QID, authorcite = 'y', first = 'y', }}	local label = LANG:ucfirst( getLabel('Q3512') ) return ' ' end

local function getOriginalCombination local ocomb = getSingleValue( ITEM, 'P601' ) ocomb = ocomb and ocomb.id	local taxoname = ocomb and getSingleValue( ocomb, 'P594' ) or '' local citation = ocomb and getSingleValue( ocomb, 'P593' ) or '' if taxoname then return formatLine( 'P601', '..taxoname..' .. ' ' .. citation ) end end

--- Creates a taxon author citation from P740 and P741 qualifiers if --- P593 (taxon author citation as string) not present since otherwise --- Taxontree already shows the citation. local function getTaxonAuthor local claims = CLAIMS['P594'] -- P594 = taxon name if #claims > 1 then return defaultFunc( 'P594' ) -- Example at 	elseif #claims == 1 then if CLAIMS['P593'] then -- P593 = taxon author citation (string) return -- Taxontree already shows citation, see Ophiogymna end local quals = claims[1].qualifiers local author = renderSnak( quals and quals['P740'] and quals['P740'][1] ) local year = renderSnak( quals and quals['P741'] and quals['P741'][1] ) if author and year then return formatLine( 'P740', author .. ', ' .. year ) elseif year then return formatLine( 'P741', year ) -- Cat:Porphyrophora polonica end end end

--- Given an area, returns a map zoom level to use with mw:Extension:Kartographer. --- Fallback output is 15. local function autoMapZoom( area ) if not area then return 15 end if area.unit == 'https://gratisdata.miraheze.org/entity/Q3514' then -- hectare area = area.amount / 100 -- convert to km² elseif area.unit == 'https://gratisdata.miraheze.org/entity/Q3516' then -- m²		area = area.amount / 1e6  -- convert to km² elseif area.unit == 'https://gratisdata.miraheze.org/entity/Q3517' then -- acre area = area.amount * 0.004 -- convert to km² else area = tonumber( area.amount ) -- assume the unit is km² end local LUT = { 5000000, 1000000, 100000, 50000, 10000, 2000, 150, 50, 19, 14, 5, 1, 0.5 } for zoom, scale in ipairs( LUT ) do		if area > scale then return zoom + 1 end end return 15 end

local function getCoordinates( pid ) local coords = getSingleValue( ITEM, pid ) if coords then local out local long = coords.longitude local lat = coords.latitude local globeId = coords.globe:match( "Q%d+" ) if globeId == 'Q476' then -- coords are on Earth local externaldata = { -- mw:Help:Extension:Kartographer type = "ExternalData", service = "geoshape", ids = QID, properties = { ['fill'] = "#999999", ['stroke'] = "#636363", ['stroke-width'] = 2 }			}

-- detect roads, mountain passes, rivers, borders etc.			if CLAIMS['P317'] or CLAIMS['P742']       -- length, transport network or CLAIMS['P743'] or CLAIMS['P744']      -- tributary, mountain range or CLAIMS['P745'] or CLAIMS['P746']      -- crosses, track gauge or CLAIMS['P327']  or CLAIMS['P526']        -- route map, traffic sign or CLAIMS['P747'] or CLAIMS['P748'] then -- electrification, route diagram externaldata.service = 'geoline' externaldata.properties['stroke'] = "#ff0000" end

local geojson = { externaldata, { type = "Feature", geometry = { type="Point", coordinates = {long, lat} }, properties = { ['marker-size'] = "medium", ['marker-color'] = "006699" },				},			}

local zoom if CLAIMS['P328'] then -- OpenStreetMap relation ID -- Let Kartographer figure out zoom level based on OSM geoshape. -- Kartographer uses Wikimedia_Maps/API -- instead of P328 to find the OSM relation but there is no Lua -- interface for that. You can help adding P328 statements using -- https://mix-n-match.toolforge.org/#/catalog/688 else local area = getSingleValue( ITEM, 'P320' ) zoom = autoMapZoom( area ) end

out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {				frameless = 1,				lang = MYLANG,				width = config.mapwidth,				height = config.mapheight,				zoom = zoom,				align = 'center',			}) if config.trackingcats then out = out ..'' end if config.coordtemplate == 1 then if primary_coordinates == 0 then out = out .. frame:callParserFunction('#coordinates:primary', lat, long) primary_coordinates = 1 end out = out .. ' '..require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, lang=MYLANG }..' ' elseif config.coordtemplate == 2 then local args = { display = 'inline,title', format = 'dms', nosave = 1, qid = QID }				out = out .. ' '..frame:expandTemplate{ title = 'Coord', args = args }..' ' end else -- coords not on Earth local globe = mw.wikibase.getLabelByLang( globeId, 'en' ) out = require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, globe=globe, lang=MYLANG } end

return '<tr class="gdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..' ' elseif config.trackingcats and (CLAIMS['P587'] or CLAIMS['P177']) then return '' end end

--- Show map using mw:Help:Map Data if ITEM has no coordinates local function getCommonsMapData if CLAIMS['P134'] then return end local commonsdata = getSingleValue( QID, 'P749' ) if not commonsdata then return end local geojson = local out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {		frameless = 1,		lang = MYLANG,		width = config.mapwidth,		height = config.mapheight,		align = 'center',	}) if config.trackingcats then out = out ..'' end return '<tr class="gdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..' ' end

local function getCelestialCoordinates local ra = getSingleValue( ITEM, 'P599' ) -- right ascension local de = getSingleValue( ITEM, 'P600' ) -- declination if ra and de then local url = 'http://www.wikisky.org/?ra='..(ra.amount / 15)..'&de='..de.amount..'&de=&show_grid=1&show_constellation_lines=1&show_constellation_boundaries=1&show_const_names=1&show_galaxies=1&img_source=DSS2&zoom=9 ' local ra_unit = getLabel( ra.unit:match('Q%d+') ) local de_unit = getLabel( de.unit:match('Q%d+') ) local ra_fmt = LANG:formatNum( tonumber(ra.amount) ) local de_fmt = LANG:formatNum( tonumber(de.amount) ) local text = LANG:ucfirst( getLabel('P599') )..' '..ra_fmt..' '..ra_unit.. ' '..LANG:ucfirst( getLabel('P600') )..' '..de_fmt..' '..de_unit return '<tr class="gdinfo_nomobile"><td colspan=2 style="text-align:center">['..url..text..'] ' end end

local autocats_by_id = { P649 = 'Archaeological monuments in Denmark with known IDs', P750 = 'ASI monuments with known IDs', P568 = 'Buildings of Madrid with COAM Register number', P751 = 'Cultural heritage monuments in Armenia with known IDs', P752 = 'Cultural heritage monuments in Austria with known IDs', P4244 = 'Cultural heritage monuments in Bavaria with known IDs', P2424 = 'Cultural heritage monuments in Berlin with known ID', P2948 = 'Cultural heritage monuments in Estonia (with known IDs)', P4009 = 'Cultural heritage monuments in Finland with known IDs', P380 = 'Cultural heritage monuments in France with known IDs', P4166 = 'Cultural heritage monuments in Georgia with known IDs', P1769 = 'Cultural heritage monuments in Hesse with known ID', P1369 = 'Cultural heritage monuments in Iran with known IDs', P1799 = 'Cultural heritage monuments in Malta with known IDs', P758 = 'Cultural heritage monuments in Norway with known IDs', P1770 = 'Cultural heritage monuments in Romania with known IDs', P1708 = 'Cultural heritage monuments in Saxony with known ID', P808 = 'Cultural heritage monuments in Spain by ID', P762 = 'Cultural monuments in the Czech Republic with known IDs', P477 = 'Heritage properties in Canada with known IDs', P5094 = 'HPIP with known IDs', P1702 = 'IGESPAR with known IDs', P5500 = 'IPHAN with known IDs', P2783 = 'Listed buildings in Denmark with known IDs', P1216 = 'Listed buildings in England with known IDs', P1460 = 'Listed buildings in Northern Ireland with known IDs', P709 = 'Listed buildings in Scotland with known IDs', P1459 = 'Listed buildings in Wales with known IDs', P649 = 'National Register of Historic Places with known IDs', P4120 = 'Ontario Heritage Trust sites with known IDs', P2961 = 'Periodicals in the Biblioteca Virtual de Prensa Histórica', P7135 = 'Rijksmonumentcomplexen with known IDs', P359 = 'Rijksmonumenten with known IDs', P1700 = 'SIPA with known IDs', P3759 = 'Uses of Gratisdata Infobox providing SAHRA ids', P809 = 'Uses of Gratisdata Infobox providing WDPA ids', }

--- qualifiers for "headquarters location" (P159) local hq_quals = table.concat({	'P6375', -- street address	'P669',   -- located on street	'P670',   -- street number	'P4856',  -- conscription number	'P281',   -- postal code	'P580',   -- start time	'P582',   -- end time	'P585',   -- point in time	'P1264',  -- valid in period	'P3831',  -- object has role	'P1810',  -- subject named as	'P5102',  -- nature of statement }, ',' )

--- associates pids with a table of arguments for GratisdataIB or with a function --- that will be called with pid as the only argument local property_logic = { P51   = getAudio,                    -- audio P989  = getAudioByLang,              -- spoken text audio P443  = getAudioByLang,              -- pronunciation audio P990  = getAudioByLang,              -- recording of subject's voice P7383 = getHieroglyphs,              -- name in hiero markup P569  = getBirth,                    -- date of birth P570  = getDeath,                    -- date of death P69   = { qual='P580,P582,P585,P512,P812' }, -- educated at	P185   = { collapse=4, maxvals=20 },  -- doctoral student P106  = defaultFuncMobileGendered,   -- occupation P39   = { qual='P642,P580,P582,P585', collapse=6 }, -- position held P2522 = { collapse=4 },              -- victory P26   = { qual='DATES' },            -- spouse, TODO: sort by date qualifier (also P793) P451  = { qual='DATES' },            -- partner P166  = { qual='P585' },             -- award received P856  = getWebsite,                  -- official website P109  = getSignature,                -- signature P31   = defaultFuncMobile,           -- instance of	P2250  = getByCountry,                -- life expectancy P4841 = getByCountry,                -- total fertility rate P5236 = getPrimeFactors,             -- prime factor P487  = getUnicodeChars,             -- Unicode character P3295 = getCodes,                    -- code P7415 = getCodeImages,               -- code (image) P3270 = getByLocation,               -- compulsory education (minimum age) P3271 = getByLocation,               -- compulsory education (maximum age) P6897 = getByLocationCollapse4,      -- literacy rate P2573 = getByLocationCollapse4,      -- number of out-of-school children P971  = { osd='no' },                -- category combines topics P180  = { list='prose', qual='' },   -- depicts P276  = getLocation,                 -- location P50   = getAuthors,                  -- author P2789 = { qual='' },                 -- connects with P85   = { qual='DATES' },            -- anthem P953  = { qual='P407', prefix="[", postfix="]" }, -- full work at	P127   = { qual='DATES' },            -- owned by	P159   = { qual=hq_quals },           -- headquarters location P466  = { collapse=5 },              -- occupant P126  = { collapse=5, maxvals=20 },  -- maintained by	P348   = { qual='P548,P577,P805' },   -- software version identifier P286  = { collapse=3 },              -- head couch P527  = { collapse=5, maxvals=20 },  -- has part P1382 = { collapse=5, maxvals=20 },  -- partially coincident with P1990 = { collapse=5 },              -- species kept P1923 = { collapse=5, maxvals=10 },  -- participating team P1346 = { collapse=5, maxvals=20 },  -- winner P112  = { maxvals=20 },              -- founded by	P577   = { linked = 'no',            -- make film categories load much quicker rank = 'preferred normal', -- See d:Property_talk:P577 },	P140  = { qual='P585' },             -- population (qual = point in time) P200  = { collapse=4, maxvals=20 },  -- lake inflows P205  = { collapse=5, maxvals=20 },  -- basin country P974  = { collapse=5, maxvals=20 },  -- tributary P726  = { collapse=5 },              -- candidate P1889 = getDifferentFrom,            -- different from P460  = { collapse=20, list='' },    -- same as (lots of values for given names) P1843 = getVernacularName,           -- taxon common name P171  = getTaxontree,                -- parent taxons P1403 = getOriginalCombination,      -- original combination P225  = getTaxonAuthor,              -- taxon name (and qualifiers) P2078 = getWebsite,                  -- user manual URL P625  = getCoordinates,              -- coordinate location P3896 = getCommonsMapData,           -- geoshape P6257 = getCelestialCoordinates,     -- right ascension }

--[==[-- This table is used by main to generate the infobox and by doc to generate Template:Gratisdata Infobox/doc/properties.

item is a human (Q5) or a fictional human (Q15632617). It defaults to false. "instance of" (P31) value of the item or `P31_allowed_values` is not present. the values for each pid in the group, even if the item has no pid statement. `logic` can also be a GratisdataIB arguments table for defaultFunc. ]==] local property_groups = { { groupname = 'Switchable images', -- this group needs to be at index 1 comment = 'Users can switch between these images using Gadget-Infobox.js.', humans_allowed = true, pids = {'P18','P117','P8224','P1442','P1801','P2716','P3383','P4640','P4291','P3451','P5252','P2713','P8592','P8517','P5555','P5775','P7417','P9721','P3311','P7420','P7457','P8195','P1543','P996','P3030','P154','P2910','P41','P94','P4004','P158','P2425','P8766','P14','P1766','P15','P8512','P181','P207','P242','P1944','P1943','P1846','P1621','P367','P491','P6655','P10','P4896','P11101'}, },	{ groupname = 'Audio and hieroglyphs', humans_allowed = true, pids = {'P51','P989','P443','P990','P7383'}, },	{ groupname = 'Human', P31_allowed_values = { 'Q5', 'Q15632617' }, humans_allowed = true, pids = {'P1559','P569','P570','P1196','P509','P157','P119','P742','P2031','P2032','P1317','P27','P1532','P551','P69','P184','P185','P106','P2416','P54','P108','P463','P102','P39','P101','P135','P66','P103','P97','P2962','P2522','P53','P22','P25','P3373','P40','P26','P451','P937','P800','P1441','P166','P856','P109'}, },	{ groupname = 'Instance/subclass of', pids = {'P31','P279'}, },	{ groupname = 'Health by region', P31_allowed_values = { 'Q64027457' }, pids = {'P2250','P4841'}, bypass_property_exists_check = true, },	{ groupname = 'Natural number', P31_allowed_values = { 'Q21199' }, pids = {'P5236','P487','P3295','P7415'}, },	{ groupname = 'Education by region', P31_allowed_values = { 'Q64801076' }, pids = {'P3270','P3271','P6897','P2573'}, bypass_property_exists_check = true, },	{ groupname = 'National economy', P31_allowed_values = { 'Q6456916' }, pids = {'P38','P2299','P4010','P2131','P2132','P2219','P1279','P2134','P2855'}, bypass_property_exists_check = true, logic = getByLocationCollapse4, },	{ groupname = 'Miscellaneous 1', pids = {'P361','P1639','P1269','P921','P629','P1559','P452','P7163','P971','P4224','P831','P2317','P138','P825','P417','P547','P180','P2596','P186','P136','P376','P3018','P7532'}, },	{ groupname = 'Location', comment = 'The properties, , , and together produce a single infobox row.', pids = {'P276'}, bypass_property_exists_check = true, },	{ groupname = 'Miscellaneous 2', pids = {'P1001','P206','P5353','P4856','P9759','P6375','P669','P495','P1885','P149','P708','P2872','P16','P2789','P59','P65','P215','P223','P196','P36','P122','P194','P208','P209','P37','P85','P38','P35','P6','P210'}, },	{ groupname = 'Author', comment = 'Will be displayed together with .', pids = {'P50'}, bypass_property_exists_check = true, },	{ groupname = 'Miscellaneous 3', pids = {'P655','P123','P1433','P84','P193','P170','P86','P676','P87','P61','P189','P98','P58','P110','P162','P175','P393','P291','P407','P2635','P437','P953','P275','P1441','P1080','P88','P6291','P199','P169','P366','P121','P127','P159','P466','P137','P126','P177','P2505','P144','P822','P115','P5138','P118','P505','P286','P527','P1990','P2522','P1427','P1444','P1923','P1132','P1346','P176','P1071','P617','P504','P532','P8047','P289','P426','P113','P114','P375','P619','P1145','P522','P664','P823','P5804','P57','P161','P195','P217','P178','P112','P400','P306','P1435','P814','P141','P348','P585','P606','P729','P730','P580','P571','P577','P5444','P575','P1619','P3999','P582','P576','P2669','P793','P516','P2957','P2109','P618','P128','P129','P111','P179'}, },	{ groupname = 'Quantities', pids = {'P1093','P2067','P2261','P2262','P2049','P2386','P2043','P3157','P2583','P2048','P5524','P2808','P2144','P3439','P4183','P5141','P4552','P2660','P2659','P610','P559','P7309','P1082','P2052','P2217','P2046','P2044','P2050','P2047'}, logic = { unitabbr='yes' }, },	{ groupname = 'Miscellaneous 4', pids = {'P140','P1083','P2351','P2324','P6801','P6855','P3032','P3137','P770','P1398','P167','P81','P197','P833','P834'}, },	{ groupname = 'Water', pids = {'P885','P403','P200','P201','P4614','P205','P974','P4792','P4661','P469','P2673','P2674'}, },	{ groupname = 'Miscellaneous 5', pids = {'P155','P156','P1365','P1366','P3730','P3729'}, },	{ groupname = 'Elections', pids = {'P991','P726','P1831','P1867','P1868','P1697','P5043','P5045','P5044'}, },	{ groupname = 'Miscellaneous 6', pids = {'P1590','P1120','P1446','P1339','P1092','P784','P783','P785','P786','P787','P788','P789','P183','P2130','P2769','P1174','P859','P218','P78','P238','P239','P1889','P460','P1382','P2010','P2009','P2033','P1531','P8193'}, },	{ groupname = 'Taxon common name', comment = "Common names are taken from the item's label, sitelink, and .", pids = {'P1843'}, bypass_property_exists_check = true, },	{ groupname = 'Taxonomy', pids = {'P171','P1403','P225'}, },	{ groupname = 'Miscellaneous 7', pids = {'P6591','P7422','P2078','P856','P6257'}, },	{ groupname = 'Maps', comment = ' is only used if no statement exists. Tracked at .', pids = {'P625','P3896'}, bypass_property_exists_check = true, }, }
 * `humans_allowed` determines whether the group should be displayed if the
 * A group will only be displayed if `P31_allowed_values` contains the
 * If `bypass_property_exists_check` is set to true, the infobox tries to fetch
 * `logic` can be a function that will be called with pid as the only argument.

local externalIDs = { { groupname = 'Authority control', pids = {'P213','P214','P227','P244','P268','P269','P270','P349','P409','P508','P640','P651','P691','P886','P902','P906','P947','P949','P950','P1003','P1006','P1015','P1048','P1157','P1207','P1225','P1415','P1695','P2558','P2581','P4819','P5034','P5587','P7293','P8189','P9371','P10539',} },	{ groupname = 'Books/magazines/authors/libraries', pids = {'P236','P271','P396','P648','P723','P724','P2961','P5199',} },	{ groupname = 'Science', pids = {'P356','P496','P549','P698','P717','P932','P1053','P2349','P3083','P8273',} },	{ groupname = 'Biology', pids = {'P428','P627','P685','P687','P6535','P815','P830','P838','P842','P846','P850','P938','P959','P960','P961','P962','P1070','P1076','P1348','P1391','P1421','P1727','P1745','P1746','P1747','P1761','P1772','P1832','P1895','P1940','P1991','P1992','P2007','P2026','P2036','P2040','P2426','P2434','P2455','P2464','P2752','P2833','P2946','P3031','P3060','P3064','P3099','P3100','P3101','P3102','P3151','P3240','P3288','P3398','P3420','P3444','P3591','P3594','P3606','P3746','P4024','P4122','P4194','P4301','P4526','P4567','P4728','P4758','P4855','P5036','P5037','P5055','P5216','P5221','P5257','P5299','P6678','P7051',} },	{ groupname = 'Art', pids = {'P245','P347','P434','P650','P781','P1212','P1882','P1901','P3293','P3634','P4399','P4659','P4701','P5950','P6506','P6631','P7704','P8386',} },	{ groupname = 'Culture', pids = {'P345','P539','P1219','P1220','P1248','P1362','P6113','P6132',} },	{ groupname = 'Sports', pids = {'P1146','P1440','P1469','P1665','P2020','P2276','P2446','P2458','P2574','P3171','P3537','P3538','P3681','P3924','P8286',} },	{ groupname = 'Cultural heritage and architecture', pids = {'P359','P380','P381','P481','P649','P709','P718','P757','P758','P762','P808','P1216','P1459','P1483','P1600','P1700','P1702','P1708','P1764','P1769','P2424','P2783','P2081','P2917','P3038','P3177','P3178','P3318','P3596','P3758','P3759','P4009','P4075','P4102','P4244','P4360','P4372','P4868','P5094','P5310','P5313','P5500','P5525','P5528','P6102','P6542','P6736','P7006','P7170','P7304','P7630','P7659','P7694','P7900','P9148','P9154','P9339','P9342','P10486',} },	{ groupname = 'Protected areas', pids = {'P809','P3425','P3613','P3974','P5965','P6602','P6230','P6280','P6478','P6560','P6659','P3296','P677',} },	{ groupname = 'Places and geographical features', pids = {'P402','P3120','P3580','P3616','P3628','P4266','P6630','P7350','P7352','P7548','P8655','P8988','P10451','P4533',} },	{ groupname = 'Administrative subdivisions', pids = {'P772','P836','P1894','P3118','P3615','P3639','P3419','P7526','P2788','P7577','P7606','P7635','P7636','P7579','P7752','P7673','P7674','P7736','P7735',} },	{ groupname = 'Other', pids = {'P458','P587','P2037','P3112','P10557','P3479','P4344','P6228','P7721',} }, }

--- @param group table local function groupIsAllowed( group ) local ishuman = INSTANCEOF['Q5'] or INSTANCEOF['Q15632617'] if ishuman and not group.humans_allowed then return false end

local allowlist = group.P31_allowed_values if not allowlist then return true end for _, class in ipairs( allowlist ) do		if INSTANCEOF[class] then return true end end return false end

local function noImage -- Gratisdata classes that don't need an image local dontNeedImg = { 'Q4167410', -- disambiguation page 'Q4167836', -- Wikimedia category 'Q11266439', -- Wikimedia template 'Q101352',  -- family name 'Q202444',  -- given name 'Q12308941', -- male given name 'Q11879590', -- female given name 'Q3409032', -- unisex given name }	for _, class in ipairs( dontNeedImg ) do		if INSTANCEOF[class] then return end end

local hasImg for _, imgPid in ipairs( property_groups[1].pids ) do		if CLAIMS[imgPid] then hasImg = true break end end if not hasImg then return '' end end

--- Returns string with all labels/descs/aliases for search engine optimization local function seo local out = {}

for lang, v in pairs( ITEM.labels or {} ) do		out[#out+1] = v.value end

for lang, v in pairs( ITEM.descriptions or {} ) do		out[#out+1] = v.value end

for lang, v in pairs( ITEM.aliases or {} ) do		for _, w in ipairs( v ) do			out[#out+1] = w.value end end

return table.concat( out, '; ' ) end

-- wikiprojects that are not Gratispaideia despite their IDs ending with 'wiki' local excludedProjects = { wikidatawiki = true, commonswiki  = true, specieswiki   = true, metawiki    = true, mediawikiwiki = true, sourceswiki = true, wikimaniawiki = true, incubatorwiki = true, }

-- Returns interwiki link if site is Gratispaideia local function interwikilink( site, title ) if site:sub(-4) == 'wiki' and not excludedProjects[site] then local iwprefix = site:sub(1, -5):gsub('_', '-') -- "zh_yuewiki" to "zh-yue" return string.format( '%s:%s', iwprefix, title ) end end

--- Adds Gratispaideia sitelinks from similar items. Example at Cat:Moore_(surname) local function interwikis local out = {}

-- ITEM is usually P301 of connected item, so this is not redundant: for site, v in pairs( ITEM.sitelinks or {} ) do		out[#out+1] = interwikilink( site, v.title ) end

for _, pid in ipairs{ 'P2354', 'P1753', 'P1753', 'P1420' } do -- has list, related list, said to be same as, taxon synonym for similar in iclaims( ITEM:getBestStatements(pid) ) do			for site, v in pairs( mw.wikibase.getEntity(similar.id).sitelinks or {} ) do				out[#out+1] = interwikilink( site, v.title ) end end end

return table.concat( out ) end

local charMap -- memoized local function stripDiacritics( str ) if not charMap then local from = 'ÁÀÂÄǍĂĀÃÅẠĄƏĆĊĈČÇĎĐḐḌÐÉÈĖÊËĚĔƐƎỀỂỄẾỆĒẼĘẸĠĜĞĢĤĦḤİÍÌÎÏǏĬĪĨĮỊĴĶĹĿĽĻŁḶḸṂŃŇÑŅṆŊÓÒÔÖǑŎŌÕǪỌŐØꝚŔŘŖⱤɌƦȐȒṘṚṜŚŜŠŞȘṢŤŢȚṬÚÙÛÜǓŬŪŨŮŲỤŰǗǛǙǕŴÝŶŸỸȲŹŻŽ'.. 'ằắắáẳàẵâäǎăāãåặầẩẫấậảạąəćċĉčçḑďđḍðéèėêëěɛǝềểễếệĕēẽęẹġĝğģḩĥħḥıíìîïǐĭīĩįịĵķĺŀľļłḷḹṃńňñņṇŋơóồòôöǒŏōõǫọőøꝛŕɽřŗṛṝɍʀȑȓṙśŝšşșṣťţțṭưúùûứừüǔŭūũůųụűǘǜǚǖŵýŷÿỹȳźżž' local to  = 'AAAAAAAAAAAACCCCCDDDDDEEEEEEEEEEEEEEEEEEGGGGHHHIIIIIIIIIIIJKLLLLLLLMNNNNNNOOOOOOOOOOOORRRRRRRRRRRRSSSSSSTTTTUUUUUUUUUUUUUUUUWYYYYYZZZ'.. 'aaaaaaaaaaaaaaaaaaaaaaaacccccdddddeeeeeeeeeeeeeeeeeegggghhhhiiiiiiiiiiijklllllllmnnnnnnoooooooooooooorrrrrrrrrrrrssssssttttuuuuuuuuuuuuuuuuuuuwyyyyyzzz' charMap = {} for i = 1, mw.ustring.len( from ) do			charMap[mw.ustring.sub(from, i, i)] = mw.ustring.sub(to, i, i)		end charMap['ß'] = 'ss'; charMap['ẞ'] = 'SS' charMap['æ'] = 'ae'; charMap['ǣ'] = 'ae'; charMap['ǽ'] = 'ae' charMap['Æ'] = 'AE'; charMap['Ǣ'] = 'AE'; charMap['Ǽ'] = 'AE' charMap['œ'] = 'oe'; charMap['Œ'] = 'OE' charMap['þ'] = 'th'; charMap['Þ'] = 'Th' end

return (string.gsub( str, '[^\128-\191][\128-\191]*', charMap )) end

local function humannames( out ) local surname   = ITEM:formatPropertyValues('P734').value:gsub(',.*', '') local givennames = ITEM:formatPropertyValues('P735').value:gsub(', ', ' ') local spanish2nd = ITEM:formatPropertyValues('P1950').value:gsub(',.*', '')

if config.trackingcats then if surname == '' then out[#out+1] = '' end if givennames == '' then out[#out+1] = '' end end

if config.autocat then for _, pid in ipairs{ 'P734', 'P1950', 'P9139' } do			for name in iclaims( ITEM:getBestStatements(pid) ) do				name = mw.wikibase.getLabelByLang( name.id, 'en' ) if givennames == '' then out[#out+1] = name and string.format('', name) else out[#out+1] = name and string.format('', name, stripDiacritics(givennames)) end end end

for name in iclaims( ITEM:getBestStatements('P735') ) do			name = mw.wikibase.getLabelByLang( name.id, 'en' ) out[#out+1] = name and string.format('', name) -- no sort key needed because DEFAULTSORT starts with family name end end

if not config.defaultsort then out[#out+1] = '' elseif surname ~= '' and surname ~= 'no value' and surname ~= 'some value' then if spanish2nd ~= '' then surname = surname .. ' ' .. spanish2nd end local sortkey = stripDiacritics( surname..', '..givennames ) out[#out+1] = frame:preprocess('') end end

--- @param pid "P569"|"P570" --- @param event "birth"|"death" local function datecat( pid, event, out ) local year = GratisdataIB._getValue{ pid, qid=QID, ps=1, df='y', plaindate='adj', lang='en', maxvals=1 } if year and year ~= 'unknown value' then local cat = 'Category:' .. year .. ' ' .. event .. 's'		if mw.title.new( cat ).exists then out[#out+1] = ..cat.. elseif config.trackingcats then mw.addWarning( 'Categorization under '..cat..' supressed' ) out[#out+1] = '' end end end

local function countrycat( out ) local exceptions = { Q30 = 'United States', }	for country in iclaims( ITEM:getBestStatements('P27') ) do		local countryLabel = exceptions[country.id] or mw.wikibase.getLabelByLang( country.id, 'en' ) if countryLabel then local sex = getSingleValue( ITEM, 'P21' ) local sexLabel = sex and ({					Q6581097 = 'Men',					Q2449503  = 'Men',					Q6581072  = 'Women',					Q1052281  = 'Women',			})[sex.id]

if sexLabel then local cat1 = 'Category:'..sexLabel..' of the '..countryLabel..' by name' local cat2 = 'Category:'..sexLabel..' of '..countryLabel..' by name' if mw.title.new( cat1 ).exists then out[#out+1] = ..cat1.. elseif mw.title.new( cat2 ).exists then out[#out+1] = ..cat2.. elseif config.trackingcats then mw.addWarning( 'Categorization under '..cat2..' supressed' ) out[#out+1] = '' end end end end end

local function autocat( out, pid, dict ) for _, claim in ipairs( ITEM:getAllStatements(pid) ) do		if claim.rank ~= "deprecated" then local dv = claim.mainsnak.datavalue local cat = dict[dv and dv.value.id] out[#out+1] = cat and '' end end end

local function metadata local out = {}

if config.trackingcats then out[#out+1] = noImage if not (CLAIMS['P31'] or CLAIMS['P279']) then out[#out+1] = '' end if INSTANCEOF['Q5'] and not CLAIMS['P569'] then out[#out+1] = '' end end

out[#out+1] = ' '..seo..' '

-- Add interwiki links from related items, inspired by Module:Interwiki if config.interwiki and mw.title.getCurrentTitle.namespace == 14 then out[#out+1] = interwikis end

if config.autocat then for pid, cat in pairs( autocats_by_id ) do			local val = getSingleValue( ITEM, pid ) out[#out+1] = val and string.format( '', cat, val ) end

out[#out+1] = CLAIMS['P757'] and ''

autocat( out, 'P1435', { -- heritage designation			Q34932610 = 'Conjuntos de Interesse Municipal in Portugal by name',			Q28419115 = 'Conjuntos de Interesse Público in Portugal by name',			Q54171320 = 'Monuments under study in Portugal by name',			Q15697324 = 'Imóveis de Interesse Público in Portugal by name',			Q11791    = 'Imóveis de Interesse Municipal in Portugal by name',			Q53806418 = 'Monuments included in classified sites in Portugal by name',			Q28423275 = 'Monumentos de Interesse Municipal in Portugal by name',			Q22222923 = 'Monumentos de Interesse Público in Portugal by name',			Q908411   = 'Monumentos Nacionais in Portugal by name',			Q28419400 = 'Sítios de Interesse Municipal in Portugal by name',			Q28419109 = 'Sítios de Interesse Público in Portugal by name',			Q54163210 = 'Pending classification monuments in Portugal by name',		})

autocat( out, 'P31', { -- instance of			Q235670   = 'Common years starting and ending on Sunday',			Q235673   = 'Common years starting and ending on Saturday',			Q235676   = 'Common years starting and ending on Wednesday',			Q235680   = 'Common years starting and ending on Friday',			Q235684   = 'Common years starting and ending on Tuesday',			Q235687   = 'Common years starting and ending on Monday',			Q235690   = 'Common years starting and ending on Thursday',			Q217041   = 'Leap years starting on Sunday and ending on Monday',			Q217026   = 'Leap years starting on Saturday and ending on Sunday',			Q217015   = 'Leap years starting on Wednesday and ending on Thursday',			Q217036   = 'Leap years starting on Friday and ending on Saturday',			Q217034   = 'Leap years starting on Tuesday and ending on Wednesday',			Q217024   = 'Leap years starting on Monday and ending on Tuesday',			Q217019   = 'Leap years starting on Thursday and ending on Friday', Q66010119 = 'Months starting on Monday', Q66010126 = 'Months starting on Tuesday', Q66010132 = 'Months starting on Wednesday', Q66010139 = 'Months starting on Thursday', Q66010148 = 'Months starting on Friday', Q66010153 = 'Months starting on Saturday', Q66010158 = 'Months starting on Sunday', Q3305213 = 'Individual painting categories', })

if INSTANCEOF['Q5'] and mw.title.getCurrentTitle.namespace == 14 then humannames( out ) datecat( 'P569', 'birth', out ) datecat( 'P570', 'death', out ) countrycat( out )

autocat( out, 'P21', { -- sex or gender				Q6581097  = 'Men by name',				Q6581072  = 'Women by name',				Q1052281  = 'LGBT people by name]][[Category:Women by name',				Q2449503  = 'LGBT people by name]][[Category:Men by name',				Q48270    = 'Non-binary people by name',				Q12964198 = 'LGBT people by name', -- genderqueer				Q1097630  = 'LGBT people by name', -- intersex				Q18116794 = 'LGBT people by name', -- genderfluid				Q505371   = 'LGBT people by name', -- agender			})

out[#out+1] = '' out[#out+1] = CLAIMS['P570'] and '' out[#out+1] = GratisdataIB.getAwardCat{ args = {qid=QID, fgd='ALL', osd=config.osd, noicon='yes'} }

if not CLAIMS['P570'] then -- This person has no death date, but are they really alive? local birth = getSingleValue( ITEM, 'P569' ) local year = tonumber( birth and birth.time:gsub('-.*', '') ) if year and os.date('%Y') - year < 100 then out[#out+1] = '' end end end end

return table.concat( out ) end

--- @return string|nil local function getImage( pid ) local claims = ITEM:getBestStatements( pid ) local claim = getClaimByLang( claims, MYLANG ) or claims[1] local ms = claim and claim.mainsnak local file = ms and ms.datavalue and ms.datavalue.value

if file then local panoramalink = (pid == 'P4640') and '|link=https://panoviewer.toolforge.org/#'..mw.uri.encode(file, 'WIKI') or '' local img = ' ' -- equivalent to

local medialegends = claim.qualifiers and claim.qualifiers['P2096'] if medialegends then return img .. ' '..extractMonolingualText( medialegends )..' ' else return img -- no image caption end end end

--- Returns images and sitelinks --- @param uploadlink? boolean: Whether to show the "Upload media" link local function header( uploadlink ) local imgs = {} for _, imgPid in ipairs( property_groups[1].pids ) do		local formatted_img = getImage(imgPid) imgs[#imgs+1] = formatted_img and { imgPid, formatted_img } end

local switcherContainer = mw.html.create( 'div' ) switcherContainer:addClass( 'switcher-container' )

-- Only show switching labels if we have more than one image to show if #imgs > 1 then for _, img in ipairs( imgs ) do			switcherContainer:tag( 'div' ) :addClass( 'center' ) :node( img[2] ) :tag( 'span' ) :attr{ class = "switcher-label", style = "display:none" } :node( ' ' .. getLabel(img[1]) .. ' ' ) end elseif #imgs == 1 then switcherContainer:tag( 'div' ) :addClass( 'center' ) :node( imgs[1][2] ) end

local images = mw.html.create( 'tr' ) images:tag( 'td' ) :attr{ colspan=2, class="gdinfo_nomobile" } :css( 'text-align', 'center' ) :tag( 'div' ) :node( ITEM:getDescription or '') :done :node( switcherContainer )

local out = {}

if INSTANCEOF['Q4167410'] or INSTANCEOF['Q15407973'] then -- disambiguation page/category if config.trackingcats then out[1] = '' end elseif uploadlink then local url = tostring(mw.uri.fullUrl('Special:UploadWizard', { categories = mw.title.getCurrentTitle.text }))		local text = mw.message.new('Cx-contributions-upload'):inLanguage(MYLANG):plain out[1] = ' <td colspan=2 style="text-align:center">['..url..' '..text..'] ' end

local sitelinks = ITEM.sitelinks if config.sitelinks and sitelinks then out[#out+1] = ' <td colspan=2 style="text-align:center; font-weight:bold">' local langId = databaseId(MYLANG) local langprefix = langId:gsub('_', '-')

local wikis = { -- wikiId,      prefix     logo,                qid,      multilang { 'wiki',       '',        'Wikipedia-logo-v2', 'Q52',    false }, { 'wikiquote',  'q',       'Wikiquote-logo',    'Q369',   false }, { 'wikisource', 's',       'Wikisource-logo',   'Q263',   false }, { 'wikibooks',  'b',       'Wikibooks-logo',    'Q367',   false }, { 'wikinews',   'n',       'Wikinews-logo',     'Q964',   false }, { 'wikiversity', 'v',      'Wikiversity-logo',  'Q370',   false }, { 'specieswiki', 'species', 'Wikispecies-logo', 'Q13679', true  }, { 'wikivoyage', 'voy',     'Wikivoyage-logo',   'Q373',   false }, }

for _, v in ipairs( wikis ) do			local wikiId, prefix, logo, qid, multilang = unpack( v ) logo = ' ' if multilang then local sitelink = sitelinks[wikiId] if sitelink then out[#out+1] = ' '..logo..''..getLabel(qid)..' ' end else local sitelink = sitelinks[langId .. wikiId] if sitelink then out[#out+1] = ' '..logo..''..getLabel(qid)..' ' end end end out[#out+1] = ' ' end

return tostring( images ) .. table.concat( out ) end

--- Returns "Edit at Gratisdata" pencil local function pencil local msg, lang = i18n( 'editlink-alttext', FALLBACKLANGS ) local out = mw.html.create( 'tr' ) out :addClass( "gdinfo_nomobile" ) :tag( 'td' ) :css( 'text-align', 'right' ) :attr{ lang = lang, colspan = 2 } :node( string.format('', QID, msg) ) return tostring( out ) end

--- Evaluates all non-image property groups and adds generated HTML rows to --- the table given as argument. local function getBodyContent( t ) for i, group in ipairs( property_groups ) do		if i > 1 and groupIsAllowed( group ) then for _, pid in ipairs( group.pids ) do				if CLAIMS[pid] or group.bypass_property_exists_check then local x = property_logic[pid] or group.logic or defaultFunc if type(x) == 'function' then t[#t+1] = x( pid ) else -- type(x) == 'table' t[#t+1] = defaultFunc( pid, x ) end end end end end end

--- Returns the infobox's main content local function body if not CLAIMS then return '' end

local out = {} getBodyContent( out )

-- If category combines at most 2 topics, show subinfoboxes for those topics. -- See Category:Uses_of_Gratisdata_Infobox_with_subinfoboxes local topics = ITEM:getBestStatements( 'P971' ) if not topics or #topics > 2 then return table.concat( out ) end

-- country (Q6256), continent (Q5107), sovereign state (Q3624078), ocean (Q9430) local geoEntities = { 'Q6256', 'Q5107', 'Q3624078', 'Q9430' }

-- The loop below modifies these variables and restores them afterwards local qid, item, claims, istaxon, instanceof = QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF

local map for _, claim in ipairs( topics ) do		QID = claim.mainsnak.datavalue.value.id		ITEM = mw.wikibase.getEntity( QID ) if not ITEM then out[#out+1] = '' break end CLAIMS = ITEM.claims or {} ISTAXON = CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843']

INSTANCEOF = {} for class in iclaims( ITEM:getBestStatements('P31') ) do			INSTANCEOF[class.id] = true end

local skip for _, geoEnt in ipairs( geoEntities ) do			if INSTANCEOF[geoEnt] then skip = true map = getCoordinates( 'P625' ) break end end

-- Skip if topic is a calendar year (Q3186692) or decade (Q39911) skip = skip or INSTANCEOF['Q3186692'] or INSTANCEOF['Q39911']

if not skip and #getBestStatements(QID, 'P279') == 0 then -- subclass of			if config.trackingcats then out[#out+1] = '' end out[#out+1] = ' '..(ITEM:getLabel or QID)..' ' out[#out+1] = header( false ) getBodyContent( out ) out[#out+1] = pencil end end out[#out+1] = map

QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF = qid, item, claims, istaxon, instanceof return table.concat( out ) end

local function authoritycontrol if not config.authoritycontrol then return '' end

local ids = {} for _, group in ipairs( externalIDs ) do		for _, pid in ipairs( group.pids ) do			if CLAIMS[pid] then local icon = getSingleValue( pid, 'P2910' ) icon = icon and ' ' or '' local fmtSt = ITEM:formatStatements( pid ) if fmtSt.value ~= '' then ids[#ids+1] = icon .. fmtSt.label .. ': ' .. fmtSt.value end end end end

local gdlogo = '' return table.concat{ ' <th style="background: #cfe3ff">', LANG:ucfirst( getLabel('Q36524') ), ' ',

' <td style="text-align: center;">', ' ',				gdlogo..' '..QID..' ', '<span class="gdinfo_nomobile">', table.concat(ids, ' '), ' ',			' ',		' ',	} end

local function helperlinks if not config.helperlinks then return '' end

local hl = {} local title = mw.title.getCurrentTitle local pagename = title.text local pagenamee = mw.uri.encode(pagename, 'WIKI')

local coords = getSingleValue( ITEM, 'P134' ) local otherplanet = coords and coords.globe ~= 'http://gratisdata.miraheze.org/entity/Q476'

hl[#hl+1] = ..getLabel('Q20155952').. hl[#hl+1] = ..getLabel('Q45340488')..

if title.namespace == 14 then hl[#hl+1] = ..getLabel('Q23665536').. hl[#hl+1] = ..getLabel('Q12483').. if not otherplanet then hl[#hl+1] = ..getLabel('Q99232292').. hl[#hl+1] = ..getLabel('Q66498380').. end end

hl[#hl+1] = ..getLabel('P3096')..

if coords and not otherplanet then hl[#hl+1] = ..getLabel('Q26964791').. hl[#hl+1] = ';node[wikidata='..QID..'];if(count(nodes)==0){way[wikidata='..QID..'];};if(count(ways)==0){rel[wikidata='..QID..'];};out 1;', 'PATH')..' '..getLabel('Q936')..']' end

for i, v in ipairs( hl ) do hl[i] = ' ' .. v .. ' '	end

hl[#hl+1] = ..i18n('search-depicted', FALLBACKLANGS).. hl[#hl+1] = ISTAXON and ..i18n('taxon-depicted', FALLBACKLANGS)..

return table.concat{ '<tr class="gdinfo_nomobile">', '<td colspan=2 style="text-align: center"> ', ' ', '' .. table.concat(hl, '</li>') .. '</li>', '</ul> ', ' ',		' ',	} end

local function footer return (config.authoritycontrol or config.helperlinks) and table.concat{ ' ',			' ',		' ',	} or '' end

--- @param eid string: Gratisdata entity ID starting with Q or P local function entityLink( eid ) local label = getLabel( eid, true ) local ns = ( eid:sub(1, 1) == 'P' ) and 'Property:' or '' return ''..label..' ('..eid..') ' end

--- Generates Template:Gratisdata Infobox/doc/properties function p.doc local out = {} for _, group in ipairs( property_groups ) do out[#out+1] = ' ' .. group.groupname .. ' '		if group.comment then out[#out+1] = frame:preprocess( group.comment ) end

if group.P31_allowed_values then local classes = {} for _, class in ipairs( group.P31_allowed_values ) do				classes[#classes+1] = entityLink( class ) end out[#out+1] = 'This group is only shown if the connected Gratisdata item is an instance of ' .. table.concat(classes, ' or ') .. '.'		elseif group.humans_allowed then out[#out+1] = 'This group is always shown.' end

local props = {} for _, pid in ipairs( group.pids ) do			props[#props+1] = entityLink( pid ) end out[#out+1] = table.concat( props, ' • ' ) end

-- authority control out[#out+1] = ' '..getLabel('Q36524')..' ' out[#out+1] = 'This group is always shown.' for _, group in ipairs( externalIDs ) do out[#out+1] = ' ' .. group.groupname .. ' '		local props = {} for _, pid in ipairs( group.pids ) do			props[#props+1] = entityLink( pid ) end out[#out+1] = table.concat( props, ' • ' ) end

return table.concat( out, '\n\n' ) end

local function configure( t ) config.defaultsort     = t['defaultsort']           == 'y'	config.interwiki        = t['interwiki']             == 'yes' config.autocat         = t['autocat']               == 'yes' config.trackingcats    = t['trackingcats']          == 'yes' config.uploadlink      = t['conf_upload']           == 'yes' config.sitelinks       = t['conf_sitelinks']        == 'yes' config.authoritycontrol = t['conf_authoritycontrol'] == 'yes' config.helperlinks     = t['conf_helperlinks']      == 'yes'

if t['conf_coordtemplate'] then config.coordtemplate = tonumber( t['conf_coordtemplate'] ) end if t['conf_mapwidth'] then config.mapwidth = t['conf_mapwidth'] end if t['conf_mapheight'] then config.mapheight = t['conf_mapheight'] end if t['conf_imagesize'] then config.imagesize = t['conf_imagesize'] end

if t['spf'] then config.spf = t['spf'] end if t['fgd'] then config.fgd = t['fgd'] end if t['osd'] then config.osd = t['osd'] end if t['noicon'] then config.noicon = t['noicon'] end end

function p.main( frame ) MYLANG = frame:callParserFunction( 'int', 'lang' ) or "en" LANG = mw.language.new( MYLANG ) FALLBACKLANGS = { MYLANG, unpack(mw.language.getFallbacksFor(MYLANG)) } QID = frame.args[1] ITEM = mw.wikibase.getEntity( QID ) if not ITEM then return '' end CLAIMS = ITEM.claims if not CLAIMS then local msg = i18n('noclaims', FALLBACKLANGS):gsub('$1', ..QID.. ) return ' ', }	if config.trackingcats and os.clock > 2.5 then -- longer than 2.5 seconds out[#out+1] = '' end return table.concat( out ) end

function p.debug( qid ) frame.args = { qid or 'Q42' } return p.main( frame ) end

return p

-- Credits: -- Original authors: Mike Peel with contributions by Jura1 -- 2022 rewrite: LennardHofmann