Для документации этого модуля может быть создана страница Модуль:Wikidata/Places/doc

local categorizeByPlace = true;

local WDS = require( 'Module:WikidataSelectors' );
local Flags = require( 'Module:Wikidata/Flags' );
local p = {
	config = {
		hideSameLabels = false,
		hidePartOfLabels = false,
		hideUnitsForCapitals = true,
		reverseOrder = false,
		catAmbiguousGeoChains = '[[Категория:Википедия:Страницы с неоднозначными геоцепочками]]',
		catLoopInGeoChains = '[[Категория:Википедия:Страницы с зацикливающимися геоцепочками]]',
		catWikibaseError = '[[Категория:Википедия:Страницы с ошибками скриптов, использующих Викиданные]]'
	}
};

local function min( prev, next )
	if prev == nil then
		return next;
	elseif prev > next then
		return next;
	else
		return prev;
	end
end

local function max( prev, next )
	if prev == nil then
		return next;
	elseif prev < next then
		return next;
	else
		return prev;
	end
end

local function getTimeBoundariesFromProperty( context, propertyId )
	local dateClaims = WDS.filter( context.entity.claims, propertyId );
	if not dateClaims or #dateClaims == 0 then
		return nil;
	end

	-- only support exact date so far, but need improvment
	local left = nil;
	local right = nil;
	for _, claim in pairs( dateClaims ) do
		if not claim.mainsnak then return nil; end
		local boundaries = context.parseTimeBoundariesFromSnak( claim.mainsnak );
		if not boundaries then return nil; end
		left = min( left, boundaries[ 1 ] );
		right = max( right, boundaries[ 2 ] );
	end

	if not left or not right then return nil; end

	return { left, right };
end

local function getTimeBoundariesFromProperties( context, propertyIds )
	for _, propertyId in ipairs( propertyIds ) do
		local result = getTimeBoundariesFromProperty( context, propertyId );
		if result then
			return result;
		end
	end

	return nil;
end

local function getParentsInBoundariesSnak( context, entityId, boundaries )
	if not entityId then error('entityId must be specified'); end
	if type(entityId) ~= 'string' then error('entityId must be string'); end
	if not boundaries then error('boundaries must be specified'); end
	if type(boundaries) ~= 'table' then error('boundaries must be table'); end

	local results = context.getPropertyInBoundaries( context, entityId, boundaries, {'P131'} ) -- located in
	if not results or #results == 0 then
		results = context.getPropertyInBoundaries( context, entityId, boundaries, {'P17'} ) -- country
	end

	for r, result in pairs( results ) do
		if result.snaktype ~= 'value' then
			return nil;
		end
		local resultId = result.datavalue.value.id;
		if resultId == entityId then
			return nil;
		end
	end
	return results;
end

local unions = { 
	Q1140229 = true, -- political union
	Q3623811 = true, -- Экономический союз
	Q4120211 = true -- региональная организация
}

local countries = {
	Q6256 = true, -- страна
	Q7275 = true, -- государство
	Q3024240 = true, -- историческое государство
	Q3624078 = true -- суверенное государство
}

local function isSkipTopLevel( entity )
	local isCountry = false;
	local isUnion = false;
	if entity and
		entity.claims and
		entity.claims.P31
	then
		for c, claim in pairs( entity.claims.P31 ) do
			if claim and
				claim.mainsnak and
				claim.mainsnak.datavalue and
				claim.mainsnak.datavalue.value and
				claim.mainsnak.datavalue.value.id
			then
				local typeId = claim.mainsnak.datavalue.value.id;
				isCountry = isCountry or countries[ typeId ];
				isUnion = isUnion or unions[ typeId ];
			end
		end
	end
	return isUnion and not isCountry;
end

local function isPartOfNext( prevLabel, nextLabel )
	return ( mw.ustring.len( prevLabel ) > mw.ustring.len( nextLabel ) )
		and ( mw.ustring.sub( prevLabel, mw.ustring.len( prevLabel ) - mw.ustring.len( nextLabel ) + 1 ) == nextLabel );
end

