Pōdź kaj inhalt

Moduł:Spōłrzyndne

Ze Wikipedia

Dokumentacja dla tego modułu może zostać utworzona pod nazwą Moduł:Spōłrzyndne/opis

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     = { mode = "EW", Q="Q2",    symbol="⨁", },
		moon      = { mode = "EW", Q="Q405",  symbol="☾", },
		mercury   = { mode = "W",  Q="Q308",  symbol="☿", },
		mars      = { mode = "W",  Q="Q111",  symbol="♂", },
		phobos    = { mode = "W",  Q="Q7547", },
		deimos    = { mode = "W",  Q="Q7548", },
		ganymede  = { mode = "W",  Q="Q3169", },
		callisto  = { mode = "W",  Q="Q3134", },
		io        = { mode = "W",  Q="Q3123", },
		europa    = { mode = "W",  Q="Q3143", },
		mimas     = { mode = "W",  Q="Q15034", },
		enceladus = { mode = "W",  Q="Q3303", },
		tethys    = { mode = "W",  Q="Q15047", },
		dione     = { mode = "W",  Q="Q15040", },
		rhea      = { mode = "W",  Q="Q15050", },
		titan     = { mode = "W",  Q="Q2565", },
		hyperion  = { mode = "?",  Q="Q15037", },
		iapetus   = { mode = "W",  Q="Q17958", },
		phoebe    = { mode = "W",  Q="Q17975", },
		venus     = { mode = "E",  Q="Q313",  symbol="♀", },
		ceres     = { mode = "E",  Q="Q596",  symbol="⚳", },
		vesta     = { mode = "E",  Q="Q3030", symbol="⚶", },
		miranda   = { mode = "E",  Q="Q3352", },
		ariel     = { mode = "E",  Q="Q3343", },
		umbriel   = { mode = "E",  Q="Q3338", },
		titania   = { mode = "E",  Q="Q3322", },
		oberon    = { mode = "E",  Q="Q3332", },
		triton    = { mode = "E",  Q="Q3359", },
		pluto     = { mode = "E",  Q="Q339",  symbol="♇", },
	},

	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 = "Spōłrzyndne: ",
	displayGlobesDefaultSymbol = "⌘",
	defaultSymbolSeparator = "\194\160",
	documentationSubpage = "ôpis",
	defaultWDPrecision = 0.00027777777777777800,
	defaultSizePrecision = 1.0,
	defaultDistanceScalingFactor = 6731000,

	geohack_root = "//tools.wmflabs.org/geohack/geohack.php?language=en",
	geohack_hint = "Karty, satelitarne fotki i inksze informacyje ô placu we tych spōłrzyndnych %s %s",

	-- template API data
	apiCoordinates = "spōłrzyndne",
	apiMapPoint = "pōnkt",

	apiCheckDistance = "Ôbocz dystans spōłrzyndnych",
	apiDistance = "Dystans",
	apiPrecisionRadius = "Prōmiyń akuratności",
	
	argScalingFactor = "mnożnik",
	argMaximumDistance = "dystans",
	argErrorMessage = "kōmunikat",

	wrappersCoordinates = "Muster:Spōłrzyndne",
	wrappersMapPoint = "Muster:Pōnkt na karcie",

	argCoordinatesCoordinates = 1,
	argCoordinatesGeohack = 2,

	argLocation = "wkludź",
	valLocationTop = "na wiyrchu",
	valLocationInline = "w tekście",
	valLocationTopAndInline = "w tekście i na wiyrchu",

	argPrecision = "akuratność",
	valPrecisionAutoDecimal = "dziesiytnie",
	valPrecisionAutoDMS = "kōntowo",

	argLink = "linkuj",
	valLinkYes = "ja",
	valLinkNo = "niy",
	valLinkGMS = "zgodliwie",

	argSymbol = "symbol",
	valSymbolYes = "ja",
	valSymbolNo = "niy",

	argName = "miano",
	
	-- apiMapPoint
	argMapPointCoordinates = 1,
	argMark = "znak",
	argMarkSize = "miara znaku",
	argDescription = "ôpis",
	argMapPointGeohack = "ôpcyje geohack",
	argDescriptionPosition = "pozycyjo",
	argAlt = "alt",

	defArgMark = "Red pog.svg",
	defArgMarkSize = 6,
	defArgGeohack = "type:city",

	mapPointMapping = {
		["Mars"] = "globe:Mars",
		["Księżyc"] = "globe:Moon",
		["Wenus"] = "globe:Venus",
		["Merkury"] = "globe:Mercury",
	},

	-- categories
	errorCategory = "[[Kategoryjo:Zajty ze felerami we parametrach mustrōw spōłrzyndnych]]",

	-- error messages
	errorInvalidMinutes = "Wielość minut je felerno (%s°%s')",
	errorExpectedIntegerMinutes = "Ôczekowano wielość minut bez kropki dziesiyntnyj eli podowane sōm sekōndy (%s°%s'%s”)",
	errorInvalidSeconds = "Wielość sekōnd je felerno (%s°%s'%s”)",
	errorInvalidPositionalArguments = "Felerne parametry",
	errorLatitudeOutOfRange = "Przekroczōny zakres szyrokości geograficznyj (%f)",
	errorLongitudeOutOfRange = "Przekroczōny zakres szyrokości geograficznyj (%f)",
	errorUnrecognizedLinkOption = "Niyprzizwolōno wartość parametru ''linkuj'': %s",
	errorUnrecognizedLocationOption = "Niyprzizwolōno wartość parametru ''wkludź'': %s",
	errorUnrecognizedPrecisionOption = "Niedozwolona wartość parametru ''akuratność'': %s",
	errorEmptySymbolOption = "Prōżny parametr ''symbol''",
	errorMissingCoordinates = "Niy mo spōłrzyndnych",
}

