Module: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