Logo Voyage

Module:Coordinates Voyage Tips and guide

You can check the original Wikivoyage article Here

    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
    


    Discover



    Powered by GetYourGuide