Moduł:Koordynaty

Z Encyklopedia Warmii i Mazur
Dokumentacja dla tego modułu może zostać utworzona pod nazwą Moduł:Koordynaty/opis
local m = {}

local geoformatdata = {
	supportedFormats = {
		{ prec = "10st", precision = 10.00000000000000000000, dms = false, secondsFormat = nil,      format = "%0.0f%s" },
		{ prec = "st",   precision =  1.00000000000000000000, dms = false, secondsFormat = nil,      format = "%0.0f%s" },
		{ prec = "1",    precision =  0.10000000000000000000, dms = false, secondsFormat = nil,      format = "%0.1f%s" },
		{ prec = "min",  precision =  0.01666666666666670000, dms = true,  secondsFormat = "%02.0f", format = "%0.0f%s%02.0f%s" },
		{ prec = "2",    precision =  0.01000000000000000000, dms = false, secondsFormat = nil,      format = "%0.2f%s" },
		{ prec = "3",    precision =  0.00100000000000000000, dms = false, secondsFormat = nil,      format = "%0.3f%s" },
		{ prec = "sek",  precision =  0.00027777777777777800, dms = true,  secondsFormat = "%02.0f", format = "%0.0f%s%02.0f%s%02.0f%s" },
		{ prec = "4",    precision =  0.00010000000000000000, dms = false, secondsFormat = nil,      format = "%0.4f%s" },
		{ prec = "sek+", precision =  0.00002777777777777780, dms = true,  secondsFormat = "%04.1f", format = "%0.0f%s%02.0f%s%04.1f%s" },
		{ prec = "5",    precision =  0.00001000000000000000, dms = false, secondsFormat = nil,      format = "%0.5f%s" },
		{ prec = "sek2", precision =  0.00000277777777777778, dms = true,  secondsFormat = "%05.2f", format = "%0.0f%s%02.0f%s%05.2f%s" },
		{ prec = "6",    precision =  0.00000100000000000000, dms = false, secondsFormat = nil,      format = "%0.6f%s" },
		{ prec = "sek3", precision =  0.00000027777777777778, dms = true,  secondsFormat = "%06.3f", format = "%0.0f%s%02.0f%s%06.3f%s" },
		{ prec = "7",    precision =  0.00000010000000000000, dms = false, secondsFormat = nil,      format = "%0.7f%s" },
		{ prec = "sek4", precision =  0.00000002777777777778, dms = true,  secondsFormat = "%07.4f", format = "%0.0f%s%02.0f%s%07.4f%s" },
	},

	displayGlobes = {
		earth     = "EW",
		moon      = "EW",
		mercury   = "W",
		mars      = "W",
		phobos    = "W",
		deimos    = "W",
		ganymede  = "W",
		callisto  = "W",
		io        = "W",
		europa    = "W",
		mimas     = "W",
		enceladus = "W",
		tethys    = "W",
		dione     = "W",
		rhea      = "W",
		titan     = "W",
		lapetus   = "W",
		phoebe    = "W",
		venus     = "E",
		ceres     = "E",
		vesta     = "E",
		miranda   = "E",
		ariel     = "E",
		umbriel   = "E",
		titania   = "E",
		oberon    = "E",
		triton    = "E",
		pluto     = "E",
	},

	latitudeLinkMarkers   = { degree="_", minute="_", second="_", positivePrefix="", positiveSuffix="N", negativePrefix="", negativeSuffix="S", },
	longitudeLinkMarkers  = { degree="_", minute="_", second="_", positivePrefix="", positiveSuffix="E", negativePrefix="", negativeSuffix="W", },
	latitudeGlobeMarkers  = { degree="°", minute="′", second="″", positivePrefix="", positiveSuffix="N", negativePrefix="", negativeSuffix="S", },
	longitudeGlobeMarkers = { degree="°", minute="′", second="″", positivePrefix="", positiveSuffix="E", negativePrefix="", negativeSuffix="W", },

	displayDecimalSeparator = ",",
	coordinatesSeparator = "\194\160",
	topPrefix = "Na mapach: ",
	documentationSubpage = "opis",

	geohack_link = "//tools.wmflabs.org/geohack/geohack.php?language=pl&pagename=%s&params=%s",
	geohack_hint = "Mapy, zdjęcia satelitarne i inne informacje dotyczące miejsca o współrzędnych geograficznych %s %s",

	-- template API data
	apiTemplateName = "szablon",
	apiMicroName = "mikro",
	apiAutoName = "auto",
	apiLatitude = "szerokość",
	apiLongitude = "długość",

	argLocation = "umieść",
	valLocationTop = "na górze",
	valLocationInline = "w tekście",
	valLocationTopAndInline = "w tekście i na górze",

	argPrecision = "dokładność",
	valPrecisionAutoDecimal = "dziesiętnie",
	valPrecisionAutoDMS = "kątowo",

	argLink = "linkuj",
	valLinkYes = "tak",
	valLinkNo = "nie",

	argName = "nazwa",
	
	argGlobe = "glob", -- for 'auto'

	-- categories
	errorCategory = "",

	-- error messages
	errorTooManyPositionalArguments = "Za dużo parametrów", 
	errorExpectedIntegerDegree = "Oczekiwana liczba stopni bez kropki dziesiętnej jeśli podawane są minuty (%s°%s')",
	errorInvalidMinutes = "Wartość minut jest nieprawidłowa (%s°%s')",
	errorExpectedIntegerMinutes = "Oczekiwana liczba minut bez kropki dziesiętnej jeśli podawane są sekundy (%s°%s'%s”)",
	errorInvalidSeconds = "Wartość sekund jest nieprawidłowa (%s°%s'%s”)",
	errorInvalidPositionalArguments = "Nieprawidłowe parametry",
	errorExpectedNonNegativeLatitude = "Oczekiwana nieujemna wartość szerokości geograficznej: %f",
	errorLatitudeOutOfRange = "Przekroczony zakres szerokości geograficznej (%f)",
	errorExpectedNonNegativeLongitude = "Oczekiwana nieujemna wartość długości geograficznej: %f",
	errorLongitudeOutOfRange = "Przekroczony zakres długości geograficznej (%f)",
	errorUnrecognizedLinkOption = "Niedozwolona wartość parametru ''link'': %s",
	errorUnrecognizedLocationOption = "Niedozwolona wartość parametru ''umieść'': %s",
}

