Modul:Coordinates
Notwendiges Modul für die Benutzung der Vorlage {{Marker}}
-- Coordinate conversion procedures
-- This module is intended to replace the functionality of MapSources extension
-- Redesign of my own MapSources_math.php
-- 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 )
-- Additional functions in [[Module:GeoData]]
-- module variable
local cd = {}
-- internationalisation
local errorMsg = {
'Keine Parameter eingegeben', -- 1: no parameter(s)
'Zu viele Parameter eingegeben', -- 2: to many parameters
'Unerlaubte Zeichen', -- 3: illegal characters
'Mehr als drei numerische Parameter', -- 4: to many numeric parameters
'Gradangabe außerhalb des Wertebereichs', -- 5: degree out of range
'Minutenangabe außerhalb des Wertebereichs', -- 6: minute out of range
'Gradangabe nicht ganzzahlig', -- 7: degree no integer
'Sekundenangabe außerhalb des Wertebereichs', -- 8: second out of range
'Minutenangabe nicht ganzzahlig', -- 9: minute no integer
'Richtung nicht letzter Parameter', -- 10: direction not last parameter
'Nicht erlaubter negativer Wert', -- 11: invalid negative value
'Falsche lat/long-Richtungsangabe', -- 12: wrong lat/long direction
'Breitenangabe außerhalb des Wertebereichs', -- 13: latitude out of range
'Kein Muster angegeben', -- 14: no pattern given
['noError'] = 'Kein Fehler', -- no Error
['unknown'] = 'Unbekannter Fehler', -- unknown error
['faulty'] = 'Fehlerhafte Koordinate' -- faulty coordinate
}
-- maintenance categories
local categories = {
['faulty'] = '[[Category:Seiten mit fehlerhaften Auszeichnungen zu Koordinaten]]',
-- faulty coordinate
-- same as defined in [[MediaWiki:Geodata-broken-tags-category]]
['dms'] = '[[Category:DMS-Koordinate]]'
-- coordinate given as dms, not as decimal
}
-- for input
-- de: O = E -> +1
-- it, fr: O = W -> -1
local inputLetters = {
['N'] = { 1, 'lat' },
['S'] = { -1, 'lat' },
['E'] = { 1, 'long' },
['W'] = { -1, 'long' },
['O'] = { 1, 'long' }
}
-- for output
local outputLetters = { ['N'] = 'N', ['S'] = 'S', ['E'] = 'O', ['W'] = 'W' }
local decimalPoint = ','
-- predefined deg-min-sec output formats
local dmsFormats = {
['f1'] = { ['delimiter'] = ' ', ['leadZeros'] = false },
['f2'] = { ['delimiter'] = ' ', ['leadZeros'] = true },
['f3'] = { ['delimiter'] = '', ['leadZeros'] = false },
['f4'] = { ['delimiter'] = '', ['leadZeros'] = true }
}
-- helper function getErrorMsg
-- returns error message by error number which
local function getErrorMsg( which )
if (which == 'noError') or (which == 0) then return errorMsg.noError
elseif which > 14 then return errorMsg.unknown
else return 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 ~= nil then
p = round( p, 0 )
if p < -1 then p = -1 end
if p > 8 then p = 8 end -- maximum 8 decimals
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 ~= nil then
if (dir == 'lat') and ( (r < -90) or (r > 90) ) then
result.error = 13
return result
end
if (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 = mw.ustring.gsub( s, "''", '"' )
s = mw.ustring.gsub( s, '[“”″]', '"' )
s = mw.ustring.gsub( s, '[−–—]', '-' )
s = mw.ustring.upper( mw.ustring.gsub( s, '[_/%c%s%z]', ' ' ) )
s = mw.ustring.gsub( s, '(%u)', ' %1' )
s = mw.ustring.gsub( s, '([°"\'])', '%1 ' )
s = mw.text.split( s, '%s' )
for i = #s, 1, -1 do
if (s[i] == nil) or (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 ) ~= nil 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 then
if (v < 0) or (v >= 60) then
result.error = 6
return result
end
if res[1] ~= round( res[1], 0 ) then
result.error = 7
return result
end
res[2] = v
elseif i == 3 then
if (v < 0) or (v >= 60) then
result.error = 8
return result
end
if res[2] ~= round( res[2], 0 ) then
result.error = 9
return result
end
res[3] = 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 = inputLetters[v]
if (mw.ustring.len( v ) ~= 1) or (l == nil) 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 = dmsFormats[f] end
local del = f.delimiter or ' '
local lz = f.leadZeros or false
local c = { ['dec'] = tonumber( coord ), ['error'] = 0, ['parts'] = 1 }
if c.dec == nil then c = cd.toDec( coord, d, 8 )
else
if (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
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 result = result .. del .. dms.min .. '′' end
if (dms.prec > 2) and (dms.prec < 5) then
result = result .. del .. dms.sec .. '″' end
if (dms.prec > 4) then
-- enforce sec decimal digits even if zero
local s = string.format("%." .. dms.prec - 4 .. "f″", dms.sec)
if decimalPoint ~= '.' then
s = mw.ustring.gsub( s, '%.', 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 = outputLetters[wp] end
if (l ~= nil) and (l ~= '') then result = result .. del .. l end
if c.parts > 1 then result = result .. 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 ) ..'">'
.. errorMsg.faulty .. '</span>' .. 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 decimalPoint ~= '.' then
d = mw.ustring.gsub( d, '%.', decimalPoint )
end
return d, math.abs( c.dec ) .. '_' .. l
else
c.dec = '<span class="error" title="' .. getErrorMsg( c.error ) ..'">'
.. errorMsg.faulty .. '</span>' .. 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.precision or ''
local s = cd.getDMSString( args.coord, args.precision, '',
args.plus, args.minus, args.format )
return s
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.precision or ''
local r = cd.toDec( args.coord, '', args.precision )
local s = r.dec
if (r.error ~= 0) then
s = '<span class="error" title="' .. getErrorMsg( r.error ) ..'">'
.. errorMsg.faulty .. '</span>' .. categories.faulty
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
local s = cd.getGeoLink( args.pattern, args.lat, args.long,
args.plusLat, args.plusLong, args.minusLat, args.minusLong,
args.precision, args.format )
return s
end
return cd