Pillars of Eternity Wiki
Advertisement
Pillars of Eternity Wiki
Template-info.png Module documentation

Used to support Template:Coord, with the entry point being p.coord.


local p = {}
local getArgs = require('Module:Arguments').getArgs

local errors =
{
	[0]  = "Unspecified error",
	[1]  = "Unknown coordinate format, refer to documentation for valid formats",
	[2]  = "Odd number of arguments specified, both sides must be balanced",
	[3]  = "Invalid cardinal indicator for latitude, must be 'N' or 'S'",
	[4]  = "Invalid cardinal indicator for longitude, must be 'E' or 'W'",
	[5]  = "All values must be positive when a cardinal indicator is used!",
	[6]  = "Latitude coordinates with a cardinal indicator must be in the range 0 to 90 inclusive",
	[7]  = "Latitude coordinates without a cardinal indicator must be in the range -90 to 90 inclusive",
	[8]  = "Longitude coordinates with a cardinal indicator must be in the range 0 to 180 inclusive",
	[9]  = "Longitude coordinates without a cardinal indicator must be in the range -180 to 180 inclusive",
	[10] = "Minutes and seconds values must be in the range 0 to 60",
	[11] = "Only the least significant value present may be a decimal value",
	[12] = "Unknown output format. Must be one of 'dec', 'dms', 'dm'"
}

function p.coord(frame)
	local args = getArgs(frame)
	local valid, coord, latLng = parseInput(args)
	local outputFormat = args["format"] or "dm"
	
	if (not valid) then
		return
	end
	
	-- Output is decimal degrees
	if (outputFormat == "dec") then
		
		-- If input system matches, just map
		-- Previously latDeg and lngDeg were made absolute
		if (coord.system == "dec") then
			return string.format("%g° %s, %g° %s", coord.latDeg, coord.latInd:upper(), coord.lngDeg, coord.lngInd:upper())
			
		-- Otherwise use latLng
		else
			return string.format("%g° %s, %g° %s", math.abs(latLng.lat), latLng.lat > 0 and 'N' or 'S',
												   math.abs(latLng.lng), latLng.lng > 0 and 'E' or 'W')
		end
		
	elseif (outputFormat == "dms") then
		
		-- If input system matches, just map, otherwise convert from latLng (reuse coord table)
		if (coord.system ~= "dms") then
			coord = p.decToDms(latLng.lat, latLng.lng)
		end
		
		return string.format("%d° %2d′ %2g″ %s", coord.latDeg, coord.latMin, coord.latSec, coord.latInd:upper())..", "..string.format("%d° %2d′ %2g″ %s", coord.lngDeg, coord.lngMin, coord.lngSec, coord.lngInd:upper())
	
	elseif (outputFormat  == "dm") then
		
		if (coord.system ~= "dm") then
			coord = p.decToDm(latLng.lat, latLng.lng)
		end
		
		return string.format("%d° %2g′ %s", coord.latDeg, coord.latMin, coord.latInd:upper())..", "..
			   string.format("%d° %2g′ %s", coord.lngDeg, coord.lngMin, coord.lngInd:upper())
		
	else
		
		throwError(12)
		
	end
end

-- Guess the coordinate system, validates the input
-- Returns the following, in order:
-- A boolean representing whether the input was validated and a system was detected
-- A coordinate table, in the same format as the input
    -- system (string) - A string, containing the type of coordinate system that was inputted, either "dm", "dms", or "dec"
	-- latDeg (number) - Degrees portion of latitude - always converted to absolute
	-- latMin (number) - Minutes portion of latitude, dm/dms only
	-- latSec (number) - Seconds portion of latitude, dm/dms only
	-- latInd (string) - Cardinal indicator of latitude in lower case, all systems even if not passed
	-- lng...
