oglap-ggp-node 1.0.0 → 1.0.2
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/README.md +401 -0
- package/package.json +31 -25
- package/oglap-ggp-node-1.0.0.tgz +0 -0
- package/test.js +0 -349
package/README.md
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# oglap-ggp-node-js
|
|
2
|
+
|
|
3
|
+
Implémentation Node.js du protocole **OGLAP** (Offline Grid Location Addressing Protocol) — un système d'adressage déterministe basé sur une grille, conçu pour les régions où les adresses postales formelles sont inexistantes ou peu fiables.
|
|
4
|
+
|
|
5
|
+
OGLAP génère des **codes LAP** compacts et lisibles (ex. `GN-CKY-QKAR-B4A4-2798`) qui identifient de façon unique n'importe quelle coordonnée à l'intérieur d'un pays configuré, hors ligne et sans API externe.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Fonctionnalités
|
|
10
|
+
|
|
11
|
+
- **Coordonnées → code LAP** — encoder toute position GPS en adresse LAP structurée
|
|
12
|
+
- **Code LAP → coordonnées** — décoder un code LAP vers son centre géographique
|
|
13
|
+
- **Geocodage inversé** — trouver la région administrative, zone et lieu contenant une coordonnée
|
|
14
|
+
- **Parsing & validation de code LAP** — analyser des codes partiels ou complets, valider le format et le contenu
|
|
15
|
+
- **Boîte englobante et centroïde** — calculer bbox et centre à partir de géométries GeoJSON
|
|
16
|
+
- **Entièrement hors ligne** — aucune connexion requise une fois les données chargées
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Format du code LAP
|
|
21
|
+
|
|
22
|
+
Un code LAP encode une localisation à quatre niveaux hiérarchiques :
|
|
23
|
+
|
|
24
|
+
### Grille locale (5 segments)
|
|
25
|
+
```
|
|
26
|
+
GN - CKY - QKAR - B4A4 - 2798
|
|
27
|
+
│ │ │ │ └─ Microspot — 4 chiffres, offset métrique (XX = est, YY = nord)
|
|
28
|
+
│ │ │ └─────── Macrobloc — 4 chars [A-J][0-9][A-J][0-9], blocs ~100 m dans la zone
|
|
29
|
+
│ │ └────────────── Zone — 4 chars, dérivé du nom de lieu local
|
|
30
|
+
│ └───────────────────── Région — 3 chars, code de région administrative
|
|
31
|
+
└──────────────────────────── Pays — code ISO alpha-2 du pays
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Grille nationale (4 segments)
|
|
35
|
+
Utilisée quand une coordonnée se situe en dehors des limites administratives de niveau 8 et au-dessus :
|
|
36
|
+
```
|
|
37
|
+
GN - CKY - AABCDE - 4250
|
|
38
|
+
│ │ │ └─ Microspot — 4 chiffres
|
|
39
|
+
│ │ └────────── Macrobloc — 6 lettres, grille kilométrique nationale
|
|
40
|
+
│ └──────────────── Région
|
|
41
|
+
└─────────────────────── Pays
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Démarrage
|
|
47
|
+
|
|
48
|
+
### 1. Ajouter la dépendance
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install oglap-ggp-node-js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Préparer les données
|
|
55
|
+
|
|
56
|
+
Le package nécessite trois fichiers JSON :
|
|
57
|
+
|
|
58
|
+
| Fichier | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `{country_code}_oglap_country_profile.json` | Paramètres de grille, codes admin, règles de nommage |
|
|
61
|
+
| `{country_code}_localities_naming.json` | Géométries GeoJSON des lieux (régions, zones, localités) |
|
|
62
|
+
| `{country_code}_full.json` | Correspondances place-ID → code OGLAP |
|
|
63
|
+
|
|
64
|
+
### 3. Initialiser avant toute utilisation
|
|
65
|
+
|
|
66
|
+
Appeler `initOglap` une seule fois au démarrage de l'application, avant toute autre fonction :
|
|
67
|
+
|
|
68
|
+
**Mode téléchargement (recommandé)** — les données sont téléchargées et mises en cache localement :
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { initOglap } from 'oglap-ggp-node-js';
|
|
72
|
+
|
|
73
|
+
const report = await initOglap({
|
|
74
|
+
version: 'latest',
|
|
75
|
+
dataDir: 'oglap-data', // dossier de cache local
|
|
76
|
+
forceDownload: false,
|
|
77
|
+
onProgress({ label, status, percent, step, totalSteps }) {
|
|
78
|
+
if (status === 'downloading') process.stdout.write(`\r↓ [${step}/${totalSteps}] ${label}: ${percent}%`);
|
|
79
|
+
if (status === 'cached') console.log(`⚡ [${step}/${totalSteps}] ${label}: depuis le cache`);
|
|
80
|
+
if (status === 'done') console.log(`\r✓ [${step}/${totalSteps}] ${label}: terminé`);
|
|
81
|
+
if (status === 'error') console.log(`✗ [${step}/${totalSteps}] ${label}: erreur`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!report.ok) throw new Error(report.error);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Mode direct** — pour les environnements sans accès disque (serverless, edge) :
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { initOglap, loadOglap } from 'oglap-ggp-node-js';
|
|
92
|
+
|
|
93
|
+
const profile = await fetch('/data/country_profile.json').then(r => r.json());
|
|
94
|
+
const localities = await fetch('/data/localities_naming.json').then(r => r.json());
|
|
95
|
+
const places = await fetch('/data/oglap_data.json').then(r => r.json());
|
|
96
|
+
|
|
97
|
+
const report = await initOglap(profile, localities);
|
|
98
|
+
if (!report.ok) throw new Error(report.error);
|
|
99
|
+
|
|
100
|
+
const loaded = loadOglap(places);
|
|
101
|
+
console.log(`${loaded.count} lieux chargés`);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Utilisation
|
|
107
|
+
|
|
108
|
+
### Encoder des coordonnées en code LAP
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { coordinatesToLap } from 'oglap-ggp-node-js';
|
|
112
|
+
|
|
113
|
+
const result = coordinatesToLap(9.5370, -13.6773); // lat, lon — Conakry, Guinée
|
|
114
|
+
|
|
115
|
+
console.log(result.lapCode); // GN-CKY-QKAR-B4A4-2798
|
|
116
|
+
console.log(result.humanAddress); // Quartier Almamya, Conakry, Kindia, Guinée
|
|
117
|
+
console.log(result.admin_level_2); // CKY (code de région)
|
|
118
|
+
console.log(result.admin_level_3); // QKAR (code de zone)
|
|
119
|
+
console.log(result.macroblock); // B4A4
|
|
120
|
+
console.log(result.microspot); // 2798
|
|
121
|
+
console.log(result.isNationalGrid); // false
|
|
122
|
+
console.log(result.originLat); // latitude d'origine de la bbox
|
|
123
|
+
console.log(result.originLon); // longitude d'origine de la bbox
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Retourne `null` si les coordonnées sont hors du territoire.
|
|
127
|
+
|
|
128
|
+
### Décoder un code LAP en coordonnées
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { lapToCoordinates } from 'oglap-ggp-node-js';
|
|
132
|
+
|
|
133
|
+
const coords = lapToCoordinates('GN-CKY-QKAR-B4A4-2798');
|
|
134
|
+
|
|
135
|
+
if (coords) {
|
|
136
|
+
console.log(`lat: ${coords.lat}, lon: ${coords.lon}`);
|
|
137
|
+
// lat: 9.5370..., lon: -13.6773...
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Le préfixe pays est optionnel
|
|
141
|
+
const coords2 = lapToCoordinates('CKY-QKAR-B4A4-2798');
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Parser et valider un code LAP
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { validateLapCode, parseLapCode } from 'oglap-ggp-node-js';
|
|
148
|
+
|
|
149
|
+
// Valider — retourne un message d'erreur, ou null si valide
|
|
150
|
+
const error = validateLapCode('GN-CKY-QKAR-B4A4-2798');
|
|
151
|
+
if (error) {
|
|
152
|
+
console.log('Invalide :', error);
|
|
153
|
+
} else {
|
|
154
|
+
console.log('Code valide');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parser en composants
|
|
158
|
+
const parsed = parseLapCode('GN-CKY-QKAR-B4A4-2798');
|
|
159
|
+
if (parsed) {
|
|
160
|
+
console.log(parsed.admin_level_2_Iso); // code ISO de la région
|
|
161
|
+
console.log(parsed.admin_level_3_code); // code de zone : QKAR
|
|
162
|
+
console.log(parsed.macroblock); // B4A4
|
|
163
|
+
console.log(parsed.microspot); // 2798
|
|
164
|
+
console.log(parsed.isNationalGrid); // false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Les codes partiels sont aussi supportés
|
|
168
|
+
parseLapCode('GN-CKY-QKAR'); // région + zone seulement
|
|
169
|
+
parseLapCode('QKAR'); // zone seulement
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Résoudre un code LAP vers un lieu
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { getPlaceByLapCode } from 'oglap-ggp-node-js';
|
|
176
|
+
|
|
177
|
+
const resolved = getPlaceByLapCode('GN-CKY-QKAR-B4A4-2798');
|
|
178
|
+
|
|
179
|
+
if (resolved) {
|
|
180
|
+
console.log(resolved.originLat); // latitude d'origine de la bbox
|
|
181
|
+
console.log(resolved.originLon); // longitude d'origine de la bbox
|
|
182
|
+
|
|
183
|
+
if (resolved.place) {
|
|
184
|
+
const addr = resolved.place.address;
|
|
185
|
+
const name = addr?.village ?? addr?.town ?? addr?.city ?? resolved.place.display_name;
|
|
186
|
+
console.log(name); // nom du lieu lisible
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Accéder aux composants parsés
|
|
190
|
+
console.log(resolved.parsed.macroblock); // B4A4
|
|
191
|
+
console.log(resolved.parsed.microspot); // 2798
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Boîte englobante et centroïde
|
|
196
|
+
|
|
197
|
+
```js
|
|
198
|
+
import { bboxFromGeometry, centroidFromBbox } from 'oglap-ggp-node-js';
|
|
199
|
+
|
|
200
|
+
const geometry = {
|
|
201
|
+
type: 'Polygon',
|
|
202
|
+
coordinates: [[
|
|
203
|
+
[-13.70, 9.50],
|
|
204
|
+
[-13.65, 9.50],
|
|
205
|
+
[-13.65, 9.55],
|
|
206
|
+
[-13.70, 9.55],
|
|
207
|
+
[-13.70, 9.50],
|
|
208
|
+
]]
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const bbox = bboxFromGeometry(geometry);
|
|
212
|
+
// [minLat, maxLat, minLon, maxLon]
|
|
213
|
+
|
|
214
|
+
if (bbox) {
|
|
215
|
+
const center = centroidFromBbox(bbox);
|
|
216
|
+
// [lat, lon] du point central de la bbox
|
|
217
|
+
console.log(`Centre : ${center[0]}, ${center[1]}`);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Accéder aux métadonnées du pays
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
import { getCountryCode, getCountrySW, getOglapPrefectures } from 'oglap-ggp-node-js';
|
|
225
|
+
|
|
226
|
+
console.log(getCountryCode()); // "GN"
|
|
227
|
+
console.log(getCountrySW()); // [lat, lon] du coin sud-ouest
|
|
228
|
+
|
|
229
|
+
const prefectures = getOglapPrefectures();
|
|
230
|
+
// { [isoCode]: prefectureOglapCode, ... }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Modèles de données
|
|
236
|
+
|
|
237
|
+
### Résultat de `coordinatesToLap`
|
|
238
|
+
|
|
239
|
+
| Champ | Type | Description |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `lapCode` | `string` | Code LAP complet, ex. `GN-CKY-QKAR-B4A4-2798` |
|
|
242
|
+
| `country` | `string` | Code pays, ex. `GN` |
|
|
243
|
+
| `admin_level_2` | `string` | Code de région, ex. `CKY` |
|
|
244
|
+
| `admin_level_3` | `string\|null` | Code de zone, ex. `QKAR` |
|
|
245
|
+
| `macroblock` | `string` | Composant macrobloc |
|
|
246
|
+
| `microspot` | `string` | Composant microspot |
|
|
247
|
+
| `isNationalGrid` | `boolean` | `true` si grille nationale utilisée |
|
|
248
|
+
| `displayName` | `string` | Nom du lieu issu du geocodage inversé |
|
|
249
|
+
| `humanAddress` | `string` | Adresse complète lisible |
|
|
250
|
+
| `address` | `object` | Composants d'adresse structurés |
|
|
251
|
+
| `originLat` | `number` | Latitude d'origine de la bbox |
|
|
252
|
+
| `originLon` | `number` | Longitude d'origine de la bbox |
|
|
253
|
+
| `pcode` | `string[]` | P-codes UNOCHA pour la localisation |
|
|
254
|
+
|
|
255
|
+
### Résultat de `parseLapCode`
|
|
256
|
+
|
|
257
|
+
| Champ | Type | Description |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `admin_level_2_Iso` | `string\|undefined` | Code ISO de la région |
|
|
260
|
+
| `admin_level_3_code` | `string\|undefined` | Code de zone |
|
|
261
|
+
| `macroblock` | `string\|undefined` | Composant macrobloc |
|
|
262
|
+
| `microspot` | `string\|undefined` | Composant microspot |
|
|
263
|
+
| `isNationalGrid` | `boolean` | `true` si grille nationale |
|
|
264
|
+
|
|
265
|
+
### Résultat de `getPlaceByLapCode`
|
|
266
|
+
|
|
267
|
+
| Champ | Type | Description |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `place` | `object\|null` | Données OSM du lieu |
|
|
270
|
+
| `parsed` | `object` | Composants LAP parsés |
|
|
271
|
+
| `originLat` | `number\|undefined` | Latitude d'origine de la bbox |
|
|
272
|
+
| `originLon` | `number\|undefined` | Longitude d'origine de la bbox |
|
|
273
|
+
|
|
274
|
+
### Rapport de `initOglap`
|
|
275
|
+
|
|
276
|
+
| Champ | Type | Description |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| `ok` | `boolean` | Initialisation réussie |
|
|
279
|
+
| `countryCode` | `string\|null` | Ex. `GN` |
|
|
280
|
+
| `countryName` | `string\|null` | Ex. `Guinée` |
|
|
281
|
+
| `bounds` | `number[][]\|null` | `[[swLat, swLon], [neLat, neLon]]` |
|
|
282
|
+
| `checks` | `Array` | Résultats de validation (`pass`, `warn`, `fail`) |
|
|
283
|
+
| `error` | `string\|null` | Message d'erreur si `!ok` |
|
|
284
|
+
| `dataDir` | `string\|undefined` | Dossier de cache local |
|
|
285
|
+
| `dataLoaded` | `object\|undefined` | Résultat du chargement des lieux |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Exemple complet de bout en bout
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import {
|
|
293
|
+
initOglap,
|
|
294
|
+
checkOglap,
|
|
295
|
+
coordinatesToLap,
|
|
296
|
+
lapToCoordinates,
|
|
297
|
+
getPlaceByLapCode,
|
|
298
|
+
validateLapCode,
|
|
299
|
+
parseLapCode,
|
|
300
|
+
} from 'oglap-ggp-node-js';
|
|
301
|
+
|
|
302
|
+
class LocationService {
|
|
303
|
+
static #initialized = false;
|
|
304
|
+
|
|
305
|
+
static async init() {
|
|
306
|
+
if (this.#initialized) return;
|
|
307
|
+
|
|
308
|
+
const report = await initOglap({
|
|
309
|
+
onProgress({ label, status, percent, step, totalSteps }) {
|
|
310
|
+
if (status === 'downloading') process.stdout.write(`\r↓ [${step}/${totalSteps}] ${label}: ${percent}%`);
|
|
311
|
+
if (status === 'cached') console.log(`⚡ [${step}/${totalSteps}] ${label}: depuis le cache`);
|
|
312
|
+
if (status === 'done') console.log(`\r✓ [${step}/${totalSteps}] ${label}: terminé`);
|
|
313
|
+
if (status === 'error') console.log(`✗ [${step}/${totalSteps}] ${label}: erreur`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (!report.ok) throw new Error(`Initialisation OGLAP échouée : ${report.error}`);
|
|
318
|
+
this.#initialized = true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Encoder la position GPS de l'utilisateur */
|
|
322
|
+
static encodePosition(lat, lon) {
|
|
323
|
+
const result = coordinatesToLap(lat, lon);
|
|
324
|
+
return result?.lapCode ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Partager une localisation : retourne le code LAP et l'adresse lisible */
|
|
328
|
+
static shareLocation(lat, lon) {
|
|
329
|
+
const result = coordinatesToLap(lat, lon);
|
|
330
|
+
if (!result) return null;
|
|
331
|
+
return {
|
|
332
|
+
code: result.lapCode,
|
|
333
|
+
label: result.humanAddress,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Naviguer vers un code LAP en le convertissant en coordonnées */
|
|
338
|
+
static decodeToCoords(lapCode) {
|
|
339
|
+
return lapToCoordinates(lapCode); // { lat, lon } ou null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Valider la saisie d'un code LAP par l'utilisateur */
|
|
343
|
+
static validateInput(input) {
|
|
344
|
+
return validateLapCode(input); // null = valide, string = message d'erreur
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Résoudre un code LAP vers les détails du lieu */
|
|
348
|
+
static resolvePlace(lapCode) {
|
|
349
|
+
const resolved = getPlaceByLapCode(lapCode);
|
|
350
|
+
if (!resolved?.place) return null;
|
|
351
|
+
|
|
352
|
+
const addr = resolved.place.address ?? {};
|
|
353
|
+
return {
|
|
354
|
+
name: addr.village ?? addr.town ?? addr.city ?? resolved.place.display_name,
|
|
355
|
+
adminCode: resolved.parsed.admin_level_3_code,
|
|
356
|
+
originLat: resolved.originLat,
|
|
357
|
+
originLon: resolved.originLon,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Utilisation
|
|
363
|
+
await LocationService.init();
|
|
364
|
+
|
|
365
|
+
// Encoder le centre de Conakry
|
|
366
|
+
const code = LocationService.encodePosition(9.5370, -13.6773);
|
|
367
|
+
console.log(code); // GN-CKY-QKAR-B4A4-2798
|
|
368
|
+
|
|
369
|
+
// Décoder
|
|
370
|
+
const coords = LocationService.decodeToCoords(code);
|
|
371
|
+
console.log(coords); // { lat: 9.537..., lon: -13.677... }
|
|
372
|
+
|
|
373
|
+
// Partager
|
|
374
|
+
const share = LocationService.shareLocation(9.5370, -13.6773);
|
|
375
|
+
console.log(share.label); // Quartier Almamya, Conakry, Kindia, Guinée
|
|
376
|
+
|
|
377
|
+
// Valider la saisie utilisateur
|
|
378
|
+
const err = LocationService.validateInput('GN-CKY-QKAR-B4A4-2798');
|
|
379
|
+
console.log(err); // null (valide)
|
|
380
|
+
|
|
381
|
+
// Résoudre un lieu
|
|
382
|
+
const place = LocationService.resolvePlace('GN-CKY-QKAR-B4A4-2798');
|
|
383
|
+
console.log(place.name); // Quartier Almamya
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Exécuter les tests
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
node test.js
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Informations complémentaires
|
|
397
|
+
|
|
398
|
+
- **Protocole** : OGLAP est conçu pour la Guinée (`GN`) mais configurable pour tout pays via le fichier profil JSON.
|
|
399
|
+
- **Offline-first** : tout l'encodage et décodage est effectué localement avec les données chargées — aucun réseau requis.
|
|
400
|
+
- **Déterministe** : les mêmes coordonnées produisent toujours le même code LAP, à données identiques.
|
|
401
|
+
- **Bugs** : signaler les problèmes dans le dépôt principal Kiraa.
|
package/package.json
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oglap-ggp-node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Permettre aux développeurs d'installer rapidement le SDK et ses dépendances pour commencer à l'utiliser dans leurs projets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "oglap.js",
|
|
7
|
+
"module": "oglap.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./oglap.js",
|
|
11
|
+
"default": "./oglap.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"oglap.js",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@turf/boolean-point-in-polygon": "^7.2.0",
|
|
23
|
+
"@turf/area": "^7.2.0"
|
|
24
|
+
},
|
|
5
25
|
"keywords": [
|
|
6
26
|
"oglap",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
27
|
+
"ggp",
|
|
28
|
+
"geocoding",
|
|
29
|
+
"coordinates",
|
|
10
30
|
"guinea",
|
|
11
|
-
"
|
|
12
|
-
"guinee",
|
|
13
|
-
"addressing",
|
|
14
|
-
"protocol",
|
|
15
|
-
"offline",
|
|
16
|
-
"address",
|
|
17
|
-
"offline",
|
|
18
|
-
"maps",
|
|
19
|
-
"oglap-maps",
|
|
20
|
-
"kiraa"
|
|
31
|
+
"geolocation"
|
|
21
32
|
],
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"url": "https://github.com/Guinee-IO/oglap-ggp-node-js/issues"
|
|
25
|
-
},
|
|
33
|
+
"author": "Guinee IO",
|
|
34
|
+
"license": "ISC",
|
|
26
35
|
"repository": {
|
|
27
36
|
"type": "git",
|
|
28
37
|
"url": "git+https://github.com/Guinee-IO/oglap-ggp-node-js.git"
|
|
29
38
|
},
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"scripts": {
|
|
35
|
-
"test": "node test.js"
|
|
36
|
-
}
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Guinee-IO/oglap-ggp-node-js/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Guinee-IO/oglap-ggp-node-js#readme"
|
|
37
43
|
}
|
package/oglap-ggp-node-1.0.0.tgz
DELETED
|
Binary file
|
package/test.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
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);
|