Module:Mapframe

From WikiZagłębie

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

--[[
UWAGA (fork dla WikiZagłębie):
- Ten moduł to zmodyfikowana wersja z enwiki: https://en.wikipedia.org/wiki/Module:Mapframe
- Działa na MediaWiki poza infrastrukturą Wikimedia.
- BEZPIECZNIE WYŁĄCZA funkcje zależne od Wikibase (Wikidane), jeśli rozszerzenie nie jest zainstalowane.
- Dodaje natywne wsparcie parametrów |lat= i |lon= (lub |long= / |lng=), dzięki czemu nie musisz używać {{Coord}}.
- Obsługa „named” (zbieranie nazwanych współrzędnych z treści) wymaga Module:Transcluder – jeśli go nie ma, zwracany jest czytelny błąd.
- Wstawianie TemplateStyles (Maplink/styles-multi.css) jest zabezpieczone – jeśli nie ma rozszerzenia/strony, moduł nadal działa.

Komentarze przetłumaczone na polski, treść oryginalna skrócona/uzupełniona tam, gdzie istotne.
]]--

--[[----------------------------------------------------------------------------
 ##### Ustawienia lokalizacyjne (L10n) #####
 Wartości w cudzysłowie można lokalizować (jeśli chcesz zmienić komunikaty).
----------------------------------------------------------------------------]]--
local L10n = {}

-- Zależności modułowe
local transcluder -- lokalna kopia Module:Transcluder (ładowany leniwie)
-- Uwaga: brak 'strict' (moduł bywa wywoływany przez inne moduły używające globali).

-- Nazwy parametrów szablonu (wersje nienumerowane)
--   Każdy wpis może być stringiem albo tablicą aliasów.
L10n.para = {
	display		= "display",
	type		= "type",
	id          = { "id", "ids" },
	from		= "from",
	raw		    = "raw",
	title		= "title",
	description	= "description",
	strokeColor = { "stroke-color", "stroke-colour" },
	strokeWidth	= "stroke-width",
	strokeOpacity = "stroke-opacity",
	fill        = "fill",
	fillOpacity = "fill-opacity",
	coord		= "coord",

	-- DODANE: natywne wsparcie współrzędnych bez {{Coord}}
	lat         = "lat",
	long        = { "long", "lon", "lng" }, -- aliasy dla długości

	marker		= "marker",
	markerColor	= { "marker-color", "marker-colour" },
	markerSize  = "marker-size",
	radius      = { "radius", "radius_m" },
	radiusKm    = "radius_km",
	radiusFt    = "radius_ft",
	radiusMi    = "radius_mi",
	edges       = "edges",
	text		= "text",
	icon		= "icon",
	zoom		= "zoom",
	frame		= "frame",
	plain		= "plain",
	frameWidth	= "frame-width",
	frameHeight	= "frame-height",
	frameCoordinates = { "frame-coordinates", "frame-coord" },
	frameLatitude    = { "frame-lat", "frame-latitude" },
	frameLongitude   = { "frame-long", "frame-longitude" },
	frameAlign       = "frame-align",
	switch           = "switch",
	overlay          = "overlay",
	overlayBorder    = "overlay-border",
	overlayHorizontalAlignment = "overlay-horizontal-alignment",
	overlayVerticalAlignment   = "overlay-vertical-alignment",
	overlayHorizontalOffset    = "overlay-horizontal-offset",
	overlayVerticalOffset      = "overlay-vertical-offset"
}

-- Nazwy innych szablonów, z których moduł potrafi wyciągać współrzędne
L10n.template = {
	coord = { -- {{Coord}} i wrappery generujące jego wynik
		"Coord", "Coord/sandbox",
		"NRHP row", "NRHP row/sandbox",
		"WikidataCoord", "WikidataCoord/sandbox", "Wikidatacoord", "Wikidata coord"
	}
}

-- Komunikaty błędów (PL)
L10n.error = {
	badDisplayPara    = "Invalid display parameter.",
	noCoords	      = "Musisz podać współrzędne w |coord= albo w parach |lat= i |lon= (pobieranie z Wikidanych jest wyłączone lub niedostępne).",
	wikidataCoords    = "No coordinates found on Wikidata (P625).",
	noCircleCoords    = "Środek okręgu musi mieć współrzędne (|coord= lub |lat=/|lon=).",
	negativeRadius    = "Circle radius must be positive.",
	noRadius          = "Circle radius is missing.",
	negativeEdges     = "The number of circle edges must be positive.",
	noSwitchPara      = "Podano tylko jedną wartość przełącznika w |switch=.",
	oneSwitchLabel    = "Podano tylko jedną etykietę w |switch=.",
	noSwitchLists     = "At least one parameter must contain a SWITCH list: ...",
	switchMismatches  = "All SWITCH lists must have the same number of values.",
	oneSwitchValue    = "Podano tylko jedną wartość przełącznika w |%s=.",
	fewerSwitchLabels = "Podano %d wartości przełącznika, ale tylko %d etykiet w |switch=.",
	noNamedCoords     = "Nie znaleziono nazwanych współrzędnych w %s.",
	noTranscluder     = "Funkcja \"named\" wymaga Module:Transcluder (brak modułu na tej wiki).",
	noWikibase        = "Wikidata integration (mw.wikibase) is unavailable on this wiki."
}

