Toggle menu
7
27
38
5.2K
Sanarchive
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Coordinates

From Sanarchive

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

--[[
This module is intended to replace the functionality of {{Coord}} and related
templates.  It provides several methods, including

{{#invoke:Coordinates | coord }} : General function formatting and displaying
coordinate values.

{{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
degree values to DMS format.

{{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
to decimal degree format.

{{#invoke:Coordinates | link }} : Export the link used to reach the tools

]]

require('strict')

local math_mod = require("Module:Math")
local coordinates = {};
local isSandbox = mw.getCurrentFrame():getTitle():find('sandbox', 1, true);

local current_page = mw.title.getCurrentTitle()
local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
local coord_link = 'https://geohack.toolforge.org/geohack.php?pagename=' .. page_name .. '&params='

--[[ Helper function, replacement for {{coord/display/title}} ]]
local function displaytitle(coords)
	return mw.getCurrentFrame():extensionTag{
		name = 'indicator',
		args = { name = 'coordinates' },
		content = '<span id="coordinates">[[Geographic coordinate system|Coordinates]]: ' .. coords .. '</span>'
	}
end

--[[ Helper function, used in detecting DMS formatting ]]
local function dmsTest(first, second)
	if type(first) ~= 'string' or type(second) ~= 'string' then
		return nil
	end
	local s = (first .. second):upper()
	return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
end


--[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
local function makeInvokeFunc(funcName)
	return function (frame)
		local args = require('Module:Arguments').getArgs(frame, {
			wrappers = 'Template:Coord'
		})
		return coordinates[funcName](args, frame)
	end
end

--[[ Helper function, handle optional args. ]]
local function optionalArg(arg, supplement)
	return arg and arg .. supplement or ''
end

--[[
Formats any error messages generated for display
]]
local function errorPrinter(errors)
	local result = ""
	for i,v in ipairs(errors) do
		result = result .. '<strong class="error">Coordinates: ' .. v[2] .. '</strong><br />'
	end
	return result
end

--[[
Determine the required CSS class to display coordinates
]]
local function displayDefault(default, mode)
	if default == "" then
		default = "dec"
	end

	if default == mode then
		return "geo-default"
	else
		return "geo-nondefault"
	end
end

--[[
specPrinter
]]
local function specPrinter(args, coordinateSpec)
	local uriComponents = coordinateSpec["param"]
	if uriComponents == "" then
		return "ERROR param was empty"
	end
	if args["name"] then
		uriComponents = uriComponents .. "&title=" .. mw.uri.encode(coordinateSpec["name"])
	end

	local geodmshtml = '<span class="geo-dms" title="Maps, aerial photos, and other data for this location">'
			.. '<span class="latitude">' .. coordinateSpec["dms-lat"] .. '</span> '
			.. '<span class="longitude">' ..coordinateSpec["dms-long"] .. '</span>'
			.. '</span>'

	local lat = tonumber( coordinateSpec["dec-lat"] ) or 0
	local geodeclat
	if lat < 0 then
		geodeclat = tostring(coordinateSpec["dec-lat"]):sub(2) .. "°S"
	else
		geodeclat = (coordinateSpec["dec-lat"] or 0) .. "°N"
	end

	local long = tonumber( coordinateSpec["dec-long"] ) or 0
	local geodeclong
	if long < 0 then
		geodeclong = tostring(coordinateSpec["dec-long"]):sub(2) .. "°W"
	else
		geodeclong = (coordinateSpec["dec-long"] or 0) .. "°E"
	end

	local geodechtml = '<span class="geo-dec" title="Maps, aerial photos, and other data for this location">'
			.. geodeclat .. ' '
			.. geodeclong
			.. '</span>'

	local geonumhtml = '<span class="geo">'
			.. coordinateSpec["dec-lat"] .. '; '
			.. coordinateSpec["dec-long"]
			.. '</span>'

	local inner = '<span class="' .. displayDefault(coordinateSpec["default"], "dms" ) .. '">' .. geodmshtml .. '</span>'
				.. '<span class="geo-multi-punct">&#xfeff; / &#xfeff;</span>'
				.. '<span class="' .. displayDefault(coordinateSpec["default"], "dec" ) .. '">';

	if not args["name"] then
		inner = inner .. geodechtml
				.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span></span>'
	else
		inner = inner .. '<span class="vcard">' .. geodechtml
				.. '<span style="display:none">&#xfeff; / ' .. geonumhtml .. '</span>'
				.. '<span style="display:none">&#xfeff; (<span class="fn org">'
				.. args["name"] .. '</span>)</span></span></span>'
	end

    local stylesheetLink = 'Module:Coordinates' .. ( isSandbox and '/sandbox' or '' ) .. '/styles.css'
	return mw.getCurrentFrame():extensionTag{
		name = 'templatestyles', args = { src = stylesheetLink }
	} .. '<span class="plainlinks nourlexpansion">[' .. coord_link .. uriComponents ..
	' ' .. inner .. ']</span>' .. '[[Category:Pages using gadget WikiMiniAtlas]]'
end

--[[ Helper functions for dec2dms ]]
local function convert_dec2dms_d(coordinate)
	local d = math_mod._round( coordinate, 0 ) .. "°"
	return d .. ""
end

local function convert_dec2dms_dm(coordinate)
	coordinate = math_mod._round( coordinate * 60, 0 );
	local m = coordinate % 60;
	coordinate = math.floor( (coordinate - m) / 60 );
	local d = coordinate % 360 .."°"
	return d .. string.format( "%02d′", m )
end

local function convert_dec2dms_dms(coordinate)
	coordinate = math_mod._round( coordinate * 60 * 60, 0 );
	local s = coordinate % 60
	coordinate = math.floor( (coordinate - s) / 60 );
	local m = coordinate % 60
	coordinate = math.floor( (coordinate - m) / 60 );
	local d = coordinate % 360 .."°"
	return d .. string.format( "%02d′", m ) .. string.format( "%02d″", s )
end

local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
	local coord = tonumber(coordinate)
	local postfix
	if coord >= 0 then
		postfix = firstPostfix
	else
		postfix = secondPostfix
	end

	precision = precision:lower();
	if precision == "dms" then
		return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
	elseif precision == "dm" then
		return convert_dec2dms_dm( math.abs( coord ) ) .. postfix;
	elseif precision == "d" then
		return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
	end
end

local function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
	local degrees = tonumber(degrees_str)
	local minutes = tonumber(minutes_str) or 0
	local seconds = tonumber(seconds_str) or 0

	local factor = 1
	if direction == "S" or direction == "W" then
		factor = -1
	end

	local precision = 0
	if seconds_str then
		precision = 5 + math.max( math_mod._precision(seconds_str), 0 );
	elseif minutes_str and minutes_str ~= '' then
		precision = 3 + math.max( math_mod._precision(minutes_str), 0 );
	else
		precision = math.max( math_mod._precision(degrees_str), 0 );
	end

	local decimal = factor * (degrees+(minutes+seconds/60)/60)
	return string.format( "%." .. precision .. "f", decimal )
end

local function validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, source, strong )
	local errors = {};
	lat_d = tonumber( lat_d ) or 0;
	lat_m = tonumber( lat_m ) or 0;
	lat_s = tonumber( lat_s ) or 0;
	long_d = tonumber( long_d ) or 0;
	long_m = tonumber( long_m ) or 0;
	long_s = tonumber( long_s ) or 0;

	if strong then
		if lat_d < 0 then table.insert(errors, {source, "latitude degrees < 0 with hemisphere flag"}) end
		if long_d < 0 then table.insert(errors, {source, "longitude degrees < 0 with hemisphere flag"}) end
	end

	if lat_d > 90 or lat_d < -90 then table.insert(errors, {source, "latitude degrees out of range"}) end
	if lat_m >= 60 or lat_m < 0 then table.insert(errors, {source, "latitude minutes out of range"}) end
	if long_d >= 360 or long_d <= -360 then table.insert(errors, {source, "longitude degrees out of range"}) end

	return errors;
end

local function parseDec( lat, long, format )
	local coordinateSpec = {}
	local errors = {}

	if not long then
		return nil, {{"parseDec", "Missing longitude"}}
	elseif not tonumber(long) then
		return nil, {{"parseDec", "Longitude could not be parsed as a number: " .. long}}
	end

	errors = validate( lat, nil, nil, long, nil, nil, 'parseDec', false );
	coordinateSpec["dec-lat"]  = lat;
	coordinateSpec["dec-long"] = long;

	local mode = coordinates.determineMode( lat, long );
	coordinateSpec["dms-lat"]  = convert_dec2dms( lat, "N", "S", mode)
	coordinateSpec["dms-long"] = convert_dec2dms( long, "E", "W", mode)

	coordinateSpec.default = format or "dec"
	return coordinateSpec, errors
end

local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
	local coordinateSpec, errors, backward = {}, {}

	lat_f = lat_f:upper();
	long_f = long_f:upper();

	if lat_f == 'E' or lat_f == 'W' then
		lat_d, long_d, lat_m, long_m, lat_s, long_s, lat_f, long_f, backward = long_d, lat_d, long_m, lat_m, long_s, lat_s, long_f, lat_f, true;
	end

	errors = validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, 'parseDMS', true );
	if not long_d then
		return nil, {{"parseDMS", "Missing longitude" }}
	elseif not tonumber(long_d) then
		return nil, {{"parseDMS", "Longitude could not be parsed as a number:" .. long_d }}
	end

	coordinateSpec["dms-lat"]  = lat_d.."°"..optionalArg(lat_m,"′") .. optionalArg(lat_s,"″") .. lat_f
	coordinateSpec["dms-long"] = long_d.."°"..optionalArg(long_m,"′") .. optionalArg(long_s,"″") .. long_f
	coordinateSpec["dec-lat"]  = convert_dms2dec(lat_f, lat_d, lat_m, lat_s)
	coordinateSpec["dec-long"] = convert_dms2dec(long_f, long_d, long_m, long_s)

	coordinateSpec.default = format or "dms"
	return coordinateSpec, errors, backward
end

local function formatTest(args)
	local result, errors
	local backward, primary = false, false

	local function getParam(args, lim)
		local ret = {}
		for i = 1, lim do ret[i] = args[i] or '' end
		return table.concat(ret, '_')
	end

	if not args[1] then
		return errorPrinter( {{"formatTest", "Missing latitude"}} )
	elseif not tonumber(args[1]) and not dmsTest(args[1], 'N') then -- Sadece sayı değilse ve DMS formatı değilse hata ver
		return errorPrinter( {{"formatTest", "Unable to parse latitude"}} )
	elseif not args[4] and not args[5] and not args[6] then
		result, errors = parseDec(args[1], args[2], args.format)
		if not result then return errorPrinter(errors); end
		result.param = table.concat({
			math.abs(tonumber(args[1])),
			((tonumber(args[1]) or 0) < 0) and 'S' or 'N',
			math.abs(tonumber(args[2])),
			((tonumber(args[2]) or 0) < 0) and 'W' or 'E',
			args[3] or ''}, '_')
	elseif dmsTest(args[4], args[8]) then
		result, errors, backward = parseDMS(args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args.format)
		if not result then return errorPrinter(errors) end
		result.param = getParam(args, 9)
	elseif dmsTest(args[3], args[6]) then
		result, errors, backward = parseDMS(args[1], args[2], nil, args[3], args[4], args[5], nil, args[6], args['format'])
		if not result then return errorPrinter(errors) end
		result.param = getParam(args, 7)
	elseif dmsTest(args[2], args[4]) then
		result, errors, backward = parseDMS(args[1], nil, nil, args[2], args[3], nil, nil, args[4], args.format)
		if not result then return errorPrinter(errors) end
		result.param = getParam(args, 5)
	else
		return errorPrinter({{"formatTest", "Unknown argument format"}})
	end
	
	result.name = args.name
	local ret = specPrinter(args, result)
	return ret, backward
end

--[[ WIKIDATA CATEGORIES DISABLED TO PREVENT LUA ERROR ]]
local function makeWikidataCategories(qid)
	return '' 
end

function coordinates.link(frame) return coord_link; end

coordinates.dec2dms = makeInvokeFunc('_dec2dms')
function coordinates._dec2dms(args)
	return convert_dec2dms(args[1], args[2] or '', args[3] or '', args[4] or '')
end

function coordinates.determineMode( value1, value2 )
	local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) );
	if precision <= 0 then return 'd' elseif precision <= 2 then return 'dm'; else return 'dms'; end
end

coordinates.dms2dec = makeInvokeFunc('_dms2dec')
function coordinates._dms2dec(args)
	return convert_dms2dec(args[1], args[2], args[3], args[4])
end

coordinates.coord = makeInvokeFunc('_coord')
function coordinates._coord(args)
	-- Disable automatic wikidata lookup to prevent errors
	local contents, backward = formatTest(args)
	local Notes = args.notes or ''
	local Display = (args.display and args.display:lower()) or 'inline'

	local function isInline(s) return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti' end
	local function isInTitle(s) return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti' end

	local text = ''
	if isInline(Display) then
		text = text .. '<span class="geo-inline">' .. contents .. Notes .. '</span>'
	end
	if isInTitle(Display) then
		if not isInline(Display) then
			text = text .. '<span class="geo-inline-hidden noexcerpt">' .. contents .. Notes .. '</span>'
		end
		text = text .. displaytitle(contents .. Notes)
	end
	
	-- Parser function call
	if not args.nosave then
		local function coord_wrapper(in_args)
			return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
		end
		-- Clean args for parser function
		local p_args = {}
		for k, v in pairs(args) do if type(k) == 'number' then p_args[k] = v end end
		if isInTitle(Display) then p_args[10] = 'primary' end
		text = text .. coord_wrapper(p_args)
	end
	
	return text
end

function coordinates._coord2text(coord,type)
	if coord == '' or not type then return nil end
	type = mw.text.trim(type)
	if type == 'lat' or type == 'long' then
        local coordString = mw.ustring.match(coord,'[%.%d]+°[NS] [%.%d]+°[EW]')
        if not coordString then return nil end
		local result = mw.text.split(coordString, ' ')
        local negative = (type == 'lat') and 'S' or 'W'
		result = (type == 'lat') and result[1] or result[2]
		result = mw.text.split(result, '°')
		if result[2] == negative then result[1] = '-'..result[1] end
		return result[1]
	else
		return mw.ustring.match(coord, 'params=.-_' .. type .. ':(.-)[ _]')
	end
end

function coordinates.coord2text(frame)
	return coordinates._coord2text(frame.args[1],frame.args[2]) or ''
end

function coordinates.coordinsert(frame)
	for i, v in ipairs(frame.args) do
		if i ~= 1 then
			if not mw.ustring.find(frame.args[1], (mw.ustring.match(frame.args[i], '^(.-:)') or '')) then
				frame.args[1] = mw.ustring.gsub(frame.args[1], '(params=.-)_? ', '%1_'..frame.args[i]..' ')
			end
		end
	end
	return frame.args[1]
end

return coordinates