(null edit) |
m (1 revision imported from wikivoyage:Module:Coordinates) |
||
(No difference)
|
Latest revision as of 22:30, 21 May 2024
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