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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /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 };