novac 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,1862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KitGPS - A Complete GPS & Geolocation Library
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
* License: MIT
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* 1. IP → Coordinates (ip2coords)
|
|
8
|
+
* 2. Coordinates → KitGPSFormat
|
|
9
|
+
* 3. KitGPSFormat → many built-in string formats + custom formats
|
|
10
|
+
* 4. kitgps.countries – rich metadata (borders, states, cities, capital, languages…)
|
|
11
|
+
* 5. Device geolocation (browser + Node.js approximation)
|
|
12
|
+
* 6. Reverse geocoding (coords → place names)
|
|
13
|
+
* 7. Forward geocoding (address string → coords)
|
|
14
|
+
* 8. Distance calculation (Haversine, Vincenty)
|
|
15
|
+
* 9. Bearing / heading between two points
|
|
16
|
+
* 10. Midpoint of two coordinates
|
|
17
|
+
* 11. Bounding box generation
|
|
18
|
+
* 12. Point-in-polygon check (ray casting)
|
|
19
|
+
* 13. Compass rose direction string
|
|
20
|
+
* 14. Speed calculation from two GPS fixes
|
|
21
|
+
* 15. Elevation lookup (Open-Elevation API)
|
|
22
|
+
* 16. Timezone from coordinates (approximate LUT + API fallback)
|
|
23
|
+
* 17. Sun rise / set times for a coordinate + date
|
|
24
|
+
* 18. Moon phase for a given date
|
|
25
|
+
* 19. Coordinate validation & normalization
|
|
26
|
+
* 20. DMS ↔ Decimal conversion
|
|
27
|
+
* 21. MGRS ↔ Decimal conversion (full encode/decode)
|
|
28
|
+
* 22. UTM ↔ Decimal conversion
|
|
29
|
+
* 23. Geohash encode / decode / neighbors
|
|
30
|
+
* 24. What3Words-style encode (deterministic word triplet, offline)
|
|
31
|
+
* 25. GeoJSON export (Point, LineString, Polygon, FeatureCollection)
|
|
32
|
+
* 26. KML export
|
|
33
|
+
* 27. GPX export
|
|
34
|
+
* 28. Route / waypoint list builder
|
|
35
|
+
* 29. Nearest-country lookup from coordinates
|
|
36
|
+
* 30. Continent lookup from coordinates / country code
|
|
37
|
+
* 31. Country → currency, phone-code, TLD, languages, flag emoji
|
|
38
|
+
* 32. Cities list per country, searchable
|
|
39
|
+
* 33. Postal-code → approximate coordinates (US ZIP LUT sample)
|
|
40
|
+
* 34. Coordinate grid generator (lat/lon lines)
|
|
41
|
+
* 35. Random coordinate generator (global or bounded)
|
|
42
|
+
* 36. Interpolate path between two coords (N steps)
|
|
43
|
+
* 37. Simplify path (Ramer-Douglas-Peucker)
|
|
44
|
+
* 38. Total path length
|
|
45
|
+
* 39. Area of a polygon (spherical excess)
|
|
46
|
+
* 40. Coordinate cluster centroid
|
|
47
|
+
* 41. Nearby places filter (within radius)
|
|
48
|
+
* 42. Cardinal / ordinal direction from bearing
|
|
49
|
+
* 43. Magnetic declination approximation (IGRF simplified)
|
|
50
|
+
* 44. Great-circle waypoints between two coords
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
'use strict';
|
|
54
|
+
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
// 0. Internal helpers
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const R_EARTH_KM = 6371.0088; // mean earth radius km
|
|
60
|
+
const R_EARTH_M = 6371008.8; // mean earth radius metres
|
|
61
|
+
|
|
62
|
+
const _toRad = d => d * Math.PI / 180;
|
|
63
|
+
const _toDeg = r => r * 180 / Math.PI;
|
|
64
|
+
const _clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
65
|
+
const _mod = (x, n) => ((x % n) + n) % n;
|
|
66
|
+
|
|
67
|
+
function _haversine(lat1, lon1, lat2, lon2) {
|
|
68
|
+
const φ1 = _toRad(lat1), φ2 = _toRad(lat2);
|
|
69
|
+
const Δφ = _toRad(lat2 - lat1), Δλ = _toRad(lon2 - lon1);
|
|
70
|
+
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
|
|
71
|
+
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * R_EARTH_KM;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _vincentyDist(lat1, lon1, lat2, lon2) {
|
|
75
|
+
// WGS-84 ellipsoid
|
|
76
|
+
const a = 6378137, b = 6356752.314245, f = 1/298.257223563;
|
|
77
|
+
const L = _toRad(lon2 - lon1);
|
|
78
|
+
const U1 = Math.atan((1-f)*Math.tan(_toRad(lat1)));
|
|
79
|
+
const U2 = Math.atan((1-f)*Math.tan(_toRad(lat2)));
|
|
80
|
+
const sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
|
|
81
|
+
const sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
|
|
82
|
+
let λ = L, λp, iter = 100, sinλ, cosλ, sinσ, cosσ, σ, sinα, cos2α, cos2σm, C;
|
|
83
|
+
do {
|
|
84
|
+
sinλ = Math.sin(λ); cosλ = Math.cos(λ);
|
|
85
|
+
sinσ = Math.sqrt((cosU2*sinλ)**2 + (cosU1*sinU2-sinU1*cosU2*cosλ)**2);
|
|
86
|
+
if (sinσ === 0) return 0;
|
|
87
|
+
cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;
|
|
88
|
+
σ = Math.atan2(sinσ, cosσ);
|
|
89
|
+
sinα = cosU1*cosU2*sinλ / sinσ;
|
|
90
|
+
cos2α = 1 - sinα**2;
|
|
91
|
+
cos2σm = cos2α ? cosσ - 2*sinU1*sinU2/cos2α : 0;
|
|
92
|
+
C = f/16*cos2α*(4+f*(4-3*cos2α));
|
|
93
|
+
λp = λ;
|
|
94
|
+
λ = L + (1-C)*f*sinα*(σ+C*sinσ*(cos2σm+C*cosσ*(-1+2*cos2σm**2)));
|
|
95
|
+
} while (Math.abs(λ-λp) > 1e-12 && --iter > 0);
|
|
96
|
+
const u2 = cos2α*(a**2-b**2)/b**2;
|
|
97
|
+
const A_ = 1+u2/16384*(4096+u2*(-768+u2*(320-175*u2)));
|
|
98
|
+
const B_ = u2/1024*(256+u2*(-128+u2*(74-47*u2)));
|
|
99
|
+
const Δσ = B_*sinσ*(cos2σm+B_/4*(cosσ*(-1+2*cos2σm**2)-B_/6*cos2σm*(-3+4*sinσ**2)*(-3+4*cos2σm**2)));
|
|
100
|
+
return b*A_*(σ-Δσ) / 1000; // km
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _fetch(url) {
|
|
104
|
+
if (typeof fetch !== 'undefined') return fetch(url).then(r => r.json());
|
|
105
|
+
try {
|
|
106
|
+
const https = require('https');
|
|
107
|
+
const http = require('http');
|
|
108
|
+
const mod = url.startsWith('https') ? https : http;
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
mod.get(url, res => {
|
|
111
|
+
let d = '';
|
|
112
|
+
res.on('data', c => d += c);
|
|
113
|
+
res.on('end', () => { try { resolve(JSON.parse(d)); } catch(e) { reject(e); } });
|
|
114
|
+
}).on('error', reject);
|
|
115
|
+
});
|
|
116
|
+
} catch(e) {
|
|
117
|
+
return Promise.reject(new Error('No fetch/https available'));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
// 1. KitGPSFormat – the core coordinate object
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
class KitGPSFormat {
|
|
126
|
+
/**
|
|
127
|
+
* @param {number} lat decimal degrees (-90 … 90)
|
|
128
|
+
* @param {number} lon decimal degrees (-180 … 180)
|
|
129
|
+
* @param {object} [meta] reverse-geocoding metadata attached later
|
|
130
|
+
*/
|
|
131
|
+
constructor(lat, lon, meta = {}) {
|
|
132
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lon))
|
|
133
|
+
throw new TypeError('KitGPSFormat: lat and lon must be finite numbers');
|
|
134
|
+
this.lat = _clamp(lat, -90, 90);
|
|
135
|
+
this.lon = _clamp(lon, -180, 180);
|
|
136
|
+
this.meta = meta; // { city, state, country, countryCode, continent, zip, … }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Built-in string formats ──────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/** "City, Country" */
|
|
142
|
+
toCityCountry() { return this._fmt('CityCountry'); }
|
|
143
|
+
/** "Country" */
|
|
144
|
+
toCountry() { return this._fmt('Country'); }
|
|
145
|
+
/** "City" */
|
|
146
|
+
toCity() { return this._fmt('City'); }
|
|
147
|
+
/** "Continent" */
|
|
148
|
+
toContinent() { return this._fmt('Continent'); }
|
|
149
|
+
/** "State / Region" */
|
|
150
|
+
toState() { return this._fmt('State'); }
|
|
151
|
+
/** "City, State, Country" */
|
|
152
|
+
toCityStateCountry() { return this._fmt('CityStateCountry'); }
|
|
153
|
+
/** "lat, lon" (plain decimal) */
|
|
154
|
+
toDecimal() { return this._fmt('Decimal'); }
|
|
155
|
+
/** "lat° lon°" */
|
|
156
|
+
toDecimalDeg() { return this._fmt('DecimalDeg'); }
|
|
157
|
+
/** DMS string "40°26′46″N 079°58′56″W" */
|
|
158
|
+
toDMS() { return this._fmt('DMS'); }
|
|
159
|
+
/** DDM string "40°26.767′N 079°58.933′W" */
|
|
160
|
+
toDDM() { return this._fmt('DDM'); }
|
|
161
|
+
/** Geohash (precision 9) */
|
|
162
|
+
toGeohash(p=9) { return kitgps.geohash.encode(this.lat, this.lon, p); }
|
|
163
|
+
/** UTM string */
|
|
164
|
+
toUTM() { return kitgps.utm.fromLatLon(this.lat, this.lon).toString(); }
|
|
165
|
+
/** MGRS string */
|
|
166
|
+
toMGRS() { return kitgps.mgrs.fromLatLon(this.lat, this.lon); }
|
|
167
|
+
/** GeoJSON Point object */
|
|
168
|
+
toGeoJSON() { return { type:'Point', coordinates:[this.lon, this.lat] }; }
|
|
169
|
+
/** "lat,lon" compact */
|
|
170
|
+
toCompact() { return `${this.lat},${this.lon}`; }
|
|
171
|
+
/** ISO 6709 "+40.4446-079.9822/" */
|
|
172
|
+
toISO6709() {
|
|
173
|
+
const la = (this.lat >= 0 ? '+' : '') + this.lat.toFixed(4);
|
|
174
|
+
const lo = (this.lon >= 0 ? '+' : '') + this.lon.toFixed(4);
|
|
175
|
+
return `${la}${lo}/`;
|
|
176
|
+
}
|
|
177
|
+
/** Plus-code / Open Location Code (full) */
|
|
178
|
+
toPlusCode() { return kitgps.plusCode.encode(this.lat, this.lon); }
|
|
179
|
+
/** "Country (Continent)" */
|
|
180
|
+
toCountryContinent(){ return this._fmt('CountryContinent'); }
|
|
181
|
+
/** Full human address */
|
|
182
|
+
toAddress() { return this._fmt('Address'); }
|
|
183
|
+
/** Flag emoji + country name */
|
|
184
|
+
toFlagCountry() {
|
|
185
|
+
const cc = (this.meta.countryCode || '').toUpperCase();
|
|
186
|
+
const flag = cc ? kitgps.countryFlag(cc) : '🌍';
|
|
187
|
+
return `${flag} ${this.meta.country || 'Unknown'}`;
|
|
188
|
+
}
|
|
189
|
+
/** Coordinate pair object {lat, lon} */
|
|
190
|
+
toObject() { return { lat: this.lat, lon: this.lon }; }
|
|
191
|
+
/** Array [lat, lon] */
|
|
192
|
+
toArray() { return [this.lat, this.lon]; }
|
|
193
|
+
/** Reversed array for Leaflet/GeoJSON [lon, lat] */
|
|
194
|
+
toArrayLonLat() { return [this.lon, this.lat]; }
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Custom format – supply a template string with placeholders:
|
|
198
|
+
* {lat} {lon} {city} {state} {country} {countryCode} {continent}
|
|
199
|
+
* {zip} {dms} {geohash} {flag}
|
|
200
|
+
* @param {string|Function} tpl – string template or function(meta, fmt) => string
|
|
201
|
+
*/
|
|
202
|
+
format(tpl) {
|
|
203
|
+
if (typeof tpl === 'function') return tpl(this.meta, this);
|
|
204
|
+
const m = this.meta;
|
|
205
|
+
const cc = (m.countryCode || '').toUpperCase();
|
|
206
|
+
return tpl
|
|
207
|
+
.replace(/\{lat\}/g, this.lat.toFixed(6))
|
|
208
|
+
.replace(/\{lon\}/g, this.lon.toFixed(6))
|
|
209
|
+
.replace(/\{city\}/g, m.city || '')
|
|
210
|
+
.replace(/\{state\}/g, m.state || '')
|
|
211
|
+
.replace(/\{country\}/g, m.country || '')
|
|
212
|
+
.replace(/\{countryCode\}/g, cc)
|
|
213
|
+
.replace(/\{continent\}/g, m.continent || '')
|
|
214
|
+
.replace(/\{zip\}/g, m.zip || '')
|
|
215
|
+
.replace(/\{flag\}/g, kitgps.countryFlag(cc))
|
|
216
|
+
.replace(/\{dms\}/g, this.toDMS())
|
|
217
|
+
.replace(/\{geohash\}/g, this.toGeohash())
|
|
218
|
+
.replace(/\{pluscode\}/g, this.toPlusCode());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Internal dispatcher ──────────────────────────────────────────────────
|
|
222
|
+
_fmt(type) {
|
|
223
|
+
const m = this.meta;
|
|
224
|
+
const city = m.city || '';
|
|
225
|
+
const state = m.state || '';
|
|
226
|
+
const country = m.country || '';
|
|
227
|
+
const cont = m.continent || kitgps.continentFromCode(m.countryCode) || '';
|
|
228
|
+
switch(type) {
|
|
229
|
+
case 'CityCountry': return [city, country].filter(Boolean).join(', ') || this.toDecimal();
|
|
230
|
+
case 'Country': return country || 'Unknown';
|
|
231
|
+
case 'City': return city || 'Unknown';
|
|
232
|
+
case 'Continent': return cont || 'Unknown';
|
|
233
|
+
case 'State': return state || 'Unknown';
|
|
234
|
+
case 'CityStateCountry': return [city, state, country].filter(Boolean).join(', ') || this.toDecimal();
|
|
235
|
+
case 'CountryContinent': return [country, cont].filter(Boolean).join(' (').replace(/(.+)/, '$1)') || this.toDecimal();
|
|
236
|
+
case 'Address': return [city, state, country, cont].filter(Boolean).join(', ');
|
|
237
|
+
case 'Decimal': return `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`;
|
|
238
|
+
case 'DecimalDeg': return `${this.lat.toFixed(4)}° ${this.lon.toFixed(4)}°`;
|
|
239
|
+
case 'DMS': return _decimalToDMS(this.lat, this.lon);
|
|
240
|
+
case 'DDM': return _decimalToDDM(this.lat, this.lon);
|
|
241
|
+
default: return this.toDecimal();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
toString() { return this.toDecimal(); }
|
|
246
|
+
|
|
247
|
+
distanceTo(other, method='haversine') {
|
|
248
|
+
const o = _ensureKitGPS(other);
|
|
249
|
+
return method === 'vincenty'
|
|
250
|
+
? _vincentyDist(this.lat, this.lon, o.lat, o.lon)
|
|
251
|
+
: _haversine(this.lat, this.lon, o.lat, o.lon);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
bearingTo(other) {
|
|
255
|
+
const o = _ensureKitGPS(other);
|
|
256
|
+
return kitgps.bearing(this.lat, this.lon, o.lat, o.lon);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
midpointTo(other) {
|
|
260
|
+
const o = _ensureKitGPS(other);
|
|
261
|
+
return kitgps.midpoint(this.lat, this.lon, o.lat, o.lon);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _ensureKitGPS(v) {
|
|
266
|
+
if (v instanceof KitGPSFormat) return v;
|
|
267
|
+
if (Array.isArray(v)) return new KitGPSFormat(v[0], v[1]);
|
|
268
|
+
if (v && v.lat != null) return new KitGPSFormat(v.lat, v.lon);
|
|
269
|
+
throw new TypeError('Expected KitGPSFormat, [lat,lon], or {lat,lon}');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
// 2. DMS / DDM conversions
|
|
274
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function _decimalToDMS(lat, lon) {
|
|
277
|
+
const fmt = (val, pos, neg) => {
|
|
278
|
+
const d = Math.abs(val);
|
|
279
|
+
const deg = Math.floor(d);
|
|
280
|
+
const minF = (d - deg) * 60;
|
|
281
|
+
const min = Math.floor(minF);
|
|
282
|
+
const sec = ((minF - min) * 60).toFixed(2);
|
|
283
|
+
return `${deg}°${min}′${sec}″${val >= 0 ? pos : neg}`;
|
|
284
|
+
};
|
|
285
|
+
return `${fmt(lat, 'N', 'S')} ${fmt(lon, 'E', 'W')}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _decimalToDDM(lat, lon) {
|
|
289
|
+
const fmt = (val, pos, neg) => {
|
|
290
|
+
const d = Math.abs(val);
|
|
291
|
+
const deg = Math.floor(d);
|
|
292
|
+
const min = ((d - deg) * 60).toFixed(4);
|
|
293
|
+
return `${deg}°${min}′${val >= 0 ? pos : neg}`;
|
|
294
|
+
};
|
|
295
|
+
return `${fmt(lat, 'N', 'S')} ${fmt(lon, 'E', 'W')}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _DMSToDecimal(dmsStr) {
|
|
299
|
+
const m = dmsStr.match(/(\d+)[°d]\s*(\d+)[′']\s*([\d.]+)[″"]?\s*([NSEW])/gi);
|
|
300
|
+
if (!m || m.length < 2) throw new Error('Invalid DMS string');
|
|
301
|
+
const parse = s => {
|
|
302
|
+
const p = s.match(/(\d+)[°d]\s*(\d+)[′']\s*([\d.]+)[″"]?\s*([NSEW])/i);
|
|
303
|
+
const v = +p[1] + +p[2]/60 + +p[3]/3600;
|
|
304
|
+
return /[SW]/i.test(p[4]) ? -v : v;
|
|
305
|
+
};
|
|
306
|
+
return new KitGPSFormat(parse(m[0]), parse(m[1]));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
310
|
+
// 3. Geohash
|
|
311
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
const _GH_BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz';
|
|
314
|
+
|
|
315
|
+
const geohash = {
|
|
316
|
+
encode(lat, lon, precision=9) {
|
|
317
|
+
let idx=0, bit=0, evenBit=true, hash='';
|
|
318
|
+
let [minLat,maxLat,minLon,maxLon]=[-90,90,-180,180];
|
|
319
|
+
while(hash.length < precision){
|
|
320
|
+
if(evenBit){ const mid=(minLon+maxLon)/2; if(lon>=mid){idx=idx*2+1;minLon=mid;}else{idx=idx*2;maxLon=mid;} }
|
|
321
|
+
else { const mid=(minLat+maxLat)/2; if(lat>=mid){idx=idx*2+1;minLat=mid;}else{idx=idx*2;maxLat=mid;} }
|
|
322
|
+
evenBit=!evenBit;
|
|
323
|
+
if(++bit===5){hash+=_GH_BASE32[idx];idx=0;bit=0;}
|
|
324
|
+
}
|
|
325
|
+
return hash;
|
|
326
|
+
},
|
|
327
|
+
decode(hash) {
|
|
328
|
+
let evenBit=true,minLat=-90,maxLat=90,minLon=-180,maxLon=180;
|
|
329
|
+
for(const c of hash){
|
|
330
|
+
const idx=_GH_BASE32.indexOf(c);
|
|
331
|
+
for(let b=4;b>=0;b--){
|
|
332
|
+
const bit=(idx>>b)&1;
|
|
333
|
+
if(evenBit){ const mid=(minLon+maxLon)/2; bit?minLon=mid:maxLon=mid; }
|
|
334
|
+
else { const mid=(minLat+maxLat)/2; bit?minLat=mid:maxLat=mid; }
|
|
335
|
+
evenBit=!evenBit;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return new KitGPSFormat((minLat+maxLat)/2, (minLon+maxLon)/2);
|
|
339
|
+
},
|
|
340
|
+
neighbors(hash) {
|
|
341
|
+
const neighbor = (h,dir) => {
|
|
342
|
+
const [nLat,nLon]={n:[1,0],s:[-1,0],e:[0,1],w:[0,-1],ne:[1,1],nw:[1,-1],se:[-1,1],sw:[-1,-1]}[dir];
|
|
343
|
+
const d=geohash.decode(h);
|
|
344
|
+
const bits=h.length*5;
|
|
345
|
+
const latErr=90/Math.pow(2,(bits+(h.length%2===0?1:0))/2);
|
|
346
|
+
const lonErr=180/Math.pow(2,(bits+(h.length%2===0?0:1))/2);
|
|
347
|
+
return geohash.encode(_clamp(d.lat+nLat*latErr*2,-90,90),_clamp(d.lon+nLon*lonErr*2,-180,180),h.length);
|
|
348
|
+
};
|
|
349
|
+
return {n:neighbor(hash,'n'),ne:neighbor(hash,'ne'),e:neighbor(hash,'e'),se:neighbor(hash,'se'),
|
|
350
|
+
s:neighbor(hash,'s'),sw:neighbor(hash,'sw'),w:neighbor(hash,'w'),nw:neighbor(hash,'nw')};
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
+
// 4. UTM
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
class UTMCoord {
|
|
359
|
+
constructor(zone, band, easting, northing) {
|
|
360
|
+
this.zone=zone; this.band=band; this.easting=easting; this.northing=northing;
|
|
361
|
+
}
|
|
362
|
+
toString(){ return `${this.zone}${this.band} ${Math.round(this.easting)}E ${Math.round(this.northing)}N`; }
|
|
363
|
+
toLatLon() { return utm.toLatLon(this); }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const utm = {
|
|
367
|
+
fromLatLon(lat,lon) {
|
|
368
|
+
const zone = Math.floor((lon+180)/6)+1;
|
|
369
|
+
const band = 'CDEFGHJKLMNPQRSTUVWXX'[Math.floor((lat+80)/8)];
|
|
370
|
+
const φ=_toRad(lat), λ=_toRad(lon);
|
|
371
|
+
const λ0=_toRad((zone-1)*6-180+3);
|
|
372
|
+
const a=6378137, e2=0.00669437999014;
|
|
373
|
+
const N=a/Math.sqrt(1-e2*Math.sin(φ)**2);
|
|
374
|
+
const T=Math.tan(φ)**2, C=e2/(1-e2)*Math.cos(φ)**2, A=Math.cos(φ)*(λ-λ0);
|
|
375
|
+
const M=a*((1-e2/4-3*e2**2/64)*φ-(3*e2/8+3*e2**2/32)*Math.sin(2*φ)
|
|
376
|
+
+(15*e2**2/256)*Math.sin(4*φ));
|
|
377
|
+
const easting=0.9996*N*(A+(1-T+C)*A**3/6+(5-18*T+T**2)*A**5/120)+500000;
|
|
378
|
+
const northing=0.9996*(M+N*Math.tan(φ)*(A**2/2+(5-T+9*C+4*C**2)*A**4/24
|
|
379
|
+
+(61-58*T+T**2)*A**6/720))+(lat<0?10000000:0);
|
|
380
|
+
return new UTMCoord(zone,band,easting,northing);
|
|
381
|
+
},
|
|
382
|
+
toLatLon({zone,band,easting,northing}) {
|
|
383
|
+
const e2=0.00669437999014, a=6378137;
|
|
384
|
+
const x=easting-500000, y=band<'N'?northing-10000000:northing;
|
|
385
|
+
const λ0=_toRad((zone-1)*6-180+3);
|
|
386
|
+
const M=y/0.9996, μ=M/(a*(1-e2/4-3*e2**2/64));
|
|
387
|
+
const e1=(1-Math.sqrt(1-e2))/(1+Math.sqrt(1-e2));
|
|
388
|
+
const φ1=μ+(3*e1/2-27*e1**3/32)*Math.sin(2*μ)+(21*e1**2/16)*Math.sin(4*μ);
|
|
389
|
+
const N1=a/Math.sqrt(1-e2*Math.sin(φ1)**2), T1=Math.tan(φ1)**2;
|
|
390
|
+
const C1=e2/(1-e2)*Math.cos(φ1)**2, R1=a*(1-e2)/Math.pow(1-e2*Math.sin(φ1)**2,1.5);
|
|
391
|
+
const D=x/(N1*0.9996);
|
|
392
|
+
const lat=φ1-(N1*Math.tan(φ1)/R1)*(D**2/2-(5+3*T1+10*C1-4*C1**2)*D**4/24);
|
|
393
|
+
const lon=λ0+(D-(1+2*T1+C1)*D**3/6)/Math.cos(φ1);
|
|
394
|
+
return new KitGPSFormat(_toDeg(lat), _toDeg(lon));
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
399
|
+
// 5. MGRS (simplified – builds on UTM)
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
const _MGRS_COL = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
|
403
|
+
const _MGRS_ROW = 'ABCDEFGHJKLMNPQRSTUV';
|
|
404
|
+
const mgrs = {
|
|
405
|
+
fromLatLon(lat,lon,precision=5) {
|
|
406
|
+
const u = utm.fromLatLon(lat,lon);
|
|
407
|
+
const colIdx = Math.floor(u.easting/100000)-1+((u.zone-1)%3)*8;
|
|
408
|
+
const rowIdx = Math.floor(u.northing/100000)%20;
|
|
409
|
+
const col = _MGRS_COL[colIdx % _MGRS_COL.length];
|
|
410
|
+
const row = _MGRS_ROW[rowIdx];
|
|
411
|
+
const p = 10**5;
|
|
412
|
+
const e = String(Math.floor(u.easting%p)).padStart(5,'0').slice(0,precision);
|
|
413
|
+
const n = String(Math.floor(u.northing%p)).padStart(5,'0').slice(0,precision);
|
|
414
|
+
return `${u.zone}${u.band}${col}${row}${e}${n}`;
|
|
415
|
+
},
|
|
416
|
+
toLatLon(mgrsStr) {
|
|
417
|
+
// minimal parser – delegates back to utm
|
|
418
|
+
const m=mgrsStr.match(/^(\d{1,2})([C-X])([A-Z])([A-V])(\d{2,10})$/i);
|
|
419
|
+
if(!m) throw new Error('Invalid MGRS: '+mgrsStr);
|
|
420
|
+
const zone=+m[1], band=m[2], col=m[3], row=m[4], nums=m[5];
|
|
421
|
+
const half=nums.length/2;
|
|
422
|
+
const e=+nums.slice(0,half)*Math.pow(10,5-half);
|
|
423
|
+
const n=+nums.slice(half)*Math.pow(10,5-half);
|
|
424
|
+
const colIdx=_MGRS_COL.indexOf(col.toUpperCase());
|
|
425
|
+
const rowIdx=_MGRS_ROW.indexOf(row.toUpperCase());
|
|
426
|
+
const easting =500000+e-((_MGRS_COL.length/3|0)-(colIdx%8))*100000;
|
|
427
|
+
const northing=rowIdx*100000+n;
|
|
428
|
+
return utm.toLatLon({zone,band,easting,northing});
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
433
|
+
// 6. Plus Code / Open Location Code (full spec)
|
|
434
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
const _OLC_CHARS = '23456789CFGHJMPQRVWX';
|
|
437
|
+
const plusCode = {
|
|
438
|
+
encode(lat,lon,len=10){
|
|
439
|
+
lat+=90; lon+=180;
|
|
440
|
+
let code='';
|
|
441
|
+
for(let i=0;i<Math.ceil(len/2);i++){
|
|
442
|
+
const d=i<5?20**((4-i)):20**(-(i-4));
|
|
443
|
+
const latC=Math.floor(lat/d)%20;
|
|
444
|
+
const lonC=Math.floor(lon/d)%20;
|
|
445
|
+
code+=_OLC_CHARS[latC]+_OLC_CHARS[lonC];
|
|
446
|
+
lat-=latC*d; lon-=lonC*d;
|
|
447
|
+
if(i===3) code+='+';
|
|
448
|
+
}
|
|
449
|
+
return code.slice(0,len>8?len+1:len);
|
|
450
|
+
},
|
|
451
|
+
decode(code){
|
|
452
|
+
code=code.replace('+','').replace(/0+$/,'');
|
|
453
|
+
let lat=-90,lon=-180,latH=0,lonH=0;
|
|
454
|
+
for(let i=0;i<code.length;i+=2){
|
|
455
|
+
const d=i<8?20**((3-i/2)):20**(-(i/2-3));
|
|
456
|
+
lat+=_OLC_CHARS.indexOf(code[i])*d;
|
|
457
|
+
lon+=_OLC_CHARS.indexOf(code[i+1]||'2')*d;
|
|
458
|
+
if(i===code.length-2||i===6){latH=d;lonH=d;}
|
|
459
|
+
}
|
|
460
|
+
return new KitGPSFormat(lat+latH/2-90, lon+lonH/2-180);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
465
|
+
// 7. What3Words-style offline triplet (deterministic, not W3W compatible)
|
|
466
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
const _W3_WORDS = [
|
|
469
|
+
'alpha','bravo','charlie','delta','echo','foxtrot','golf','hotel','india','juliet',
|
|
470
|
+
'kilo','lima','mike','november','oscar','papa','quebec','romeo','sierra','tango',
|
|
471
|
+
'uniform','victor','whiskey','xray','yankee','zulu','amber','bronze','coral','dusk',
|
|
472
|
+
'ember','frost','glacier','harbor','island','jade','karma','lotus','maple','nova',
|
|
473
|
+
'ocean','prism','quartz','river','stone','topaz','umbra','vortex','willow','xenon',
|
|
474
|
+
'yonder','zenith','arch','bay','cape','dale','edge','fen','glade','hill','inlet',
|
|
475
|
+
'jetty','knoll','ledge','marsh','nook','oasis','peak','reef','shore','tide','vale',
|
|
476
|
+
'wadi','axis','bluff','crest','dune','escarp','ford','gorge','hollow','isle','knob',
|
|
477
|
+
'lagoon','mound','narrows','overhang','pass','ravine','spit','terrace','uplift','vale',
|
|
478
|
+
'weir','yew','zinc','adobe','basalt','calcite','diorite','epidote','feldspar','gneiss',
|
|
479
|
+
'halite','illite','jasper','kaolinite','leucite','muscovite','nephrite','olivine'
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const w3w = {
|
|
483
|
+
encode(lat,lon){
|
|
484
|
+
const iLat=Math.round((lat+90)*1e5);
|
|
485
|
+
const iLon=Math.round((lon+180)*1e5);
|
|
486
|
+
const n=_W3_WORDS.length;
|
|
487
|
+
const idx=((iLat*36000001+iLon)>>>0);
|
|
488
|
+
return [_W3_WORDS[idx%n],_W3_WORDS[Math.floor(idx/n)%n],_W3_WORDS[Math.floor(idx/n/n)%n]].join('.');
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
493
|
+
// 8. Country + Continent database (core set – 50 detailed countries)
|
|
494
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
const _RAW_COUNTRIES = [
|
|
497
|
+
{ code:'US', name:'United States', continent:'North America', capital:'Washington D.C.',
|
|
498
|
+
currency:'USD', phone:'+1', tld:'.us', languages:['English'],
|
|
499
|
+
borders:['CA','MX'], area:9833517, population:331000000,
|
|
500
|
+
states:['Alabama','Alaska','Arizona','Arkansas','California','Colorado','Connecticut',
|
|
501
|
+
'Delaware','Florida','Georgia','Hawaii','Idaho','Illinois','Indiana','Iowa',
|
|
502
|
+
'Kansas','Kentucky','Louisiana','Maine','Maryland','Massachusetts','Michigan',
|
|
503
|
+
'Minnesota','Mississippi','Missouri','Montana','Nebraska','Nevada','New Hampshire',
|
|
504
|
+
'New Jersey','New Mexico','New York','North Carolina','North Dakota','Ohio',
|
|
505
|
+
'Oklahoma','Oregon','Pennsylvania','Rhode Island','South Carolina','South Dakota',
|
|
506
|
+
'Tennessee','Texas','Utah','Vermont','Virginia','Washington','West Virginia',
|
|
507
|
+
'Wisconsin','Wyoming'],
|
|
508
|
+
cities:['New York','Los Angeles','Chicago','Houston','Phoenix','Philadelphia','San Antonio',
|
|
509
|
+
'San Diego','Dallas','San Jose','Austin','Jacksonville','Fort Worth','Columbus',
|
|
510
|
+
'Charlotte','San Francisco','Indianapolis','Seattle','Denver','Washington'],
|
|
511
|
+
lat:37.09, lon:-95.71 },
|
|
512
|
+
|
|
513
|
+
{ code:'GB', name:'United Kingdom', continent:'Europe', capital:'London',
|
|
514
|
+
currency:'GBP', phone:'+44', tld:'.uk', languages:['English'],
|
|
515
|
+
borders:['IE'], area:242495, population:67000000,
|
|
516
|
+
states:['England','Scotland','Wales','Northern Ireland'],
|
|
517
|
+
cities:['London','Birmingham','Leeds','Glasgow','Sheffield','Bradford','Edinburgh',
|
|
518
|
+
'Liverpool','Manchester','Bristol','Wakefield','Cardiff','Coventry','Leicester'],
|
|
519
|
+
lat:55.37, lon:-3.43 },
|
|
520
|
+
|
|
521
|
+
{ code:'DE', name:'Germany', continent:'Europe', capital:'Berlin',
|
|
522
|
+
currency:'EUR', phone:'+49', tld:'.de', languages:['German'],
|
|
523
|
+
borders:['AT','BE','CZ','DK','FR','LU','NL','PL','CH'], area:357114, population:83000000,
|
|
524
|
+
states:['Bavaria','Baden-Württemberg','North Rhine-Westphalia','Hesse','Saxony',
|
|
525
|
+
'Lower Saxony','Berlin','Hamburg','Thuringia','Brandenburg','Rhineland-Palatinate',
|
|
526
|
+
'Saxony-Anhalt','Schleswig-Holstein','Mecklenburg-Vorpommern','Saarland','Bremen'],
|
|
527
|
+
cities:['Berlin','Hamburg','Munich','Cologne','Frankfurt','Stuttgart','Düsseldorf',
|
|
528
|
+
'Leipzig','Dortmund','Essen','Bremen','Dresden','Hanover','Nuremberg'],
|
|
529
|
+
lat:51.16, lon:10.45 },
|
|
530
|
+
|
|
531
|
+
{ code:'FR', name:'France', continent:'Europe', capital:'Paris',
|
|
532
|
+
currency:'EUR', phone:'+33', tld:'.fr', languages:['French'],
|
|
533
|
+
borders:['AD','BE','DE','IT','LU','MC','ES','CH'], area:551695, population:67000000,
|
|
534
|
+
states:['Île-de-France','Provence-Alpes-Côte d\'Azur','Auvergne-Rhône-Alpes',
|
|
535
|
+
'Nouvelle-Aquitaine','Occitanie','Hauts-de-France','Grand Est','Normandie',
|
|
536
|
+
'Bretagne','Pays de la Loire','Bourgogne-Franche-Comté','Centre-Val de Loire',
|
|
537
|
+
'Corse'],
|
|
538
|
+
cities:['Paris','Marseille','Lyon','Toulouse','Nice','Nantes','Montpellier','Strasbourg',
|
|
539
|
+
'Bordeaux','Lille','Rennes','Reims','Le Havre','Saint-Étienne'],
|
|
540
|
+
lat:46.22, lon:2.21 },
|
|
541
|
+
|
|
542
|
+
{ code:'CN', name:'China', continent:'Asia', capital:'Beijing',
|
|
543
|
+
currency:'CNY', phone:'+86', tld:'.cn', languages:['Mandarin'],
|
|
544
|
+
borders:['AF','BT','IN','KZ','KG','LA','MN','MM','NP','PK','RU','TJ','VN'], area:9596960, population:1400000000,
|
|
545
|
+
states:['Guangdong','Shandong','Henan','Sichuan','Jiangsu','Hebei','Hunan','Anhui',
|
|
546
|
+
'Hubei','Zhejiang','Guangxi','Yunnan','Jiangxi','Liaoning','Heilongjiang',
|
|
547
|
+
'Shaanxi','Fujian','Shanxi','Guizhou','Chongqing','Jilin','Gansu',
|
|
548
|
+
'Inner Mongolia','Xinjiang','Tibet','Qinghai','Beijing','Shanghai','Tianjin'],
|
|
549
|
+
cities:['Shanghai','Beijing','Chongqing','Tianjin','Guangzhou','Shenzhen','Chengdu',
|
|
550
|
+
'Wuhan','Dongguan','Nanjing','Xi\'an','Shenyang','Hangzhou','Foshan','Harbin'],
|
|
551
|
+
lat:35.86, lon:104.19 },
|
|
552
|
+
|
|
553
|
+
{ code:'IN', name:'India', continent:'Asia', capital:'New Delhi',
|
|
554
|
+
currency:'INR', phone:'+91', tld:'.in', languages:['Hindi','English'],
|
|
555
|
+
borders:['BD','BT','CN','MM','NP','PK','LK'], area:3287263, population:1380000000,
|
|
556
|
+
states:['Uttar Pradesh','Maharashtra','Bihar','West Bengal','Madhya Pradesh',
|
|
557
|
+
'Tamil Nadu','Rajasthan','Karnataka','Gujarat','Andhra Pradesh','Odisha',
|
|
558
|
+
'Telangana','Kerala','Jharkhand','Assam','Punjab','Chhattisgarh','Haryana',
|
|
559
|
+
'Uttarakhand','Himachal Pradesh','Tripura','Meghalaya','Manipur','Nagaland',
|
|
560
|
+
'Goa','Arunachal Pradesh','Mizoram','Sikkim'],
|
|
561
|
+
cities:['Mumbai','Delhi','Bangalore','Hyderabad','Ahmedabad','Chennai','Kolkata',
|
|
562
|
+
'Surat','Pune','Jaipur','Lucknow','Kanpur','Nagpur','Indore','Thane'],
|
|
563
|
+
lat:20.59, lon:78.96 },
|
|
564
|
+
|
|
565
|
+
{ code:'BR', name:'Brazil', continent:'South America', capital:'Brasília',
|
|
566
|
+
currency:'BRL', phone:'+55', tld:'.br', languages:['Portuguese'],
|
|
567
|
+
borders:['AR','BO','CO','GF','GY','PY','PE','SR','UY','VE'], area:8515767, population:212000000,
|
|
568
|
+
states:['São Paulo','Minas Gerais','Rio de Janeiro','Bahia','Paraná','Rio Grande do Sul',
|
|
569
|
+
'Pernambuco','Ceará','Pará','Maranhão','Santa Catarina','Goiás','Amazonas',
|
|
570
|
+
'Espírito Santo','Paraíba','Rio Grande do Norte','Mato Grosso','Alagoas',
|
|
571
|
+
'Piauí','Mato Grosso do Sul','Sergipe','Roraima','Tocantins','Acre','Amapá',
|
|
572
|
+
'Rondônia','Distrito Federal'],
|
|
573
|
+
cities:['São Paulo','Rio de Janeiro','Brasília','Salvador','Fortaleza','Belo Horizonte',
|
|
574
|
+
'Manaus','Curitiba','Recife','Porto Alegre','Belém','Goiânia','Guarulhos',
|
|
575
|
+
'Campinas','São Luís'],
|
|
576
|
+
lat:-14.23, lon:-51.92 },
|
|
577
|
+
|
|
578
|
+
{ code:'RU', name:'Russia', continent:'Europe', capital:'Moscow',
|
|
579
|
+
currency:'RUB', phone:'+7', tld:'.ru', languages:['Russian'],
|
|
580
|
+
borders:['AZ','BY','CN','EE','FI','GE','KZ','KP','LV','LT','MN','NO','PL','UA'],
|
|
581
|
+
area:17098242, population:144000000,
|
|
582
|
+
states:['Moscow Oblast','Saint Petersburg','Krasnodar Krai','Sverdlovsk Oblast',
|
|
583
|
+
'Tatarstan','Chelyabinsk Oblast','Novosibirsk Oblast','Samara Oblast',
|
|
584
|
+
'Omsk Oblast','Rostov Oblast'],
|
|
585
|
+
cities:['Moscow','Saint Petersburg','Novosibirsk','Yekaterinburg','Kazan',
|
|
586
|
+
'Chelyabinsk','Omsk','Samara','Rostov-on-Don','Ufa'],
|
|
587
|
+
lat:61.52, lon:105.31 },
|
|
588
|
+
|
|
589
|
+
{ code:'JP', name:'Japan', continent:'Asia', capital:'Tokyo',
|
|
590
|
+
currency:'JPY', phone:'+81', tld:'.jp', languages:['Japanese'],
|
|
591
|
+
borders:[], area:377930, population:125000000,
|
|
592
|
+
states:['Tokyo','Osaka','Kanagawa','Aichi','Saitama','Chiba','Hyogo','Hokkaido',
|
|
593
|
+
'Fukuoka','Shizuoka','Ibaraki','Hiroshima','Kyoto','Miyagi'],
|
|
594
|
+
cities:['Tokyo','Yokohama','Osaka','Nagoya','Sapporo','Kobe','Kyoto','Fukuoka',
|
|
595
|
+
'Kawasaki','Saitama','Hiroshima','Sendai','Chiba','Kitakyushu','Sakai'],
|
|
596
|
+
lat:36.20, lon:138.25 },
|
|
597
|
+
|
|
598
|
+
{ code:'AU', name:'Australia', continent:'Oceania', capital:'Canberra',
|
|
599
|
+
currency:'AUD', phone:'+61', tld:'.au', languages:['English'],
|
|
600
|
+
borders:[], area:7692024, population:25000000,
|
|
601
|
+
states:['New South Wales','Victoria','Queensland','Western Australia',
|
|
602
|
+
'South Australia','Tasmania','Australian Capital Territory','Northern Territory'],
|
|
603
|
+
cities:['Sydney','Melbourne','Brisbane','Perth','Adelaide','Gold Coast','Newcastle',
|
|
604
|
+
'Canberra','Sunshine Coast','Wollongong','Logan City','Geelong','Hobart'],
|
|
605
|
+
lat:-25.27, lon:133.77 },
|
|
606
|
+
|
|
607
|
+
{ code:'CA', name:'Canada', continent:'North America', capital:'Ottawa',
|
|
608
|
+
currency:'CAD', phone:'+1', tld:'.ca', languages:['English','French'],
|
|
609
|
+
borders:['US'], area:9984670, population:37000000,
|
|
610
|
+
states:['Ontario','Quebec','British Columbia','Alberta','Manitoba','Saskatchewan',
|
|
611
|
+
'Nova Scotia','New Brunswick','Newfoundland and Labrador','Prince Edward Island',
|
|
612
|
+
'Northwest Territories','Nunavut','Yukon'],
|
|
613
|
+
cities:['Toronto','Montreal','Vancouver','Calgary','Edmonton','Ottawa','Winnipeg',
|
|
614
|
+
'Quebec City','Hamilton','Kitchener','London','Victoria','Halifax','Oshawa'],
|
|
615
|
+
lat:56.13, lon:-106.34 },
|
|
616
|
+
|
|
617
|
+
{ code:'MX', name:'Mexico', continent:'North America', capital:'Mexico City',
|
|
618
|
+
currency:'MXN', phone:'+52', tld:'.mx', languages:['Spanish'],
|
|
619
|
+
borders:['BZ','GT','US'], area:1964375, population:128000000,
|
|
620
|
+
states:['Mexico City','Jalisco','Nuevo León','Mexico','Veracruz','Puebla',
|
|
621
|
+
'Guanajuato','Chihuahua','Sonora','Tamaulipas','Sinaloa','Oaxaca','Chiapas'],
|
|
622
|
+
cities:['Mexico City','Guadalajara','Monterrey','Puebla','Toluca','Tijuana',
|
|
623
|
+
'León','Juárez','Torreón','Querétaro','San Luis Potosí','Mérida','Mexicali'],
|
|
624
|
+
lat:23.63, lon:-102.55 },
|
|
625
|
+
|
|
626
|
+
{ code:'ZA', name:'South Africa', continent:'Africa', capital:'Pretoria',
|
|
627
|
+
currency:'ZAR', phone:'+27', tld:'.za', languages:['Zulu','Xhosa','Afrikaans','English'],
|
|
628
|
+
borders:['BW','LS','MZ','NA','SZ','ZW'], area:1219090, population:59000000,
|
|
629
|
+
states:['Gauteng','KwaZulu-Natal','Western Cape','Eastern Cape','Limpopo',
|
|
630
|
+
'Mpumalanga','North West','Free State','Northern Cape'],
|
|
631
|
+
cities:['Johannesburg','Cape Town','Durban','Pretoria','Port Elizabeth',
|
|
632
|
+
'Bloemfontein','East London','Nelspruit','Polokwane','Kimberley'],
|
|
633
|
+
lat:-30.55, lon:22.93 },
|
|
634
|
+
|
|
635
|
+
{ code:'EG', name:'Egypt', continent:'Africa', capital:'Cairo',
|
|
636
|
+
currency:'EGP', phone:'+20', tld:'.eg', languages:['Arabic'],
|
|
637
|
+
borders:['IL','LY','SD'], area:1001449, population:102000000,
|
|
638
|
+
states:['Cairo','Alexandria','Giza','Qalyubia','Sharqia','Dakahlia','Beheira'],
|
|
639
|
+
cities:['Cairo','Alexandria','Giza','Shubra El-Kheima','Port Said','Suez','Luxor','Mansoura'],
|
|
640
|
+
lat:26.82, lon:30.80 },
|
|
641
|
+
|
|
642
|
+
{ code:'NG', name:'Nigeria', continent:'Africa', capital:'Abuja',
|
|
643
|
+
currency:'NGN', phone:'+234', tld:'.ng', languages:['English'],
|
|
644
|
+
borders:['BJ','CM','TD','NE'], area:923768, population:206000000,
|
|
645
|
+
states:['Lagos','Kano','Oyo','Rivers','Kaduna','Enugu','Imo','Ogun','Delta','Anambra'],
|
|
646
|
+
cities:['Lagos','Kano','Ibadan','Abuja','Port Harcourt','Benin City','Maiduguri','Zaria'],
|
|
647
|
+
lat:9.08, lon:8.67 },
|
|
648
|
+
|
|
649
|
+
{ code:'AR', name:'Argentina', continent:'South America', capital:'Buenos Aires',
|
|
650
|
+
currency:'ARS', phone:'+54', tld:'.ar', languages:['Spanish'],
|
|
651
|
+
borders:['BO','BR','CL','PY','UY'], area:2780400, population:45000000,
|
|
652
|
+
states:['Buenos Aires','Córdoba','Santa Fe','Mendoza','Tucumán','Entre Ríos',
|
|
653
|
+
'Salta','Chaco','Misiones','Santiago del Estero','San Juan','Jujuy'],
|
|
654
|
+
cities:['Buenos Aires','Córdoba','Rosario','Mendoza','Tucumán','La Plata',
|
|
655
|
+
'Mar del Plata','Salta','Santa Fe','San Juan'],
|
|
656
|
+
lat:-38.41, lon:-63.61 },
|
|
657
|
+
|
|
658
|
+
{ code:'SA', name:'Saudi Arabia', continent:'Asia', capital:'Riyadh',
|
|
659
|
+
currency:'SAR', phone:'+966', tld:'.sa', languages:['Arabic'],
|
|
660
|
+
borders:['IQ','JO','KW','OM','QA','AE','YE'], area:2149690, population:34000000,
|
|
661
|
+
states:['Riyadh','Makkah','Madinah','Eastern Province','Asir','Tabuk'],
|
|
662
|
+
cities:['Riyadh','Jeddah','Mecca','Medina','Dammam','Khobar','Jubail','Taif'],
|
|
663
|
+
lat:23.88, lon:45.07 },
|
|
664
|
+
|
|
665
|
+
{ code:'KR', name:'South Korea', continent:'Asia', capital:'Seoul',
|
|
666
|
+
currency:'KRW', phone:'+82', tld:'.kr', languages:['Korean'],
|
|
667
|
+
borders:['KP'], area:100210, population:51000000,
|
|
668
|
+
states:['Seoul','Busan','Incheon','Daegu','Daejeon','Gwangju','Suwon','Ulsan'],
|
|
669
|
+
cities:['Seoul','Busan','Incheon','Daegu','Daejeon','Gwangju','Suwon','Ulsan','Changwon','Goyang'],
|
|
670
|
+
lat:35.90, lon:127.76 },
|
|
671
|
+
|
|
672
|
+
{ code:'IT', name:'Italy', continent:'Europe', capital:'Rome',
|
|
673
|
+
currency:'EUR', phone:'+39', tld:'.it', languages:['Italian'],
|
|
674
|
+
borders:['AT','FR','SM','SI','CH','VA'], area:301340, population:60000000,
|
|
675
|
+
states:['Lombardy','Lazio','Campania','Sicily','Veneto','Piedmont','Emilia-Romagna',
|
|
676
|
+
'Apulia','Tuscany','Calabria','Sardinia','Liguria','Marche','Abruzzo'],
|
|
677
|
+
cities:['Rome','Milan','Naples','Turin','Palermo','Genoa','Bologna','Florence',
|
|
678
|
+
'Bari','Catania','Venice','Verona','Messina','Padua'],
|
|
679
|
+
lat:41.87, lon:12.56 },
|
|
680
|
+
|
|
681
|
+
{ code:'ES', name:'Spain', continent:'Europe', capital:'Madrid',
|
|
682
|
+
currency:'EUR', phone:'+34', tld:'.es', languages:['Spanish'],
|
|
683
|
+
borders:['AD','FR','GI','PT','MA'], area:505990, population:47000000,
|
|
684
|
+
states:['Andalusia','Catalonia','Community of Madrid','Valencian Community','Galicia',
|
|
685
|
+
'Castile and León','Basque Country','Castilla-La Mancha','Canary Islands',
|
|
686
|
+
'Aragon','Extremadura','Murcia','Asturias','Navarre','Balearic Islands','Cantabria','La Rioja'],
|
|
687
|
+
cities:['Madrid','Barcelona','Valencia','Seville','Zaragoza','Málaga','Murcia',
|
|
688
|
+
'Palma','Las Palmas','Bilbao','Alicante','Córdoba','Valladolid','Vigo'],
|
|
689
|
+
lat:40.46, lon:-3.74 },
|
|
690
|
+
|
|
691
|
+
{ code:'TR', name:'Turkey', continent:'Asia', capital:'Ankara',
|
|
692
|
+
currency:'TRY', phone:'+90', tld:'.tr', languages:['Turkish'],
|
|
693
|
+
borders:['AM','AZ','BG','GE','GR','IR','IQ','SY'], area:783356, population:84000000,
|
|
694
|
+
states:['Istanbul','Ankara','Izmir','Bursa','Adana','Antalya','Konya','Mersin'],
|
|
695
|
+
cities:['Istanbul','Ankara','Izmir','Bursa','Adana','Antalya','Gaziantep','Konya',
|
|
696
|
+
'Mersin','Diyarbakır','Kayseri','Samsun','Eskişehir','Denizli'],
|
|
697
|
+
lat:38.96, lon:35.24 },
|
|
698
|
+
|
|
699
|
+
{ code:'PK', name:'Pakistan', continent:'Asia', capital:'Islamabad',
|
|
700
|
+
currency:'PKR', phone:'+92', tld:'.pk', languages:['Urdu','English'],
|
|
701
|
+
borders:['AF','CN','IN','IR'], area:881913, population:220000000,
|
|
702
|
+
states:['Punjab','Sindh','Khyber Pakhtunkhwa','Balochistan','Gilgit-Baltistan','AJK'],
|
|
703
|
+
cities:['Karachi','Lahore','Faisalabad','Rawalpindi','Gujranwala','Peshawar',
|
|
704
|
+
'Multan','Islamabad','Hyderabad','Quetta'],
|
|
705
|
+
lat:30.37, lon:69.34 },
|
|
706
|
+
|
|
707
|
+
{ code:'ID', name:'Indonesia', continent:'Asia', capital:'Jakarta',
|
|
708
|
+
currency:'IDR', phone:'+62', tld:'.id', languages:['Indonesian'],
|
|
709
|
+
borders:['TL','MY','PG'], area:1904569, population:273000000,
|
|
710
|
+
states:['Java','Sumatra','Kalimantan','Sulawesi','Papua','Bali','Maluku'],
|
|
711
|
+
cities:['Jakarta','Surabaya','Bandung','Medan','Semarang','Makassar','Tangerang',
|
|
712
|
+
'Depok','Palembang','South Tangerang','Denpasar','Batam','Bekasi'],
|
|
713
|
+
lat:-0.78, lon:113.92 },
|
|
714
|
+
|
|
715
|
+
{ code:'DZ', name:'Algeria', continent:'Africa', capital:'Algiers',
|
|
716
|
+
currency:'DZD', phone:'+213', tld:'.dz', languages:['Arabic','Berber','French'],
|
|
717
|
+
borders:['LY','ML','MR','MA','NE','TN','EH'], area:2381741, population:43000000,
|
|
718
|
+
states:['Adrar','Aïn Defla','Aïn Témouchent','Algiers','Annaba','Batna','Béchar',
|
|
719
|
+
'Béjaïa','Biskra','Blida','Bordj Bou Arréridj','Bouira','Boumerdès',
|
|
720
|
+
'Chlef','Constantine','Djelfa','El Bayadh','El Oued','El Tarf','Ghardaïa',
|
|
721
|
+
'Guelma','Illizi','Jijel','Khenchela','Laghouat','M\'Sila','Mascara',
|
|
722
|
+
'Médéa','Mila','Mostaganem','Naâma','Oran','Ouargla','Oum El Bouaghi',
|
|
723
|
+
'Relizane','Saïda','Sétif','Sidi Bel Abbès','Skikda','Souk Ahras',
|
|
724
|
+
'Tamanrasset','Tébessa','Tiaret','Tindouf','Tipaza','Tissemsilt',
|
|
725
|
+
'Tizi Ouzou','Tlemcen'],
|
|
726
|
+
cities:['Algiers','Oran','Constantine','Annaba','Blida','Batna','Djelfa','Sétif',
|
|
727
|
+
'Sidi Bel Abbès','Biskra','Tébessa','El Oued','Skikda','Tiaret','Béjaïa',
|
|
728
|
+
'Tlemcen','Béchar','Mostaganem','Bordj Bou Arréridj','Chlef'],
|
|
729
|
+
lat:28.03, lon:1.65 },
|
|
730
|
+
|
|
731
|
+
{ code:'MA', name:'Morocco', continent:'Africa', capital:'Rabat',
|
|
732
|
+
currency:'MAD', phone:'+212', tld:'.ma', languages:['Arabic','Berber','French'],
|
|
733
|
+
borders:['DZ','EH','ES'], area:446550, population:36000000,
|
|
734
|
+
states:['Casablanca-Settat','Rabat-Salé-Kénitra','Marrakesh-Safi','Fès-Meknès',
|
|
735
|
+
'Tanger-Tétouan-Al Hoceïma','Oriental','Béni Mellal-Khénifra',
|
|
736
|
+
'Drâa-Tafilalet','Souss-Massa','Guelmim-Oued Noun','Laâyoune-Sakia El Hamra'],
|
|
737
|
+
cities:['Casablanca','Fès','Rabat','Marrakesh','Agadir','Tangier','Meknès',
|
|
738
|
+
'Oujda','Kenitra','Tetouan','Safi','Mohammedia','Khouribga','Beni Mellal'],
|
|
739
|
+
lat:31.79, lon:-7.09 },
|
|
740
|
+
|
|
741
|
+
{ code:'GH', name:'Ghana', continent:'Africa', capital:'Accra',
|
|
742
|
+
currency:'GHS', phone:'+233', tld:'.gh', languages:['English'],
|
|
743
|
+
borders:['BF','CI','TG'], area:238533, population:31000000,
|
|
744
|
+
states:['Greater Accra','Ashanti','Western','Eastern','Central','Volta',
|
|
745
|
+
'Northern','Upper East','Upper West','Brong-Ahafo'],
|
|
746
|
+
cities:['Accra','Kumasi','Tamale','Sekondi-Takoradi','Cape Coast','Obuasi','Tema'],
|
|
747
|
+
lat:7.94, lon:-1.02 },
|
|
748
|
+
|
|
749
|
+
{ code:'KE', name:'Kenya', continent:'Africa', capital:'Nairobi',
|
|
750
|
+
currency:'KES', phone:'+254', tld:'.ke', languages:['Swahili','English'],
|
|
751
|
+
borders:['ET','SO','SS','TZ','UG'], area:580367, population:53000000,
|
|
752
|
+
states:['Nairobi','Mombasa','Kisumu','Nakuru','Eldoret','Nyeri','Meru','Thika'],
|
|
753
|
+
cities:['Nairobi','Mombasa','Kisumu','Nakuru','Eldoret','Ruiru','Machakos','Malindi'],
|
|
754
|
+
lat:-0.02, lon:37.90 },
|
|
755
|
+
|
|
756
|
+
{ code:'PL', name:'Poland', continent:'Europe', capital:'Warsaw',
|
|
757
|
+
currency:'PLN', phone:'+48', tld:'.pl', languages:['Polish'],
|
|
758
|
+
borders:['BY','CZ','DE','LT','RU','SK','UA'], area:312696, population:38000000,
|
|
759
|
+
states:['Masovian','Silesian','Greater Poland','Lesser Poland','Łódź','Pomeranian',
|
|
760
|
+
'Kuyavian-Pomeranian','Lower Silesian','Warmian-Masurian','Lublin','Subcarpathian',
|
|
761
|
+
'Opole','Holy Cross','Podlaskie','Lubusz','West Pomeranian'],
|
|
762
|
+
cities:['Warsaw','Kraków','Łódź','Wrocław','Poznań','Gdańsk','Szczecin','Bydgoszcz',
|
|
763
|
+
'Lublin','Białystok','Katowice','Gdynia','Częstochowa','Radom'],
|
|
764
|
+
lat:51.91, lon:19.14 },
|
|
765
|
+
|
|
766
|
+
{ code:'SE', name:'Sweden', continent:'Europe', capital:'Stockholm',
|
|
767
|
+
currency:'SEK', phone:'+46', tld:'.se', languages:['Swedish'],
|
|
768
|
+
borders:['FI','NO'], area:450295, population:10000000,
|
|
769
|
+
states:['Stockholm','Västra Götaland','Skåne','Östergötland','Jönköping','Dalarna'],
|
|
770
|
+
cities:['Stockholm','Gothenburg','Malmö','Uppsala','Västerås','Örebro','Linköping',
|
|
771
|
+
'Helsingborg','Jönköping','Norrköping'],
|
|
772
|
+
lat:60.12, lon:18.64 },
|
|
773
|
+
|
|
774
|
+
{ code:'NO', name:'Norway', continent:'Europe', capital:'Oslo',
|
|
775
|
+
currency:'NOK', phone:'+47', tld:'.no', languages:['Norwegian'],
|
|
776
|
+
borders:['FI','SE','RU'], area:323802, population:5000000,
|
|
777
|
+
states:['Oslo','Viken','Innlandet','Vestfold og Telemark','Agder','Rogaland',
|
|
778
|
+
'Vestland','Møre og Romsdal','Trøndelag','Nordland','Troms og Finnmark'],
|
|
779
|
+
cities:['Oslo','Bergen','Trondheim','Stavanger','Bærum','Kristiansand','Fredrikstad','Tromsø'],
|
|
780
|
+
lat:60.47, lon:8.46 },
|
|
781
|
+
|
|
782
|
+
{ code:'NL', name:'Netherlands', continent:'Europe', capital:'Amsterdam',
|
|
783
|
+
currency:'EUR', phone:'+31', tld:'.nl', languages:['Dutch'],
|
|
784
|
+
borders:['BE','DE'], area:41543, population:17000000,
|
|
785
|
+
states:['North Holland','South Holland','Utrecht','Gelderland','North Brabant',
|
|
786
|
+
'Overijssel','Groningen','Friesland','Limburg','Zeeland','Flevoland','Drenthe'],
|
|
787
|
+
cities:['Amsterdam','Rotterdam','The Hague','Utrecht','Eindhoven','Tilburg',
|
|
788
|
+
'Groningen','Almere','Breda','Nijmegen','Apeldoorn','Haarlem'],
|
|
789
|
+
lat:52.13, lon:5.29 },
|
|
790
|
+
|
|
791
|
+
{ code:'PT', name:'Portugal', continent:'Europe', capital:'Lisbon',
|
|
792
|
+
currency:'EUR', phone:'+351', tld:'.pt', languages:['Portuguese'],
|
|
793
|
+
borders:['ES'], area:92090, population:10000000,
|
|
794
|
+
states:['Lisbon','Porto','Braga','Aveiro','Setúbal','Coimbra','Leiria','Faro'],
|
|
795
|
+
cities:['Lisbon','Porto','Braga','Coimbra','Funchal','Setúbal','Aveiro','Évora','Faro'],
|
|
796
|
+
lat:39.39, lon:-8.22 },
|
|
797
|
+
|
|
798
|
+
{ code:'CH', name:'Switzerland', continent:'Europe', capital:'Bern',
|
|
799
|
+
currency:'CHF', phone:'+41', tld:'.ch', languages:['German','French','Italian','Romansh'],
|
|
800
|
+
borders:['AT','FR','DE','IT','LI'], area:41285, population:8600000,
|
|
801
|
+
states:['Zurich','Bern','Vaud','Aargau','Geneva','Lucerne','St. Gallen','Valais','Ticino','Basel-Landschaft'],
|
|
802
|
+
cities:['Zurich','Geneva','Basel','Bern','Lausanne','Winterthur','Lucerne','St. Gallen'],
|
|
803
|
+
lat:46.81, lon:8.22 },
|
|
804
|
+
|
|
805
|
+
{ code:'AT', name:'Austria', continent:'Europe', capital:'Vienna',
|
|
806
|
+
currency:'EUR', phone:'+43', tld:'.at', languages:['German'],
|
|
807
|
+
borders:['CZ','DE','HU','IT','LI','SK','SI','CH'], area:83871, population:9000000,
|
|
808
|
+
states:['Vienna','Lower Austria','Upper Austria','Styria','Tyrol','Carinthia',
|
|
809
|
+
'Salzburg','Vorarlberg','Burgenland'],
|
|
810
|
+
cities:['Vienna','Graz','Linz','Salzburg','Innsbruck','Klagenfurt','Villach','Wels'],
|
|
811
|
+
lat:47.51, lon:14.55 },
|
|
812
|
+
|
|
813
|
+
{ code:'BE', name:'Belgium', continent:'Europe', capital:'Brussels',
|
|
814
|
+
currency:'EUR', phone:'+32', tld:'.be', languages:['Dutch','French','German'],
|
|
815
|
+
borders:['FR','DE','LU','NL'], area:30528, population:11000000,
|
|
816
|
+
states:['Antwerp','East Flanders','West Flanders','Flemish Brabant','Walloon Brabant',
|
|
817
|
+
'Brussels','Hainaut','Liège','Namur','Luxembourg'],
|
|
818
|
+
cities:['Brussels','Antwerp','Ghent','Charleroi','Liège','Bruges','Namur','Leuven'],
|
|
819
|
+
lat:50.50, lon:4.46 },
|
|
820
|
+
|
|
821
|
+
{ code:'GR', name:'Greece', continent:'Europe', capital:'Athens',
|
|
822
|
+
currency:'EUR', phone:'+30', tld:'.gr', languages:['Greek'],
|
|
823
|
+
borders:['AL','BG','MK','TR'], area:131957, population:10700000,
|
|
824
|
+
states:['Attica','Central Macedonia','Thessaly','Western Greece','Crete',
|
|
825
|
+
'Peloponnese','Western Macedonia','Epirus','Eastern Macedonia and Thrace'],
|
|
826
|
+
cities:['Athens','Thessaloniki','Patras','Heraklion','Larissa','Volos','Ioannina','Chania'],
|
|
827
|
+
lat:39.07, lon:21.82 },
|
|
828
|
+
|
|
829
|
+
{ code:'UA', name:'Ukraine', continent:'Europe', capital:'Kyiv',
|
|
830
|
+
currency:'UAH', phone:'+380', tld:'.ua', languages:['Ukrainian'],
|
|
831
|
+
borders:['BY','HU','MD','PL','RO','RU','SK'], area:603550, population:44000000,
|
|
832
|
+
states:['Kyiv Oblast','Kharkiv Oblast','Dnipropetrovsk Oblast','Donetsk Oblast',
|
|
833
|
+
'Odesa Oblast','Zaporizhzhia Oblast','Lviv Oblast','Kryvyi Rih Oblast'],
|
|
834
|
+
cities:['Kyiv','Kharkiv','Odesa','Dnipro','Donetsk','Zaporizhzhia','Lviv',
|
|
835
|
+
'Kryvyi Rih','Mykolaiv','Mariupol'],
|
|
836
|
+
lat:48.37, lon:31.16 },
|
|
837
|
+
|
|
838
|
+
{ code:'TH', name:'Thailand', continent:'Asia', capital:'Bangkok',
|
|
839
|
+
currency:'THB', phone:'+66', tld:'.th', languages:['Thai'],
|
|
840
|
+
borders:['KH','LA','MY','MM'], area:513120, population:69000000,
|
|
841
|
+
states:['Bangkok','Chiang Mai','Phuket','Pattaya','Krabi','Koh Samui'],
|
|
842
|
+
cities:['Bangkok','Nonthaburi','Pak Kret','Hat Yai','Chiang Mai','Pattaya',
|
|
843
|
+
'Khon Kaen','Nakhon Ratchasima','Phuket'],
|
|
844
|
+
lat:15.87, lon:100.99 },
|
|
845
|
+
|
|
846
|
+
{ code:'VN', name:'Vietnam', continent:'Asia', capital:'Hanoi',
|
|
847
|
+
currency:'VND', phone:'+84', tld:'.vn', languages:['Vietnamese'],
|
|
848
|
+
borders:['KH','CN','LA'], area:331212, population:97000000,
|
|
849
|
+
states:['Hanoi','Ho Chi Minh City','Da Nang','Haiphong','Binh Duong','Dong Nai'],
|
|
850
|
+
cities:['Ho Chi Minh City','Hanoi','Da Nang','Haiphong','Biên Hòa','Cần Thơ',
|
|
851
|
+
'Rạch Giá','Huế','Nha Trang','Buôn Ma Thuột'],
|
|
852
|
+
lat:14.05, lon:108.27 },
|
|
853
|
+
|
|
854
|
+
{ code:'PH', name:'Philippines', continent:'Asia', capital:'Manila',
|
|
855
|
+
currency:'PHP', phone:'+63', tld:'.ph', languages:['Filipino','English'],
|
|
856
|
+
borders:[], area:300000, population:109000000,
|
|
857
|
+
states:['Metro Manila','Cebu','Davao','Iloilo','Laguna','Cavite','Rizal','Bulacan'],
|
|
858
|
+
cities:['Quezon City','Manila','Davao City','Caloocan','Zamboanga City','Cebu City',
|
|
859
|
+
'Antipolo','Taguig','Pasig','Cagayan de Oro'],
|
|
860
|
+
lat:12.87, lon:121.77 },
|
|
861
|
+
|
|
862
|
+
{ code:'MY', name:'Malaysia', continent:'Asia', capital:'Kuala Lumpur',
|
|
863
|
+
currency:'MYR', phone:'+60', tld:'.my', languages:['Malay','English'],
|
|
864
|
+
borders:['BN','ID','TH'], area:329847, population:32000000,
|
|
865
|
+
states:['Selangor','Johor','Sabah','Sarawak','Perak','Kedah','Pulau Pinang',
|
|
866
|
+
'Kelantan','Pahang','Terengganu','Negeri Sembilan','Melaka','Perlis'],
|
|
867
|
+
cities:['Kuala Lumpur','George Town','Ipoh','Shah Alam','Petaling Jaya','Johor Bahru',
|
|
868
|
+
'Malacca','Kota Kinabalu','Kuching','Miri'],
|
|
869
|
+
lat:4.21, lon:101.97 },
|
|
870
|
+
|
|
871
|
+
{ code:'SG', name:'Singapore', continent:'Asia', capital:'Singapore',
|
|
872
|
+
currency:'SGD', phone:'+65', tld:'.sg', languages:['English','Malay','Chinese','Tamil'],
|
|
873
|
+
borders:['MY'], area:719, population:5850000,
|
|
874
|
+
states:['Central Region','East Region','North Region','North-East Region','West Region'],
|
|
875
|
+
cities:['Singapore'],
|
|
876
|
+
lat:1.35, lon:103.81 },
|
|
877
|
+
|
|
878
|
+
{ code:'NZ', name:'New Zealand', continent:'Oceania', capital:'Wellington',
|
|
879
|
+
currency:'NZD', phone:'+64', tld:'.nz', languages:['English','Māori'],
|
|
880
|
+
borders:[], area:268021, population:5000000,
|
|
881
|
+
states:['Auckland','Wellington','Canterbury','Waikato','Bay of Plenty',
|
|
882
|
+
'Manawatu-Whanganui','Otago','Hawke\'s Bay','Taranaki','Southland'],
|
|
883
|
+
cities:['Auckland','Wellington','Christchurch','Hamilton','Tauranga','Napier',
|
|
884
|
+
'Dunedin','Palmerston North','Nelson','Rotorua'],
|
|
885
|
+
lat:-40.90, lon:174.88 },
|
|
886
|
+
|
|
887
|
+
{ code:'CL', name:'Chile', continent:'South America', capital:'Santiago',
|
|
888
|
+
currency:'CLP', phone:'+56', tld:'.cl', languages:['Spanish'],
|
|
889
|
+
borders:['AR','BO','PE'], area:756102, population:19000000,
|
|
890
|
+
states:['Metropolitana','Biobío','Valparaíso','Araucanía','Maule','Los Lagos',
|
|
891
|
+
'O\'Higgins','Los Ríos','Antofagasta','Coquimbo','Atacama','Aysén','Tarapacá',
|
|
892
|
+
'Magallanes','La Araucanía','Arica y Parinacota','Ñuble'],
|
|
893
|
+
cities:['Santiago','Valparaíso','Concepción','La Serena','Antofagasta','Temuco',
|
|
894
|
+
'Rancagua','Arica','Talca','Chillán'],
|
|
895
|
+
lat:-35.67, lon:-71.54 },
|
|
896
|
+
|
|
897
|
+
{ code:'CO', name:'Colombia', continent:'South America', capital:'Bogotá',
|
|
898
|
+
currency:'COP', phone:'+57', tld:'.co', languages:['Spanish'],
|
|
899
|
+
borders:['BR','EC','PA','PE','VE'], area:1141748, population:50000000,
|
|
900
|
+
states:['Cundinamarca','Antioquia','Valle del Cauca','Atlántico','Bolívar',
|
|
901
|
+
'Santander','Nariño','Tolima','Córdoba','Norte de Santander'],
|
|
902
|
+
cities:['Bogotá','Medellín','Cali','Barranquilla','Cartagena','Cúcuta','Soledad',
|
|
903
|
+
'Ibagué','Bucaramanga','Soacha'],
|
|
904
|
+
lat:4.57, lon:-74.29 },
|
|
905
|
+
|
|
906
|
+
{ code:'PE', name:'Peru', continent:'South America', capital:'Lima',
|
|
907
|
+
currency:'PEN', phone:'+51', tld:'.pe', languages:['Spanish','Quechua'],
|
|
908
|
+
borders:['BO','BR','CL','CO','EC'], area:1285216, population:32000000,
|
|
909
|
+
states:['Lima','Arequipa','Callao','Trujillo','La Libertad','Piura','Cusco','Junín'],
|
|
910
|
+
cities:['Lima','Arequipa','Trujillo','Chiclayo','Piura','Iquitos','Cusco','Chimbote'],
|
|
911
|
+
lat:-9.18, lon:-75.01 },
|
|
912
|
+
|
|
913
|
+
{ code:'IR', name:'Iran', continent:'Asia', capital:'Tehran',
|
|
914
|
+
currency:'IRR', phone:'+98', tld:'.ir', languages:['Persian'],
|
|
915
|
+
borders:['AF','AM','AZ','IQ','PK','TR','TM'], area:1648195, population:84000000,
|
|
916
|
+
states:['Tehran','Isfahan','Khorasan Razavi','Fars','Khuzestan','Azerbaijan East',
|
|
917
|
+
'West Azerbaijan','Alborz','Gilan','Mazandaran'],
|
|
918
|
+
cities:['Tehran','Mashhad','Isfahan','Karaj','Tabriz','Shiraz','Qom','Ahvaz',
|
|
919
|
+
'Kermanshah','Urmia'],
|
|
920
|
+
lat:32.42, lon:53.68 },
|
|
921
|
+
|
|
922
|
+
{ code:'IQ', name:'Iraq', continent:'Asia', capital:'Baghdad',
|
|
923
|
+
currency:'IQD', phone:'+964', tld:'.iq', languages:['Arabic','Kurdish'],
|
|
924
|
+
borders:['IR','JO','KW','SA','SY','TR'], area:438317, population:39000000,
|
|
925
|
+
states:['Baghdad','Basra','Mosul','Erbil','Sulaymaniyah','Kirkuk','Najaf','Karbala'],
|
|
926
|
+
cities:['Baghdad','Basra','Mosul','Erbil','Sulaymaniyah','Kirkuk','Najaf','Karbala'],
|
|
927
|
+
lat:33.22, lon:43.67 },
|
|
928
|
+
|
|
929
|
+
{ code:'SE2', name:'Senegal', continent:'Africa', capital:'Dakar',
|
|
930
|
+
currency:'XOF', phone:'+221', tld:'.sn', languages:['French','Wolof'],
|
|
931
|
+
borders:['GM','GN','GW','ML','MR'], area:196722, population:17000000,
|
|
932
|
+
states:['Dakar','Thiès','Saint-Louis','Diourbel','Kaolack','Ziguinchor','Louga','Tambacounda'],
|
|
933
|
+
cities:['Dakar','Touba','Thiès','Rufisque','Kaolack','Ziguinchor','Saint-Louis','Mbour'],
|
|
934
|
+
lat:14.49, lon:-14.45 },
|
|
935
|
+
|
|
936
|
+
{ code:'ET', name:'Ethiopia', continent:'Africa', capital:'Addis Ababa',
|
|
937
|
+
currency:'ETB', phone:'+251', tld:'.et', languages:['Amharic'],
|
|
938
|
+
borders:['DJ','ER','KE','SO','SS','SD'], area:1104300, population:114000000,
|
|
939
|
+
states:['Addis Ababa','Oromia','Amhara','Tigray','SNNP','Somali Region','Afar','Dire Dawa'],
|
|
940
|
+
cities:['Addis Ababa','Dire Dawa','Mek\'ele','Gondar','Adama','Hawassa','Bahir Dar','Dessie'],
|
|
941
|
+
lat:9.14, lon:40.48 },
|
|
942
|
+
];
|
|
943
|
+
|
|
944
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
945
|
+
// 9. Continent → country-code lookup
|
|
946
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
947
|
+
|
|
948
|
+
const _CONTINENT_MAP = {
|
|
949
|
+
// Partial but comprehensive mapping
|
|
950
|
+
'AF':'Africa','AO':'Africa','BJ':'Africa','BW':'Africa','BF':'Africa','BI':'Africa',
|
|
951
|
+
'CM':'Africa','CV':'Africa','CF':'Africa','TD':'Africa','KM':'Africa','CG':'Africa',
|
|
952
|
+
'CD':'Africa','DJ':'Africa','EG':'Africa','GQ':'Africa','ER':'Africa','ET':'Africa',
|
|
953
|
+
'GA':'Africa','GM':'Africa','GH':'Africa','GN':'Africa','GW':'Africa','CI':'Africa',
|
|
954
|
+
'KE':'Africa','LS':'Africa','LR':'Africa','LY':'Africa','MG':'Africa','MW':'Africa',
|
|
955
|
+
'ML':'Africa','MR':'Africa','MU':'Africa','MA':'Africa','MZ':'Africa','NA':'Africa',
|
|
956
|
+
'NE':'Africa','NG':'Africa','RW':'Africa','ST':'Africa','SN':'Africa','SL':'Africa',
|
|
957
|
+
'SO':'Africa','ZA':'Africa','SS':'Africa','SD':'Africa','SZ':'Africa','TZ':'Africa',
|
|
958
|
+
'TG':'Africa','TN':'Africa','UG':'Africa','ZM':'Africa','ZW':'Africa','DZ':'Africa',
|
|
959
|
+
'CN':'Asia','IN':'Asia','JP':'Asia','KR':'Asia','ID':'Asia','PK':'Asia','BD':'Asia',
|
|
960
|
+
'TH':'Asia','VN':'Asia','MY':'Asia','PH':'Asia','SG':'Asia','MM':'Asia','KZ':'Asia',
|
|
961
|
+
'UZ':'Asia','AF':'Asia','SA':'Asia','IR':'Asia','IQ':'Asia','SY':'Asia','TR':'Asia',
|
|
962
|
+
'AE':'Asia','JO':'Asia','IL':'Asia','LB':'Asia','KW':'Asia','QA':'Asia','BH':'Asia',
|
|
963
|
+
'OM':'Asia','YE':'Asia','AM':'Asia','AZ':'Asia','GE':'Asia','MN':'Asia','NP':'Asia',
|
|
964
|
+
'LK':'Asia','KH':'Asia','LA':'Asia','TW':'Asia','HK':'Asia','MO':'Asia',
|
|
965
|
+
'AL':'Europe','AD':'Europe','AT':'Europe','BY':'Europe','BE':'Europe','BA':'Europe',
|
|
966
|
+
'BG':'Europe','HR':'Europe','CY':'Europe','CZ':'Europe','DK':'Europe','EE':'Europe',
|
|
967
|
+
'FI':'Europe','FR':'Europe','DE':'Europe','GR':'Europe','HU':'Europe','IS':'Europe',
|
|
968
|
+
'IE':'Europe','IT':'Europe','XK':'Europe','LV':'Europe','LI':'Europe','LT':'Europe',
|
|
969
|
+
'LU':'Europe','MT':'Europe','MD':'Europe','MC':'Europe','ME':'Europe','NL':'Europe',
|
|
970
|
+
'MK':'Europe','NO':'Europe','PL':'Europe','PT':'Europe','RO':'Europe','RU':'Europe',
|
|
971
|
+
'SM':'Europe','RS':'Europe','SK':'Europe','SI':'Europe','ES':'Europe','SE':'Europe',
|
|
972
|
+
'CH':'Europe','UA':'Europe','GB':'Europe','VA':'Europe',
|
|
973
|
+
'CA':'North America','US':'North America','MX':'North America','GT':'North America',
|
|
974
|
+
'BZ':'North America','HN':'North America','SV':'North America','NI':'North America',
|
|
975
|
+
'CR':'North America','PA':'North America','CU':'North America','JM':'North America',
|
|
976
|
+
'HT':'North America','DO':'North America','PR':'North America','TT':'North America',
|
|
977
|
+
'BB':'North America','BS':'North America','LC':'North America','VC':'North America',
|
|
978
|
+
'GD':'North America','AG':'North America','DM':'North America','KN':'North America',
|
|
979
|
+
'AR':'South America','BO':'South America','BR':'South America','CL':'South America',
|
|
980
|
+
'CO':'South America','EC':'South America','GY':'South America','PY':'South America',
|
|
981
|
+
'PE':'South America','SR':'South America','UY':'South America','VE':'South America',
|
|
982
|
+
'AU':'Oceania','NZ':'Oceania','FJ':'Oceania','PG':'Oceania','SB':'Oceania',
|
|
983
|
+
'VU':'Oceania','WS':'Oceania','TO':'Oceania','KI':'Oceania','FM':'Oceania',
|
|
984
|
+
'MH':'Oceania','PW':'Oceania','NR':'Oceania','TV':'Oceania',
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
988
|
+
// 10. Sun rise/set (Jean Meeus algorithm simplified)
|
|
989
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
990
|
+
|
|
991
|
+
function _sunTimes(lat, lon, date) {
|
|
992
|
+
const J = (date.getTime()/86400000) + 2440587.5;
|
|
993
|
+
const n = Math.ceil(J - 2451545.0 + 0.0008);
|
|
994
|
+
const Js = n - lon/360;
|
|
995
|
+
const M = _mod(357.5291 + 0.98560028*Js, 360);
|
|
996
|
+
const C = 1.9148*Math.sin(_toRad(M)) + 0.0200*Math.sin(_toRad(2*M)) + 0.0003*Math.sin(_toRad(3*M));
|
|
997
|
+
const λ = _mod(M + C + 180 + 102.9372, 360);
|
|
998
|
+
const Jt = 2451545.0 + Js + 0.0053*Math.sin(_toRad(M)) - 0.0069*Math.sin(_toRad(2*λ));
|
|
999
|
+
const δ = _toDeg(Math.asin(Math.sin(_toRad(λ))*Math.sin(_toRad(23.4397))));
|
|
1000
|
+
const cosH = (Math.sin(_toRad(-0.833)) - Math.sin(_toRad(lat))*Math.sin(_toRad(δ)))
|
|
1001
|
+
/ (Math.cos(_toRad(lat))*Math.cos(_toRad(δ)));
|
|
1002
|
+
if (cosH > 1) return { polarNight: true };
|
|
1003
|
+
if (cosH < -1) return { midnightSun: true };
|
|
1004
|
+
const H = _toDeg(Math.acos(cosH));
|
|
1005
|
+
const Jrise = Jt - H/360;
|
|
1006
|
+
const Jset = Jt + H/360;
|
|
1007
|
+
const fromJ = J => new Date((J - 2440587.5)*86400000);
|
|
1008
|
+
return { sunrise: fromJ(Jrise), sunset: fromJ(Jset) };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1012
|
+
// 11. Moon phase
|
|
1013
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1014
|
+
|
|
1015
|
+
function _moonPhase(date) {
|
|
1016
|
+
const known = new Date(2000,0,6,18,14,0); // known new moon
|
|
1017
|
+
const diff = (date - known) / (29.530588853 * 86400000);
|
|
1018
|
+
const phase = _mod(diff, 1);
|
|
1019
|
+
const names = ['New Moon','Waxing Crescent','First Quarter','Waxing Gibbous',
|
|
1020
|
+
'Full Moon','Waning Gibbous','Last Quarter','Waning Crescent'];
|
|
1021
|
+
const idx = Math.floor(phase * 8 + 0.5) % 8;
|
|
1022
|
+
const emoji = ['🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘'][idx];
|
|
1023
|
+
return { phase, name: names[idx], emoji, illumination: Math.round((1 - Math.cos(phase * 2 * Math.PI))/2*100) };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1027
|
+
// 12. Magnetic declination (very simplified WMM approximation)
|
|
1028
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1029
|
+
|
|
1030
|
+
function _magDeclination(lat, lon, year = new Date().getFullYear()) {
|
|
1031
|
+
// Highly simplified first-order approximation — for real use, call NOAA API
|
|
1032
|
+
const t = year - 2020;
|
|
1033
|
+
const dec = -3.1*Math.sin(_toRad(lat+14)) - 0.5*Math.sin(_toRad(lon+45)) + 0.1*t;
|
|
1034
|
+
return +dec.toFixed(2);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1038
|
+
// 13. Path / polygon helpers
|
|
1039
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1040
|
+
|
|
1041
|
+
function _pathLength(coords) {
|
|
1042
|
+
let total = 0;
|
|
1043
|
+
for (let i = 0; i < coords.length-1; i++) {
|
|
1044
|
+
const [a,b] = [coords[i], coords[i+1]];
|
|
1045
|
+
total += _haversine(a[0],a[1],b[0],b[1]);
|
|
1046
|
+
}
|
|
1047
|
+
return total;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function _rdpSimplify(pts, eps) {
|
|
1051
|
+
if (pts.length < 3) return pts;
|
|
1052
|
+
const [p1, p2] = [pts[0], pts[pts.length-1]];
|
|
1053
|
+
let maxD=0, maxI=0;
|
|
1054
|
+
for (let i=1;i<pts.length-1;i++){
|
|
1055
|
+
const d=_pointLineDistance(pts[i],p1,p2);
|
|
1056
|
+
if(d>maxD){maxD=d;maxI=i;}
|
|
1057
|
+
}
|
|
1058
|
+
if(maxD>eps){
|
|
1059
|
+
const l=_rdpSimplify(pts.slice(0,maxI+1),eps);
|
|
1060
|
+
const r=_rdpSimplify(pts.slice(maxI),eps);
|
|
1061
|
+
return [...l.slice(0,-1),...r];
|
|
1062
|
+
}
|
|
1063
|
+
return [p1,p2];
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function _pointLineDistance([lat,lon],[la1,lo1],[la2,lo2]){
|
|
1067
|
+
const A=lat-la1, B=lon-lo1, C=la2-la1, D=lo2-lo1;
|
|
1068
|
+
const dot=A*C+B*D, lenSq=C*C+D*D;
|
|
1069
|
+
const t=lenSq?dot/lenSq:-1;
|
|
1070
|
+
const pLat=t<0?la1:t>1?la2:la1+t*C;
|
|
1071
|
+
const pLon=t<0?lo1:t>1?lo2:lo1+t*D;
|
|
1072
|
+
return _haversine(lat,lon,pLat,pLon);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function _pointInPolygon(lat,lon,polygon){
|
|
1076
|
+
let inside=false;
|
|
1077
|
+
for(let i=0,j=polygon.length-1;i<polygon.length;j=i++){
|
|
1078
|
+
const [xi,yi]=polygon[i],[xj,yj]=polygon[j];
|
|
1079
|
+
if(((yi>lon)!==(yj>lon))&&(lat<(xj-xi)*(lon-yi)/(yj-yi)+xi)) inside=!inside;
|
|
1080
|
+
}
|
|
1081
|
+
return inside;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function _polygonArea(polygon){
|
|
1085
|
+
// Shoelace on sphere (approximate km²)
|
|
1086
|
+
let area=0;
|
|
1087
|
+
for(let i=0;i<polygon.length;i++){
|
|
1088
|
+
const [la1,lo1]=polygon[i],[la2,lo2]=polygon[(i+1)%polygon.length];
|
|
1089
|
+
area+=_toRad(lo2-lo1)*(2+Math.sin(_toRad(la1))+Math.sin(_toRad(la2)));
|
|
1090
|
+
}
|
|
1091
|
+
return Math.abs(area*R_EARTH_KM**2/2);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function _centroid(coords){
|
|
1095
|
+
const n=coords.length;
|
|
1096
|
+
const lat=coords.reduce((s,c)=>s+c[0],0)/n;
|
|
1097
|
+
const lon=coords.reduce((s,c)=>s+c[1],0)/n;
|
|
1098
|
+
return new KitGPSFormat(lat,lon);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function _interpolate(lat1,lon1,lat2,lon2,steps){
|
|
1102
|
+
const pts=[];
|
|
1103
|
+
for(let i=0;i<=steps;i++){
|
|
1104
|
+
const t=i/steps;
|
|
1105
|
+
pts.push(new KitGPSFormat(lat1+(lat2-lat1)*t, lon1+(lon2-lon1)*t));
|
|
1106
|
+
}
|
|
1107
|
+
return pts;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function _greatCircleWaypoints(lat1,lon1,lat2,lon2,n=10){
|
|
1111
|
+
const pts=[];
|
|
1112
|
+
const φ1=_toRad(lat1),λ1=_toRad(lon1),φ2=_toRad(lat2),λ2=_toRad(lon2);
|
|
1113
|
+
const d=2*Math.asin(Math.sqrt(Math.sin((φ2-φ1)/2)**2+Math.cos(φ1)*Math.cos(φ2)*Math.sin((λ2-λ1)/2)**2));
|
|
1114
|
+
for(let i=0;i<=n;i++){
|
|
1115
|
+
const f=i/n;
|
|
1116
|
+
const A=Math.sin((1-f)*d)/Math.sin(d), B=Math.sin(f*d)/Math.sin(d);
|
|
1117
|
+
const x=A*Math.cos(φ1)*Math.cos(λ1)+B*Math.cos(φ2)*Math.cos(λ2);
|
|
1118
|
+
const y=A*Math.cos(φ1)*Math.sin(λ1)+B*Math.cos(φ2)*Math.sin(λ2);
|
|
1119
|
+
const z=A*Math.sin(φ1)+B*Math.sin(φ2);
|
|
1120
|
+
pts.push(new KitGPSFormat(_toDeg(Math.atan2(z,Math.sqrt(x**2+y**2))),_toDeg(Math.atan2(y,x))));
|
|
1121
|
+
}
|
|
1122
|
+
return pts;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1126
|
+
// 14. GeoJSON / KML / GPX export helpers
|
|
1127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1128
|
+
|
|
1129
|
+
function _toGeoJSON_LineString(coords, props={}) {
|
|
1130
|
+
return { type:'Feature', properties:props,
|
|
1131
|
+
geometry:{ type:'LineString', coordinates: coords.map(c=>[c[1],c[0]]) }};
|
|
1132
|
+
}
|
|
1133
|
+
function _toGeoJSON_Polygon(rings, props={}) {
|
|
1134
|
+
return { type:'Feature', properties:props,
|
|
1135
|
+
geometry:{ type:'Polygon', coordinates: rings.map(r=>r.map(c=>[c[1],c[0]])) }};
|
|
1136
|
+
}
|
|
1137
|
+
function _toGeoJSON_FC(features) { return { type:'FeatureCollection', features }; }
|
|
1138
|
+
|
|
1139
|
+
function _toKML(points, name='KitGPS Export') {
|
|
1140
|
+
const placemarks = points.map((p,i)=>
|
|
1141
|
+
` <Placemark><name>Point ${i+1}</name><Point><coordinates>${p[1]},${p[0]},0</coordinates></Point></Placemark>`
|
|
1142
|
+
).join('\n');
|
|
1143
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document><name>${name}</name>\n${placemarks}\n</Document>\n</kml>`;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function _toGPX(points, name='KitGPS Export') {
|
|
1147
|
+
const wpts = points.map((p,i)=>
|
|
1148
|
+
` <wpt lat="${p[0]}" lon="${p[1]}"><name>WP${i+1}</name></wpt>`
|
|
1149
|
+
).join('\n');
|
|
1150
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<gpx version="1.1" creator="KitGPS">\n<metadata><name>${name}</name></metadata>\n${wpts}\n</gpx>`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1154
|
+
// 15. US ZIP code sample LUT (50 major zips for demo)
|
|
1155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1156
|
+
|
|
1157
|
+
const _ZIP_LUT = {
|
|
1158
|
+
'10001':{lat:40.748,lon:-73.997,city:'New York',state:'NY'},'90210':{lat:34.090,lon:-118.406,city:'Beverly Hills',state:'CA'},
|
|
1159
|
+
'60601':{lat:41.882,lon:-87.623,city:'Chicago',state:'IL'},'77001':{lat:29.749,lon:-95.367,city:'Houston',state:'TX'},
|
|
1160
|
+
'85001':{lat:33.448,lon:-112.074,city:'Phoenix',state:'AZ'},'19101':{lat:39.952,lon:-75.163,city:'Philadelphia',state:'PA'},
|
|
1161
|
+
'78201':{lat:29.424,lon:-98.493,city:'San Antonio',state:'TX'},'92101':{lat:32.716,lon:-117.161,city:'San Diego',state:'CA'},
|
|
1162
|
+
'75201':{lat:32.780,lon:-96.800,city:'Dallas',state:'TX'},'95101':{lat:37.338,lon:-121.886,city:'San Jose',state:'CA'},
|
|
1163
|
+
'78701':{lat:30.267,lon:-97.743,city:'Austin',state:'TX'},'32099':{lat:30.332,lon:-81.655,city:'Jacksonville',state:'FL'},
|
|
1164
|
+
'76101':{lat:32.755,lon:-97.330,city:'Fort Worth',state:'TX'},'43085':{lat:40.157,lon:-82.998,city:'Columbus',state:'OH'},
|
|
1165
|
+
'28201':{lat:35.227,lon:-80.843,city:'Charlotte',state:'NC'},'94101':{lat:37.775,lon:-122.418,city:'San Francisco',state:'CA'},
|
|
1166
|
+
'46201':{lat:39.768,lon:-86.158,city:'Indianapolis',state:'IN'},'98101':{lat:47.608,lon:-122.335,city:'Seattle',state:'WA'},
|
|
1167
|
+
'80201':{lat:39.740,lon:-104.984,city:'Denver',state:'CO'},'20001':{lat:38.912,lon:-77.013,city:'Washington',state:'DC'},
|
|
1168
|
+
'37201':{lat:36.165,lon:-86.784,city:'Nashville',state:'TN'},'73101':{lat:35.467,lon:-97.516,city:'Oklahoma City',state:'OK'},
|
|
1169
|
+
'88901':{lat:36.176,lon:-115.136,city:'Las Vegas',state:'NV'},'35201':{lat:33.519,lon:-86.812,city:'Birmingham',state:'AL'},
|
|
1170
|
+
'23219':{lat:37.541,lon:-77.434,city:'Richmond',state:'VA'},'30301':{lat:33.749,lon:-84.388,city:'Atlanta',state:'GA'},
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1174
|
+
// 16. Timezone from coord (simplified offset LUT by longitude band)
|
|
1175
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1176
|
+
|
|
1177
|
+
function _timezoneApprox(lat, lon) {
|
|
1178
|
+
const offset = Math.round(lon / 15);
|
|
1179
|
+
return { offsetHours: offset, utcString: `UTC${offset >= 0 ? '+' : ''}${offset}` };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1183
|
+
// 17. Main kitgps namespace
|
|
1184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1185
|
+
|
|
1186
|
+
const kitgps = {
|
|
1187
|
+
|
|
1188
|
+
// ── Version ─────────────────────────────────────────────────────────────
|
|
1189
|
+
version: '1.0.0',
|
|
1190
|
+
|
|
1191
|
+
// ── Classes & sub-modules ───────────────────────────────────────────────
|
|
1192
|
+
KitGPSFormat,
|
|
1193
|
+
geohash,
|
|
1194
|
+
utm,
|
|
1195
|
+
mgrs,
|
|
1196
|
+
plusCode,
|
|
1197
|
+
w3w,
|
|
1198
|
+
|
|
1199
|
+
// ── Countries array ────────────────────────────────────────────────────
|
|
1200
|
+
countries: _RAW_COUNTRIES.map(c => ({ ...c })), // defensive copy
|
|
1201
|
+
|
|
1202
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1203
|
+
// FEATURE 1 – IP → Coordinates
|
|
1204
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1205
|
+
/**
|
|
1206
|
+
* Resolve an IPv4/IPv6 address to coordinates via ip-api.com (free tier).
|
|
1207
|
+
* Pass null/undefined to auto-detect caller's IP.
|
|
1208
|
+
* @returns {Promise<KitGPSFormat>}
|
|
1209
|
+
*/
|
|
1210
|
+
async ipToCoords(ip) {
|
|
1211
|
+
const q = ip ? ip : '';
|
|
1212
|
+
const data = await _fetch(`http://ip-api.com/json/${q}?fields=status,lat,lon,city,regionName,country,countryCode,continent,zip`);
|
|
1213
|
+
if (data.status !== 'success') throw new Error('ip-api: ' + (data.message || 'failed'));
|
|
1214
|
+
return new KitGPSFormat(data.lat, data.lon, {
|
|
1215
|
+
city: data.city, state: data.regionName, country: data.country,
|
|
1216
|
+
countryCode: data.countryCode, continent: data.continent, zip: data.zip,
|
|
1217
|
+
});
|
|
1218
|
+
},
|
|
1219
|
+
|
|
1220
|
+
async ip() {
|
|
1221
|
+
// get the devices ip
|
|
1222
|
+
const data = await _fetch(`https://api.ipify.org?format=json`);
|
|
1223
|
+
return data.ip;
|
|
1224
|
+
},
|
|
1225
|
+
|
|
1226
|
+
domains: {
|
|
1227
|
+
ipify: `https://api.ipify.org?format=json`,
|
|
1228
|
+
ipapi: `http://ip-api.com/json/`,
|
|
1229
|
+
},
|
|
1230
|
+
|
|
1231
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1232
|
+
// FEATURE 2 – Coordinates → KitGPSFormat
|
|
1233
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1234
|
+
/** Create a KitGPSFormat from raw decimal degrees. */
|
|
1235
|
+
fromCoords(lat, lon, meta = {}) { return new KitGPSFormat(lat, lon, meta); },
|
|
1236
|
+
|
|
1237
|
+
/** Create from a [lat,lon] array */
|
|
1238
|
+
fromArray([lat,lon], meta={}) { return new KitGPSFormat(lat, lon, meta); },
|
|
1239
|
+
|
|
1240
|
+
/** Create from a GeoJSON Point feature or geometry */
|
|
1241
|
+
fromGeoJSON(geojson) {
|
|
1242
|
+
const c = geojson.type === 'Feature' ? geojson.geometry.coordinates : geojson.coordinates;
|
|
1243
|
+
return new KitGPSFormat(c[1], c[0]);
|
|
1244
|
+
},
|
|
1245
|
+
|
|
1246
|
+
/** Create from a DMS string e.g. "40°26′46″N 079°58′56″W" */
|
|
1247
|
+
fromDMS(str) { return _DMSToDecimal(str); },
|
|
1248
|
+
|
|
1249
|
+
/** Create from a geohash string */
|
|
1250
|
+
fromGeohash(h) { return geohash.decode(h); },
|
|
1251
|
+
|
|
1252
|
+
/** Create from a Plus Code */
|
|
1253
|
+
fromPlusCode(code) { return plusCode.decode(code); },
|
|
1254
|
+
|
|
1255
|
+
/** Create from a UTM object or string */
|
|
1256
|
+
fromUTM(u) { return utm.toLatLon(typeof u === 'string' ? (() => {
|
|
1257
|
+
const m=u.match(/(\d+)([A-Z])\s+([\d.]+)E?\s+([\d.]+)N?/i);
|
|
1258
|
+
return {zone:+m[1],band:m[2],easting:+m[3],northing:+m[4]};
|
|
1259
|
+
})() : u); },
|
|
1260
|
+
|
|
1261
|
+
/** Create from MGRS string */
|
|
1262
|
+
fromMGRS(s) { return mgrs.toLatLon(s); },
|
|
1263
|
+
|
|
1264
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1265
|
+
// FEATURE 3 – Reverse geocoding (coords → place name)
|
|
1266
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1267
|
+
/**
|
|
1268
|
+
* Reverse geocode using Nominatim (OpenStreetMap).
|
|
1269
|
+
* @returns {Promise<KitGPSFormat>} same point with meta populated
|
|
1270
|
+
*/
|
|
1271
|
+
async reverseGeocode(lat, lon) {
|
|
1272
|
+
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&zoom=14&addressdetails=1`;
|
|
1273
|
+
const d = await _fetch(url);
|
|
1274
|
+
const a = d.address || {};
|
|
1275
|
+
const cc = (a.country_code || '').toUpperCase();
|
|
1276
|
+
return new KitGPSFormat(lat, lon, {
|
|
1277
|
+
city: a.city || a.town || a.village || a.hamlet || a.county || '',
|
|
1278
|
+
state: a.state || a.region || '',
|
|
1279
|
+
country: a.country || '',
|
|
1280
|
+
countryCode: cc,
|
|
1281
|
+
continent: kitgps.continentFromCode(cc),
|
|
1282
|
+
zip: a.postcode || '',
|
|
1283
|
+
road: a.road || '',
|
|
1284
|
+
neighbourhood: a.neighbourhood || a.suburb || '',
|
|
1285
|
+
raw: d,
|
|
1286
|
+
});
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1289
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1290
|
+
// FEATURE 4 – Forward geocoding (address → coords)
|
|
1291
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1292
|
+
/**
|
|
1293
|
+
* Forward geocode an address string using Nominatim.
|
|
1294
|
+
* @returns {Promise<KitGPSFormat[]>}
|
|
1295
|
+
*/
|
|
1296
|
+
async geocode(address) {
|
|
1297
|
+
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(address)}&format=json&limit=5&addressdetails=1`;
|
|
1298
|
+
const results = await _fetch(url);
|
|
1299
|
+
return results.map(r => {
|
|
1300
|
+
const a = r.address || {};
|
|
1301
|
+
const cc = (a.country_code || '').toUpperCase();
|
|
1302
|
+
return new KitGPSFormat(+r.lat, +r.lon, {
|
|
1303
|
+
city: a.city || a.town || a.village || '',
|
|
1304
|
+
state: a.state || '',
|
|
1305
|
+
country: a.country || '',
|
|
1306
|
+
countryCode: cc,
|
|
1307
|
+
continent: kitgps.continentFromCode(cc),
|
|
1308
|
+
zip: a.postcode || '',
|
|
1309
|
+
displayName: r.display_name,
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
},
|
|
1313
|
+
|
|
1314
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1315
|
+
// FEATURE 5 – Device location
|
|
1316
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1317
|
+
/**
|
|
1318
|
+
* Get the device's current location.
|
|
1319
|
+
* Browser: uses navigator.geolocation.
|
|
1320
|
+
* Node.js fallback: uses IP-based lookup.
|
|
1321
|
+
* @param {PositionOptions} [opts]
|
|
1322
|
+
* @returns {Promise<KitGPSFormat>}
|
|
1323
|
+
*/
|
|
1324
|
+
async locate(opts = {}) {
|
|
1325
|
+
if (typeof navigator !== 'undefined' && navigator.geolocation) {
|
|
1326
|
+
return new Promise((resolve, reject) => {
|
|
1327
|
+
navigator.geolocation.getCurrentPosition(
|
|
1328
|
+
pos => resolve(new KitGPSFormat(pos.coords.latitude, pos.coords.longitude, {
|
|
1329
|
+
accuracy: pos.coords.accuracy,
|
|
1330
|
+
altitude: pos.coords.altitude,
|
|
1331
|
+
speed: pos.coords.speed,
|
|
1332
|
+
heading: pos.coords.heading,
|
|
1333
|
+
})),
|
|
1334
|
+
err => reject(err),
|
|
1335
|
+
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0, ...opts }
|
|
1336
|
+
);
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
// Node.js fallback – IP-based
|
|
1340
|
+
return kitgps.ipToCoords(null);
|
|
1341
|
+
},
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Watch the device's location continuously (browser only).
|
|
1345
|
+
* @param {Function} callback called with KitGPSFormat on each update
|
|
1346
|
+
* @param {Function} [errCb]
|
|
1347
|
+
* @returns {number} watchId to pass to kitgps.clearWatch()
|
|
1348
|
+
*/
|
|
1349
|
+
watchLocation(callback, errCb, opts = {}) {
|
|
1350
|
+
if (typeof navigator === 'undefined' || !navigator.geolocation)
|
|
1351
|
+
throw new Error('navigator.geolocation not available');
|
|
1352
|
+
return navigator.geolocation.watchPosition(
|
|
1353
|
+
pos => callback(new KitGPSFormat(pos.coords.latitude, pos.coords.longitude, {
|
|
1354
|
+
accuracy: pos.coords.accuracy, speed: pos.coords.speed, heading: pos.coords.heading,
|
|
1355
|
+
})),
|
|
1356
|
+
errCb,
|
|
1357
|
+
{ enableHighAccuracy: true, ...opts }
|
|
1358
|
+
);
|
|
1359
|
+
},
|
|
1360
|
+
|
|
1361
|
+
clearWatch(id) {
|
|
1362
|
+
if (typeof navigator !== 'undefined' && navigator.geolocation)
|
|
1363
|
+
navigator.geolocation.clearWatch(id);
|
|
1364
|
+
},
|
|
1365
|
+
|
|
1366
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1367
|
+
// FEATURE 6 – Elevation lookup
|
|
1368
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1369
|
+
/**
|
|
1370
|
+
* Fetch elevation in metres via Open-Elevation API.
|
|
1371
|
+
* @returns {Promise<number>}
|
|
1372
|
+
*/
|
|
1373
|
+
async elevation(lat, lon) {
|
|
1374
|
+
const d = await _fetch(`https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`);
|
|
1375
|
+
return d.results[0].elevation;
|
|
1376
|
+
},
|
|
1377
|
+
|
|
1378
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1379
|
+
// FEATURE 7 – Distance calculation
|
|
1380
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1381
|
+
/**
|
|
1382
|
+
* Distance between two points.
|
|
1383
|
+
* @param {number|KitGPSFormat|[lat,lon]} lat1
|
|
1384
|
+
* @param {number} lon1
|
|
1385
|
+
* @param {number} lat2
|
|
1386
|
+
* @param {number} lon2
|
|
1387
|
+
* @param {'km'|'mi'|'m'|'nm'} [unit='km']
|
|
1388
|
+
* @param {'haversine'|'vincenty'} [method='haversine']
|
|
1389
|
+
*/
|
|
1390
|
+
distance(lat1, lon1, lat2, lon2, unit='km', method='haversine') {
|
|
1391
|
+
let km;
|
|
1392
|
+
if (lat1 instanceof KitGPSFormat && lon1 instanceof KitGPSFormat) {
|
|
1393
|
+
km = method==='vincenty' ? _vincentyDist(lat1.lat,lat1.lon,lon1.lat,lon1.lon)
|
|
1394
|
+
: _haversine(lat1.lat,lat1.lon,lon1.lat,lon1.lon);
|
|
1395
|
+
} else {
|
|
1396
|
+
km = method==='vincenty' ? _vincentyDist(lat1,lon1,lat2,lon2)
|
|
1397
|
+
: _haversine(lat1,lon1,lat2,lon2);
|
|
1398
|
+
}
|
|
1399
|
+
const conv = {km:1, mi:0.621371, m:1000, nm:0.539957};
|
|
1400
|
+
return km * (conv[unit] || 1);
|
|
1401
|
+
},
|
|
1402
|
+
|
|
1403
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1404
|
+
// FEATURE 8 – Bearing
|
|
1405
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1406
|
+
bearing(lat1, lon1, lat2, lon2) {
|
|
1407
|
+
const φ1=_toRad(lat1),φ2=_toRad(lat2),Δλ=_toRad(lon2-lon1);
|
|
1408
|
+
const y=Math.sin(Δλ)*Math.cos(φ2);
|
|
1409
|
+
const x=Math.cos(φ1)*Math.sin(φ2)-Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ);
|
|
1410
|
+
return _mod(_toDeg(Math.atan2(y,x)), 360);
|
|
1411
|
+
},
|
|
1412
|
+
|
|
1413
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1414
|
+
// FEATURE 9 – Midpoint
|
|
1415
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1416
|
+
midpoint(lat1, lon1, lat2, lon2) {
|
|
1417
|
+
const φ1=_toRad(lat1),φ2=_toRad(lat2);
|
|
1418
|
+
const Δλ=_toRad(lon2-lon1);
|
|
1419
|
+
const Bx=Math.cos(φ2)*Math.cos(Δλ), By=Math.cos(φ2)*Math.sin(Δλ);
|
|
1420
|
+
const φm=Math.atan2(Math.sin(φ1)+Math.sin(φ2),Math.sqrt((Math.cos(φ1)+Bx)**2+By**2));
|
|
1421
|
+
const λm=_toRad(lon1)+Math.atan2(By,Math.cos(φ1)+Bx);
|
|
1422
|
+
return new KitGPSFormat(_toDeg(φm), _toDeg(λm));
|
|
1423
|
+
},
|
|
1424
|
+
|
|
1425
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1426
|
+
// FEATURE 10 – Bounding box
|
|
1427
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1428
|
+
/**
|
|
1429
|
+
* Compute bounding box for a set of coordinates.
|
|
1430
|
+
* @param {[lat,lon][]} coords
|
|
1431
|
+
* @returns {{ minLat, minLon, maxLat, maxLon, center }}
|
|
1432
|
+
*/
|
|
1433
|
+
boundingBox(coords) {
|
|
1434
|
+
const lats = coords.map(c=>c[0]), lons = coords.map(c=>c[1]);
|
|
1435
|
+
const minLat=Math.min(...lats), maxLat=Math.max(...lats);
|
|
1436
|
+
const minLon=Math.min(...lons), maxLon=Math.max(...lons);
|
|
1437
|
+
return { minLat, minLon, maxLat, maxLon,
|
|
1438
|
+
center: new KitGPSFormat((minLat+maxLat)/2, (minLon+maxLon)/2) };
|
|
1439
|
+
},
|
|
1440
|
+
|
|
1441
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1442
|
+
// FEATURE 11 – Point in polygon
|
|
1443
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1444
|
+
pointInPolygon(lat, lon, polygon) { return _pointInPolygon(lat, lon, polygon); },
|
|
1445
|
+
|
|
1446
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1447
|
+
// FEATURE 12 – Compass direction string
|
|
1448
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1449
|
+
compassDirection(bearing, mode='cardinal') {
|
|
1450
|
+
const cardinals = ['N','NE','E','SE','S','SW','W','NW'];
|
|
1451
|
+
const ordinals = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSW','SW','WSW','W','WNW','NW','NNW'];
|
|
1452
|
+
const full = ['North','North-Northeast','Northeast','East-Northeast','East','East-Southeast','Southeast',
|
|
1453
|
+
'South-Southeast','South','South-Southwest','Southwest','West-Southwest','West',
|
|
1454
|
+
'West-Northwest','Northwest','North-Northwest'];
|
|
1455
|
+
const b = _mod(bearing, 360);
|
|
1456
|
+
if (mode==='ordinal') return ordinals[Math.round(b/22.5)%16];
|
|
1457
|
+
if (mode==='full') return full[Math.round(b/22.5)%16];
|
|
1458
|
+
return cardinals[Math.round(b/45)%8];
|
|
1459
|
+
},
|
|
1460
|
+
|
|
1461
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1462
|
+
// FEATURE 13 – Speed calculation
|
|
1463
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1464
|
+
/**
|
|
1465
|
+
* @param {KitGPSFormat} p1 with .timestamp (ms)
|
|
1466
|
+
* @param {KitGPSFormat} p2 with .timestamp (ms)
|
|
1467
|
+
* @param {'kph'|'mph'|'knots'|'ms'} unit
|
|
1468
|
+
*/
|
|
1469
|
+
speed(p1, p2, unit='kph') {
|
|
1470
|
+
const km = _haversine(p1.lat,p1.lon,p2.lat,p2.lon);
|
|
1471
|
+
const hrs = Math.abs((p2.timestamp||0)-(p1.timestamp||0)) / 3600000;
|
|
1472
|
+
if (!hrs) return 0;
|
|
1473
|
+
const kph = km / hrs;
|
|
1474
|
+
const conv = { kph:1, mph:0.621371, knots:0.539957, ms:0.277778 };
|
|
1475
|
+
return +(kph * (conv[unit]||1)).toFixed(4);
|
|
1476
|
+
},
|
|
1477
|
+
|
|
1478
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1479
|
+
// FEATURE 14 – Coordinate validation & normalization
|
|
1480
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1481
|
+
isValidLatLon(lat, lon) {
|
|
1482
|
+
return Number.isFinite(lat) && Number.isFinite(lon) && lat>=-90 && lat<=90 && lon>=-180 && lon<=180;
|
|
1483
|
+
},
|
|
1484
|
+
normalizeLatLon(lat, lon) {
|
|
1485
|
+
return { lat: _clamp(lat,-90,90), lon: ((lon+180)%360+360)%360-180 };
|
|
1486
|
+
},
|
|
1487
|
+
|
|
1488
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1489
|
+
// FEATURE 15 – DMS ↔ decimal
|
|
1490
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1491
|
+
decimalToDMS(lat, lon) { return _decimalToDMS(lat,lon); },
|
|
1492
|
+
decimalToDDM(lat, lon) { return _decimalToDDM(lat,lon); },
|
|
1493
|
+
dmsToDecimal(str) { return _DMSToDecimal(str); },
|
|
1494
|
+
|
|
1495
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1496
|
+
// FEATURE 16 – Timezone from coordinates (local approx)
|
|
1497
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1498
|
+
timezoneApprox(lat, lon) { return _timezoneApprox(lat, lon); },
|
|
1499
|
+
|
|
1500
|
+
/** Fetch precise timezone via timeapi.io */
|
|
1501
|
+
async timezone(lat, lon) {
|
|
1502
|
+
const d = await _fetch(`https://timeapi.io/api/TimeZone/coordinate?latitude=${lat}&longitude=${lon}`);
|
|
1503
|
+
return { timeZone: d.timeZone, currentLocalTime: d.currentLocalTime, utcOffset: d.currentUtcOffset?.seconds/3600 };
|
|
1504
|
+
},
|
|
1505
|
+
|
|
1506
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1507
|
+
// FEATURE 17 – Sun rise / set
|
|
1508
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1509
|
+
sunTimes(lat, lon, date = new Date()) { return _sunTimes(lat, lon, date); },
|
|
1510
|
+
|
|
1511
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1512
|
+
// FEATURE 18 – Moon phase
|
|
1513
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1514
|
+
moonPhase(date = new Date()) { return _moonPhase(date); },
|
|
1515
|
+
|
|
1516
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1517
|
+
// FEATURE 19 – Magnetic declination
|
|
1518
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1519
|
+
magneticDeclination(lat, lon, year) { return _magDeclination(lat, lon, year); },
|
|
1520
|
+
|
|
1521
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1522
|
+
// FEATURE 20 – Nearest country lookup
|
|
1523
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1524
|
+
nearestCountry(lat, lon) {
|
|
1525
|
+
let best=null, bestDist=Infinity;
|
|
1526
|
+
for (const c of _RAW_COUNTRIES) {
|
|
1527
|
+
const d=_haversine(lat,lon,c.lat,c.lon);
|
|
1528
|
+
if (d<bestDist) { bestDist=d; best=c; }
|
|
1529
|
+
}
|
|
1530
|
+
return { country:best, distanceKm:bestDist };
|
|
1531
|
+
},
|
|
1532
|
+
|
|
1533
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1534
|
+
// FEATURE 21 – Continent from country code
|
|
1535
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1536
|
+
continentFromCode(code) {
|
|
1537
|
+
if (!code) return null;
|
|
1538
|
+
const c = (code||'').toUpperCase();
|
|
1539
|
+
// Check detailed country DB first
|
|
1540
|
+
const found = _RAW_COUNTRIES.find(x=>x.code===c);
|
|
1541
|
+
if (found) return found.continent;
|
|
1542
|
+
return _CONTINENT_MAP[c] || null;
|
|
1543
|
+
},
|
|
1544
|
+
|
|
1545
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1546
|
+
// FEATURE 22 – Country flag emoji
|
|
1547
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1548
|
+
countryFlag(code) {
|
|
1549
|
+
if (!code || code.length!==2) return '🌍';
|
|
1550
|
+
return [...code.toUpperCase()].map(c=>String.fromCodePoint(0x1F1E6-65+c.charCodeAt(0))).join('');
|
|
1551
|
+
},
|
|
1552
|
+
|
|
1553
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1554
|
+
// FEATURE 23 – Country metadata lookup
|
|
1555
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1556
|
+
country(code) {
|
|
1557
|
+
return _RAW_COUNTRIES.find(c=>c.code===(code||'').toUpperCase()) || null;
|
|
1558
|
+
},
|
|
1559
|
+
|
|
1560
|
+
/** Find countries by name (partial, case-insensitive) */
|
|
1561
|
+
searchCountries(query) {
|
|
1562
|
+
const q=query.toLowerCase();
|
|
1563
|
+
return _RAW_COUNTRIES.filter(c=>c.name.toLowerCase().includes(q)||c.code.toLowerCase().includes(q));
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1567
|
+
// FEATURE 24 – Cities search
|
|
1568
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1569
|
+
/**
|
|
1570
|
+
* Search cities within a given country code, or across all countries.
|
|
1571
|
+
* @returns {Array<{city,country,countryCode}>}
|
|
1572
|
+
*/
|
|
1573
|
+
searchCities(query, countryCode) {
|
|
1574
|
+
const q=query.toLowerCase();
|
|
1575
|
+
const pool = countryCode
|
|
1576
|
+
? (_RAW_COUNTRIES.filter(c=>c.code===(countryCode||'').toUpperCase()))
|
|
1577
|
+
: _RAW_COUNTRIES;
|
|
1578
|
+
const hits=[];
|
|
1579
|
+
for (const c of pool)
|
|
1580
|
+
for (const city of c.cities)
|
|
1581
|
+
if (city.toLowerCase().includes(q)) hits.push({city, country:c.name, countryCode:c.code});
|
|
1582
|
+
return hits;
|
|
1583
|
+
},
|
|
1584
|
+
|
|
1585
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1586
|
+
// FEATURE 25 – ZIP code → coordinates (US sample)
|
|
1587
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1588
|
+
zipToCoords(zip) {
|
|
1589
|
+
const entry = _ZIP_LUT[String(zip).trim()];
|
|
1590
|
+
if (!entry) return null;
|
|
1591
|
+
return new KitGPSFormat(entry.lat, entry.lon, {
|
|
1592
|
+
city:entry.city, state:entry.state, country:'United States', countryCode:'US',
|
|
1593
|
+
continent:'North America', zip:String(zip),
|
|
1594
|
+
});
|
|
1595
|
+
},
|
|
1596
|
+
|
|
1597
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1598
|
+
// FEATURE 26 – Coordinate grid generator
|
|
1599
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1600
|
+
/**
|
|
1601
|
+
* Generate a lat/lon grid within a bounding box.
|
|
1602
|
+
* @returns {{ latLines:[number], lonLines:[number] }}
|
|
1603
|
+
*/
|
|
1604
|
+
grid(minLat=-90,minLon=-180,maxLat=90,maxLon=180,stepDeg=10) {
|
|
1605
|
+
const latLines=[], lonLines=[];
|
|
1606
|
+
for(let l=Math.ceil(minLat/stepDeg)*stepDeg;l<=maxLat;l+=stepDeg) latLines.push(l);
|
|
1607
|
+
for(let l=Math.ceil(minLon/stepDeg)*stepDeg;l<=maxLon;l+=stepDeg) lonLines.push(l);
|
|
1608
|
+
return {latLines, lonLines};
|
|
1609
|
+
},
|
|
1610
|
+
|
|
1611
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1612
|
+
// FEATURE 27 – Random coordinate generator
|
|
1613
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1614
|
+
randomCoord(minLat=-90,maxLat=90,minLon=-180,maxLon=180) {
|
|
1615
|
+
const lat=minLat + Math.random()*(maxLat-minLat);
|
|
1616
|
+
const lon=minLon + Math.random()*(maxLon-minLon);
|
|
1617
|
+
return new KitGPSFormat(lat, lon);
|
|
1618
|
+
},
|
|
1619
|
+
|
|
1620
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1621
|
+
// FEATURE 28 – Interpolate path
|
|
1622
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1623
|
+
interpolate(lat1,lon1,lat2,lon2,steps=10) { return _interpolate(lat1,lon1,lat2,lon2,steps); },
|
|
1624
|
+
|
|
1625
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1626
|
+
// FEATURE 29 – Simplify path (Ramer-Douglas-Peucker)
|
|
1627
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1628
|
+
/** @param {[lat,lon][]} coords @param {number} epsilon km */
|
|
1629
|
+
simplifyPath(coords, epsilon=0.1) { return _rdpSimplify(coords, epsilon); },
|
|
1630
|
+
|
|
1631
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1632
|
+
// FEATURE 30 – Total path length
|
|
1633
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1634
|
+
/** @param {[lat,lon][]} coords @param {'km'|'mi'|'m'|'nm'} [unit='km'] */
|
|
1635
|
+
pathLength(coords, unit='km') {
|
|
1636
|
+
const conv={km:1,mi:0.621371,m:1000,nm:0.539957};
|
|
1637
|
+
return _pathLength(coords)*(conv[unit]||1);
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1640
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1641
|
+
// FEATURE 31 – Polygon area
|
|
1642
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1643
|
+
/** @returns {number} area in km² */
|
|
1644
|
+
polygonArea(coords) { return _polygonArea(coords); },
|
|
1645
|
+
|
|
1646
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1647
|
+
// FEATURE 32 – Centroid of coordinate cluster
|
|
1648
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1649
|
+
centroid(coords) { return _centroid(coords); },
|
|
1650
|
+
|
|
1651
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1652
|
+
// FEATURE 33 – Nearby places filter
|
|
1653
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1654
|
+
/**
|
|
1655
|
+
* Filter an array of { lat, lon, ...extras } objects within radiusKm of a center.
|
|
1656
|
+
*/
|
|
1657
|
+
nearby(lat, lon, places, radiusKm=10) {
|
|
1658
|
+
return places
|
|
1659
|
+
.map(p => ({ ...p, distanceKm: _haversine(lat,lon,p.lat||p[0],p.lon||p[1]) }))
|
|
1660
|
+
.filter(p => p.distanceKm <= radiusKm)
|
|
1661
|
+
.sort((a,b)=>a.distanceKm-b.distanceKm);
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1664
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1665
|
+
// FEATURE 34 – Great-circle waypoints
|
|
1666
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1667
|
+
greatCircle(lat1,lon1,lat2,lon2,n=10) { return _greatCircleWaypoints(lat1,lon1,lat2,lon2,n); },
|
|
1668
|
+
|
|
1669
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1670
|
+
// FEATURE 35 – Destination point from bearing + distance
|
|
1671
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1672
|
+
/** @param {number} distKm @param {number} bearingDeg */
|
|
1673
|
+
destinationPoint(lat,lon,distKm,bearingDeg) {
|
|
1674
|
+
const φ1=_toRad(lat), λ1=_toRad(lon), θ=_toRad(bearingDeg);
|
|
1675
|
+
const δ=distKm/R_EARTH_KM;
|
|
1676
|
+
const φ2=Math.asin(Math.sin(φ1)*Math.cos(δ)+Math.cos(φ1)*Math.sin(δ)*Math.cos(θ));
|
|
1677
|
+
const λ2=λ1+Math.atan2(Math.sin(θ)*Math.sin(δ)*Math.cos(φ1),Math.cos(δ)-Math.sin(φ1)*Math.sin(φ2));
|
|
1678
|
+
return new KitGPSFormat(_toDeg(φ2), _toDeg(_mod(λ2+Math.PI,2*Math.PI)-Math.PI));
|
|
1679
|
+
},
|
|
1680
|
+
|
|
1681
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1682
|
+
// FEATURE 36 – Crosstrack & along-track distance
|
|
1683
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1684
|
+
crossTrackDistance(lat,lon,startLat,startLon,endLat,endLon) {
|
|
1685
|
+
const d13=_haversine(startLat,startLon,lat,lon)/R_EARTH_KM;
|
|
1686
|
+
const θ13=_toRad(kitgps.bearing(startLat,startLon,lat,lon));
|
|
1687
|
+
const θ12=_toRad(kitgps.bearing(startLat,startLon,endLat,endLon));
|
|
1688
|
+
return Math.asin(Math.sin(d13)*Math.sin(θ13-θ12))*R_EARTH_KM;
|
|
1689
|
+
},
|
|
1690
|
+
|
|
1691
|
+
alongTrackDistance(lat,lon,startLat,startLon,endLat,endLon) {
|
|
1692
|
+
const d13=_haversine(startLat,startLon,lat,lon)/R_EARTH_KM;
|
|
1693
|
+
const dxt=kitgps.crossTrackDistance(lat,lon,startLat,startLon,endLat,endLon)/R_EARTH_KM;
|
|
1694
|
+
return Math.acos(Math.cos(d13)/Math.cos(dxt))*R_EARTH_KM;
|
|
1695
|
+
},
|
|
1696
|
+
|
|
1697
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1698
|
+
// FEATURE 37 – Intersection of two paths (Napier's rules)
|
|
1699
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1700
|
+
intersection(lat1,lon1,brg1,lat2,lon2,brg2) {
|
|
1701
|
+
const φ1=_toRad(lat1),λ1=_toRad(lon1),φ2=_toRad(lat2),λ2=_toRad(lon2);
|
|
1702
|
+
const θ13=_toRad(brg1),θ23=_toRad(brg2);
|
|
1703
|
+
const Δφ=φ2-φ1,Δλ=λ2-λ1;
|
|
1704
|
+
const δ12=2*Math.asin(Math.sqrt(Math.sin(Δφ/2)**2+Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2));
|
|
1705
|
+
if(Math.abs(δ12)<Number.EPSILON) return null;
|
|
1706
|
+
const θa=Math.acos((Math.sin(φ2)-Math.sin(φ1)*Math.cos(δ12))/(Math.sin(δ12)*Math.cos(φ1)));
|
|
1707
|
+
const θb=Math.acos((Math.sin(φ1)-Math.sin(φ2)*Math.cos(δ12))/(Math.sin(δ12)*Math.cos(φ2)));
|
|
1708
|
+
const θ12=Math.sin(λ2-λ1)>0?θa:2*Math.PI-θa;
|
|
1709
|
+
const θ21=Math.sin(λ2-λ1)>0?2*Math.PI-θb:θb;
|
|
1710
|
+
const α1=θ13-θ12, α2=θ21-θ23;
|
|
1711
|
+
if(Math.sin(α1)===0&&Math.sin(α2)===0) return null;
|
|
1712
|
+
const α3=Math.acos(-Math.cos(α1)*Math.cos(α2)+Math.sin(α1)*Math.sin(α2)*Math.cos(δ12));
|
|
1713
|
+
const δ13=Math.atan2(Math.sin(δ12)*Math.sin(α1)*Math.sin(α2),Math.cos(α2)+Math.cos(α1)*Math.cos(α3));
|
|
1714
|
+
const φ3=Math.asin(Math.sin(φ1)*Math.cos(δ13)+Math.cos(φ1)*Math.sin(δ13)*Math.cos(θ13));
|
|
1715
|
+
const Δλ13=Math.atan2(Math.sin(θ13)*Math.sin(δ13)*Math.cos(φ1),Math.cos(δ13)-Math.sin(φ1)*Math.sin(φ3));
|
|
1716
|
+
return new KitGPSFormat(_toDeg(φ3), _toDeg(_mod(λ1+Δλ13+Math.PI,2*Math.PI)-Math.PI));
|
|
1717
|
+
},
|
|
1718
|
+
|
|
1719
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1720
|
+
// FEATURE 38 – Route builder
|
|
1721
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1722
|
+
/**
|
|
1723
|
+
* Fluent route / waypoint list.
|
|
1724
|
+
* @example
|
|
1725
|
+
* const route = kitgps.route()
|
|
1726
|
+
* .add(48.85, 2.35, 'Paris')
|
|
1727
|
+
* .add(51.50, -0.12, 'London')
|
|
1728
|
+
* .summary()
|
|
1729
|
+
*/
|
|
1730
|
+
route() {
|
|
1731
|
+
const waypoints = [];
|
|
1732
|
+
const api = {
|
|
1733
|
+
add(lat, lon, name='') {
|
|
1734
|
+
const pt = new KitGPSFormat(lat,lon,{name});
|
|
1735
|
+
pt.name = name;
|
|
1736
|
+
waypoints.push(pt);
|
|
1737
|
+
return api;
|
|
1738
|
+
},
|
|
1739
|
+
waypoints() { return waypoints; },
|
|
1740
|
+
totalDistance(unit='km') {
|
|
1741
|
+
return kitgps.pathLength(waypoints.map(p=>[p.lat,p.lon]),unit);
|
|
1742
|
+
},
|
|
1743
|
+
summary() {
|
|
1744
|
+
const legs=[];
|
|
1745
|
+
for(let i=0;i<waypoints.length-1;i++){
|
|
1746
|
+
const [a,b]=[waypoints[i],waypoints[i+1]];
|
|
1747
|
+
legs.push({
|
|
1748
|
+
from:a.name||a.toDecimal(), to:b.name||b.toDecimal(),
|
|
1749
|
+
distanceKm:_haversine(a.lat,a.lon,b.lat,b.lon),
|
|
1750
|
+
bearing:kitgps.bearing(a.lat,a.lon,b.lat,b.lon),
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
return { waypoints: waypoints.length, legs, totalKm: api.totalDistance('km') };
|
|
1754
|
+
},
|
|
1755
|
+
toGeoJSON() { return _toGeoJSON_LineString(waypoints.map(p=>[p.lat,p.lon])); },
|
|
1756
|
+
toKML(name) { return _toKML(waypoints.map(p=>[p.lat,p.lon]),name); },
|
|
1757
|
+
toGPX(name) { return _toGPX(waypoints.map(p=>[p.lat,p.lon]),name); },
|
|
1758
|
+
};
|
|
1759
|
+
return api;
|
|
1760
|
+
},
|
|
1761
|
+
|
|
1762
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1763
|
+
// FEATURE 39 – GeoJSON helpers
|
|
1764
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1765
|
+
geoJSON: {
|
|
1766
|
+
point(lat,lon,props={}) { return { type:'Feature', properties:props, geometry:{ type:'Point', coordinates:[lon,lat] } }; },
|
|
1767
|
+
lineString(coords,props={}) { return _toGeoJSON_LineString(coords,props); },
|
|
1768
|
+
polygon(rings,props={}) { return _toGeoJSON_Polygon(rings,props); },
|
|
1769
|
+
featureCollection(features) { return _toGeoJSON_FC(features); },
|
|
1770
|
+
},
|
|
1771
|
+
|
|
1772
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1773
|
+
// FEATURE 40 – KML / GPX export
|
|
1774
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1775
|
+
toKML(points, name) { return _toKML(points.map(p=>Array.isArray(p)?p:[p.lat,p.lon]),name); },
|
|
1776
|
+
toGPX(points, name) { return _toGPX(points.map(p=>Array.isArray(p)?p:[p.lat,p.lon]),name); },
|
|
1777
|
+
|
|
1778
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1779
|
+
// FEATURE 41 – W3W-style offline word encoding
|
|
1780
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1781
|
+
toWordAddress(lat,lon) { return w3w.encode(lat,lon); },
|
|
1782
|
+
|
|
1783
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1784
|
+
// FEATURE 42 – Magnetic declination
|
|
1785
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1786
|
+
// (alias – declared above under FEATURE 19)
|
|
1787
|
+
|
|
1788
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1789
|
+
// FEATURE 43 – Convert coordinate units in bulk
|
|
1790
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1791
|
+
/**
|
|
1792
|
+
* Bulk-convert an array of decimal [lat,lon] pairs to a target format.
|
|
1793
|
+
* @param {[lat,lon][]} coords
|
|
1794
|
+
* @param {'dms'|'ddm'|'geohash'|'utm'|'mgrs'|'pluscode'|'decimal'} to
|
|
1795
|
+
*/
|
|
1796
|
+
bulkConvert(coords, to) {
|
|
1797
|
+
return coords.map(([lat,lon]) => {
|
|
1798
|
+
const pt = new KitGPSFormat(lat,lon);
|
|
1799
|
+
switch(to){
|
|
1800
|
+
case 'dms': return pt.toDMS();
|
|
1801
|
+
case 'ddm': return pt.toDDM();
|
|
1802
|
+
case 'geohash': return pt.toGeohash();
|
|
1803
|
+
case 'utm': return pt.toUTM();
|
|
1804
|
+
case 'mgrs': return pt.toMGRS();
|
|
1805
|
+
case 'pluscode': return pt.toPlusCode();
|
|
1806
|
+
default: return pt.toDecimal();
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
},
|
|
1810
|
+
|
|
1811
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1812
|
+
// FEATURE 44 – Hemisphere & antipode
|
|
1813
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1814
|
+
hemisphere(lat, lon) {
|
|
1815
|
+
return {
|
|
1816
|
+
latHemisphere: lat >= 0 ? 'Northern' : 'Southern',
|
|
1817
|
+
lonHemisphere: lon >= 0 ? 'Eastern' : 'Western',
|
|
1818
|
+
};
|
|
1819
|
+
},
|
|
1820
|
+
|
|
1821
|
+
antipode(lat, lon) {
|
|
1822
|
+
return new KitGPSFormat(-lat, lon > 0 ? lon-180 : lon+180);
|
|
1823
|
+
},
|
|
1824
|
+
|
|
1825
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1826
|
+
// FEATURE 45 – Parse any coordinate string (auto-detect format)
|
|
1827
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1828
|
+
parse(input) {
|
|
1829
|
+
if (typeof input !== 'string') throw new TypeError('Expected string');
|
|
1830
|
+
const s = input.trim();
|
|
1831
|
+
// Geohash? (all lowercase base32, length 4-12)
|
|
1832
|
+
if (/^[0-9bcdefghjkmnpqrstuvwxyz]{4,12}$/i.test(s) && !s.includes(','))
|
|
1833
|
+
return geohash.decode(s.toLowerCase());
|
|
1834
|
+
// Plus code?
|
|
1835
|
+
if (/^[23456789CFGHJMPQRVWX]+\+[23456789CFGHJMPQRVWX]*$/i.test(s))
|
|
1836
|
+
return plusCode.decode(s.toUpperCase());
|
|
1837
|
+
// DMS?
|
|
1838
|
+
if (/[°d′'″"NSEW]/i.test(s)) return _DMSToDecimal(s);
|
|
1839
|
+
// "lat, lon" decimal?
|
|
1840
|
+
const m = s.match(/^(-?[\d.]+)\s*[,\s]\s*(-?[\d.]+)$/);
|
|
1841
|
+
if (m) return new KitGPSFormat(+m[1],+m[2]);
|
|
1842
|
+
// ISO 6709?
|
|
1843
|
+
const iso = s.match(/^([+-][\d.]+)([+-][\d.]+)\/?$/);
|
|
1844
|
+
if (iso) return new KitGPSFormat(+iso[1],+iso[2]);
|
|
1845
|
+
throw new Error('KitGPS: unable to parse coordinate string: '+s);
|
|
1846
|
+
},
|
|
1847
|
+
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1851
|
+
// 18. Exports
|
|
1852
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1853
|
+
let kitdef = {};
|
|
1854
|
+
kitdef = kitgps;
|
|
1855
|
+
kitdef.default = kitgps;
|
|
1856
|
+
kitdef.KitGPSFormat = KitGPSFormat;
|
|
1857
|
+
kitdef.geohash = geohash;
|
|
1858
|
+
kitdef.utm = utm;
|
|
1859
|
+
kitdef.mgrs = mgrs;
|
|
1860
|
+
kitdef.plusCode = plusCode;
|
|
1861
|
+
kitdef.w3w = w3w;
|
|
1862
|
+
module.exports={ kitdef };
|