-- A coordinate table, which is always in degrees decimal format containing "lat" and "lng" with no indicators (will be converted to negative if S/W)
function parseInput(args)
	local count = 0
	local valid = false
	local system = nil
	
	local nums = {}
	local strs = {}
	
	-- Input coordinate
	local coord = {}
	
	-- Output coord
	local latLng = {}
	
	-- Count number of unnamed arguments, and split into numbers and strings
	for k,v in pairs(args) do
		
		-- Only if indexed (unnamed) argument
		if (type(k) == "number") then
			local vl = v:lower();
			local vn = tonumber(vl)
			
			-- Is number
			if (vn ~= nil) then
				table.insert(nums, vn)
				
			-- Is string
			else
				table.insert(strs, vl)
			end
				
			count = count + 1
		end
	end
	
	-- Assume degree decimal system if 2 number arguments, and optionally 2 strings
	if (#nums == 2 and (#strs == 2 or #strs == 0)) then
		system = "dec"
		valid = validateCoords(system, nums[1], nil, nil, strs[1] or nil,
									   nums[2], nil, nil, strs[2] or nil)
		
		if (valid) then
			
			coord.latDeg = math.abs(nums[1]); coord.latInd = strs[1] or (nums[1] < 0 and 's' or 'n');
			coord.lngDeg = math.abs(nums[2]); coord.lngInd = strs[2] or (nums[2] < 0 and 'w' or 'e')
			
			latLng.lat = nums[1]; latLng.lng = nums[2];
			
			-- If indicator was present, the decimal coordinates must be converted to negative or positive
			if (strs[1] == 's') then latLng.lat = latLng.lat * -1 end
			if (strs[2] == 'w') then latLng.lng = latLng.lng * -1 end
			
		end
	
	-- Assume degrees-minutes system if 4 numbers and optionally 2 strings
	elseif (#nums == 4 and (#strs == 2 or #strs == 0)) then
		system = "dm"
		valid = validateCoords(system, nums[1], nums[2], nil, strs[1] or nil,
									   nums[3], nums[4], nil, strs[2] or nil)
		
		if (valid) then
			coord.latDeg = math.abs(nums[1]); coord.latMin = nums[2];
			coord.latInd = strs[1] or (nums[1] < 0 and 's' or 'n');
			coord.lngDeg = math.abs(nums[3]); coord.lngMin = nums[4];
			coord.lngInd = strs[2] or (nums[3] < 0 and 'w' or 'e')
			
			latLng.lat, latLng.lng = p.dmsToDec(coord.latDeg, coord.latMin, nil, coord.latInd,
				                                coord.lngDeg, coord.lngMin, nil, coord.lngInd)
        end
		
	-- Assume degrees-minutes-seconds system if 6 numbers and optionally 2 strings
	elseif (#nums == 6 and (#strs == 2 or #strs == 0)) then
		system = "dms"
		valid = validateCoords(system, nums[1], nums[2], nums[3], strs[1] or nil,
									   nums[4], nums[5], nums[6], strs[2] or nil)
		
		if (valid) then
			coord.latDeg = math.abs(nums[1]); coord.latMin = nums[2]; coord.latSec = nums[3];
			coord.latInd = strs[1] or (nums[1] < 0 and 's' or 'n')
			coord.lngDeg = math.abs(nums[4]); coord.lngMin = nums[5]; coord.lngSec = nums[6];
			coord.lngInd = strs[2] or (nums[4] < 0 and 'w' or 'e')
			
			latLng.lat, latLng.lng = p.dmsToDec(coord.latDeg, coord.latMin, coord.latSec, coord.latInd,
			                        	        coord.lngDeg, coord.lngMin, coord.lngSec, coord.lngInd)
		end
		
	else
		if (count % 2 ~= 0) then
			throwError(2)
		else
			throwError(1)
		end
	end
	
	coord.system = system
	
	return valid, coord, latLng
end

function validateCoords(system, latDeg, latMin, latSec, latInd, lngDeg, lngMin, lngSec, lngInd)
	
	local usesIndicator = (latInd ~= nil or lngInd ~= nil)
	
	-- Validate indicators, if present
	if (usesIndicator == true and validateCardinalIndicators(latInd, lngInd) == false) then
		return false
	end
	
	-- Validate range for degrees (should be present in all systems)
	if (not validateDegCoordRange(latDeg, lngDeg, usesIndicator)) then
		return false
	end
	
	-- Validate range and precision in DM/DMS system
	if (system == "dm" or system == "dms") then
		
		if (not validateMinSecCoordRange(latMin, latSec, lngMin, lngSec)) then
			return false
		end
		
		-- Make sure all but the least significant value are integer values (although fractionals can be converted, this will only be confusing and we want to maintain the input format)
		if (isNotInt(latDeg) or isNotInt(lngDeg) or
			(system == "dms" and (isNotInt(latMin) or isNotInt(lngMin))) ) then
			throwError(11); return false;
		end
	end
	
	return true
end

function validateDegCoordRange(latDeg, lngDeg, usesIndicator)
	
	-- Ensure degrees are not negative if cardinal indicators are present
	if (usesIndicator and (latDeg < 0 or lngDeg < 0)) then
		throwError(5); return false;
	end
	
	-- Latitude between 0 and 90 with indicator, or -90 and 90 without
	if (usesIndicator and not (latDeg >= 0 and latDeg <= 90) or not (latDeg >= -90 and latDeg <= 90)) then
		throwError(usesIndicator and 6 or 7); return false;
	end
	
	-- Longitude between 0 and 180 with indicator, or -180 and 180 without
	if (usesIndicator and not (lngDeg >= 0 and lngDeg <= 180) or not (lngDeg >= -180 and lngDeg <= 180)) then
		throwError(usesIndicator and 8 or 9); return false
	end
	
	return true
end

function validateMinSecCoordRange(latMin, latSec, lngMin, lngSec)
	local withinRange = function(n)
		if (n == nil) then
			return true
		else
			return (n and n >= 0 and n < 60)
		end
	end
	local valid = withinRange(latMin) and withinRange(latSec) and withinRange(lngMin) and withinRange(lngSec)
	
	if (not valid) then
		throwError(10); return false
	end
	
	return true
end

-- Validate cardinal indicator is correct N/S for lat, E/W for long
function validateCardinalIndicators(latInd, lngInd)
	if (latInd ~= "n" and latInd ~= "s") then
		throwError(3); return false;
	end
	if (lngInd ~= "e" and lngInd ~= "w") then
		throwError(4); return false;
	end
	
	return true
end

function isInt(n)
	return n == math.floor(n)
end

function isNotInt(n)
	return isInt(n) == false
end

function throwError(code)
	error(errors[code])
end

-- Convert a full lat-lng coordinate set from degrees-minutes-seconds to decimal degrees
-- Returns a table containing lat and lng
-- No validation is performed
function p.dmsToDec(latDeg, latMin, latSec, latInd, lngDeg, lngMin, lngSec, lngInd)
	local lat = p.dmsToDecCoord(latDeg, latMin, latSec, latInd)
	local lng = p.dmsToDecCoord(lngDeg, lngMin, lngSec, lngInd)
	return lat, lng
end

-- Convert a degrees-minutes-seconds coordinate to a decimal degrees coordinate
-- Indicator is optional, and if it is 's' or 'w', the result will be negative
-- No validation is performed
function p.dmsToDecCoord(degrees, minutes, seconds, indicator)
	local dec = degrees + ((minutes or 0) / 60) + ((seconds or 0) / 3600)
	local sign = (indicator and type(indicator) == "string" and (indicator:lower() == 's' or indicator:lower() == 'w')) and -1 or 1
	return dec * sign
end

-- Convert a full lat-lng coordinate pair from decimal degrees to degrees-minutes-seconds 
-- Returns a table containing latDeg, latMin, latSec, latInd, etc.
-- No validation is performed
function p.decToDms(lat, lng)
	local coord = {}
	coord.latDeg, coord.latMin, coord.latSec = p.decToDmsCoord(lat)
	coord.latInd = lat > 0 and 'n' or 's'
	coord.lngDeg, coord.lngMin, coord.lngSec = p.decToDmsCoord(lng)
	coord.lngInd = lng > 0 and 'e' or 'w'
	return coord
end

function p.decToDm(lat, lng)
	local coord = {}
	coord.latDeg, coord.latMin = p.decToDmCoord(lat)
	coord.latInd = lat > 0 and 'n' or 's'
	coord.lngDeg, coord.lngMin = p.decToDmCoord(lng)
	coord.lngInd = lng > 0 and 'e' or 'w'
	return coord
end

-- Convert a decimal degrees coordinate to a degrees-minutes-seconds coordinate
-- Returns degrees, minutes, seconds in that order
-- No validation is performed
function p.decToDmsCoord(decimal)
	
	local absolute = math.abs(decimal);
    local degrees = math.floor(absolute);
    local minutesNonTrunc = (absolute - degrees) * 60;
    local minutes = math.floor(minutesNonTrunc);
    local seconds = math.floor((minutesNonTrunc - minutes) * 60);
    return degrees, minutes, seconds
end

function p.decToDmCoord(decimal)
	
	local absolute = math.abs(decimal);
    local degrees = math.floor(absolute);
    local minutes = (absolute - degrees) * 60;
    return degrees, minutes;
    
end

return p
Advertisement