--------------------------------------------------------------------------------
-- Coordinates class methods
--------------------------------------------------------------------------------

local CoordinatesMetatable = {}
local CoordinatesMethodtable = {}

CoordinatesMetatable.__index = CoordinatesMethodtable

function CoordinatesMethodtable:parse(coordinates, params, displayPrecision)
	
	local lang = mw.getContentLanguage()

	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 (displayPrecision == geoformatdata.valPrecisionAutoDecimal)  then
			dms = false
		elseif not displayPrecision or (displayPrecision == 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 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

	if not coordinates or (#mw.text.trim(coordinates) <= 0) then
		return false, geoformatdata.errorMissingCoordinates
	end

	local function parseSimpleText()

		local d1, m1, s1, h1, d2, m2, s2, h2 = mw.ustring.match(coordinates, "^%s*([%d,%.]+)[°_]?%s*([%d,%.]*)['′_]?%s*([%d,%.]*)[\"″”_]?%s*([NSEW])[,;]?%s+([%d,%.]+)[°_]?%s*([%d,%.]*)['′_]?%s*([%d,%.]*)[\"″”_]?%s*([EWNS])%s*$")
		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(coordinates, "^%s*(-?[0-9%.,]+)%s+(-?[0-9%.,]+)%s*$")
		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

	local data = false
	if params then
		local p = mw.text.trim(params)
		if #p > 0 then
			self.params = p
			local trace = false
			for i, v in ipairs(mw.text.split(p, '_', true)) do
				local globe = string.match(v, "^globe:(%a+)$")
				if globe then
					if data then
						-- more than one globe, data undetermined
						trace = "undetermined"
						data = nil
						break
					end
					
					globe = string.lower(globe)
					data = geoformatdata.displayGlobes[globe]
					if not data then
						-- unrecognized data
						trace = "unrecognized"
						data = nil
						break
					else
						trace = globe
					end
				end
			end
		
			if trace then
				_ = mw.title.new("Module:Spōłrzyndne/globe:"..trace).id
			end
		end
	end

	if data and not displayPrecision then
		displayPrecision = data.Q == "Q2" and geoformatdata.valPrecisionAutoDMS or geoformatdata.valPrecisionAutoDecimal
	end
	
	self.displayData = data or geoformatdata.displayGlobes.earth

	local errorMessage = parseSimpleText()
	if errorMessage then
		return false, errorMessage
	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 CoordinatesMethodtable:normalize()
	assert(self,"Did you use '.' instead of ':' while calling the function?")
	local mode = false
	if self.displayData then
		mode = self.displayData.mode
	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 CoordinatesMethodtable:format()
	
	local function selectFormat(precision)
		local supportedFormats = geoformatdata.supportedFormats
		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 v
			end
		end

		-- use the last one with highest precision
		return 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 format = selectFormat(self.precision)
	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 linkLatitude = false
	local linkLongitude = false
	if self.link == "gms" then
		linkLatitude = formatAngle(self.latitude,  format, geoformatdata.latitudeLinkMarkers)
		linkLongitude = formatAngle(self.longitude, format, geoformatdata.longitudeLinkMarkers)
	end

	local geohack_link = self:geohack(fullpagenamee(), linkLatitude, linkLongitude)

	local degreeLatitude  = formatDegree(self.latitude,  geoformatdata.displayDecimalSeparator)
	local degreeLongitude = formatDegree(self.longitude, geoformatdata.displayDecimalSeparator)
	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 CoordinatesMethodtable:display(inlinePrefix)
	
	local text = self:format{}

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

	local function drawGlobeSymbol(displayData)
		local symbol = displayData.symbol or geoformatdata.displayGlobesDefaultSymbol
		if not displayData.Q then
			return symbol..geoformatdata.defaultSymbolSeparator
		end

		local link = mw.wikibase.sitelink(displayData.Q)
		if not link then
			return symbol..geoformatdata.defaultSymbolSeparator
		end
		
		return "[["..link.."|"..symbol.."]]"..geoformatdata.defaultSymbolSeparator
	end

	if inlinePrefix == nil then
		if self.symbol == false then
			inlinePrefix = ""
		elseif self.symbol == true then
			inlinePrefix = drawGlobeSymbol(self.displayData) or ""
		elseif self.symbol then
			inlinePrefix = self.symbol
		elseif self.displayData.Q == "Q2" then
			-- !symbol & Q2
			inlinePrefix = ""
		else
			-- !symbol & !Q2
			inlinePrefix = drawGlobeSymbol(self.displayData) or ""
		end
	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, inlinePrefix or "", text)
		result:wikitext(mw.getCurrentFrame():extensionTag{name = 'indicator', content = tostring(indicator), args = { name='coordinates' } } or "")
	end

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

	return tostring(result)
end

function CoordinatesMethodtable: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

function CoordinatesMethodtable:geohack(pagename, linkLatitude, linkLongitude)
	local result = {}
	table.insert(result, geoformatdata.geohack_root)
	if pagename then
		table.insert(result, "&pagename=")
		table.insert(result, pagename)
	end
	
	table.insert(result, "&params=")
	if linkLatitude and linkLongitude then
		table.insert(result, linkLatitude)
	elseif self.latitude < 0 then
		table.insert(result, tostring(-self.latitude))
		table.insert(result, "_S")
	else
		table.insert(result, tostring(self.latitude))
		table.insert(result, "_N")
	end
	
	table.insert(result, "_")
	if linkLatitude and linkLongitude then
		table.insert(result, linkLongitude)
	elseif self.longitude < 0 then
		table.insert(result, tostring(-self.longitude))
		table.insert(result, "_W")
	else
		table.insert(result, tostring(self.longitude))
		table.insert(result, "_E")
	end
	
	if self.params then
		table.insert(result, "_")
		table.insert(result, self.params)
	end

	if self.name then
		table.insert(result, "&title=")
		table.insert(result, mw.uri.encode(self.name))
	end
	
	return table.concat(result)
end

local function create()
	-- initialize default data
	local self = {
		latitude  = 0,
		longitude = 0,
		precision = 1,
		params    = nil,
		inline    = false,
		top       = false,
		link      = true,
	}
	setmetatable(self, CoordinatesMetatable)
	return self;
end

--------------------------------------------------------------------------------
-- utilities
--------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------
-- Minimalistic Wikidata support
--------------------------------------------------------------------------------

local function selectProperty(claims, 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

	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

local function wd(property, argGlobe)

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

	function selectGlobe(globe)
		-- the most often case
		if not globe or (globe == "http://www.wikidata.org/entity/Q2") then
			return { symbol=geoformatdata.displayGlobes.earth.symbol, link="" }
		end
		
		for k, v in pairs(geoformatdata.displayGlobes) do
			if globe == mw.wikibase.getEntityUrl(v.Q) then
				return { link="globe:"..k, data=v }
			end
		end

		return nil
	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(claims, 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(claims, property) if not status1 then return nil end
	local status2, autocoords = selectValue(coordinates[1], "globecoordinate") if not status2 then return nil end
	local globe = argGlobe == "" and { symbol="", link="", data=false } or selectGlobe(argGlobe or autocoords.globe) or { symbol="", link=false, data=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 result = {
		latitude = autocoords.latitude,
		longitude = autocoords.longitude,
		precision = autocoords.precision or geoformatdata.defaultWDPrecision,
		params = table.concat(params,"_"),
		displayData = data or geoformatdata.displayGlobes.earth,
		globeSymbol = globe.symbol,
	}

	return result
end

local function parseDisplayPrecision(coordinates, displayPrecision)
	
	local function adjustPrecision(dms)
		if not coordinates.precision or (coordinates.precision >= 1) then
			return
		end

		local eps = coordinates.precision / 1024
		for i, v in ipairs(geoformatdata.supportedFormats) do
			if (v.dms == dms) and ((v.precision - coordinates.precision) < eps) then
				coordinates.precision = v.precision
				break
			end
		end
	end

	local function findAndSetPrecision()		
		-- find wikipedia template precision
		for i, v in ipairs(geoformatdata.supportedFormats) do
			if displayPrecision == v.prec then
				coordinates.precision = v.precision
				return true
			end
		end
	end

	if displayPrecision == geoformatdata.valPrecisionAutoDMS then
		adjustPrecision(true)
	elseif displayPrecision == geoformatdata.valPrecisionAutoDecimal then
		adjustPrecision(false)
	elseif not findAndSetPrecision() then
		return false
	end

	return true
end

local function distance(A, B)
	-- [[Ortodroma]]
	-- <math>D = \operatorname{arc cos}((\sin \varphi_1 \sin \varphi_2)+(\cos \varphi_1 \cos \varphi_2 \cos \Delta\lambda)),</math>
	local phiA = math.pi * A.latitude / 180.0
	local phiB = math.pi * B.latitude / 180.0
	local delta = math.pi * (B.longitude - A.longitude) / 180.0
	return math.acos(math.sin(phiA)*math.sin(phiB) + math.cos(phiA)*math.cos(phiB)*math.cos(delta))
end

local function size(A)
	local precision = A.precision or geoformatdata.defaultSizePrecision
	local B = {}
	B.latitude = A.latitude < 0 and A.latitude + precision or A.latitude - precision
	B.longitude = A.longitude + precision
	return distance(A,B)
end

--------------------------------------------------------------------------------
-- public module methods
--------------------------------------------------------------------------------

return {

[geoformatdata.apiCoordinates] = function (frame)
	local args = require('Module:Arguments').getArgs(frame, {
		trim = false,
		removeBlanks = false,
		wrappers = geoformatdata.wrappersCoordinates,
	})
	local coords = args[geoformatdata.argCoordinatesCoordinates]
	local geohack = args[geoformatdata.argCoordinatesGeohack]
	local name = args[geoformatdata.argName]
	local location = args[geoformatdata.argLocation]
	local displayPrecision = args[geoformatdata.argPrecision]
	local link = args[geoformatdata.argLink]
	local symbol = args[geoformatdata.argSymbol]
	
	if symbol == geoformatdata.valSymbolYes then
		symbol = true
	elseif symbol == geoformatdata.valSymbolNo then
		symbol = false
	elseif symbol and (#symbol==0) then
		return showError(geoformatdata.errorEmptySymbolOption, {})
	end
	
	if not coords and not geohack and not name and not displayPrecision and not link and (location == geoformatdata.valLocationTop) and (symbol == nil) then
		local autocoords = wd("P625", false)
		if not autocoords then
			-- missing data in  WD
			return
		end
		
		local coordinates = create()
		coordinates.latitude = autocoords.latitude
		coordinates.longitude = autocoords.longitude
		coordinates.precision = autocoords.precision
		coordinates.params = autocoords.params
		coordinates.displayData = autocoords.displayData
		coordinates.inline = false
		coordinates.top	   = true
		coordinates.link   = true
		coordinates:normalize()
		return coordinates:display()..coordinates:extensionGeoData()
	end
	
	local coordinates = create()
	local status, errorMessage = coordinates:parse(coords, geohack, displayPrecision)
	if not status then
		return showError(errorMessage, args)
	end

	coordinates.symbol = symbol
	coordinates.name = name

	local full = location or displayPrecision or link or (symbol ~= nil) or (coordinates.displayData and (coordinates.displayData.Q ~= "Q2"))
	if full then
		
		if displayPrecision and not parseDisplayPrecision(coordinates, displayPrecision) then
			return showError(string.format(geoformatdata.errorUnrecognizedPrecisionOption, displayPrecision), {})
		end
		
		if link == geoformatdata.valLinkYes then
			coordinates.link = true
		elseif link == geoformatdata.valLinkNo then
			coordinates.link = false
		elseif link == geoformatdata.valLinkGMS then
			coordinates.link = "gms"
		elseif link then
			return showError(string.format(geoformatdata.errorUnrecognizedLinkOption, link), {})
		else -- default is "yes"
			coordinates.link = true
		end

		if location == geoformatdata.valLocationTop then
			coordinates.top = true
			coordinates.inline = false
		elseif location == geoformatdata.valLocationInline then
			coordinates.top = false
			coordinates.inline = true
		elseif location == geoformatdata.valLocationTopAndInline then
			coordinates.top = true
			coordinates.inline = true
		elseif location then
			return showError(string.format(geoformatdata.errorUnrecognizedLocationOption, location), {})
		else -- default if not given
			coordinates.top = false
			coordinates.inline = true
		end
	else -- micro
		-- 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
	end

	coordinates:normalize()
	local result = {}
	table.insert(result, coordinates:display())
	if full then
		table.insert(result, coordinates:extensionGeoData())
	end
	
	return table.concat(result)
end,

[geoformatdata.apiMapPoint] = function(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		trim = false,
		removeBlanks = false,
		wrappers = geoformatdata.wrappersMapPoint,
	})
	local coordinates = create()
	local description = args[geoformatdata.argDescription]
	local symbol = args[geoformatdata.argSymbol]

	geohack = geoformatdata.mapPointMapping[description] or args[geoformatdata.argMapPointGeohack]
	local status, errorMessage, fromWD = false, false, false
	if args[geoformatdata.argMapPointCoordinates] then
		status, errorMessage = coordinates:parse(args[geoformatdata.argMapPointCoordinates],geohack)
	else
		local autocoords = wd("P625", false)
		if not autocoords then
			-- missing data in WD
			return
		end

		coordinates.latitude = autocoords.latitude
		coordinates.longitude = autocoords.longitude
		coordinates.precision = autocoords.precision
		coordinates.params = autocoords.params or geohack
		coordinates.displayData = autocoords.displayData
		status = true
		fromWD = true
	end

	local point = {}
	if not status then
		point.error = showError(errorMessage, args)
	else
		coordinates:normalize()
		point.latitude = coordinates.latitude
		point.longitude = coordinates.longitude
		point.link = coordinates:geohack(false, false, false)
		if args.display then
			if symbol == geoformatdata.valSymbolYes then
				coordinates.symbol = true
			elseif symbol == geoformatdata.valSymbolNo then
				coordinates.symbol = false
			elseif symbol and (#symbol==0) then
				point.error = showError(geoformatdata.errorEmptySymbolOption, {})
			else
				coordinates.symbol = symbol
			end

			coordinates.top = mw.title.getCurrentTitle().namespace == 0
			coordinates.inline = true
			if fromWD and (args.display == "#coordinates") then
				point.display = coordinates:display()..coordinates:extensionGeoData()
			else
				point.display = coordinates:display()
			end
		end
	end

	point.mark = args[geoformatdata.argMark] or geoformatdata.defArgMark
	point.size = tonumber(args[geoformatdata.argMarkSize] or geoformatdata.defArgMarkSize)
	point.description = description
	point.position = args[geoformatdata.argDescriptionPosition]
	point.alt = args[geoformatdata.argAlt]
	if not coordinates.params then
		point.geohack = geoformatdata.defArgGeohack
	end
	
	return mw.text.jsonEncode(point)..","
end,

[geoformatdata.apiCheckDistance] = function(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		trim = false,
		removeBlanks = false,
	})
	local scalingFactor = tonumber(args[geoformatdata.argScalingFactor]) or 6731000
	local displayPrecision = args[geoformatdata.argPrecision]
	local maximumDistance = tonumber(args[geoformatdata.argMaximumDistance])
	local errorMessage = args[geoformatdata.argErrorMessage]
	local coords = args[geoformatdata.argCoordinatesCoordinates]
	if not errorMessage or not maximumDistance or not coords then
		-- nie ma nic do wyślwietlenia lub sprawdzenia
		mw.log("apiCheckDistance: nic nie ma")
		return
	end
	
	local A = create()
	local status, error
	status, error =  A:parse(coords, nil, displayPrecision)
	if not status then
		mw.logObject(error, "apiCheckDistance: parse error")
		return
	end
	
	if displayPrecision and not parseDisplayPrecision(A, display) then
		mw.logObject(error, "apiCheckDistance: parsePrecision error")
		return
	end
	
	local B = wd("P625", false)
	if not B then
		mw.logObject(B, "apiCheckDistance: missing data in WD")
		return
	end
	
	A.radius = scalingFactor * size(A)
	B.radius = scalingFactor * size(B)
	
	local distance = scalingFactor * distance(A,B)
	if distance <= maximumDistance then
		-- brak błędów
		return
	end
	
	-- zalogujmy co się da
	mw.logObject({A, WD = B, distance = distance}, "apiCheckDistance")
	
	-- parametry komunikatu
	local parameters =
	{
		distance = tostring(math.floor(distance + 0.5)),
		radiusA = tostring(math.floor(A.radius + 0.5)),
		radiusB = tostring(math.floor(B.radius + 0.5)),
	}
	local message, _ = string.gsub(errorMessage, "%(%(%((.-)%)%)%)", parameters)
	
	return message
end,

[geoformatdata.apiDistance] = function(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		trim = false,
		removeBlanks = false,
	})

	local scalingFactor = tonumber(args[geoformatdata.argScalingFactor]) or geoformatdata.defaultDistanceScalingFactor
	local coordsA = args[1]
	local coordsB = args[2]

	local A = create()
	local status, error
	status, error =  A:parse(coordsA, nil, nil)
	if not status then
		mw.logObject(error, "apiDistance: parse error A")
		return
	end
	
	local B
	if coordsB then
		B = create()
		status, error =  B:parse(coordsB, nil, nil)
		if not status then
			mw.logObject(error, "apiDistance: parse error B")
			return
		end
	else
		B = wd("P625", false)
		if not B then
			mw.logObject(B, "apiDistance: missing data in WD")
			return
		end
	end

	return scalingFactor * distance(A,B)
end,

[geoformatdata.apiPrecisionRadius] = function(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		trim = false,
		removeBlanks = false,
	})

	local scalingFactor = tonumber(args[geoformatdata.argScalingFactor]) or geoformatdata.defaultDistanceScalingFactor
	local displayPrecision = args[geoformatdata.argPrecision]
	local coords = args[geoformatdata.argCoordinatesCoordinates]

	local A
	if coords then
		A = create()
		local status, error
		status, error =  A:parse(coords, nil, displayPrecision)
		if not status then
			mw.logObject(error, "apiPrecisionRadius: parse error")
			return
		end
		
		if displayPrecision and not parseDisplayPrecision(A, displayPrecision) then
			mw.logObject(displayPrecision, "apiPrecisionRadius: parsePrecision error")
			return
		end
		
	else
		A = wd("P625", false)
		if not A then
			mw.logObject(A, "apiPrecisionRadius: missing data in WD")
			return
		end
	end
	
	return scalingFactor * size(A)
end,

}