-- Inne stringi
L10n.str = {
	-- wartości display
	inline		= "inline",
	title		= "title",
	dsep		= ",",

	-- wartości type
	line		= "line",
	shape		= "shape",
	shapeInverse= "shape-inverse",
	data		= "data",
	point		= "point",
	circle      = "circle",
	named       = "named",

	-- słowo-klucz do list SWITCH
	switch = "SWITCH",

	-- wartości „prawda/fałsz” rozpoznawane w wybranych parametrach
	affirmedWords = ' '..table.concat({
		"add","added","affirm","affirmed","include","included","on","true","yes","y"
	}, ' ')..' ',
	declinedWords = ' '..table.concat({
		"decline","declined","exclude","excluded","false","none","not","no","n","off","omit","omitted","remove","removed"
	}, ' ')..' '
}

-- Wartości domyślne
L10n.defaults = {
	display		= L10n.str.inline,
	text		= "Mapa",
	frameWidth	= "300",
	frameHeight	= "200",
	frameAlign   = "right",
	markerColor	= "5E74F3",
	markerSize	= nil,
	strokeColor	= "#ff0000",
	strokeWidth	= 6,
	edges = 32, -- liczba krawędzi do aproksymacji okręgu
	overlayBorder = "1px solid white",
	overlayHorizontalAlignment = "right",
	overlayHorizontalOffset = "0",
	overlayVerticalAlignment = "bottom",
	overlayVerticalOffset = "0"
}

-- #### Koniec ustawień L10n ####

--[[----------------------------------------------------------------------------
 Metody pomocnicze (util)
----------------------------------------------------------------------------]]--
local util = {}

-- Pobranie wartości parametru po kluczu z L10n.para (+opcjonalny sufiks liczbowy)
function util.getParameterValue(args, param_id, suffix)
	suffix = suffix or ''
	if type( L10n.para[param_id] ) ~= 'table' then
		return args[L10n.para[param_id]..suffix]
	end
	for _i, paramAlias in ipairs(L10n.para[param_id]) do
		if args[paramAlias..suffix] then
			return args[paramAlias..suffix]
		end
	end
	return nil
end

-- Trymowanie/oczyszczanie parametrów (zostawiamy kontrolne w text/description)
function util.trimArgs(argsTable)
	local cleanArgs = {}
	for key, val in pairs(argsTable) do
		if type(key) == 'string'  and type(val) == 'string' then
			val = val:match('^%s*(.-)%s*$')
			if val ~= '' then
				if util.matchesParam('text', key) or util.matchesParam('description', key, key:gsub('^%D+(%d+)$', '%1') ) then
					cleanArgs[key] = val
				else
					cleanArgs[key] = val:gsub('%c',' ')
				end
			end
		else
			cleanArgs[key] = val
		end
	end
	return cleanArgs
end

-- Sprawdzenie, czy nazwa odpowiada zlokalizowanemu parametrowi
function util.matchesParam(key, name, suffix)
	local param = L10n.para[key]
	suffix = suffix or ''
	if type(param) == 'table' then
		for _, v in pairs(param) do
			if (v .. suffix) == name then return true end
		end
		return false
	end
	return ((param .. suffix) == name)
end

-- Rozpoznanie wartości „prawda”
function util.isAffirmed(val)
	if not val then return false end
	return string.find(L10n.str.affirmedWords, ' '..val..' ', 1, true ) and true or false
end

-- Rozpoznanie wartości „fałsz”
function util.isDeclined(val)
	if not val then return false end
	return string.find(L10n.str.declinedWords , ' '..val..' ', 1, true ) and true or false
end

-- Czy nazwa to {{Coord}} (lub wrapper) – uwzględnij lokalny NS Szablonu
function util.isCoordTemplateOrWrapper(name)
	name = mw.text.trim(name)
	local templateNs = mw.site.namespaces[10] and mw.site.namespaces[10].name or 'Template'
	local inputTitle = mw.title.new(name, templateNs)
	if not inputTitle then
		return false
	end
	if not L10n.template.titles then
		L10n.template.titles = {}
		for _, v in pairs(L10n.template.coord) do
			table.insert(L10n.template.titles, mw.title.new(v, templateNs))
		end
	end
	for _, templateTitle in pairs(L10n.template.titles) do
		if mw.title.equals(inputTitle, templateTitle) then
			return true
		end
	end
	return false
