Modul:Coordinates

aus Wikisource, der freien Quellensammlung

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