--Property:P19, Property:P20, Property:P119
function p.formatPlaceWithQualifiers( context, options, statement )
	local property = mw.ustring.upper( options.property );

	local actualDateBoundariesProperties = nil;
	if property == 'P19' then
		actualDateBoundariesProperties = {'P569','P570'}
	elseif property == 'P20' then
		actualDateBoundariesProperties = {'P570','P569'}
	elseif property == 'P119' then
		actualDateBoundariesProperties = {'P570','P569'}
	elseif property == 'P131' or property == 'P159' then
		actualDateBoundariesProperties = {'P576'}
	end

	local boundaries = nil;
	if actualDateBoundariesProperties ~= nil then
		boundaries = getTimeBoundariesFromProperties( context, actualDateBoundariesProperties );
		if (boundaries == nil) and (property == 'P131' or property == 'P159') then
			boundaries = {os.time() * 1000, os.time() * 1000};
		end
	end

	local entriesToLookupCategory = {};

	local circumstances = context.getSourcingCircumstances( statement );
	local result = '';
	local baseResult = context.formatSnak( options, statement.mainsnak, circumstances );
	if not baseResult then
		return nil;
	end
	
	insertFromSnak( statement.mainsnak, entriesToLookupCategory )

	local hasAdditionalQualifiers = false;
	if statement.qualifiers then
		--parent divisions
		if statement.qualifiers.P131 then
			for i, qualifier in ipairs( statement.qualifiers.P131 ) do
				if qualifier.datavalue then
					local parentOptions = context.cloneOptions( options );
					local qualifierEntityId = qualifier.datavalue.value.id;
					parentOptions['text'] = getLabel( context, qualifierEntityId, boundaries );
					local link = context.formatSnak( parentOptions, qualifier );
	
					if p.config.reverseOrder then
						result = link .. ', ' .. result;
					else
						result = result .. ', ' .. link;
					end
					insertFromSnak( qualifier, entriesToLookupCategory )
					hasAdditionalQualifiers = true;
				end
			end
		end

		--country
		if statement.qualifiers.P17 then
			for i, qualifier in ipairs( statement.qualifiers.P17 ) do
				if qualifier.datavalue then
					local parentOptions = context.cloneOptions( options );
					local qualifierEntityId = qualifier.datavalue.value.id;
					parentOptions[ 'text' ] = getLabel( context, qualifierEntityId, boundaries );
					local link = context.formatSnak( parentOptions, qualifier );
	
					if p.config.reverseOrder then
						result = link .. ', ' .. result;
					else
						result = result .. ', ' .. link;
					end
					insertFromSnak( qualifier, entriesToLookupCategory )
					hasAdditionalQualifiers = true;
				end
			end
		end
	end

	if statement.mainsnak and
		statement.mainsnak.datavalue and
		statement.mainsnak.datavalue.value and
		statement.mainsnak.datavalue.value.id
	then
		local entityId = statement.mainsnak.datavalue.value.id;
		local parentSnaks = { statement.mainsnak };
		local parentEntityIds = { entityId };

		if actualDateBoundariesProperties ~= nil then
			local filterCapitalOf = {
				[ entityId ] = context.getPropertyInBoundaries( context, entityId, boundaries, {'P1376'} )
			};
			if boundaries then
				local entityOptions = context.cloneOptions( options );
				entityOptions['text'] = getLabel( context, entityId, boundaries );
				baseResult = context.formatSnak( entityOptions, statement.mainsnak, circumstances );

				local parentId = entityId;
				while parentId ~= nil do
					-- get parent
					local newParentSnaks = getParentsInBoundariesSnak( context, parentId, boundaries );
					if not newParentSnaks or #newParentSnaks == 0 then
						parentId = nil;
					elseif #newParentSnaks == 1 then
						local parentSnak = newParentSnaks[ 1 ];
						parentId = parentSnak.datavalue.value.id;
						
						local hasLoop = false
						for _, parentEntityId in pairs(parentEntityIds) do
							if parentEntityId == parentId then
								hasLoop = true
							end
						end
						
						if hasLoop then
							if p.config and p.config.catLoopInGeoChains then
								result = result .. p.config.catLoopInGeoChains;
							end
							break -- while parentId ~= nil do
						end
								
						table.insert( parentSnaks, parentSnak );
						table.insert( parentEntityIds, parentId );
						filterCapitalOf[ parentId ] = context.getPropertyInBoundaries( context, parentId, boundaries, { 'P1376' } );
					else
						parentId = nil;
						if p.config and p.config.catAmbiguousGeoChains then
							result = result .. p.config.catAmbiguousGeoChains;
						end
					end
				end

				if not hasAdditionalQualifiers then
					for i = 2, #parentSnaks, 1 do
						local parentSnak = parentSnaks[ i ];
						insertFromSnak( parentSnak, entriesToLookupCategory )
					end
				end

				-- do not output similar countries like "Denmark, the Kingdom of Denmark"
				local simularCountries = {
					['Q41304'] = 'Q1206012', -- Weimar Republic / German Reich
					['Q7318'] = 'Q1206012', -- Weimar Republic / Nazi Germany
					['Q35'] = 'Q756617', -- Denmark / Danish Realm
					['Q55'] = 'Q29999', -- Netherlands / Kingdom of the Netherlands
					['Q32081'] = 'Q865', -- Taiwan Province / Taiwan
				}
				if (#parentSnaks > 1) then
					for smallerCountryId, largerCountryId in pairs( simularCountries ) do
						if parentSnaks[ #parentSnaks ].datavalue.value.id == largerCountryId
							and parentSnaks[ #parentSnaks - 1 ].datavalue.value.id == smallerCountryId
						then
							table.remove( parentSnaks, #parentSnaks );
							table.remove( parentEntityIds, #parentEntityIds );
						end
					end
				end

				-- optimization for capital regions
				if (#parentSnaks > 3) then
					if parentSnaks[ #parentSnaks - 2 ].datavalue.value.id == 'Q23939248' --Greater London, Greater London
						and parentSnaks[ #parentSnaks - 3 ].datavalue.value.id == 'Q23306'
					then
						table.remove( parentSnaks, #parentSnaks - 2 );
						table.remove( parentEntityIds, #parentEntityIds - 2 );
					end
				end
				if (#parentSnaks > 2) then
					if parentSnaks[ #parentSnaks - 1 ].datavalue.value.id == 'Q240' --Brussels-Capital, Brussels
						and parentSnaks[ #parentSnaks - 2 ].datavalue.value.id == 'Q90870'
					then
						table.remove( parentSnaks, #parentSnaks - 2 );
						table.remove( parentEntityIds, #parentEntityIds - 2 );
					elseif parentSnaks[ #parentSnaks - 1 ].datavalue.value.id == 'Q1490' --Tokyo, Tokyo
						and parentSnaks[ #parentSnaks - 2 ].datavalue.value.id == 'Q7473516'
					then
						baseResult = '';
					elseif parentSnaks[ #parentSnaks - 1 ].datavalue.value.id == 'Q633490' --Tadjik districts of Republican Subordination
					then
						table.remove( parentSnaks, #parentSnaks - 1 );
						table.remove( parentEntityIds, #parentEntityIds - 1 );
					elseif parentSnaks[ #parentSnaks - 2 ].datavalue.value.id == 'Q633490' --Tadjik SSR districts of Republican Subordination
					then
						table.remove( parentSnaks, #parentSnaks - 2 );
						table.remove( parentEntityIds, #parentEntityIds - 2 );
					end
				end

				-- do not output (maternity) hospitals, houses and streets but do it for manor and English country houses
				local unignoredTypes = {
					'Q879050', -- manor house
					'Q1343246', -- English country house
				}
				local ignoredTypes = {
					'Q3947', -- house
					'Q16917', -- hospital
					'Q34442', -- road
					'Q79007', -- street
					'Q174782', -- square
					'Q958822', -- maternity hospital
					'Q1059324', -- university hospital
					'Q2087181', -- historic house museum
				}
				if (#parentSnaks > 1) then
					local p31 = mw.wikibase.getAllStatements( parentEntityIds[ 1 ], 'P31' );
					local doignore = true;
					for _, iOf in ipairs( p31 ) do
						for _, unignoredTypeId in ipairs( unignoredTypes ) do
							if ( iOf.mainsnak.datavalue and iOf.mainsnak.datavalue.value.id == unignoredTypeId ) then
								doignore = false;
								unignoredTypes = {};
								ignoredTypes = {};
								break;
							end
						end
					end
					
					if (doignore) then
						for _, iOf in ipairs( p31 ) do
							for _, ignoredTypeId in ipairs( ignoredTypes ) do
								if ( iOf.mainsnak.datavalue and iOf.mainsnak.datavalue.value.id == ignoredTypeId ) then
									baseResult = '';
									unignoredTypes = {};
									ignoredTypes = {};
									break;
								end
							end
						end
					end
				end

				do
					local capofstate = false;
					local i = #parentSnaks;
					while i > 1 do
						local prevEntityId = parentEntityIds[ i - 1 ];
						-- TODO: use English labels, if there is no current language labels
						local prevLabel = getLabel( context, prevEntityId, boundaries ) or '';
						local nextEntityId = parentEntityIds[ i ];
						local nextLabel = getLabel( context, nextEntityId, boundaries ) or '';
						if p.config and p.config.hideSameLabels == true and prevLabel == nextLabel then
							-- do not output same label twice (NY, NY, USA)
							table.remove( parentSnaks, i );
							table.remove( parentEntityIds, i );
						elseif p.config and p.config.hidePartOfLabels == true and isPartOfNext( prevLabel, ' ' .. nextLabel ) then
							-- do not output same label if it's part of previos
							table.remove( parentSnaks, i - 1 );
							table.remove( parentEntityIds, i - 1 );
						elseif p.config and p.config.hideUnitsForCapitals == true then
							-- do not ouput items whose capital is the first item
							local capitalId = nil;
							for _capitalId, capitalSnaks in pairs( filterCapitalOf ) do
								if #capitalSnaks > 0 then
									for __, capitalSnak in pairs( capitalSnaks ) do
										if	capitalSnak.datavalue and 
											parentSnaks[ i ].datavalue.value.id == capitalSnak.datavalue.value.id then
											capitalId = _capitalId;
											if (i == #parentSnaks) then
												capofstate = true;
											end
											break;
										end
									end
								end
							end
							if capitalId ~= nil then
								if i == #parentSnaks then
									i = i - 1;
								end
								-- always ouput constituent countries like England or Russian SFSR
								if (i == (#parentSnaks-1)) and (capofstate == false) then
									local p31 = mw.wikibase.getAllStatements(parentEntityIds[ i ], 'P31');
										for _, iOf in pairs (p31) do
											if (iOf.mainsnak.datavalue.value['numeric-id'] == 236036) or (iOf.mainsnak.datavalue.value['numeric-id'] == 3336843) or (iOf.mainsnak.datavalue.value['numeric-id'] == 12959600) or (iOf.mainsnak.datavalue.value['numeric-id'] == 56219758) or (iOf.mainsnak.datavalue.value['numeric-id'] == 15304003) or (iOf.mainsnak.datavalue.value['numeric-id'] == 66724388) then
												i = i - 1;
										end
									end
								end
								while i > 1 and parentEntityIds[ i ] ~= capitalId do
									table.remove( parentSnaks, i );
									table.remove( parentEntityIds, i );
									i = i - 1;
								end
							end
						end
						i = i - 1;
					end
				end

				if isSkipTopLevel( parentEntityIds[ #parentEntityIds ] ) then
					table.remove( parentSnaks, #parentEntityIds );
					table.remove( parentEntityIds, #parentEntityIds );
				end

				if not hasAdditionalQualifiers then
					for i = 2, #parentSnaks, 1 do
						local parentSnak = parentSnaks[ i ];

						local parentOptions = context.cloneOptions( options );
						parentOptions['text'] = getLabel( context, parentEntityIds[ i ], boundaries );

						local comma;
						if ((baseResult == '') and (i == 2)) then
							comma = '';
						else
							comma = ', ';
						end

						if p.config.reverseOrder then
							result = context.formatSnak( parentOptions, parentSnak ) .. comma .. result;
						else
							result = result .. comma .. context.formatSnak( parentOptions, parentSnak );
						end
					end
				end
			end
		end
	end

	if options[ 'thisLocationOnly' ] then
		result = baseResult;
	elseif p.config.reverseOrder then
		result = result .. baseResult;
	else
		result = baseResult .. result;
	end
	if options.references then
		result = result .. context.formatRefs( options, statement );
	end

	if categorizeByPlace then
		if property == 'P19' then result = result .. getCategory( 'P1464', entriesToLookupCategory ); end
		if property == 'P20' then result = result .. getCategory( 'P1465', entriesToLookupCategory ); end
		if property == 'P119' then result = result .. getCategory( 'P1791', entriesToLookupCategory ); end
	end

	return result;
end

-- append entity id from snak to result
function insertFromSnak( snak, result )
	if not categorizeByPlace then
		return;
	end
	if snak and
		snak.datavalue and
		snak.datavalue.type == 'wikibase-entityid' and
		snak.datavalue.value and
		snak.datavalue.value[ 'entity-type' ] == 'item'
	then
		table.insert( result, snak.datavalue.value.id );
	end
end

function getCategory( propertyId, entriesToLookupCategoryFor )
	if mw.title.getCurrentTitle().namespace == 0 then
		for _, placeId in pairs( entriesToLookupCategoryFor ) do
			local claims = mw.wikibase.getBestStatements(placeId, propertyId);
	
			if claims then
				for _, claim in pairs( claims ) do
					if claim.mainsnak and
						claim.mainsnak and
						claim.mainsnak.datavalue and
						claim.mainsnak.datavalue.type == 'wikibase-entityid'
					then
						local catEntityId = claim.mainsnak.datavalue.value.id;
						local catSitelink = mw.wikibase.getSitelink(catEntityId);
						if (catSitelink) then
							return '[[' .. catSitelink .. ']]';
						end
					end
				end
			end
		end
	end
	return '';
end


local historicNamesProperties = { 'P1813', 'P1448', 'P1705' };

local langCode = mw.language.getContentLanguage():getCode();
local historicNamesPropertySelectors = {
		P1813 = 'P1813[language:' .. langCode .. '][!P3831,P3831:Q105690470]',
		P1448 = 'P1448[language:' .. langCode .. '][!P3831,P3831:Q105690470]',
		P1705 = 'P1705[language:' .. langCode .. '][!P3831,P3831:Q105690470]'
};

-- get current of historic name of place
function getLabel( context, entityId, boundaries )
	if not entityId then
		return nil;
	end
	if (type(entityId) ~= 'string') then error('incorrect type of entityId argument'); end;

    local label = nil;

	-- name from properties
	local results = context.getPropertyInBoundaries( context, entityId, boundaries,
		historicNamesProperties, historicNamesPropertySelectors);

	for r, result in pairs( results ) do
		if result.datavalue and
			result.datavalue.value and
			result.datavalue.value.text
		then
			label = result.datavalue.value.text;
			break;
		end
	end

    -- name from label
    if label == nil then
		label = mw.wikibase.getLabel( entityId );
    end

    return label;
end

p.getLabel = getLabel;

local function calculateEndDateTimestamp( context, options, statement )
	if not context then error( 'context not specified' ) end;
	if not options then error( 'options not specified' ) end;
	if not options.entity then error( 'options.entity missing' ) end;
	if not statement then error( 'statement not specified' ) end;

	if statement.qualifiers and statement.qualifiers.P582 then
		for i, qualifier in ipairs( statement.qualifiers.P582 ) do
			local parsedTime = context.parseTimeFromSnak( qualifier );
			if parsedTime then
				return parsedTime;
			end
		end
	end

	-- check death day... do we have it at all?
	for h, propertyId in pairs( { "P570", "P577", "P576" } ) do
		local dateClaims = context.selectClaims( options, propertyId );
		if dateClaims then
			for i, statement in ipairs( dateClaims ) do
				local parsedTime = context.parseTimeFromSnak( statement.mainsnak );
				if parsedTime then
					return parsedTime;
				end
			end
		end
	end

	-- TODO: check other "end" properties

	-- no death day
	return os.time() * 1000;
end

local function deleteTwinAncestors( countryEntityId, propertyId, entityId ) --do not display countries which have twin ancestors
	local badTwins
	if ( countryEntityId == 'Q174193' ) then				--Great Britain and Ireland
		badTwins = {'Q145'}									--Great Brirani & NI
    elseif ( countryEntityId == 'Q161885' ) then			--Great Britain
		badTwins = {'Q174193'}								--Great Britain and Ireland
	elseif ( countryEntityId == 'Q43287' ) then				--German Impire
		badTwins = {'Q41304', 'Q7318', 'Q2415901', 'Q183'}	--Weimar Republic or Nazi Germany or Allied-occupied Germany or Germany
	elseif ( countryEntityId == 'Q41304' ) then				--Weimar Republic
		badTwins = {'Q7318', 'Q2415901', 'Q183'}			--Nazi Germany or Allied-occupied Germany or Germany
	elseif ( countryEntityId == 'Q7318' ) then				--Nazi Germany
		badTwins = {'Q2415901', 'Q183'}						--Allied-occupied Germany or Germany
    elseif ( countryEntityId == 'Q2415901' ) then			--Allied-occupied Germany
		badTwins = {'Q183'}									--Germany
	elseif ( countryEntityId == 'Q696908' ) then			--Kingdom of Poland
		badTwins = {'Q207272', 'Q211274', 'Q36'}			--Second Polish Republic or Polish People's Republic or Poland
	elseif ( countryEntityId == 'Q207272' ) then			--Second Polish Republic
		badTwins = {'Q211274', 'Q36'}						--Polish People's Republic or Poland
	elseif ( countryEntityId == 'Q211274' ) then			--Polish People's Republic
		badTwins = {'Q36'}									--Poland
	elseif ( countryEntityId == 'Q203493' ) then			--Kingdom of Romania
		badTwins = {'Q842794', 'Q218'}						--Socialist Republic of Romania or Romania
	elseif ( countryEntityId == 'Q842794' ) then			--Socialist Republic of Romania
		badTwins = {'Q218'}									--Romania
	elseif ( countryEntityId == 'Q838261' ) then			--FR of Yugoslavia
		badTwins = {'Q37024'}								--Serbia & Montenegro
	elseif ( countryEntityId == 'Q7779' ) then				--Commonwealth of Independent States
		return false								
	else
		return true;
	end

	local listforcheck
	if propertyId == 'P1532' then
		listforcheck = mw.wikibase.getAllStatements( entityId, propertyId );
	else
		listforcheck = mw.wikibase.getBestStatements( entityId, propertyId );
	end

	for _, claim in pairs( listforcheck ) do
		if ( claim and claim.mainsnak
			and claim.mainsnak.datavalue
			and claim.mainsnak.datavalue.value
			and claim.mainsnak.datavalue.value.id ) then
			local actualId = claim.mainsnak.datavalue.value.id;
			for index, value in ipairs(badTwins) do
				if ( value == actualId ) then
            		return false;
        		end
			end
		end
	end
	return true; 
end

function p.formatCountryClaimWithFlag( context, options, statement )
	if not context then error('context not specified') end;
	if not options then error('options not specified') end;
	if not options.entity then error('options.entity is missing') end;
	if not statement then error('statement not specified') end;

	if not statement.mainsnak or
		not statement.mainsnak.datavalue or
		not statement.mainsnak.datavalue.value or
		not statement.mainsnak.datavalue.value.id
	then
		local result = context.formatStatementDefault( context, options, statement );
		if not result then
			return '';
		end
		return '<span class="country-name">' .. result .. '</span>';
	end
	
	local countryEntityId = statement.mainsnak.datavalue.value.id;
	local endDateTimestamp = calculateEndDateTimestamp( context, options, statement );
	local boundaries = getTimeBoundariesFromProperties( context, {'P570', 'P577', 'P571'} );

	if deleteTwinAncestors( countryEntityId, string.upper(options.property), options.entity.id ) then

		local countryOptions = context.cloneOptions( options );
		if not countryOptions['text'] or countryOptions['text'] == '' then
			countryOptions['text'] = getLabel( context, countryEntityId, boundaries );
		end

		local flag = Flags.getFlag( context, countryEntityId, endDateTimestamp );	
		if flag then
			return flag .. '&nbsp;<span class="country-name">' ..
				context.formatStatementDefault( context, countryOptions, statement ) ..
				'</span>';
		end

		return '<span class="country-name">' ..
			context.formatStatementDefault( context, countryOptions, statement ) ..
			'</span>';
	else
		return nil;
	end
end

return p;