oglap-ggp-node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/oglap-ggp-node-1.0.0.tgz +0 -0
- package/oglap.js +1800 -0
- package/package.json +37 -0
- package/test.js +349 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oglap-ggp-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Permettre aux développeurs d'installer rapidement le SDK et ses dépendances pour commencer à l'utiliser dans leurs projets.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"oglap",
|
|
7
|
+
"oglap-ggp",
|
|
8
|
+
"geolocation",
|
|
9
|
+
"maps",
|
|
10
|
+
"guinea",
|
|
11
|
+
"conakry",
|
|
12
|
+
"guinee",
|
|
13
|
+
"addressing",
|
|
14
|
+
"protocol",
|
|
15
|
+
"offline",
|
|
16
|
+
"address",
|
|
17
|
+
"offline",
|
|
18
|
+
"maps",
|
|
19
|
+
"oglap-maps",
|
|
20
|
+
"kiraa"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/Guinee-IO/oglap-ggp-node-js#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Guinee-IO/oglap-ggp-node-js/issues"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Guinee-IO/oglap-ggp-node-js.git"
|
|
29
|
+
},
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"author": "Guinee IO",
|
|
32
|
+
"type": "commonjs",
|
|
33
|
+
"main": "oglap.js",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node test.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// test.js — Full OGLAP engine test
|
|
2
|
+
import {
|
|
3
|
+
// Init & state
|
|
4
|
+
initOglap,
|
|
5
|
+
loadOglap,
|
|
6
|
+
checkOglap,
|
|
7
|
+
getPackageVersion,
|
|
8
|
+
getCountryProfile,
|
|
9
|
+
getCountryCode,
|
|
10
|
+
getCountrySW,
|
|
11
|
+
getOglapPrefectures,
|
|
12
|
+
getOglapPlaces,
|
|
13
|
+
// Core functions
|
|
14
|
+
parseLapCode,
|
|
15
|
+
validateLapCode,
|
|
16
|
+
getPlaceByLapCode,
|
|
17
|
+
lapToCoordinates,
|
|
18
|
+
coordinatesToLap,
|
|
19
|
+
bboxFromGeometry,
|
|
20
|
+
centroidFromBbox,
|
|
21
|
+
} from './oglap.js';
|
|
22
|
+
|
|
23
|
+
// ── Helpers ──
|
|
24
|
+
let passed = 0, failed = 0;
|
|
25
|
+
function section(title) { console.log(`\n${'═'.repeat(60)}\n ${title}\n${'═'.repeat(60)}`); }
|
|
26
|
+
function log(label, value) { console.log(` ${label}:`, value); }
|
|
27
|
+
function ok(label, value) { passed++; console.log(` ✓ ${label}:`, value); }
|
|
28
|
+
function fail(label, value) { failed++; console.error(` ✗ ${label}:`, value); }
|
|
29
|
+
|
|
30
|
+
// ══════════════════════════════════════════════════════════════
|
|
31
|
+
// 1. PRE-INIT STATE
|
|
32
|
+
// ══════════════════════════════════════════════════════════════
|
|
33
|
+
section('1. Pre-init state');
|
|
34
|
+
|
|
35
|
+
log('Package version', getPackageVersion());
|
|
36
|
+
|
|
37
|
+
const preCheck = checkOglap();
|
|
38
|
+
if (!preCheck.ok) ok('checkOglap() before init', preCheck.error);
|
|
39
|
+
else fail('checkOglap() should not be ok before init', preCheck);
|
|
40
|
+
|
|
41
|
+
// ══════════════════════════════════════════════════════════════
|
|
42
|
+
// 2. INIT (download mode)
|
|
43
|
+
// ══════════════════════════════════════════════════════════════
|
|
44
|
+
section('2. initOglap() — downloading latest');
|
|
45
|
+
|
|
46
|
+
const report = await initOglap({
|
|
47
|
+
onProgress({ label, status, percent, step, totalSteps }) {
|
|
48
|
+
if (status === 'downloading') {
|
|
49
|
+
process.stdout.write(`\r ↓ [${step}/${totalSteps}] ${label}: ${percent}% `);
|
|
50
|
+
} else if (status === 'cached') {
|
|
51
|
+
console.log(` ⚡ [${step}/${totalSteps}] ${label}: loaded from cache`);
|
|
52
|
+
} else if (status === 'slow') {
|
|
53
|
+
console.log(`\n ⚠ Slow network detected for ${label}`);
|
|
54
|
+
} else if (status === 'done') {
|
|
55
|
+
console.log(`\r ✓ [${step}/${totalSteps}] ${label}: done `);
|
|
56
|
+
} else if (status === 'error') {
|
|
57
|
+
console.log(`\n ✗ [${step}/${totalSteps}] ${label}: ERROR`);
|
|
58
|
+
} else if (status === 'validating') {
|
|
59
|
+
console.log(` … Validating configuration`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log('\n --- Init report ---');
|
|
65
|
+
log('OK', report.ok);
|
|
66
|
+
log('Country', `${report.countryName} (${report.countryCode})`);
|
|
67
|
+
log('Bounds', report.bounds);
|
|
68
|
+
log('Data dir', report.dataDir);
|
|
69
|
+
if (report.dataLoaded) log('Places loaded', report.dataLoaded.message);
|
|
70
|
+
console.log(' --- Checks ---');
|
|
71
|
+
report.checks.forEach(c => {
|
|
72
|
+
const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '⚠' : '✗';
|
|
73
|
+
console.log(` ${icon} [${c.id}] ${c.message}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!report.ok) {
|
|
77
|
+
fail('initOglap failed — cannot continue', report.error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
ok('initOglap', 'success');
|
|
81
|
+
|
|
82
|
+
// ══════════════════════════════════════════════════════════════
|
|
83
|
+
// 3. STATE GETTERS
|
|
84
|
+
// ══════════════════════════════════════════════════════════════
|
|
85
|
+
section('3. State getters');
|
|
86
|
+
|
|
87
|
+
const postCheck = checkOglap();
|
|
88
|
+
postCheck.ok ? ok('checkOglap()', `ok, country=${postCheck.countryCode}`) : fail('checkOglap()', postCheck);
|
|
89
|
+
|
|
90
|
+
log('getCountryCode()', getCountryCode());
|
|
91
|
+
log('getCountrySW()', getCountrySW());
|
|
92
|
+
|
|
93
|
+
const profile = getCountryProfile();
|
|
94
|
+
log('getCountryProfile() schema', profile.schema_id);
|
|
95
|
+
|
|
96
|
+
const prefectures = getOglapPrefectures();
|
|
97
|
+
const prefKeys = Object.keys(prefectures);
|
|
98
|
+
log('getOglapPrefectures()', `${prefKeys.length} entries (first 5: ${prefKeys.slice(0, 5).join(', ')})`);
|
|
99
|
+
|
|
100
|
+
const places = getOglapPlaces();
|
|
101
|
+
log('getOglapPlaces()', `${places.length} places`);
|
|
102
|
+
|
|
103
|
+
// ══════════════════════════════════════════════════════════════
|
|
104
|
+
// 4. coordinatesToLap — encode GPS → LAP code
|
|
105
|
+
// ══════════════════════════════════════════════════════════════
|
|
106
|
+
section('4. coordinatesToLap — GPS → LAP');
|
|
107
|
+
|
|
108
|
+
const testCoords = [
|
|
109
|
+
{ name: 'Conakry center', lat: 9.5370, lon: -13.6785 },
|
|
110
|
+
{ name: 'Nzérékoré', lat: 7.7562, lon: -8.8179 },
|
|
111
|
+
{ name: 'Kankan', lat: 10.3854, lon: -9.3057 },
|
|
112
|
+
{ name: 'Labé', lat: 11.3183, lon: -12.2860 },
|
|
113
|
+
{ name: 'Kindia', lat: 10.0565, lon: -12.8665 },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const generatedLaps = [];
|
|
117
|
+
for (const { name, lat, lon } of testCoords) {
|
|
118
|
+
try {
|
|
119
|
+
const result = coordinatesToLap(lat, lon, places);
|
|
120
|
+
if (result?.lapCode) {
|
|
121
|
+
ok(`${name} (${lat}, ${lon})`, `${result.lapCode} → ${result.humanAddress}`);
|
|
122
|
+
generatedLaps.push({ name, lat, lon, lap: result.lapCode, result });
|
|
123
|
+
} else {
|
|
124
|
+
fail(`${name} (${lat}, ${lon})`, 'returned null/undefined');
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
fail(`${name} (${lat}, ${lon})`, err.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ══════════════════════════════════════════════════════════════
|
|
132
|
+
// 4b. coordinatesToLap — National grid (fallback for admin_level < 9)
|
|
133
|
+
// ══════════════════════════════════════════════════════════════
|
|
134
|
+
section('4b. coordinatesToLap — National grid');
|
|
135
|
+
|
|
136
|
+
const nationalTestCoords = [
|
|
137
|
+
{ name: 'Rural Siguiri (Kankan)', lat: 11.70, lon: -9.30 },
|
|
138
|
+
{ name: 'Rural Macenta (Nzérékoré)', lat: 8.40, lon: -9.40 },
|
|
139
|
+
{ name: 'Rural Boké (Boké)', lat: 11.20, lon: -14.20 },
|
|
140
|
+
{ name: 'Rural Faranah (Faranah)', lat: 10.10, lon: -10.80 },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const generatedNationalLaps = [];
|
|
144
|
+
for (const { name, lat, lon } of nationalTestCoords) {
|
|
145
|
+
try {
|
|
146
|
+
const result = coordinatesToLap(lat, lon, places);
|
|
147
|
+
if (result?.lapCode) {
|
|
148
|
+
if (result.isNationalGrid) {
|
|
149
|
+
ok(`${name} (${lat}, ${lon})`, `${result.lapCode} → ${result.humanAddress} [NATIONAL]`);
|
|
150
|
+
} else {
|
|
151
|
+
ok(`${name} (${lat}, ${lon})`, `${result.lapCode} → ${result.humanAddress} [LOCAL — zone found]`);
|
|
152
|
+
}
|
|
153
|
+
generatedNationalLaps.push({ name, lat, lon, lap: result.lapCode, result });
|
|
154
|
+
} else {
|
|
155
|
+
fail(`${name} (${lat}, ${lon})`, 'returned null/undefined');
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
fail(`${name} (${lat}, ${lon})`, err.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Verify at least one is actually national grid
|
|
163
|
+
const nationalCount = generatedNationalLaps.filter(g => g.result.isNationalGrid).length;
|
|
164
|
+
if (nationalCount > 0) {
|
|
165
|
+
ok('National grid coverage', `${nationalCount}/${generatedNationalLaps.length} used national grid`);
|
|
166
|
+
} else {
|
|
167
|
+
fail('National grid coverage', 'None of the test coordinates triggered national grid fallback — adjust coordinates');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Merge into generatedLaps for sections 5-10
|
|
171
|
+
generatedLaps.push(...generatedNationalLaps);
|
|
172
|
+
|
|
173
|
+
// ══════════════════════════════════════════════════════════════
|
|
174
|
+
// 4c. coordinatesToLap — Out-of-bounds rejection
|
|
175
|
+
// ══════════════════════════════════════════════════════════════
|
|
176
|
+
section('4c. coordinatesToLap — Out-of-bounds rejection');
|
|
177
|
+
|
|
178
|
+
const outOfBoundsCoords = [
|
|
179
|
+
// Far away — caught by bbox
|
|
180
|
+
{ name: 'Dakar, Senegal', lat: 14.6928, lon: -17.4467 },
|
|
181
|
+
{ name: 'Atlantic Ocean', lat: 9.00, lon: -18.00 },
|
|
182
|
+
// Inside bbox but outside Guinea polygon — caught by country border check
|
|
183
|
+
{ name: 'Bamako, Mali', lat: 12.6392, lon: -8.0029 },
|
|
184
|
+
{ name: 'Freetown, Sierra Leone', lat: 8.4657, lon: -13.2317 },
|
|
185
|
+
// Tricky: neighboring countries very close to Guinea border
|
|
186
|
+
{ name: 'Bissau, Guinea-Bissau', lat: 11.8617, lon: -15.5977 },
|
|
187
|
+
{ name: 'Monrovia, Liberia', lat: 6.3156, lon: -10.8074 },
|
|
188
|
+
{ name: 'Kédougou, Senegal (near GN border)', lat: 12.5605, lon: -12.1747 },
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const { name, lat, lon } of outOfBoundsCoords) {
|
|
192
|
+
try {
|
|
193
|
+
const result = coordinatesToLap(lat, lon, places);
|
|
194
|
+
if (result === null) {
|
|
195
|
+
ok(`${name} (${lat}, ${lon})`, 'correctly rejected — outside country bounds');
|
|
196
|
+
} else {
|
|
197
|
+
fail(`${name} (${lat}, ${lon})`, `should be null but got ${result.lapCode}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
fail(`${name} (${lat}, ${lon})`, err.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ══════════════════════════════════════════════════════════════
|
|
205
|
+
// 5. parseLapCode — parse a LAP code into segments
|
|
206
|
+
// ══════════════════════════════════════════════════════════════
|
|
207
|
+
section('5. parseLapCode');
|
|
208
|
+
|
|
209
|
+
for (const { name, lap } of generatedLaps) {
|
|
210
|
+
try {
|
|
211
|
+
const parsed = parseLapCode(lap);
|
|
212
|
+
parsed ? ok(`parse "${lap}"`, JSON.stringify(parsed)) : fail(`parse "${lap}"`, 'returned null');
|
|
213
|
+
} catch (err) {
|
|
214
|
+
fail(`parse "${lap}"`, err.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ══════════════════════════════════════════════════════════════
|
|
219
|
+
// 6. validateLapCode
|
|
220
|
+
// ══════════════════════════════════════════════════════════════
|
|
221
|
+
section('6. validateLapCode');
|
|
222
|
+
|
|
223
|
+
for (const { lap } of generatedLaps) {
|
|
224
|
+
try {
|
|
225
|
+
const result = validateLapCode(lap);
|
|
226
|
+
// null = valid (no error), string = error message
|
|
227
|
+
result === null ? ok(`validate "${lap}"`, 'valid') : fail(`validate "${lap}"`, result);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
fail(`validate "${lap}"`, err.message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Test with an invalid code — should return an error string
|
|
234
|
+
try {
|
|
235
|
+
const bad = validateLapCode('QQ-ZZZ-GARBAGE');
|
|
236
|
+
bad ? ok('validate invalid "QQ-ZZZ-GARBAGE"', bad) : fail('validate invalid should return error string', 'got null');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
ok('validate invalid throws', err.message);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ══════════════════════════════════════════════════════════════
|
|
242
|
+
// 7. lapToCoordinates — decode LAP → GPS
|
|
243
|
+
// ══════════════════════════════════════════════════════════════
|
|
244
|
+
section('7. lapToCoordinates — LAP → GPS');
|
|
245
|
+
|
|
246
|
+
// lapToCoordinates(lapCode) — just pass the LAP code string
|
|
247
|
+
for (const { name, lat, lon, lap, result: encResult } of generatedLaps) {
|
|
248
|
+
try {
|
|
249
|
+
const parsed = parseLapCode(lap);
|
|
250
|
+
const grid = parsed.isNationalGrid ? 'national' : 'local';
|
|
251
|
+
const coords = lapToCoordinates(lap);
|
|
252
|
+
if (coords) {
|
|
253
|
+
const dist = Math.sqrt((coords.lat - lat) ** 2 + (coords.lon - lon) ** 2);
|
|
254
|
+
ok(`decode "${lap}" [${grid}]`, `lat=${coords.lat.toFixed(6)}, lon=${coords.lon.toFixed(6)} (Δ≈${(dist * 111320).toFixed(1)}m from original)`);
|
|
255
|
+
} else {
|
|
256
|
+
fail(`decode "${lap}" [${grid}]`, 'returned null');
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
fail(`decode "${lap}"`, err.message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Test without country prefix (strip "GN-" from a code)
|
|
264
|
+
try {
|
|
265
|
+
const sampleLap = generatedLaps[0]?.lap;
|
|
266
|
+
if (sampleLap && sampleLap.startsWith(getCountryCode() + '-')) {
|
|
267
|
+
const withoutCC = sampleLap.slice(getCountryCode().length + 1);
|
|
268
|
+
const coords = lapToCoordinates(withoutCC);
|
|
269
|
+
coords ? ok(`decode without CC "${withoutCC}"`, `lat=${coords.lat.toFixed(6)}, lon=${coords.lon.toFixed(6)}`)
|
|
270
|
+
: fail(`decode without CC "${withoutCC}"`, 'returned null');
|
|
271
|
+
}
|
|
272
|
+
} catch (err) {
|
|
273
|
+
fail('decode without CC', err.message);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ══════════════════════════════════════════════════════════════
|
|
277
|
+
// 8. getPlaceByLapCode — look up place from LAP code
|
|
278
|
+
// ══════════════════════════════════════════════════════════════
|
|
279
|
+
section('8. getPlaceByLapCode');
|
|
280
|
+
|
|
281
|
+
for (const { lap } of generatedLaps) {
|
|
282
|
+
try {
|
|
283
|
+
// getPlaceByLapCode returns { place, parsed, originLat?, originLon? }
|
|
284
|
+
const match = getPlaceByLapCode(lap);
|
|
285
|
+
if (match?.place) {
|
|
286
|
+
const addr = match.place.address || {};
|
|
287
|
+
const pName = addr.village || addr.town || addr.city || addr.suburb || '(unnamed)';
|
|
288
|
+
ok(`lookup "${lap}"`, `place_id=${match.place.place_id}, name="${pName}"`);
|
|
289
|
+
} else if (match) {
|
|
290
|
+
ok(`lookup "${lap}"`, `matched (parsed: ${JSON.stringify(match.parsed)}, no place — likely national grid)`);
|
|
291
|
+
} else {
|
|
292
|
+
log(`lookup "${lap}"`, 'no match found');
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
fail(`lookup "${lap}"`, err.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ══════════════════════════════════════════════════════════════
|
|
300
|
+
// 9. bboxFromGeometry & centroidFromBbox
|
|
301
|
+
// ══════════════════════════════════════════════════════════════
|
|
302
|
+
section('9. bboxFromGeometry & centroidFromBbox');
|
|
303
|
+
|
|
304
|
+
const samplePlace = places.find(p => p.geojson?.type === 'Polygon' || p.geojson?.type === 'MultiPolygon');
|
|
305
|
+
if (samplePlace) {
|
|
306
|
+
try {
|
|
307
|
+
const bbox = bboxFromGeometry(samplePlace.geojson);
|
|
308
|
+
ok('bboxFromGeometry', JSON.stringify(bbox));
|
|
309
|
+
|
|
310
|
+
const centroid = centroidFromBbox(bbox);
|
|
311
|
+
ok('centroidFromBbox', JSON.stringify(centroid));
|
|
312
|
+
} catch (err) {
|
|
313
|
+
fail('bboxFromGeometry/centroidFromBbox', err.message);
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
log('skip', 'no polygon geometry found in places');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ══════════════════════════════════════════════════════════════
|
|
320
|
+
// 10. Round-trip: encode → decode → re-encode
|
|
321
|
+
// ══════════════════════════════════════════════════════════════
|
|
322
|
+
section('10. Round-trip consistency');
|
|
323
|
+
|
|
324
|
+
for (const { name, lat, lon, lap, result: encResult } of generatedLaps) {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = parseLapCode(lap);
|
|
327
|
+
const grid = parsed.isNationalGrid ? 'national' : 'local';
|
|
328
|
+
const decoded = lapToCoordinates(lap);
|
|
329
|
+
if (!decoded) { fail(`round-trip ${name} [${grid}]`, 'decode returned null'); continue; }
|
|
330
|
+
const reResult = coordinatesToLap(decoded.lat, decoded.lon, places);
|
|
331
|
+
const reEncoded = reResult?.lapCode;
|
|
332
|
+
if (reEncoded === lap) {
|
|
333
|
+
ok(`${name} [${grid}]: encode→decode→encode`, `${lap} ✓`);
|
|
334
|
+
} else {
|
|
335
|
+
fail(`${name} [${grid}]: encode→decode→encode`, `${lap} → (${decoded.lat},${decoded.lon}) → ${reEncoded}`);
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
fail(`round-trip ${name}`, err.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ══════════════════════════════════════════════════════════════
|
|
343
|
+
// SUMMARY
|
|
344
|
+
// ══════════════════════════════════════════════════════════════
|
|
345
|
+
section('SUMMARY');
|
|
346
|
+
console.log(` Passed: ${passed}`);
|
|
347
|
+
console.log(` Failed: ${failed}`);
|
|
348
|
+
console.log(` Total: ${passed + failed}`);
|
|
349
|
+
process.exit(failed > 0 ? 1 : 0);
|