local function create()

	-- initialize default data
	local result = {
		latitude  = 0,
		longitude = 0,
		precision = 1,
		params    = nil,
		inline    = false,
		top       = false,
		link      = true,
	}

	function result:parseCoordinates(args)

		local function isInt(s)
			-- up to 3 digits is enough for coordinates
			return s:match"^-?%d%d?%d?$"
		end

		local lang = mw.getContentLanguage()

		local function parseTypes()
			local types = {}
			for i = 1, 9 do
				local arg = mw.text.trim(args[i] or "")
				if #arg==0 then
					table.insert(types, "_")
				elseif arg == "N" or arg=="E" or arg=="S" or arg=="W" then
					table.insert(types, arg)
				elseif lang:parseFormattedNumber(arg) then
					local scientific = arg:match"[eE]"
					table.insert(types, scientific and "X" or "#")
				else
					table.insert(types, "X")
				end
			end
 
			return table.concat(types, "")
		end
 
		local function calculateDecimalPrecision(s)
			local s1 = string.gsub(s,"%d","0")
			local s2 = string.gsub(s1,"^-","0")
			local s3 = string.gsub(s2,"0$","1")
			local result = lang:parseFormattedNumber(s3)
			return result > 0 and result or 1.0
		end

		local function selectAutoPrecision(p1, p2)
			local dms = nil
			if (args[geoformatdata.argPrecision] == geoformatdata.valPrecisionAutoDecimal)  then
				dms = false
			elseif not args[geoformatdata.argPrecision] or (args[geoformatdata.argPrecision] == geoformatdata.valPrecisionAutoDMS) then
				dms = true
			else
				-- precision is selected explicit in the parameter
				return
			end

			-- select automatic precision
			local precision = p1 < p2 and p1 or p2
 
			-- find best DMS or decimal precision
			if precision < 1 then
				local eps = precision / 1024
				for i,v in ipairs(geoformatdata.supportedFormats) do
					if (v.dms == dms) and ((v.precision - precision) < eps) then
						precision = v.precision
						break
					end
				end
			end

			self.precision = precision
		end
		
		local function parseAngle(index, extra)
 
			local degree = mw.text.trim(args[index])
			local result = lang:parseFormattedNumber(degree)
			if extra == 0 then
				return true, result, calculateDecimalPrecision(degree)
			end
 
			local minutes = mw.text.trim(args[index+1])
			if not isInt(degree) then
				return false, string.format(geoformatdata.errorExpectedIntegerDegree, degree, minutes)
			end
 
			local precision = isInt(minutes) and 0.01666666666666670000 or 0.00027777777777777800
 
			local value = lang:parseFormattedNumber(minutes)
			if value < 0 or value >= 60 then
				return false, string.format(geoformatdata.errorInvalidMinutes, degree, minutes)
			end
 
			if result < 0 then
				result = result * 60 - value
			else
				result = result * 60 + value
			end
 
			if extra == 1 then
				return true, result / 60, precision
			end
 
			local seconds = mw.text.trim(args[index+2])
			if not isInt(minutes) then
				return false, string.format(geoformatdata.errorExpectedIntegerMinutes, degree, minutes, seconds)
			end
 
			precision = 0.00027777777777777800 * calculateDecimalPrecision(seconds)
			local value = lang:parseFormattedNumber(seconds)
			if value < 0 or value >= 60 then
				return false, string.format(geoformatdata.errorInvalidSeconds, degree, minutes, seconds)
			end
 
			if result < 0 then
				result = result * 60 - value
			else
				result = result * 60 + value
			end
 
			return true, result / 3600, precision
		end
		
		local function analyzeAngle(degree, minutes, seconds)
			local result = lang:parseFormattedNumber(degree)
			if not result then
				return false, geoformatdata.errorInvalidPositionalArguments
			end
 
			if not string.match(degree, "^%d+$") then
				if (#minutes > 0) or (#seconds > 0) then
					-- expected empty minutes and empty seconds if float degree is given
					return false, geoformatdata.errorInvalidPositionalArguments
				end
 
				return true, result, calculateDecimalPrecision(degree)
			end
 
			if #minutes == 0 then
				if #seconds > 0 then
					-- expected empty seconds if minute is not given
					return false, geoformatdata.errorInvalidPositionalArguments
				end
			
				return true, result, calculateDecimalPrecision(degree)
			end
 
			local minute = lang:parseFormattedNumber(minutes)
			if not minute or (minute >= 60) then
				return false, string.format(geoformatdata.errorInvalidMinutes, degree, minutes)
			end
 
			result = result * 60 + minute
			if not string.match(minutes, "^%d+$") then
				if #seconds > 0 then
					return false, string.format(geoformatdata.errorExpectedIntegerMinutes, degree, minutes, seconds)
				end

				return true, result/60, 0.00027777777777777800
			end
 
			if #seconds == 0 then
				return true, result/60, 0.01666666666666670000
			end
 
			local second = lang:parseFormattedNumber(seconds)
			if not second or (second >= 60) then
				return false, string.format(geoformatdata.errorInvalidSeconds, degree, minutes, seconds)
			end

			result = result*60 + second
			return true, result/3600, calculateDecimalPrecision(seconds)*0.00027777777777777800
		end
 
		assert(args, "Missing template arguments")
		if not args[1] then
			-- display nothing if no positional arguments are provided
			return false, nil
		end
 
		if args[10] then
			return false, geoformatdata.errorTooManyPositionalArguments
		end

		local types = parseTypes()

		local function parseSimpleText()
			local arg = mw.text.trim(args[1])
			if types == "XX_______" then
				self.params = mw.text.trim(args[2])
			end

			local d1, m1, s1, h1, d2, m2, s2, h2 = mw.ustring.match(arg, "^([0-9,.]+)[°_]?%s*([0-9,.]*)['′_]?%s*([0-9,.]*)[\"″_]?%s*([NSEW])[,;]?%s+([0-9,.]+)[°_]?%s*([0-9,.]*)['′_]?%s*([0-9,.]*)[\"″_]?%s*([EWNS])$")
			if d1 then
				if (((h1 == "N") or (h1 == "S")) and ((h2 == "N") or (h2 == "S"))) or (((h1 == "E") or (h1 == "W")) and ((h2 == "E") or (h2 == "W"))) then
					return geoformatdata.errorInvalidPositionalArguments
				end

				local status1, v1, p1 = analyzeAngle(d1, m1, s1)
				if not status1 then
					return v1
				end

				local status2, v2, p2 = analyzeAngle(d2, m2, s2)
				if not status2 then
					return v2
				end

				if (h1 == "S") or (h1 == "W") then
					v1 = -v1;
				end
				if (h2 == "S") or (h2 == "W") then
					v2 = -v2;
				end

				self.latitude  = ((h1 == "N") or (h1 == "S")) and v1 or v2
				self.longitude = ((h1 == "E") or (h1 == "W")) and v1 or v2
				selectAutoPrecision(p1, p2)
				return nil
			end

			local lat, lon = string.match(arg, "^(-?[0-9.,]+)%s+(-?[0-9.,]+)$")
			if lat then
				local latitude = lang:parseFormattedNumber(lat)
				local longitude = lang:parseFormattedNumber(lon)
				if latitude and longitude then
					self.latitude = latitude
					self.longitude = longitude
					selectAutoPrecision(calculateDecimalPrecision(lat), calculateDecimalPrecision(lon))
					return nil
				end
			end

			return geoformatdata.errorInvalidPositionalArguments
		end

		if (types == "X________") or (types == "XX_______") then
			local errorMessage = parseSimpleText()
			if errorMessage then
				return false, errorMessage
			end
		else
			local mapping = mw.loadData("Module:Koordynaty/parserData")[types]
			if not mapping then
				return false, geoformatdata.errorInvalidPositionalArguments
			end

			if mapping[7] ~= 0 then
				self.params = mw.text.trim(args[mapping[7]])
			end

			local status1, latitude, latPrecision = parseAngle(mapping[1], mapping[2])
			if not status1 then
				return false, latitude
			end

			if mapping[3] ~= 0 then
				assert(mapping[3] == 1 or mapping[3] == -1, "Invalid adjust mode: " .. mapping[3]);
				if latitude < 0 then
					return false, string.format(geoformatdata.errorExpectedNonNegativeLatitude, latitude)
				end
				latitude = mapping[3] * latitude
			end

			local status2, longitude, lonPrecision = parseAngle(mapping[4], mapping[5])
			if not status2 then
				return false, longitude
			end

			if mapping[6] ~= 0 then
				assert(mapping[6] == 1 or mapping[6] == -1, "Invalid adjust mode: " .. mapping[6]);
				if longitude < 0 then
					return false, string.format(geoformatdata.errorExpectedNonNegativeLongitude, longitude)
				end
				longitude = mapping[6] * longitude
			end

			self.latitude  = latitude
			self.longitude = longitude
			selectAutoPrecision(latPrecision, lonPrecision)
		end

		if self.latitude < -90 or self.latitude > 90 then
			return false, string.format(geoformatdata.errorLatitudeOutOfRange, self.latitude)
		end
 
		if self.longitude < -360 or self.longitude > 360 then
			return false, string.format(geoformatdata.errorLongitudeOutOfRange, self.longitude)
		end
		
		return true, nil
	end

	function result:normalize()
		assert(self,"Did you use '.' instead of ':' while calling the function?")
		local mode = false
		if self.params then
			for i, v in ipairs(mw.text.split( self.params, '_', true )) do
				if mode then
					-- more than one globe, display as given
					return
				end
				
				local globe = string.match(v, "^globe:(%a+)$")
				if globe then
					mode = geoformatdata.displayGlobes[string.lower(globe)]
					if not mode then
						-- unrecognized display as given
						return
					end
				end
			end
		end

		if mode == "?" then
			-- unrecognized left as given
		elseif mode == "W" then
			if self.longitude > 0 then
				self.longitude = self.longitude - 360
			end
		elseif mode == "E" then
			if self.longitude < 0 then
				self.longitude = self.longitude + 360
			end
		elseif self.longitude < -180 then
			self.longitude = self.longitude + 360
		elseif self.longitude > 180 then
			self.longitude = self.longitude - 360
		end
	end

	function result:parseOptions(args)
		-- TODO process notation in conjuction with precision
		local precision = args[geoformatdata.argPrecision]
		if precision and (precision ~= geoformatdata.valPrecisionAutoDecimal) and (precision ~= geoformatdata.valPrecisionAutoDMS) then
			self.precision = precision
		end

		self.name = args[geoformatdata.argName]

		local link = args[geoformatdata.argLink]
		if link == geoformatdata.valLinkYes then
			self.link = true
		elseif link == geoformatdata.valLinkNo then
			self.link = false
		elseif link then
			return false, string.format(geoformatdata.errorUnrecognizedLinkOption, link)
		else -- default is yes
			self.link = true
		end

		local location = args[geoformatdata.argLocation]
		if location == geoformatdata.valLocationTop then
			self.top = true
			self.inline = false
		elseif location == geoformatdata.valLocationInline then
			self.top = false
			self.inline = true
		elseif location == geoformatdata.valLocationTopAndInline then
			self.top = true
			self.inline = true
		elseif location then
			return false, string.format(geoformatdata.errorUnrecognizedLocationOption, location)
		elseif mw.title.getCurrentTitle().isTalkPage then
			-- an exception for talk pages
			self.top = false
			self.inline = true
		else -- default if not given
			self.top = true
			self.inline = false
		end

		return true, nil
	end
 
	function result:format()
		
		local function selectFormat(precision)
			local supportedFormats = geoformatdata.supportedFormats
			local precisionType = type(precision)
			if precisionType == "string" then
				-- find wikipedia template precision
				for i, v in ipairs(supportedFormats) do
					if (precision == v.prec) then
						return true, v
					end
				end
			elseif precisionType == "number" then
				-- find wikidata precision
				for i, v in ipairs(supportedFormats) do
					local prec = v.precision
					local eps = prec / 64
					local minPrec = prec - eps
					local maxPrec = prec + eps
					if (minPrec < precision) and (precision < maxPrec) then
						return true, v
					end
				end
			end

			-- use the last one with highest precision
			return false, supportedFormats[#supportedFormats]
		end

		local function formatAngle(value, format, markers, decimalSeparator)
			assert(type(value) == "number")
			local prefix = value < 0 and markers.negativePrefix or markers.positivePrefix
			local suffix = value < 0 and markers.negativeSuffix or markers.positiveSuffix

			value = math.abs(value)

			local result = nil

			if not format.dms then
				-- format decimal value
				if format.precision > 1 then
					-- round the value
					value = math.floor(value / format.precision) * format.precision
				end

				result = string.format(format.format, value, markers.degree)
			else
				-- format dms value
				local angle   = math.floor(value)
				local minutes = math.floor((value - angle) * 60)
				local seconds = tonumber(string.format(format.secondsFormat, (value - angle) * 3600 - minutes * 60))

				-- fix rounded seconds
				if seconds == 60 then
					minutes = minutes + 1
					seconds = 0
					if minutes == 60 then
						angle = angle + 1
						minutes = 0
					end
				end

				if format.precision > 0.01 then
					-- round the value
					if seconds >= 30 then
						minutes = minutes + 1
					end
					seconds = 0
					if minutes == 60 then
						angle = angle + 1
						minutes = 0
					end
				end

				result = string.format(format.format, angle, markers.degree, minutes, markers.minute, seconds, markers.second)
			end

			if decimalSeparator then
				result = string.gsub(result, "%.", decimalSeparator)
			end

			return prefix .. result .. suffix
		end

		local function formatDegree(value, decimalSeparator)
			local result = string.format("%f", value)

			if decimalSeparator then
				result = string.gsub(result, "%.", decimalSeparator)
			end
		
			return result
		end

		local function fullpagenamee()
			local title = mw.title.getCurrentTitle()
			return title.namespace == 0
				and title:partialUrl()
				or  title.nsText .. ":" .. title:partialUrl()
		end

		local status, format = selectFormat(self.precision)
		assert(format)

		local prettyLatitude  = formatAngle(self.latitude,  format, geoformatdata.latitudeGlobeMarkers,  geoformatdata.displayDecimalSeparator)
		local prettyLongitude = formatAngle(self.longitude, format, geoformatdata.longitudeGlobeMarkers, geoformatdata.displayDecimalSeparator)

		if not self.link then
			return mw.text.nowiki(prettyLatitude .. geoformatdata.coordinatesSeparator .. prettyLongitude)
		end

		local params = {
			formatAngle(self.latitude,  format, geoformatdata.latitudeLinkMarkers),
			formatAngle(self.longitude, format, geoformatdata.longitudeLinkMarkers),
		}

		if self.params then
			table.insert(params, self.params)
		end

		local degreeLatitude  = formatDegree(self.latitude,  geoformatdata.displayDecimalSeparator)
		local degreeLongitude = formatDegree(self.longitude, geoformatdata.displayDecimalSeparator)

		local geohack_link = string.format(geoformatdata.geohack_link, fullpagenamee(), table.concat(params,"_"))
		if self.name then
			geohack_link = geohack_link .. "&title=" .. mw.uri.encode(self.name)
		end

		local pretty_hint = string.format(geoformatdata.geohack_hint, prettyLatitude, prettyLongitude)
		local degree_hint = string.format(geoformatdata.geohack_hint, degreeLatitude, degreeLongitude)
		local separator = mw.text.nowiki(geoformatdata.coordinatesSeparator)

		local node = false
		local result = mw.html.create():wikitext("[", geohack_link, " ")
		node = result:tag("span"):attr("class", "geo-default")
			:tag("span"):attr("class", "geo-dms"):attr("title", mw.text.nowiki(pretty_hint))
		node:tag("span"):attr("class", "latitude"):wikitext(mw.text.nowiki(prettyLatitude))
		node:wikitext(separator)
		node:tag("span"):attr("class", "longitude"):wikitext(mw.text.nowiki(prettyLongitude))
		result:tag("span"):attr("class", "geo-multi-punct"):wikitext("/")
		node = result:tag("span"):attr("class", "geo-nondefault")
			:tag("span"):attr("class", "geo-dms"):attr("title", mw.text.nowiki(degree_hint))
		node:tag("span"):attr("class", "latitude"):wikitext(mw.text.nowiki(degreeLatitude))
		node:wikitext(separator)
		node:tag("span"):attr("class", "longitude"):wikitext(mw.text.nowiki(degreeLongitude))
		result:wikitext("]")
		
		return tostring(result)
	end

	function result:display(inlinePrefix)
		local text = self:format()

		if not self.top and not self.inline then
			return text
		end

		local result = mw.html.create()

		if self.top then
			local indicator = mw.html.create("span")
				:attr("id", "coordinates")
				:attr("class", "coordinates plainlinks")
				:wikitext(geoformatdata.topPrefix, text)
			result:wikitext(mw.getCurrentFrame():extensionTag{name = 'indicator', content = tostring(indicator), args = { name='coordinates' } } or "")
		end

		if self.inline then
			result:wikitext(inlinePrefix or "")
				:tag("span")
					:attr("class", self.top and "coordinates inline inline-and-top plainlinks" or "coordinates inline plainlinks")
					:wikitext(text)
		end

		return tostring(result)
	end

	function result:extensionGeoData()
		local params = {}
 
		local title = mw.title.getCurrentTitle()
		if self.top and not title.isTalkPage and (title.subpageText ~= geoformatdata.documentationSubpage) then
			table.insert(params, "primary")
		end

		if self.latitude >= 0 then
			table.insert(params, string.format("%f", self.latitude))
			table.insert(params, "N")
		else
			table.insert(params, string.format("%f", -self.latitude))
			table.insert(params, "S")
		end

		if mode == "W" then
			if self.longitude > 0 then
				table.insert(params, string.format("%f", 360-self.longitude))
			else
				table.insert(params, string.format("%f", -self.longitude))
			end
			table.insert(params, "W")
		elseif mode == "E" then
			if self.longitude >= 0 then
				table.insert(params, string.format("%f", self.longitude))
			else
				table.insert(params, string.format("%f", 360+self.longitude))
			end
			table.insert(params, "E")
		elseif self.longitude >= 0 then
			table.insert(params, string.format("%f", self.longitude))
			table.insert(params, "E")
		else
			table.insert(params, string.format("%f", -self.longitude))
			table.insert(params, "W")
		end

		if self.params then
			table.insert(params, self.params)
		end

		if self.name then
			params.name = self.name
		end
		
		-- https://bugzilla.wikimedia.org/show_bug.cgi?id=50863 RESOLVED
		return mw.getCurrentFrame():callParserFunction("#coordinates", params) or ""
	end

	return result;
end

local function showError(message, args)
	if not message then
		return geoformatdata.errorCategory
	end

	local result = {}
	table.insert(result, "<span style=\"color:red\">")
	assert(type(message) == "string", "Expected string message")
	table.insert(result, message)
	local i = 1
	while args[i] do
		if i == 1 then
			table.insert(result, ": {")
		else
			table.insert(result, "&#x7C;")
		end
		
		table.insert(result, args[i])
		i = i + 1
	end
	if i > 1 then
		table.insert(result, "}")
	end

	table.insert(result, "</span>")

	if mw.title.getCurrentTitle().namespace == 0 then
		table.insert(result, geoformatdata.errorCategory)
	end

	return table.concat(result, "")
end

local function parse(frame, link)
	local coordinates = create()
	local args = frame.args
	local status, errorMessage = coordinates:parseCoordinates(args)
	if not status then
		return showError(errorMessage, args)
	end

	local status, errorMessage = coordinates:parseOptions(args)
	if not status then
		return showError(errorMessage, args)
	end

	coordinates.link = link
	coordinates:normalize()
	return coordinates:display()..coordinates:extensionGeoData()
end

function m.format(frame)
	return parse(frame, false)
end

function m.link(frame)
	return parse(frame, true)
end

m[geoformatdata.apiTemplateName] = function (frame)
	local coordinates = create()
	local args = frame:getParent().args
	local status, errorMessage = coordinates:parseCoordinates(args)
	if not status then
		return showError(errorMessage, args)
	end

	local status, errorMessage = coordinates:parseOptions(args)
	if not status then
		return showError(errorMessage, args)
	end

	coordinates:normalize()
	return coordinates:display()..coordinates:extensionGeoData()
end

m[geoformatdata.apiMicroName] = function (frame)
	local coordinates = create()
	local args = frame:getParent().args
	local status, errorMessage = coordinates:parseCoordinates(args)
	if not status then
		return showError(errorMessage, args)
	end

	-- the only available option
	coordinates.name = args[geoformatdata.argName]

	-- options are implied in micro variant
	if coordinates.precision > 0.00027777777777777800 then
		coordinates.precision = 0.00027777777777777800 -- seconds
	elseif coordinates.precision < 0.00002777777777777780 then
		coordinates.precision = 0.00002777777777777780 -- seconds with one decimal digit
	end

	if not coordinates.params then
		coordinates.params	= "scale:5000" -- bonus
	end

	coordinates.inline = true
	coordinates.top	   = false
	coordinates.link   = true

	-- simple link without geodata extension
	coordinates:normalize()
	return coordinates:display()
end

m[geoformatdata.apiAutoName] = function(frame)

	local entity = mw.wikibase.getEntityObject() if not entity then mw.log("missing entity") return nil end -- missing entity
	local claims = entity.claims if not claims then mw.log("missing claims") return nil end -- missing claims

	local function selectProperty(pid)
		local prop = claims[pid] if not prop then return false end -- missing property

		-- load preferred statements
		local result = {}
		for _, v in ipairs(prop) do
			if v.rank == "preferred" then
				table.insert(result, v)
			end
		end

		if #result ~= 0 then return true, result end

		for _, v in  ipairs(prop) do
			if v.rank == "normal" then
				table.insert(result, v)
			end
		end

		if #result ~= 0 then return true, result end

		mw.log("empty property table")
		return false -- empty property table
	end

	local function selectValue(prop, expectedType)
		if not prop then return false end
		if prop.type ~= "statement" then return false end
		local snak = prop.mainsnak
		if not snak or snak.snaktype ~= "value" then return false end
		local datavalue = snak.datavalue
		if not datavalue or datavalue.type ~= expectedType then return false end
		local value = datavalue.value
		if not value then return false end
		return true, value
	end

	function selectGlobe(globe)
		local globes = {
			["http://www.wikidata.org/entity/Q2"]   = { symbol="[[Plik:Geographylogo.svg|20px|alt=Ziemia|link=Ziemia]] ", link="" },
			["http://www.wikidata.org/entity/Q405"] = { symbol="[[Plik:Nuvola apps kmoon left.png|15px|alt=Księżyc|link=Księżyc]] ", link="globe:Moon" },
			["http://www.wikidata.org/entity/Q111"] = { symbol="[[Plik:Blue Mars symbol.svg|15px|alt=Mars|link=Mars]] ", link="globe:Mars" },
			["http://www.wikidata.org/entity/Q308"] = { symbol="[[Plik:Blue Mercury symbol.svg|12px|alt=Merkury|link=Merkury]] ", link="globe:Mercury" },
			["http://www.wikidata.org/entity/Q313"] = { symbol="[[Plik:Symbol venus blue.svg|12px|alt=Wenus|link=Wenus]] ", link="globe:Venus" },
		}

		mw.log("GLOBE: "..(globe or "http://www.wikidata.org/entity/Q2"))
		return globes[globe or "http://www.wikidata.org/entity/Q2"]
	end

	function selectType()
		local types = {
			unknownType = "type:city",
			{
				property = "P300",
				[150093] = "type:adm1st",
				[247073] = "type:adm2nd",
				[925381] = "type:adm2nd",
				[3504085] = "type:adm3rd",
				[3491915] = "type:adm3rd",
				[2616791] = "type:adm3rd",
			},
			{
				property = "P31",
				[515]  = "type:city",
				[6256] = "type:country",
				[5107] = "type:satellite",
				[165]  = "type:satellite",
			},
		}

		for _, pset in ipairs(types) do
			local status, classes = selectProperty(pset.property)
			if status then
				for _, p in ipairs(classes) do
					local status2, v = selectValue(p, "wikibase-entityid")
					if status2 and v["entity-type"] == "item" then
						local result = pset[v["numeric-id"]]
						if result then return result end
					end
				end
			end
		end
	
		return types.unknownType
	end

	local status1, coordinates = selectProperty("P" .. (frame.args[1] or "625")) if not status1 then return nil end
	local status2, autocoords = selectValue(coordinates[1], "globecoordinate") if not status2 then return nil end
	local argGlobe  = frame.args[geoformatdata.argGlobe]
	local globe = argGlobe == "" and { symbol="", link="" } or selectGlobe(argGlobe or autocoords.globe) or { symbol="", link=false }
	if not globe.link then return nil end -- not supported globe

	local params = {
		selectType(),
	}
	if #globe.link > 0 then
		table.insert(params, globe.link)
	end

	local coords = create()
	coords:parseOptions(frame.args)
	coords.latitude = autocoords.latitude
	coords.longitude = autocoords.longitude
	coords.precision = autocoords.precision or 1
	coords.params = table.concat(params,"_")
 
	coords:normalize()
	return coords:display(globe.symbol)
end

m[geoformatdata.apiLatitude] = function (frame)
	local coordinates = create()
	local status = coordinates:parseCoordinates(frame.args)
	return status and coordinates.latitude or ""
end

m[geoformatdata.apiLongitude] = function (frame)
	local coordinates = create()
	local status = coordinates:parseCoordinates(frame.args)
	return status and coordinates.longitude or ""
end

return m