end

-- Rekurencyjnie wyciągnij szablony coord mające parametr name=
function util.extractCoordTemplates(wikitext)
	local output = {}
	local templates = mw.ustring.gmatch(wikitext, '{%b{}}')
	local subtemplates = {}
	for template in templates do
		local templateName = mw.ustring.match(template, '{{([^}|]+)')
		local nameParam = mw.ustring.match(template, "|%s*name%s*=%s*[^}|]+")
		if util.isCoordTemplateOrWrapper(templateName) then
			if nameParam then table.insert(output, template) end
		elseif mw.ustring.find(mw.ustring.sub(template, 2), "{{") then
			local subOutput = util.extractCoordTemplates(mw.ustring.sub(template, 2))
			for _, t in pairs(subOutput) do
				table.insert(output, t)
			end
		end
	end
	-- wymuś display=inline (bez tytułu)
	for k, v in pairs(output) do
		output[k] = mw.ustring.gsub(v, "|%s*display%s*=[^|}]+", "|display=inline")
	end
	return output
end

-- Zbierz nazwane współrzędne z podanej strony/sekcji (wymaga Module:Transcluder)
function util.getNamedCoords(page)
	if transcluder == nil then
		local ok, mod = pcall(require, "Module:Transcluder")
		if ok and mod then
			transcluder = mod
		else
			error(L10n.error.noTranscluder, 0)
		end
	end
	local parts = mw.text.split(page or "", "#", true)
	local name = parts[1] == "" and mw.title.getCurrentTitle().prefixedText or parts[1]
	local section = parts[2]
	local pageWikitext = transcluder.get(section and name.."#"..section or name)
	local coordTemplates = util.extractCoordTemplates(pageWikitext)
	if #coordTemplates == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
	local frame = mw.getCurrentFrame()
	local sep = "________"
	local expandedContent = frame:preprocess(table.concat(coordTemplates, sep))
	local expandedTemplates = mw.text.split(expandedContent, sep)
	local namedCoords = {}
	for _, expandedTemplate in pairs(expandedTemplates) do
		local coord = mw.ustring.match(expandedTemplate, "<span class=\"geo%-dec\".->(.-)</span>")
		if coord then
			local nameTxt = (
				mw.ustring.match(expandedTemplate, "<span class=\"mapframe%-coord%-name\">(.-)</span>") or
				mw.ustring.match(expandedTemplate, "<span class=\"fn org\">(.-)</span>") or
				coord
			)
			local description = nameTxt ~= coord and coord
			local coordNorm = mw.ustring.gsub(coord, "[° ]", "_")
			table.insert(namedCoords, {coord=coordNorm, name=nameTxt, description=description})
		end
	end
	if #namedCoords == 0 then error(string.format(L10n.error.noNamedCoords, page or name), 0) end
	return namedCoords
end

