Module:Coordinates
Appearance
Documentation for this module may be created at Module:Coordinates/doc
-- Coordinate conversion procedures
-- This module is intended to replace the functionality of MapSources extension
-- designed for use both in modules and for direct invoking
-- functions for use in modules
-- toDec( coord, aDir, prec )
-- returns a decimal coordinate from decimal or deg-min-sec-letter strings
-- getDMSString( coord, prec, aDir, plus, minus, aFormat )
-- formats a decimal/dms coordinate to a deg-min-sec-letter string
-- getGeoLink( pattern, lat, long, plusLat, plusLong, minusLat, minusLong, prec, aFormat )
-- converts a complete dms geographic coordinate without reapplying the toDec function
-- getDecGeoLink( pattern, lat, long, prec )
-- converts a complete decimal geographic coordinate without reapplying the toDec function
-- Invokable functions
-- dec2dms( frame )
-- dms2dec( frame )
-- geoLink( frame )
-- documentation
local Coordinates = {
suite = 'Coordinates',
serial = '2020-08-18',
item = 7348344
}
-- module import
local ci = require( 'Module:Coordinates/i18n' )
-- module variable
local cd = {}
-- helper function getErrorMsg
-- returns error message by error number which
local function getErrorMsg( which )
if which == 'noError' or which == 0 then
return ci.errorMsg.noError
elseif which > #ci.errorMsg then
return ci.errorMsg.unknown
else
return ci.errorMsg[ which ]
end
end
-- helper function round
-- num: value to round
-- idp: number of digits after the decimal point
local function round( n, idp )
local m = 10^( idp or 0 )
if n >= 0 then
return math.floor( n * m + 0.5 ) / m
else
return math.ceil( n * m - 0.5 ) / m
end
end
-- helper function getPrecision
-- returns integer precision number
-- possible values: numbers, D, DM, DMS
-- default result: 4
local function getPrecision( prec )
local p = tonumber( prec )
if p then
p = round( p, 0 )
if p < -1 then
p = -1
elseif p > 8 then -- maximum 8 decimals
p = 8
end
return p
else
p = prec and prec:upper() or 'DMS'
if p == 'D' then
return 0
elseif p == 'DM' then
return 2
else
return 4 -- DMS = default
end
end
end
-- helper function toDMS
-- splits a decimal coordinate dec to degree, minute and second depending on the
-- precision. prec <= 0 means only degree, prec < 3 degree and minute, and so on
-- returns a result array
local function toDMS( dec, prec )
local result = { dec = 0, deg = 0, min = 0, sec = 0, sign = 1,
NS = 'N', EW = 'E', prec = getPrecision( prec ) }
local p = result.prec
result.dec = round( dec, 8 )
if result.dec < 0 then
result.sign = -1
result.NS = 'S'
result.EW = 'W'
end
local angle = math.abs( round( result.dec, p ) )
result.deg = math.floor( angle )
result.min = ( angle - result.deg ) * 60
if p > 4 then
result.sec = round( ( result.min - math.floor( result.min ) ) * 60, p - 4 )
else
result.sec = round( ( result.min - math.floor( result.min ) ) * 60 )
end
result.min = math.floor( result.min )
if result.sec >= 60 then
result.sec = result.sec - 60
result.min = result.min + 1
end
if p < 3 and result.sec >= 30 then
result.min = result.min + 1
end
if p < 3 then
result.sec = 0
end
if result.min >= 60 then
result.min = result.min - 60
result.deg = result.deg + 1
end
if p < 1 and result.min >= 30 then
result.deg = result.deg + 1
end
if p < 1 then
result.min = 0
end
return result
end
-- toDec converts decimal and hexagesimal DMS formatted coordinates to decimal
-- coordinates
-- input
-- dec: coordinate
-- prec: number of digits after the decimal point
-- aDir: lat/long directions
-- returns a result array
-- output
-- dec: decimal value
-- error: error number
-- parts: number of DMS parts, usually 1 (already decimal) ... 4
function cd.toDec( coord, aDir, prec )
local result = { dec = 0, error = 0, parts = 1 }
local s = mw.text.trim( coord )
if s == '' then
result.error = 1
return result
end
-- pretest if already a decimal
local dir = aDir or ''
local r = tonumber( s )
if r then
if dir == 'lat' and ( r < -90 or r > 90 ) then
result.error = 13
return result
elseif r <= -180 or r > 180 then
result.error = 5
return result
end
result.dec = round( r, getPrecision ( prec ) )
return result
end
s = mw.ustring.gsub( s, '[‘’′´`]', "'" )
s = s:gsub( "''", '"' )
s = mw.ustring.gsub( s, '[“”″]', '"' )
s = mw.ustring.gsub( s, '[−–—]', '-' )
s = mw.ustring.upper( mw.ustring.gsub( s, '[_/%c%s%z]', ' ' ) )
local mStr = '^[ %.%-°\'"0-9' -- string to match, illegal characters?
for key, value in pairs( ci.inputLetters ) do
mStr = mStr .. key
end
mStr = mStr .. ']+$'
if not mw.ustring.match( s, mStr ) then
result.error = 3
return result
end
s = mw.ustring.gsub( s, '(%u)', ' %1' )
s = mw.ustring.gsub( s, '%s*([°"\'])', '%1 ' )
s = mw.text.split( s, '%s' )
for i = #s, 1, -1 do
if mw.text.trim( s[ i ] ) == '' then
table.remove( s, i )
end
end
result.parts = #s
if #s < 1 or #s > 4 then
result.error = 2
return result
end
local units = { '°', "'", '"', ' ' }
local res = { 0, 0, 0, 1 } -- 1 = positive direction
local v
local l
for i = 1, #s, 1 do
v = mw.ustring.gsub( s[ i ], units[ i ], '' )
if tonumber( v ) then
if i > 3 then -- this position is for direction letter, not for number
result.error = 4
return result
end
v = tonumber( v )
if i == 1 then
if v <= -180 or v > 180 then
result.error = 5
return result
end
res[ 1 ] = v
elseif i == 2 or i == 3 then
if v < 0 or v >= 60 then
result.error = 2 + 2 * i
return result
end
if res[ i - 1 ] ~= round( res[ i - 1 ], 0 ) then
result.error = 3 + 2 * i
return result
end
res[ i ] = v
end
else -- no number
if i ~= #s then -- allowed only at the last position
result.error = 10
return result
end
if res[ 1 ] < 0 then
result.error = 11
return result
end
l = ci.inputLetters[ v ]
if mw.ustring.len( v ) ~= 1 or not l then
result.error = 3
return result
end
-- l[1]: factor
-- l[2]: lat/long
if ( dir == 'long' and l[ 2 ] ~= 'long' ) or
( dir == 'lat' and l[ 2 ] ~= 'lat' ) then
result.error = 12
return result
else
dir = l[ 2 ]
end
res[ 4 ] = l[ 1 ]
end
end
if dir == 'lat' and ( res[ 1 ] < -90 or res[ 1 ] > 90 ) then
result.error = 13
return result
end
if res[ 1 ] >= 0 then
result.dec = ( res[ 1 ] + res[ 2 ] / 60 + res[ 3 ] / 3600 ) * res[ 4 ]
else
result.dec = ( res[ 1 ] - res[ 2 ] / 60 - res[ 3 ] / 3600 ) * res[ 4 ]
end
result.dec = round( result.dec, getPrecision ( prec ) )
return result
end
-- getDMSString formats a degree-minute-second string for output in accordance
-- to a given format specification
-- input
-- coord: decimal or hexagesimal DMS coordinate
-- prec: precion of the coorninate string: D, DM, DMS
-- aDir: lat/long direction to add correct direction letters
-- plus: alternative direction string for positive directions
-- minus: alternative direction string for negative directions
-- aFormat: format array with delimiter and leadZeros values or a predefined
-- dmsFormats key. Default format key is f1.
-- outputs 3 results
-- 1: formatted string or error message for display
-- 2: decimal coordinate
-- 3: absolute decimal coordinate including the direction letter like 51.2323_N
function cd.getDMSString( coord, prec, aDir, aPlus, aMinus, aFormat )
local d = aDir or ''
local p = aPlus or ''
local m = aMinus or ''
-- format
local f = aFormat or 'f1'
if type( f ) ~= 'table' then
f = ci.dmsFormats[ f ]
end
local del = f.delimiter or ' '
local lz = f.leadZeros or false
local c = { dec = tonumber( coord ), error = 0, parts = 1 }
if not c.dec then
c = cd.toDec( coord, d, 8 )
elseif c.dec <= -180 or c.dec > 180 then
c.error = 5
elseif d == 'lat' and ( c.dec < -90 or c.dec > 90 ) then
c.error = 5
end
local l = ''
local wp = ''
local result = ''
if c.error == 0 then
local dms = toDMS( c.dec, prec )
if dms.dec < 0 and d == '' and m == '' then
dms.deg = -dms.deg
end
if lz and dms.min < 10 then
dms.min = '0' .. dms.min
end
if lz and dms.sec < 10 then
dms.sec = '0' .. dms.sec
end
result = dms.deg .. '°'
if dms.prec > 0 then
if ((dms.sec ~= '00') and (dms.sec ~= '0') and (dms.sec ~= 0)) or ((dms.min ~= '00') and (dms.min ~= '0') and (dms.min ~= 0)) then
result = result .. del .. dms.min .. '′'
end
end
if dms.prec > 2 and dms.prec < 5 then
if (dms.sec ~= '00') and (dms.sec ~= '0') and (dms.sec ~= 0) then
result = result .. del .. dms.sec .. '″'
end
end
if dms.prec > 4 then
-- enforce sec decimal digits even if zero
local s = string.format( "%." .. dms.prec - 4 .. "f″", dms.sec )
if ci.decimalPoint ~= '.' then
s = mw.ustring.gsub( s, '%.', ci.decimalPoint )
end
result = result .. del .. s
end
if d == 'lat' then
wp = dms.NS
elseif d == 'long' then
wp = dms.EW
end
if dms.dec >= 0 and p ~= '' then
l = p
elseif dms.dec < 0 and m ~= '' then
l = m
else
l = ci.outputLetters[ wp ]
end
if l and l ~= '' then
result = result .. del .. l
end
if c.parts > 1 then
result = result .. ci.categories.dms
end
return result--, dms.dec, math.abs( dms.dec ) .. '_' .. wp
else
if d == 'lat' then
wp = 'N'
elseif d == 'long' then
wp = 'E'
end
result = '<span class="error" title="' .. getErrorMsg( c.error ) ..'">'
.. ci.errorMsg.faulty .. '</span>' .. ci.categories.faulty
return result, '0', '0_' .. wp
end
return result
end
-- getGeoLink returns complete dms geographic coordinate without reapplying the toDec
-- and toDMS functions. Pattern can contain placeholders $1 ... $6
-- $1: latitude in Wikipedia syntax including the direction letter like 51.2323_N
-- $2: longitude in Wikipedia syntax including the direction letter like 51.2323_E
-- $3: latitude in degree, minute and second format considering the strings for
-- the cardinal directions and the precision
-- $4: longitude in degree, minute and second format considering the strings
-- for the cardinal directions and the precision
-- $5: latitude
-- $6: longitude
-- aFormat: format array with delimiter and leadZeros values or a predefined
-- dmsFormats key. Default format key is f1.
-- outputs 3 results
-- 1: formatted string or error message for display
-- 2: decimal latitude
-- 3: decimal longitude
function cd.getGeoLink( pattern, lat, long, plusLat, plusLong, minusLat,
minusLong, prec, aFormat )
local lat_s, lat_dec, lat_wp =
cd.getDMSString( lat, prec, 'lat', plusLat, minusLat, aFormat )
local long_s, long_dec, long_wp =
cd.getDMSString( long, prec, 'long', plusLong, minusLong, aFormat )
local s = pattern
s = mw.ustring.gsub( s, '($1)', lat_wp )
s = mw.ustring.gsub( s, '($2)', long_wp )
s = mw.ustring.gsub( s, '($3)', lat_s )
s = mw.ustring.gsub( s, '($4)', long_s )
s = mw.ustring.gsub( s, '($5)', lat_dec )
s = mw.ustring.gsub( s, '($6)', long_dec )
return s, lat_dec, long_dec
end
-- getDecGeoLink returns complete decimal geographic coordinate without reapplying
-- the toDec function. Pattern can contain placeholders $1 ... $4
function cd.getDecGeoLink( pattern, lat, long, prec )
local function getDec( coord, prec, aDir, aPlus, aMinus )
local l = aPlus
local c = cd.toDec( coord, aDir, 8 )
if c.error == 0 then
if c.dec < 0 then
l = aMinus
end
local d = round( c.dec, prec ) .. ''
if ci.decimalPoint ~= '.' then
d = mw.ustring.gsub( d, '%.', ci.decimalPoint )
end
return d, math.abs( c.dec ) .. '_' .. l
else
c.dec = '<span class="error" title="' .. getErrorMsg( c.error ) ..'">'
.. ci.errorMsg.faulty .. '</span>' .. ci.categories.faulty
return c.dec, '0_' .. l
end
end
local lat_dec, lat_wp = getDec( lat, prec, 'lat', 'N', 'S' )
local long_dec, long_wp = getDec( long, prec, 'long', 'E', 'W' )
local s = pattern
s = mw.ustring.gsub( s, '($1)', lat_wp)
s = mw.ustring.gsub( s, '($2)', long_wp)
s = mw.ustring.gsub( s, '($3)', lat_dec)
s = mw.ustring.gsub( s, '($4)', long_dec)
return s, lat_dec, long_dec
end
-- Invokable functions
-- identical to MapSources #dd2dms tag
-- frame input
-- 1 or coord: decimal or hexagesimal coordinate
-- precision: precion of the coorninate string: D, DM, DMS
-- plus: alternative direction string for positive directions
-- minus: alternative direction string for negative directions
-- format: Predefined dmsFormats key. Default format key is f1.
function cd.dec2dms( frame )
local args = frame:getParent().args
args.coord = args[ 1 ] or args.coord or ''
args.precision = args[ 2 ] or args.precision or ''
return cd.getDMSString( args.coord, args.precision, '',
args.plus, args.minus, args.format )
end
-- identical to MapSources #deg2dd tag
function cd.dms2dec( frame )
local args = frame:getParent().args
args.coord = args[ 1 ] or args.coord or ''
args.precision = args[ 2 ] or args.precision or ''
local r = cd.toDec( args.coord, '', args.precision )
local s = r.dec
if r.error ~= 0 then
if args.coord == '' then
s = ci.categories.faulty
else
s = '<span class="error" title="' .. getErrorMsg( r.error ) ..'">'
.. ci.errorMsg.faulty .. '</span>' .. ci.categories.faulty
end
end
return s
end
-- identical to MapSources #geoLink tag
-- This function can be extended to add Extension:GeoData #coordinates because
-- cd.getGeoLink returns lat and long, too
function cd.geoLink( frame )
local args = frame:getParent().args
args.pattern = args[ 1 ] or args.pattern or ''
if args.pattern == '' then
return errorMsg[ 14 ]
end
return cd.getGeoLink( args.pattern, args.lat, args.long,
args.plusLat, args.plusLong, args.minusLat, args.minusLong,
args.precision, args.format )
end
return cd