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/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);