-- Parsowanie stringa geohack („1_2_N_3_4_E” lub z „params=”)
function util.parseCoords(coords)
	local coordsPatt
	if mw.ustring.find(coords, "params=", 1, true) then
		coordsPatt = 'params=([_%.%d]+[NS][_%.%d]+[EW])'
	else
		coordsPatt = '[_%.%d]+[NS][_%.%d]+[EW]'
	end
	local parts = mw.text.split((mw.ustring.match(coords, coordsPatt) or ''), '_')

	local lat_d = tonumber(parts[1])
	assert(lat_d, "Could not read latitude from: '"..coords.."'.")
	local lat_m = tonumber(parts[2])
	local lat_s = lat_m and tonumber(parts[3])
	local lat = lat_d + (lat_m or 0)/60 + (lat_s or 0)/3600
	if parts[#parts/2] == 'S' then lat = -lat end

	local long_d = tonumber(parts[1+#parts/2])
	assert(long_d, "Could not read longitude from: '"..coords.."'.")
	local long_m = tonumber(parts[2+#parts/2])
	local long_s = long_m and tonumber(parts[3+#parts/2])
	local long = long_d + (long_m or 0)/60 + (long_s or 0)/3600
	if parts[#parts] == 'W' then long = -long end

	return lat, long
end

-- Pobranie współrzędnych z Wikidanych (jeśli Wikibase dostępny)
function util.wikidataCoords(item_id)
	if not (mw.wikibase and type(mw.wikibase.getBestStatements) == 'function' and
	        type(mw.wikibase.isValidEntityId) == 'function' and
	        type(mw.wikibase.entityExists) == 'function') then
		error(L10n.error.noWikibase, 0)
	end
	if not (item_id and mw.wikibase.isValidEntityId(item_id) and mw.wikibase.entityExists(item_id)) then
		error(L10n.error.noCoords, 0)
	end
	local coordStatements = mw.wikibase.getBestStatements(item_id, 'P625')
	if not coordStatements or #coordStatements == 0 then
		error(L10n.error.wikidataCoords, 0)
	end
	local hasNoValue = ( coordStatements[1].mainsnak and (coordStatements[1].mainsnak.snaktype == 'novalue' or coordStatements[1].mainsnak.snaktype == 'somevalue') )
	if hasNoValue then
		error(L10n.error.wikidataCoords, 0)
	end
	local wdCoords = coordStatements[1]['mainsnak']['datavalue']['value']
	return tonumber(wdCoords['latitude']), tonumber(wdCoords['longitude'])
end

-- Aproksymacja okręgu wielokątem
function util.circleToPolygon(lat, long, radius, n)
	local function offset(cLat, cLon, distance, bearing)
		local lat1 = math.rad(cLat)
		local lon1 = math.rad(cLon)
		local dByR = distance / 6378137 -- promień Ziemi WGS84
		local lat = math.asin(
			math.sin(lat1) * math.cos(dByR) +
			math.cos(lat1) * math.sin(dByR) * math.cos(bearing)
		)
		local lon = lon1 + math.atan2(
			math.sin(bearing) * math.sin(dByR) * math.cos(lat1),
			math.cos(dByR) - math.sin(lat1) * math.sin(lat)
		)
		return {math.deg(lon), math.deg(lat)}
	end
	local coordinates = {}
	for i = 0, n-1 do
		table.insert(coordinates, offset(lat, long, radius, (2*math.pi*i*-1)/n))
	end
	table.insert(coordinates, offset(lat, long, radius, 0))
	return coordinates
end

-- Policz pary klucz-wartość w tabeli (dla nie-sekwencji)
function util.tableCount(t)
	local count = 0
	for _k, _v in pairs(t) do
		count = count + 1
	end
	return count
end

-- Zwróć liczbę elementów w podtabelach, jeśli wszystkie są równe; inaczej nil
function util.subTablesCount(t)
	local count = nil
	for _k, v in pairs(t) do
		if count == nil then
			count = util.tableCount(v)
		elseif count ~= util.tableCount(v) then
			return nil
		end
	end
	return count
end

-- Podziel listę na sekwencję (separator: „###”, potem „;”, potem „,”)
function util.tableFromList(listString)
	if type(listString) ~= "string" or listString == "" then return nil end
	local separator = (mw.ustring.find(listString, "###", 0, true ) and "###") or
		(mw.ustring.find(listString, ";", 0, true ) and ";") or ","
	local pattern = "%s*"..separator.."%s*"
	return mw.text.split(listString, pattern)
end

-- Flaga: czy Kartographer wyliczy współrzędne z geometrii (geoline/shape)
local coordsDerivedFromFeatures = false

--[[----------------------------------------------------------------------------
 Tworzenie danych/wyjścia (make)
----------------------------------------------------------------------------]]--
local make = {}

-- Zawartość do tagu <maplink>/<mapframe>
function make.content(args)
	if util.getParameterValue(args, 'raw') then
		coordsDerivedFromFeatures = true
		return util.getParameterValue(args, 'raw')
	end

	local content = {}
	local argsExpanded = {}
	for k, v in pairs(args) do
		local index = string.match( k, '^[^0-9]+([0-9]*)$' )
		if index ~= nil then
			local indexNumber = (index ~= '' and tonumber(index)) or 1
			argsExpanded[indexNumber] = argsExpanded[indexNumber] or {}
			argsExpanded[indexNumber][ string.gsub(k, index, '') ] = v
		end
	end

	for _contentIndex, contentArgs in pairs(argsExpanded) do
		local argType = util.getParameterValue(contentArgs, "type")
		if not coordsDerivedFromFeatures then
			coordsDerivedFromFeatures = ( argType == L10n.str.line or argType == L10n.str.shape ) and true or false
		end
		if argType == L10n.str.named then
			local namedCoords = util.getNamedCoords(util.getParameterValue(contentArgs, "from"))
			local typeKey = type(L10n.para.type) == "table" and L10n.para.type[1] or L10n.para.type
			local coordKey = type(L10n.para.coord) == "table" and L10n.para.coord[1] or L10n.para.coord
			local titleKey = type(L10n.para.title) == "table" and L10n.para.title[1] or L10n.para.title
			local descKey = type(L10n.para.description) == "table" and L10n.para.description[1] or L10n.para.description
			for _, namedCoord in pairs(namedCoords) do
				contentArgs[typeKey] = "point"
				contentArgs[coordKey]  = namedCoord.coord
				contentArgs[titleKey]  = namedCoord.name
				contentArgs[descKey]   = namedCoord.description
				content[#content+1] = make.contentJson(contentArgs)
			end
		else
			content[#content + 1] = make.contentJson(contentArgs)
		end
	end

	if #content == 1 then return content[1] end
	return '[\n' .. table.concat( content, ',\n') .. '\n]'
end

-- Współrzędne: z |coord= (GeoHack), ALBO z |lat=/|lon=, ALBO z Wikidanych (jeśli dostępne)
function make.coords(args, plainOutput)
	local lat, long
	local frame = mw.getCurrentFrame()

	-- 1) coord= (może być {{Coord|...}} → będzie link z params=)
	if util.getParameterValue(args, 'coord') then
		local coords = frame:preprocess( util.getParameterValue(args, 'coord') )
		lat, long = util.parseCoords(coords)

	-- 2) natywne lat/lon
	elseif util.getParameterValue(args, 'lat') and util.getParameterValue(args, 'long') then
		lat  = tonumber(util.getParameterValue(args, 'lat'))
		long = tonumber(util.getParameterValue(args, 'long'))
		if not (lat and long) then error(L10n.error.noCoords, 0) end

	-- 3) próba Wikidanych (jeśli Wikibase jest dostępny)
	elseif (mw.wikibase and type(mw.wikibase.getEntityIdForCurrentPage) == 'function') then
		local id = util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()
		local ok, a, b = pcall(util.wikidataCoords, id)
		if ok then lat, long = a, b end
	end

	if not (lat and long) then
		error(L10n.error.noCoords, 0)
	end

	if plainOutput then
		return lat, long
	end
	return {[0] = long, [1] = lat}
end

-- Wielokąt okręgu
function make.circleCoords(args)
	local lat, long = make.coords(args, true)
	local radius = util.getParameterValue(args, 'radius')
	if not radius then
		radius = util.getParameterValue(args, 'radiusKm') and tonumber(util.getParameterValue(args, 'radiusKm'))*1000
		if not radius then
			radius = util.getParameterValue(args, 'radiusMi') and tonumber(util.getParameterValue(args, 'radiusMi'))*1609.344
			if not radius then
				radius = util.getParameterValue(args, 'radiusFt') and tonumber(util.getParameterValue(args, 'radiusFt'))*0.3048
			end
		end
	end
	local edges = util.getParameterValue(args, 'edges') or L10n.defaults.edges
	if not lat or not long then
		error(L10n.error.noCircleCoords, 0)
	elseif not radius then
		error(L10n.error.noRadius, 0)
	elseif tonumber(radius) <= 0 then
		error(L10n.error.negativeRadius, 0)
	elseif tonumber(edges) <= 0 then
		error(L10n.error.negativeEdges, 0)
	end
	return util.circleToPolygon(lat, long, radius, tonumber(edges))
end

-- Zbuduj JSON pojedynczej cechy (Feature / ExternalData)
function make.contentJson(contentArgs)
	local data = {}

	if util.getParameterValue(contentArgs, 'type') == L10n.str.point
		or util.getParameterValue(contentArgs, 'type') == L10n.str.circle then

		local isCircle = util.getParameterValue(contentArgs, 'type') == L10n.str.circle
		data.type = "Feature"
		data.geometry = {
			type = isCircle and "LineString" or "Point",
			coordinates = isCircle and make.circleCoords(contentArgs) or make.coords(contentArgs)
		}
		data.properties = {
			title = util.getParameterValue(contentArgs, 'title') or mw.getCurrentFrame():getParent():getTitle()
		}
		if isCircle then
			data.properties.stroke = util.getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor
			data.properties["stroke-width"] = tonumber(util.getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
			local strokeOpacity = util.getParameterValue(contentArgs, 'strokeOpacity')
			if strokeOpacity then data.properties['stroke-opacity'] = tonumber(strokeOpacity) end
			local fill = util.getParameterValue(contentArgs, 'fill')
			if fill then
				data.properties.fill = fill
				local fillOpacity = util.getParameterValue(contentArgs, 'fillOpacity')
				data.properties['fill-opacity'] = fillOpacity and tonumber(fillOpacity) or 0.6
			end
		else
			local markerSymbol = util.getParameterValue(contentArgs, 'marker') or L10n.defaults.marker
			if markerSymbol ~= "blank" then
				data.properties["marker-symbol"] = markerSymbol
			end
			data.properties["marker-color"] = util.getParameterValue(contentArgs, 'markerColor') or L10n.defaults.markerColor
			data.properties["marker-size"] = util.getParameterValue(contentArgs, 'markerSize') or L10n.defaults.markerSize
		end

	else
		data.type = "ExternalData"

		-- Ustal źródło danych
		if util.getParameterValue(contentArgs, 'type') == L10n.str.data or util.getParameterValue(contentArgs, 'from') then
			data.service = "page"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.line then
			data.service = "geoline"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shape then
			data.service = "geoshape"
		elseif util.getParameterValue(contentArgs, 'type') == L10n.str.shapeInverse then
			data.service = "geomask"
		end

		-- Preferuj jawny |id= ; jeśli brak i mamy Wikibase – można użyć QID bieżącej strony
		if util.getParameterValue(contentArgs, 'id') then
			data.ids = util.getParameterValue(contentArgs, 'id')
		elseif util.getParameterValue(contentArgs, 'from') then
			data.title = util.getParameterValue(contentArgs, 'from')
		elseif (mw.wikibase and type(mw.wikibase.getEntityIdForCurrentPage) == 'function') then
			data.ids = mw.wikibase.getEntityIdForCurrentPage()
		end

		data.properties = {
			stroke = util.getParameterValue(contentArgs, 'strokeColor') or L10n.defaults.strokeColor,
			["stroke-width"] = tonumber(util.getParameterValue(contentArgs, 'strokeWidth')) or L10n.defaults.strokeWidth
		}
		local strokeOpacity = util.getParameterValue(contentArgs, 'strokeOpacity')
		if strokeOpacity then data.properties['stroke-opacity'] = tonumber(strokeOpacity) end
		local fill = util.getParameterValue(contentArgs, 'fill')
		if fill and (data.service == "geoshape" or data.service == "geomask") then
			data.properties.fill = fill
			local fillOpacity = util.getParameterValue(contentArgs, 'fillOpacity')
			if fillOpacity then data.properties['fill-opacity'] = tonumber(fillOpacity) end
		end
	end

	data.properties.title = util.getParameterValue(contentArgs, 'title') or mw.title.getCurrentTitle().text
	if util.getParameterValue(contentArgs, 'description') then
		data.properties.description = util.getParameterValue(contentArgs, 'description')
	end

	return mw.text.jsonEncode(data)
end

-- Atrybuty tagów <maplink>/<mapframe>
function make.tagAttribs(args, isTitle)
	local attribs = {}
	if util.getParameterValue(args, 'zoom') then
		attribs.zoom = util.getParameterValue(args, 'zoom')
	end
	if util.isDeclined(util.getParameterValue(args, 'icon')) then
		attribs.class = "no-icon"
	end
	if util.getParameterValue(args, 'type') == L10n.str.point and not coordsDerivedFromFeatures then
		local lat, long = make.coords(args, 'plainOutput')
		attribs.latitude = tostring(lat)
		attribs.longitude = tostring(long)
	end
	if util.isAffirmed(util.getParameterValue(args, 'frame')) and not(isTitle) then
		attribs.width = util.getParameterValue(args, 'frameWidth') or L10n.defaults.frameWidth
		attribs.height = util.getParameterValue(args, 'frameHeight') or L10n.defaults.frameHeight
		if util.getParameterValue(args, 'frameCoordinates') then
			local frameLat, frameLong = util.parseCoords(util.getParameterValue(args, 'frameCoordinates'))
			attribs.latitude = frameLat
			attribs.longitude = frameLong
		else
			if util.getParameterValue(args, 'frameLatitude') then
				attribs.latitude = util.getParameterValue(args, 'frameLatitude')
			end
			if util.getParameterValue(args, 'frameLongitude') then
				attribs.longitude = util.getParameterValue(args, 'frameLongitude')
			end
		end
		if not attribs.latitude and not attribs.longitude and not coordsDerivedFromFeatures then
			if (mw.wikibase and type(mw.wikibase.getEntityIdForCurrentPage) == 'function') then
				local id = util.getParameterValue(args, 'id') or mw.wikibase.getEntityIdForCurrentPage()
				local ok, lat, long = pcall(util.wikidataCoords, id)
				if ok then
					attribs.latitude = tostring(lat)
					attribs.longitude = tostring(long)
				end
			end
		end
		if util.getParameterValue(args, 'frameAlign') then
			attribs.align = util.getParameterValue(args, 'frameAlign')
		end
		if util.isAffirmed(util.getParameterValue(args, 'plain')) then
			attribs.frameless = "1"
		else
			attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text
		end
	else
		attribs.text = util.getParameterValue(args, 'text') or L10n.defaults.text
	end
	return attribs
end

-- Wyjście w tytule strony (w prawym górnym rogu) – używa <indicator>
function make.titleOutput(args, tagContent)
	local titleTag = mw.text.tag('maplink', make.tagAttribs(args, true), tagContent)
	local spanAttribs = { style = "font-size: small;", id = "mapframe-coordinates" }
	local indicatorContent = mw.text.tag('span', spanAttribs, titleTag)
	return mw.getCurrentFrame():extensionTag {
		name = "indicator",
		content = indicatorContent,
		args = { name = "zzz-mapframe" } -- „zzz”: ostatni wskaźnik
	}
end

-- Wyjście inline (<maplink> lub <mapframe>)
function make.inlineOutput(args, tagContent)
	local tagName = 'maplink'
	if util.getParameterValue(args, 'frame') then
		tagName = 'mapframe'
	end
	return mw.text.tag(tagName, make.tagAttribs(args), tagContent)
end

-- HTML dla przełącznika map (multi) – wstrzykuj TemplateStyles, jeśli możliwe
function make.switcherHtml(params, options)
	options = options or {}
	local frame = mw.getCurrentFrame()
	local styles = ""
	local ok, rendered = pcall(function()
		return frame:extensionTag{ name = "templatestyles", args = { src = "Template:Maplink/styles-multi.css" } }
	end)
	if ok and rendered then styles = rendered end

	local container = mw.html.create("div")
		:addClass("switcher-container")
		:addClass("mapframe-multi-container")
	if options.alignment == "left" or options.alignment == "right" then
		container:addClass("float"..options.alignment)
	else
		container:addClass("center")
	end
	for i = 1, #params do
		container
			:tag("div")
				:wikitext(params[i].map)
				:tag("span")
					:addClass("switcher-label")
					:css("display", "none")
					:wikitext(mw.text.trim(params[i].label))
	end
	if not options.isThumbnail then
		return styles .. tostring(container)
	end
	local classlist = container:getAttr("class")
	classlist = mw.ustring.gsub(classlist, "%a*"..options.alignment, "")
	container:attr("class", classlist)
	local outerCountainer = mw.html.create("div")
		:addClass("mapframe-multi-outer-container")
		:addClass("mw-kartographer-container")
		:addClass("thumb")
	if options.alignment == "left" or options.alignment == "right" then
		outerCountainer:addClass("t"..options.alignment)
	else
		outerCountainer:addClass("tnone"):addClass("center")
	end
	outerCountainer
		:tag("div")
			:addClass("thumbinner")
			:css("width", options.width.."px")
			:node(container)
			:node(options.caption and mw.html.create("div")
				:addClass("thumbcaption")
				:wikitext(options.caption)
			)
	return styles .. tostring(outerCountainer)
end

-- HTML dla mapy nakładkowej (overlay)
function make.overlayHtml(overlayMap, baseMap, options)
	options = options or {}
	local containerFloatClass = "float"..(options.align or "none")
	if options.align == "center" then containerFloatClass = "center" end
	local containerStyle = {
		position = "relative",
		width = options.width .. "px",
		height = options.height .. "px",
		overflow = "hidden"
	}
	if options.align == "center" then
		containerStyle["margin-left"] = "auto"
		containerStyle["margin-right"] = "auto"
	end
	local container = mw.html.create("div")
		:addClass("mapframe-withOverlay-container")
		:addClass(containerFloatClass)
		:addClass("noresize")
		:css(containerStyle)

	local overlayStyle = {
		position = "absolute",
		["z-index"] = "1",
		border = options.border or "1px solid white"
	}
	if options.horizontalAlignment == "right" then
		overlayStyle.right = options.horizontalOffset .. "px"
	else
		overlayStyle.left = options.horizontalOffset .. "px"
	end
	if options.verticalAlignment == "bottom" then
		overlayStyle.bottom = options.verticalOffset .. "px"
	else
		overlayStyle.top = options.verticalOffset .. "px"
	end
	local overlayDiv = mw.html.create("div")
		:css(overlayStyle)
		:wikitext(overlayMap)

	container:node(overlayDiv):wikitext(baseMap)

	if not options.isThumbnail then
		return tostring(container)
	end
	local classlist = container:getAttr("class")
	classlist = mw.ustring.gsub(classlist, "%a*"..options.align, "")
	container:attr("class", classlist)
	local outerCountainer = mw.html.create("div")
		:addClass("mapframe-withOverlay-outerContainer")
		:addClass("mw-kartographer-container")
		:addClass("thumb")
	if options.align == "left" or options.align == "right" then
		outerCountainer:addClass("t"..options.align)
	else
		outerCountainer:addClass("tnone"):addClass("center")
	end
	outerCountainer
		:tag("div")
			:addClass("thumbinner")
			:css("width", options.width.."px")
			:node(container)
			:node(options.caption and mw.html.create("div")
				:addClass("thumbcaption")
				:wikitext(options.caption)
			)
	return tostring(outerCountainer)
end

--[[----------------------------------------------------------------------------
 Interfejs eksportowany (funkcje widoczne dla szablonów/innych modułów)
----------------------------------------------------------------------------]]--
local p = {}

-- Punkt wejścia dla szablonów
function p.main(frame)
	local parent = frame.getParent(frame)
	local overlay = util.getParameterValue(parent.args, 'overlay')
	local hasOverlay = overlay and mw.text.trim(overlay) ~= ""
	local switch = util.getParameterValue(parent.args, 'switch')
	local isMulti = switch and mw.text.trim(switch) ~= ""
	local output
	if hasOverlay then
		output = p.withOverlay(parent.args)
	elseif isMulti then
		output = p.multi(parent.args)
	else
		output = p._main(parent.args)
	end
	return frame:preprocess(output)
end

-- Główna logika
function p._main(_args)
	local args = util.trimArgs(_args)
	local tagContent = make.content(args)

	local display = mw.text.split(util.getParameterValue(args, 'display') or L10n.defaults.display, '%s*' .. L10n.str.dsep .. '%s*')
	local displayInTitle = display[1] ==  L10n.str.title or display[2] ==  L10n.str.title
	local displayInline = display[1] ==  L10n.str.inline or display[2] ==  L10n.str.inline

	local output
	if displayInTitle and displayInline then
		output = make.titleOutput(args, tagContent) .. make.inlineOutput(args, tagContent)
	elseif displayInTitle then
		output = make.titleOutput(args, tagContent)
	elseif displayInline then
		output = make.inlineOutput(args, tagContent)
	else
		error(L10n.error.badDisplayPara)
	end

	return output
end

-- Wariant z przełącznikiem map
function p.multi(_args)
	local args = util.trimArgs(_args)
	if not args[L10n.para.switch] then error(L10n.error.noSwitchPara, 0) end
	local switchParamValue = util.getParameterValue(args, 'switch')
	local switchLabels = util.tableFromList(switchParamValue)
	if #switchLabels == 1 then error(L10n.error.oneSwitchLabel, 0) end

	local mapframeArgs = {}
	local switchParams = {}
	for name, val in pairs(args) do
		if val ~= switchParamValue and not string.match(val, "^"..L10n.str.switch..":") then
			mapframeArgs[name] = val
		end
		local switchList = string.match(val, "^"..L10n.str.switch..":(.+)")
		if switchList ~= nil then
			local values = util.tableFromList(switchList)
			if #values == 1 then
				error(string.format(L10n.error.oneSwitchValue, name), 0)
			end
			switchParams[name] = values
		end
	end
	if util.tableCount(switchParams) == 0 then
		error(L10n.error.noSwitchLists, 0)
	end
	local switchCount = util.subTablesCount(switchParams)
	if not switchCount then
		error(L10n.error.switchMismatches, 0)
	elseif switchCount > #switchLabels then
		error(string.format(L10n.error.fewerSwitchLabels, switchCount, #switchLabels), 0)
	end

	-- Wymuś „plain” (miniaturę z etykietami budujemy w HTML switchera)
	mapframeArgs.plain = "yes"

	local switcher = {}
	for i = 1, switchCount do
		local label = switchLabels[i]
		for name, values in pairs(switchParams) do
			mapframeArgs[name] = values[i]
		end
		table.insert(switcher, {
			map = p._main(mapframeArgs),
			label = "Show "..label
		})
	end
	return make.switcherHtml(switcher, {
		alignment = args["frame-align"] or "right",
		isThumbnail = (args.frame and not args.plain) and true or false,
		width = args["frame-width"] or L10n.defaults.frameWidth,
		caption = args.text
	})
end

-- Wariant z nakładką
function p.withOverlay(_args)
	local overlayMap = _args.overlay
	if type(overlayMap) == 'string' then
		overlayMap = overlayMap:match('^%s*(.-)%s*$')
	end
	local isThumbnail = (util.getParameterValue(_args, "frame") and not util.getParameterValue(_args, "plain")) and true or false
	local args = util.trimArgs(_args)
	args.plain = "yes"
	local basemap = p._main(args)
	local overlayOptions = {
		width = util.getParameterValue(args, "frameWidth") or L10n.defaults.frameWidth,
		height = util.getParameterValue(args, "frameHeight") or L10n.defaults.frameHeight,
		align = util.getParameterValue(args, "frameAlign") or L10n.defaults.frameAlign,
		border = util.getParameterValue(args, "overlayBorder") or L10n.defaults.overlayBorder,
		horizontalAlignment = util.getParameterValue(args, "overlayHorizontalAlignment") or L10n.defaults.overlayHorizontalAlignment,
		horizontalOffset = util.getParameterValue(args, "overlayHorizontalOffset") or L10n.defaults.overlayHorizontalOffset,
		verticalAlignment = util.getParameterValue(args, "overlayVerticalAlignment") or L10n.defaults.overlayVerticalAlignment,
		verticalOffset = util.getParameterValue(args, "overlayVerticalOffset") or L10n.defaults.overlayVerticalOffset,
		isThumbnail = isThumbnail,
		caption = util.getParameterValue(args, "text") or L10n.defaults.text
	}
	return make.overlayHtml(overlayMap, basemap, overlayOptions)
end

return p