Compare commits
10 Commits
223511f718
...
v2.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 67142428ac | |||
| 84c0363067 | |||
| 70f448a4f7 | |||
| 79b2ef72a9 | |||
| e4a0bfd045 | |||
| fab020ba75 | |||
| 2e6f7e8830 | |||
| 19b83159a9 | |||
| 2f8867497a | |||
| c5e925b3c4 |
@@ -0,0 +1,2 @@
|
|||||||
|
# Paquet YunoHost (dépôt séparé : ORGANISATION-KA-INTERNATIONALE/jwe_ynh)
|
||||||
|
jwe_ynh/
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# JWE
|
# JWE
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
JWE est un jeu de géolocalisation souverain, dans l'esprit de GeoGuessr, centré sur 4 régions : <strong>Guadeloupe, Martinique, Guyane, La Réunion</strong>. Le joueur voit une photo unique d'un lieu et place un marqueur sur une carte pour deviner la commune — puis découvre une fiche éducative Wikipédia/Wikidata… ou un appel à contribution quand le lieu n'y est pas documenté (<em>Pwen blindé</em>).
|
JWE est un jeu de géolocalisation souverain, dans l'esprit de GeoGuessr, centré sur 4 régions : <strong>Guadeloupe, Martinique, Guyane, La Réunion</strong>. Le joueur voit une photo unique d'un lieu — ou navigue dans une rue en visionneuse Mapillary (mode <em>An lari a</em>) — et place un marqueur sur une carte pour deviner la commune — puis découvre une fiche éducative Wikipédia/Wikidata… ou un appel à contribution quand le lieu n'y est pas documenté (<em>Pwen blindé</em>).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -12,10 +12,11 @@ JWE est un jeu de géolocalisation souverain, dans l'esprit de GeoGuessr, centr
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
- **Souverain** : zéro Google, zéro Mapbox. MapLibre + tiles libres, Wikipédia/Wikidata, auto-hébergeable sur un petit VPS.
|
- **Souverain** : zéro Google, zéro Mapbox. MapLibre + tiles libres, Wikipédia/Wikidata, auto-hébergeable sur un petit VPS. (Exception documentée : le mode street « An lari a » utilise la visionneuse Mapillary — images CC BY-SA 4.0, lib MIT, token client gratuit.)
|
||||||
- **Éthique** : pas de comptes, pas de leaderboard, pas de mécanique dopaminergique.
|
- **Éthique** : pas de comptes, pas de leaderboard, pas de mécanique dopaminergique.
|
||||||
- **Bilingue** : français / créole.
|
- **Bilingue** : français / créole.
|
||||||
- **Stack** : Svelte 5 + SvelteKit 2, MapLibre GL JS, Node ≥ 20, licence MIT.
|
- **5 modes** : Konnèt moniman, Kote mwen ye ?, Défi 5 rounds, Aprann (explorer), An lari a (street-level Mapillary).
|
||||||
|
- **Stack** : Svelte 5 + SvelteKit 2, MapLibre GL JS, mapillary-js, Node ≥ 20, licence MIT.
|
||||||
|
|
||||||
## Utilisation
|
## Utilisation
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ JWE est un jeu de géolocalisation souverain, dans l'esprit de GeoGuessr, centr
|
|||||||
git clone https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE.git
|
git clone https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE.git
|
||||||
cd JWE/app
|
cd JWE/app
|
||||||
npm ci
|
npm ci
|
||||||
|
cp .env.example .env # puis renseignez PUBLIC_MAPILLARY_TOKEN (token client gratuit)
|
||||||
npm run dev # développement
|
npm run dev # développement
|
||||||
npm run build && node build # production (adapter-node)
|
npm run build && node build # production (adapter-node)
|
||||||
```
|
```
|
||||||
@@ -33,7 +35,6 @@ npm run build && node build # production (adapter-node)
|
|||||||
- [`docs/deploiement.md`](docs/deploiement.md) — auto-hébergement (SvelteKit + tileserver-gl).
|
- [`docs/deploiement.md`](docs/deploiement.md) — auto-hébergement (SvelteKit + tileserver-gl).
|
||||||
- [`docs/contribution.md`](docs/contribution.md) — ajouter un lieu et lier sa fiche Wikidata.
|
- [`docs/contribution.md`](docs/contribution.md) — ajouter un lieu et lier sa fiche Wikidata.
|
||||||
- [`docs/audit-oki.md`](docs/audit-oki.md) — audit qualité et doctrine OKI.
|
- [`docs/audit-oki.md`](docs/audit-oki.md) — audit qualité et doctrine OKI.
|
||||||
- [`prompts/`](prompts/) — prompts de mission et d'audit ayant produit cette refonte.
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
@@ -47,4 +48,4 @@ La version 1 (PHP, cartes des communes) a été remplacée par cette refonte en
|
|||||||
|
|
||||||
Copyright (C) 2024-2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
Copyright (C) 2024-2026 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
|
||||||
|
|
||||||
Code sous licence **MIT** (voir [LICENSE](LICENSE)). Les photographies restent la propriété de leurs auteurs sous leurs licences Wikimedia Commons (voir [`content/credits.md`](content/credits.md)) ; les extraits Wikipédia sont sous CC BY-SA 4.0.
|
Code sous licence **MIT** (voir [LICENSE](LICENSE)). Les photographies restent la propriété de leurs auteurs sous leurs licences Wikimedia Commons ; les images street-level Mapillary sont sous CC BY-SA 4.0 par leurs contributeurs (voir [`content/credits.md`](content/credits.md)) ; les extraits Wikipédia sont sous CC BY-SA 4.0.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Token client Mapillary (gratuit) : https://www.mapillary.com/dashboard/developers
|
||||||
|
# Requis uniquement pour le mode street « An lari a » (visionneuse mapillary-js).
|
||||||
|
# Token client : conçu pour être exposé côté navigateur (rate-limité par app).
|
||||||
|
PUBLIC_MAPILLARY_TOKEN=
|
||||||
|
|
||||||
|
# Optionnel : URL d'un style/tiles auto-hébergé (tileserver-gl) — voir docs/deploiement.md
|
||||||
|
# PUBLIC_TILES_URL=
|
||||||
Generated
+291
-9
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "jwe",
|
"name": "jwe",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jwe",
|
"name": "jwe",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mapillary-js": "^4.1.2",
|
||||||
"maplibre-gl": "^5.1.0",
|
"maplibre-gl": "^5.1.0",
|
||||||
"sharp": "^0.35.3"
|
"sharp": "^0.35.3"
|
||||||
},
|
},
|
||||||
@@ -2586,6 +2587,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/earcut": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -2609,6 +2616,24 @@
|
|||||||
"undici-types": "~8.3.0"
|
"undici-types": "~8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pako": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/pbf": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/polylabel": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/proj4": {
|
"node_modules/@types/proj4": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.2.tgz",
|
||||||
@@ -2631,6 +2656,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.134.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.134.0.tgz",
|
||||||
|
"integrity": "sha512-4YB+99Rgqq27EjiYTItEoZtdjLnTh8W9LxowgpC9eWsjaQJIL4Kn/ZcUKAnW3gB/jS4hqGN8iqmid+RcUZDzpA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -2638,6 +2669,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/virtual-dom": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/virtual-dom/-/virtual-dom-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-Y7L/frVydXRd16MevczslJZQu+QWsrqZlj6ytk7mST3xen0fkx7Ollw31By/89A8Wq+nfNWm/IoTR1ac/0fRhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.13",
|
"version": "0.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
@@ -2836,6 +2873,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/browser-split": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -2884,6 +2927,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||||
@@ -3221,6 +3273,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-walk": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||||
|
},
|
||||||
"node_modules/duplexer": {
|
"node_modules/duplexer": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
@@ -3245,6 +3302,16 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/error": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"string-template": "~0.2.0",
|
||||||
|
"xtend": "~4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-errors": {
|
"node_modules/es-errors": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
@@ -3329,6 +3396,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ev-store": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"individual": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/event-stream": {
|
"node_modules/event-stream": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
|
||||||
@@ -3631,6 +3706,16 @@
|
|||||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/global": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"min-document": "^2.19.0",
|
||||||
|
"process": "^0.11.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -3721,7 +3806,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3754,6 +3838,11 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/individual": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g=="
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
@@ -3867,6 +3956,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-object": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
@@ -3983,6 +4081,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -4000,6 +4107,63 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/mapillary-js": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mapillary-js/-/mapillary-js-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-LYj6xbp/Y9wfBkr/prRwgJGb3+KwvzUri0yl5QEpMEH7oC1p52ErshWmPXysKefmm6JaFML4D7FJ89viwJ4T5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/earcut": "^2.1.1",
|
||||||
|
"@types/node": "^14.14.31",
|
||||||
|
"@types/pako": "^1.0.2",
|
||||||
|
"@types/pbf": "^3.0.2",
|
||||||
|
"@types/polylabel": "^1.0.5",
|
||||||
|
"@types/rbush": "^3.0.0",
|
||||||
|
"@types/three": "^0.134.0",
|
||||||
|
"@types/virtual-dom": "^2.1.1",
|
||||||
|
"earcut": "^2.2.3",
|
||||||
|
"martinez-polygon-clipping": "^0.7.1",
|
||||||
|
"pako": "^2.0.4",
|
||||||
|
"pbf": "^3.2.1",
|
||||||
|
"polylabel": "^1.1.0",
|
||||||
|
"rbush": "^3.0.1",
|
||||||
|
"rxjs": "^7.3.0",
|
||||||
|
"s2-geometry": "^1.2.10",
|
||||||
|
"three": "^0.134.0",
|
||||||
|
"virtual-dom": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mapillary-js/node_modules/@types/node": {
|
||||||
|
"version": "14.18.63",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
|
||||||
|
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mapillary-js/node_modules/@types/rbush": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-knSt9cCW8jj1ZSFcFeBZaX++OucmfPxxHiRwTahZfJlnQsek7O0bazTJHWD2RVj9LEoejUYF2de3/stf+QXcXw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mapillary-js/node_modules/earcut": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/mapillary-js/node_modules/pbf": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ieee754": "^1.1.12",
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/maplibre-gl": {
|
"node_modules/maplibre-gl": {
|
||||||
"version": "5.24.0",
|
"version": "5.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
|
||||||
@@ -4100,6 +4264,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/martinez-polygon-clipping": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"robust-predicates": "^2.0.4",
|
||||||
|
"splaytree": "^0.1.4",
|
||||||
|
"tinyqueue": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/martinez-polygon-clipping/node_modules/robust-predicates": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
|
"node_modules/martinez-polygon-clipping/node_modules/splaytree": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/martinez-polygon-clipping/node_modules/tinyqueue": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/mgrs": {
|
"node_modules/mgrs": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
|
||||||
@@ -4121,6 +4314,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/min-document": {
|
||||||
|
"version": "2.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
|
||||||
|
"integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-walk": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
@@ -4260,6 +4462,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
"version": "3.94.0",
|
"version": "3.94.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
|
||||||
@@ -4382,7 +4590,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz",
|
||||||
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
"integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4489,6 +4696,21 @@
|
|||||||
"splaytree": "^3.1.0"
|
"splaytree": "^3.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/polylabel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyqueue": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/polylabel/node_modules/tinyqueue": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.19",
|
"version": "8.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||||
@@ -4570,7 +4792,6 @@
|
|||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
@@ -4638,7 +4859,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz",
|
||||||
"integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
|
"integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"quickselect": "^2.0.0"
|
"quickselect": "^2.0.0"
|
||||||
@@ -4648,7 +4868,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
|
||||||
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/rc": {
|
"node_modules/rc": {
|
||||||
@@ -4862,6 +5081,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/s2-geometry": {
|
||||||
|
"version": "1.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/s2-geometry/-/s2-geometry-1.2.10.tgz",
|
||||||
|
"integrity": "sha512-5WejfQu1XZ25ZerW8uL6xP1sM2krcOYKhI6TbfybGRf+vTQLrm3E+4n0+1lWg+MYqFjPzoe51zKhn2sBRMCt5g==",
|
||||||
|
"license": "(MIT or Apache-2 or ISC)",
|
||||||
|
"dependencies": {
|
||||||
|
"long": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -5117,6 +5354,11 @@
|
|||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-template": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw=="
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
@@ -5284,6 +5526,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.134.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.134.0.tgz",
|
||||||
|
"integrity": "sha512-LbBerg7GaSPjYtTOnu41AMp7tV6efUNR3p4Wk5NzkSsNTBuA5mDGOfwwZL1jhhVMLx9V20HolIUo0+U3AXehbg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/through": {
|
"node_modules/through": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||||
@@ -5346,8 +5594,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD",
|
"license": "0BSD"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tsscmp": {
|
"node_modules/tsscmp": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
@@ -5419,6 +5666,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/virtual-dom": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browser-split": "0.0.1",
|
||||||
|
"error": "^4.3.0",
|
||||||
|
"ev-store": "^7.0.0",
|
||||||
|
"global": "^4.3.0",
|
||||||
|
"is-object": "^1.0.1",
|
||||||
|
"next-tick": "^0.2.2",
|
||||||
|
"x-is-array": "0.1.0",
|
||||||
|
"x-is-string": "0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.3",
|
"version": "6.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||||
@@ -5573,6 +5836,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/x-is-array": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/x-is-array/-/x-is-array-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA=="
|
||||||
|
},
|
||||||
|
"node_modules/x-is-string": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w=="
|
||||||
|
},
|
||||||
"node_modules/xml-utils": {
|
"node_modules/xml-utils": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||||
@@ -5580,6 +5853,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zarrita": {
|
"node_modules/zarrita": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jwe",
|
"name": "jwe",
|
||||||
"version": "2.0.0",
|
"version": "2.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"mapillary-js": "^4.1.2",
|
||||||
"maplibre-gl": "^5.1.0",
|
"maplibre-gl": "^5.1.0",
|
||||||
"sharp": "^0.35.3"
|
"sharp": "^0.35.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Curation du corpus street (« An lari a ») — mode Mapillary.
|
||||||
|
* « Fetch, pas recall » : pour chaque commune cible, interroge l'API Mapillary
|
||||||
|
* (bbox autour du centre communal, préférence 360° + qualité + récence),
|
||||||
|
* geo.api.gouv.fr (centre, superficie), api-adresse (nom de rue) et Wikidata
|
||||||
|
* (QID de la commune via le sitelink frwiki).
|
||||||
|
*
|
||||||
|
* Usage : MAPILLARY_TOKEN=… node scripts/curate-street.mjs
|
||||||
|
* (le token est aussi lu depuis .env : MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN)
|
||||||
|
* Écrit src/lib/server/data/street.json.
|
||||||
|
*/
|
||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
|
function chargeEnv() {
|
||||||
|
try {
|
||||||
|
for (const ligne of readFileSync(join(ROOT, '.env'), 'utf8').split('\n')) {
|
||||||
|
const m = ligne.match(/^\s*([A-Z_]+)\s*=\s*(.+?)\s*$/);
|
||||||
|
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* pas de .env */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chargeEnv();
|
||||||
|
const TOKEN = process.env.MAPILLARY_TOKEN || process.env.PUBLIC_MAPILLARY_TOKEN;
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.error('Token Mapillary manquant (MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN).');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UA = { 'User-Agent': 'JWE-OKI/2.0 (curation corpus street; contact: o-k-i.net)' };
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
const getJSON = async (url, headers = UA) => {
|
||||||
|
const res = await fetch(url, { headers });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status} sur ${url}`);
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Communes cibles : 5 par région, difficulté = précision exigée (pas de FACILE :
|
||||||
|
// rayon 50 km = trivial en mode street). titreWiki = titre frwiki exact (désambiguïsé).
|
||||||
|
const SPOTS = [
|
||||||
|
// — Guadeloupe (971) —
|
||||||
|
{ region: 'GUADELOUPE', commune: 'Pointe-à-Pitre', titreWiki: 'Pointe-à-Pitre', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Port de commerce principal de l’archipel' },
|
||||||
|
{ region: 'GUADELOUPE', commune: 'Les Abymes', titreWiki: 'Les Abymes', difficulte: 'MOYEN', indice1: 'Île papillon, aile droite', indice2: 'Commune la plus peuplée de l’archipel' },
|
||||||
|
{ region: 'GUADELOUPE', commune: 'Le Gosier', titreWiki: 'Le Gosier', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Station balnéaire au sud de la Grande-Terre' },
|
||||||
|
{ region: 'GUADELOUPE', commune: 'Sainte-Anne', titreWiki: 'Sainte-Anne (Guadeloupe)', difficulte: 'DIFFICILE', indice1: 'Île papillon, aile droite', indice2: 'Plages au sud de la Grande-Terre' },
|
||||||
|
{ region: 'GUADELOUPE', commune: 'Basse-Terre', titreWiki: 'Basse-Terre', difficulte: 'EXPERT', indice1: 'Île papillon, aile gauche', indice2: 'Chef-lieu au pied du volcan' },
|
||||||
|
// — Martinique (972) —
|
||||||
|
{ region: 'MARTINIQUE', commune: 'Fort-de-France', titreWiki: 'Fort-de-France', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Chef-lieu, sur une grande baie' },
|
||||||
|
{ region: 'MARTINIQUE', commune: 'Le Lamentin', titreWiki: 'Le Lamentin', difficulte: 'MOYEN', indice1: 'Île aux fleurs', indice2: 'Banlieue du chef-lieu, zone aéroportuaire' },
|
||||||
|
{ region: 'MARTINIQUE', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (Martinique)', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte caraïbe nord', indice2: 'Ville détruite par une éruption en 1902' },
|
||||||
|
{ region: 'MARTINIQUE', commune: 'Le François', titreWiki: 'Le François', difficulte: 'DIFFICILE', indice1: 'Île aux fleurs, côte atlantique', indice2: 'Bourg face aux îlets du sud' },
|
||||||
|
{ region: 'MARTINIQUE', commune: 'Sainte-Marie', titreWiki: 'Sainte-Marie (Martinique)', difficulte: 'EXPERT', indice1: 'Île aux fleurs, côte atlantique nord', indice2: 'Musée de la banane et rhum' },
|
||||||
|
// — Guyane (973) —
|
||||||
|
{ region: 'GUYANE', commune: 'Cayenne', titreWiki: 'Cayenne', difficulte: 'MOYEN', indice1: 'Terre de forêt amazonienne', indice2: 'Chef-lieu, sur l’île du même nom' },
|
||||||
|
{ region: 'GUYANE', commune: 'Kourou', titreWiki: 'Kourou', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne', indice2: 'Ville des fusées' },
|
||||||
|
{ region: 'GUYANE', commune: 'Saint-Laurent-du-Maroni', titreWiki: 'Saint-Laurent-du-Maroni', difficulte: 'DIFFICILE', indice1: 'Terre de forêt amazonienne, à l’ouest', indice2: 'Ville du fleuve Maroni, ancien bagne' },
|
||||||
|
{ region: 'GUYANE', commune: 'Matoury', titreWiki: 'Matoury', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Banlieue sud du chef-lieu' },
|
||||||
|
{ region: 'GUYANE', commune: 'Remire-Montjoly', titreWiki: 'Remire-Montjoly', difficulte: 'EXPERT', indice1: 'Terre de forêt amazonienne', indice2: 'Plages à l’est du chef-lieu' },
|
||||||
|
// — La Réunion (974) —
|
||||||
|
{ region: 'REUNION', commune: 'Saint-Denis', titreWiki: 'Saint-Denis (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de l’océan Indien', indice2: 'Chef-lieu, au nord' },
|
||||||
|
{ region: 'REUNION', commune: 'Saint-Pierre', titreWiki: 'Saint-Pierre (La Réunion)', difficulte: 'MOYEN', indice1: 'Île intense de l’océan Indien', indice2: 'Capitale du sud, front de mer animé' },
|
||||||
|
{ region: 'REUNION', commune: 'Le Tampon', titreWiki: 'Le Tampon', difficulte: 'DIFFICILE', indice1: 'Île intense de l’océan Indien', indice2: 'Commune des hauts, dans le sud' },
|
||||||
|
{ region: 'REUNION', commune: 'Saint-Paul', titreWiki: 'Saint-Paul (La Réunion)', difficulte: 'DIFFICILE', indice1: 'Île intense de l’océan Indien', indice2: 'Côte ouest, premier site de peuplement' },
|
||||||
|
{ region: 'REUNION', commune: 'Saint-André', titreWiki: 'Saint-André (La Réunion)', difficulte: 'EXPERT', indice1: 'Île intense de l’océan Indien', indice2: 'Côte est, pays de la canne' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const PREFIXE_REGION = { GUADELOUPE: '971', MARTINIQUE: '972', GUYANE: '973', REUNION: '974' };
|
||||||
|
|
||||||
|
async function centreCommune(spot) {
|
||||||
|
const data = await getJSON(
|
||||||
|
`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(spot.commune)}&codeDepartement=${PREFIXE_REGION[spot.region]}&fields=centre,surface,code,mairie&limit=8`
|
||||||
|
);
|
||||||
|
const c = data[0];
|
||||||
|
if (!c) throw new Error(`commune introuvable sur geo.api.gouv.fr`);
|
||||||
|
// La mairie est un meilleur ancrage que le centroïde (les communes DOM
|
||||||
|
// englobent des reliefs : le centroïde tombe souvent hors du centre-ville).
|
||||||
|
const p = c.mairie?.coordinates ? c.mairie.coordinates : c.centre.coordinates;
|
||||||
|
return { lat: p[1], lon: p[0], surfaceHa: c.surface };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chercherImage(lat, lon) {
|
||||||
|
const champs = 'id,is_pano,captured_at,computed_geometry,geometry,creator,quality_score';
|
||||||
|
const note = (img) => (img.quality_score ?? 0.5) * 1e13 + (img.captured_at ?? 0);
|
||||||
|
// 1) bbox autour de l'ancre (< 0,01° de côté, contrainte API), pano d'abord
|
||||||
|
const demi = 0.0045;
|
||||||
|
const bbox = `${lon - demi},${lat - demi},${lon + demi},${lat + demi}`;
|
||||||
|
for (const pano of ['&is_pano=true', '']) {
|
||||||
|
const data = await getJSON(
|
||||||
|
`https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&bbox=${bbox}&limit=200${pano}`
|
||||||
|
);
|
||||||
|
if (data.data?.length) {
|
||||||
|
const triees = [...data.data].sort((a, b) => note(b) - note(a));
|
||||||
|
return { image: triees[0], total: data.data.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2) grille de recherches par rayon (50 m) autour de l'ancre
|
||||||
|
const parId = new Map();
|
||||||
|
for (const dLat of [-0.004, 0, 0.004]) {
|
||||||
|
for (const dLon of [-0.004, 0, 0.004]) {
|
||||||
|
try {
|
||||||
|
const data = await getJSON(
|
||||||
|
`https://graph.mapillary.com/images?access_token=${encodeURIComponent(TOKEN)}&fields=${champs}&lat=${lat + dLat}&lng=${lon + dLon}&radius=50&limit=10`
|
||||||
|
);
|
||||||
|
for (const img of data.data ?? []) parId.set(img.id, img);
|
||||||
|
} catch {
|
||||||
|
/* point sans couverture */
|
||||||
|
}
|
||||||
|
await sleep(120);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parId.size) return null;
|
||||||
|
const triees = [...parId.values()].sort((a, b) => note(b) - note(a));
|
||||||
|
return { image: triees[0], total: parId.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nomDeRue(lat, lon) {
|
||||||
|
try {
|
||||||
|
const data = await getJSON(`https://api-adresse.data.gouv.fr/reverse/?lon=${lon}&lat=${lat}&type=street`);
|
||||||
|
const f = data.features?.[0]?.properties;
|
||||||
|
return f?.name ?? f?.street ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QIDs des communes via sitelinks frwiki (un seul appel batch)
|
||||||
|
async function qidsCommunes() {
|
||||||
|
const titres = SPOTS.map((s) => s.titreWiki);
|
||||||
|
const data = await getJSON(
|
||||||
|
`https://www.wikidata.org/w/api.php?action=wbgetentities&sites=frwiki&titles=${encodeURIComponent(titres.join('|'))}&props=sitelinks&sitefilter=frwiki&format=json&redirects=yes`
|
||||||
|
);
|
||||||
|
const parTitre = {};
|
||||||
|
for (const [qid, e] of Object.entries(data.entities ?? {})) {
|
||||||
|
if (e.sitelinks?.frwiki?.title) parTitre[e.sitelinks.frwiki.title] = qid;
|
||||||
|
}
|
||||||
|
const redirige = {};
|
||||||
|
for (const r of data.redirects ?? []) redirige[r.from] = r.to;
|
||||||
|
const res = {};
|
||||||
|
for (const s of SPOTS) {
|
||||||
|
const t = redirige[s.titreWiki] ?? s.titreWiki;
|
||||||
|
res[s.titreWiki] = parTitre[t] ?? null;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qids = await qidsCommunes();
|
||||||
|
const lieux = [];
|
||||||
|
let n = 0;
|
||||||
|
for (const spot of SPOTS) {
|
||||||
|
n++;
|
||||||
|
try {
|
||||||
|
const centre = await centreCommune(spot);
|
||||||
|
await sleep(150);
|
||||||
|
const trouve = await chercherImage(centre.lat, centre.lon);
|
||||||
|
if (!trouve) {
|
||||||
|
console.log(`VIDE ${spot.commune} (${spot.region}) — aucune image dans la bbox`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const img = trouve.image;
|
||||||
|
const geo = img.computed_geometry ?? img.geometry;
|
||||||
|
const coords = { lat: geo.coordinates[1], lon: geo.coordinates[0] };
|
||||||
|
await sleep(150);
|
||||||
|
const rue = await nomDeRue(coords.lat, coords.lon);
|
||||||
|
const qid = qids[spot.titreWiki];
|
||||||
|
lieux.push({
|
||||||
|
id: `st-${String(n).padStart(2, '0')}`,
|
||||||
|
nom: rue ? `${rue}, ${spot.commune}` : `Centre de ${spot.commune}`,
|
||||||
|
nom_creole: `Lari ${spot.commune}`,
|
||||||
|
region: spot.region,
|
||||||
|
commune: spot.commune,
|
||||||
|
coordonnees: coords,
|
||||||
|
difficulte: spot.difficulte,
|
||||||
|
mapillary_id: img.id,
|
||||||
|
credit: {
|
||||||
|
username: img.creator?.username ?? 'contributeur Mapillary',
|
||||||
|
captured_at: new Date(img.captured_at).toISOString().slice(0, 10)
|
||||||
|
},
|
||||||
|
is_pano: !!img.is_pano,
|
||||||
|
wikidata_id: qid,
|
||||||
|
indices: [
|
||||||
|
{ niveau: 1, texte: spot.indice1 },
|
||||||
|
{ niveau: 2, texte: spot.indice2 }
|
||||||
|
],
|
||||||
|
aire_commune_km2: Math.round(centre.surfaceHa) / 100 // geo.api : hectares → km²
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`OK ${spot.commune.padEnd(24)} img ${img.id} pano=${img.is_pano ? 'oui' : 'NON'} q=${img.quality_score?.toFixed(2) ?? '—'} pool=${trouve.total} qid=${qid ?? 'ABSENT'} rue=${rue ?? '—'}`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`ERREUR ${spot.commune} — ${e.message}`);
|
||||||
|
}
|
||||||
|
await sleep(150);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(join(ROOT, 'src/lib/server/data/street.json'), JSON.stringify(lieux, null, 2) + '\n');
|
||||||
|
console.log(`\n${lieux.length} lieux écrits dans src/lib/server/data/street.json`);
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Validation du corpus street (« An lari a », mode Mapillary).
|
||||||
|
* « Fetch, pas recall » : chaque mapillary_id est revérifié contre l'API
|
||||||
|
* Mapillary (existence, computed_geometry < 100 m de la vérité curée, crédit
|
||||||
|
* auteur, flag pano) et chaque wikidata_id contre Wikidata (sitelink frwiki).
|
||||||
|
*
|
||||||
|
* Usage : node scripts/validate-street.mjs
|
||||||
|
* Token lu depuis .env (MAPILLARY_TOKEN ou PUBLIC_MAPILLARY_TOKEN).
|
||||||
|
* Exit code 1 si au moins un FAIL.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const STREET = JSON.parse(readFileSync(join(ROOT, 'src/lib/server/data/street.json'), 'utf8'));
|
||||||
|
const UA = { 'User-Agent': 'JWE-OKI/2.0 (validation corpus street; contact: o-k-i.net)' };
|
||||||
|
|
||||||
|
function chargeEnv() {
|
||||||
|
try {
|
||||||
|
for (const ligne of readFileSync(join(ROOT, '.env'), 'utf8').split('\n')) {
|
||||||
|
const m = ligne.match(/^\s*([A-Z_]+)\s*=\s*(.+?)\s*$/);
|
||||||
|
if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* pas de .env */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chargeEnv();
|
||||||
|
const TOKEN = process.env.MAPILLARY_TOKEN || process.env.PUBLIC_MAPILLARY_TOKEN;
|
||||||
|
|
||||||
|
const getJSON = async (url, headers = UA, essais = 3) => {
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
const res = await fetch(url, { headers });
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
// 429 (rate limit Wikidata/Mapillary) ou 5xx : backoff puis retry
|
||||||
|
if ((res.status === 429 || res.status >= 500) && i < essais - 1) {
|
||||||
|
await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${res.status} sur ${url}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let fails = 0;
|
||||||
|
const fail = (id, msg) => {
|
||||||
|
fails++;
|
||||||
|
console.log(`FAIL ${id} — ${msg}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const distKm = (a, b) => {
|
||||||
|
const R = 6371,
|
||||||
|
dLa = ((b.lat - a.lat) * Math.PI) / 180,
|
||||||
|
dLo = ((b.lon - a.lon) * Math.PI) / 180;
|
||||||
|
const h =
|
||||||
|
Math.sin(dLa / 2) ** 2 +
|
||||||
|
Math.cos((a.lat * Math.PI) / 180) * Math.cos((b.lat * Math.PI) / 180) * Math.sin(dLo / 2) ** 2;
|
||||||
|
return 2 * R * Math.asin(Math.sqrt(h));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Schéma ------------------------------------------------------------------
|
||||||
|
const CHAMPS = ['id', 'nom', 'nom_creole', 'region', 'commune', 'coordonnees', 'difficulte', 'mapillary_id', 'credit', 'wikidata_id', 'indices'];
|
||||||
|
const REGIONS = ['GUADELOUPE', 'MARTINIQUE', 'GUYANE', 'REUNION'];
|
||||||
|
const ids = new Set();
|
||||||
|
const mlyIds = new Set();
|
||||||
|
for (const l of STREET) {
|
||||||
|
for (const c of CHAMPS) if (!(c in l)) fail(l.nom, `champ manquant: ${c}`);
|
||||||
|
if (ids.has(l.id)) fail(l.nom, `id dupliqué ${l.id}`);
|
||||||
|
ids.add(l.id);
|
||||||
|
if (mlyIds.has(l.mapillary_id)) fail(l.nom, `mapillary_id dupliqué ${l.mapillary_id}`);
|
||||||
|
mlyIds.add(l.mapillary_id);
|
||||||
|
if (!REGIONS.includes(l.region)) fail(l.nom, `région invalide: ${l.region}`);
|
||||||
|
if (!['FACILE', 'MOYEN', 'DIFFICILE', 'EXPERT'].includes(l.difficulte)) fail(l.nom, `difficulté invalide`);
|
||||||
|
if (!l.wikidata_id) fail(l.nom, `wikidata_id null (le mode street documente la commune)`);
|
||||||
|
if (!(l.aire_commune_km2 > 0)) fail(l.nom, `aire_commune_km2 invalide`);
|
||||||
|
}
|
||||||
|
const parRegion = Object.groupBy(STREET, (l) => l.region);
|
||||||
|
for (const r of REGIONS) {
|
||||||
|
const n = parRegion[r]?.length ?? 0;
|
||||||
|
if (n < 3) fail(r, `seulement ${n} lieux (< 3)`);
|
||||||
|
}
|
||||||
|
console.log(`Schéma : ${STREET.length} lieux contrôlés (${REGIONS.map((r) => `${r}=${parRegion[r]?.length ?? 0}`).join(', ')}).`);
|
||||||
|
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.log('Token Mapillary absent — vérifications API sautées (schéma seul).');
|
||||||
|
} else {
|
||||||
|
// --- Mapillary : existence, géométrie, crédit, pano -------------------------
|
||||||
|
console.log('\n| Lieu | mapillary_id | écart coords | crédit | pano | PASS/FAIL |');
|
||||||
|
console.log('|---|---|---|---|---|---|');
|
||||||
|
for (const l of STREET) {
|
||||||
|
try {
|
||||||
|
const img = await getJSON(
|
||||||
|
`https://graph.mapillary.com/${l.mapillary_id}?access_token=${encodeURIComponent(TOKEN)}&fields=id,is_pano,captured_at,computed_geometry,creator`
|
||||||
|
);
|
||||||
|
const geo = img.computed_geometry;
|
||||||
|
const d = distKm(l.coordonnees, { lat: geo.coordinates[1], lon: geo.coordinates[0] });
|
||||||
|
const notes = [];
|
||||||
|
let ok = true;
|
||||||
|
if (d > 0.1) {
|
||||||
|
ok = false;
|
||||||
|
notes.push(`écart ${(d * 1000).toFixed(0)} m > 100 m`);
|
||||||
|
}
|
||||||
|
if ((img.creator?.username ?? null) !== l.credit.username) {
|
||||||
|
ok = false;
|
||||||
|
notes.push(`crédit « ${l.credit.username} » ≠ « ${img.creator?.username} »`);
|
||||||
|
}
|
||||||
|
if (!!img.is_pano !== !!l.is_pano) notes.push(`pano API=${img.is_pano} corpus=${l.is_pano} (non bloquant)`);
|
||||||
|
if (!ok) fail(l.nom, notes.join(' ; '));
|
||||||
|
console.log(`| ${l.nom} | ${l.mapillary_id} | ${(d * 1000).toFixed(0)} m | ${l.credit.username} | ${img.is_pano ? 'oui' : 'non'} | ${ok ? 'PASS' : 'FAIL'} |`);
|
||||||
|
} catch (e) {
|
||||||
|
fail(l.nom, `Mapillary : ${e.message}`);
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 120));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Wikidata : sitelink frwiki pour chaque commune ---------------------------
|
||||||
|
const qids = STREET.map((l) => l.wikidata_id).filter(Boolean).join('|');
|
||||||
|
const data = await getJSON(
|
||||||
|
`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qids}&props=sitelinks&sitefilter=frwiki&format=json`
|
||||||
|
);
|
||||||
|
console.log('\n| Lieu | QID commune | sitelink frwiki | PASS/FAIL |');
|
||||||
|
console.log('|---|---|---|---|');
|
||||||
|
for (const l of STREET) {
|
||||||
|
const site = data.entities?.[l.wikidata_id]?.sitelinks?.frwiki?.title;
|
||||||
|
if (!site) fail(l.nom, `${l.wikidata_id} sans sitelink frwiki`);
|
||||||
|
console.log(`| ${l.nom} | ${l.wikidata_id} | ${site ?? 'ABSENT'} | ${site ? 'PASS' : 'FAIL'} |`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(fails ? `\n${fails} FAIL.` : '\nTout est conforme.');
|
||||||
|
process.exit(fails ? 1 : 0);
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
guess?: Coordonnees | null;
|
guess?: Coordonnees | null;
|
||||||
real?: Coordonnees | null;
|
real?: Coordonnees | null;
|
||||||
|
distanceKm?: number | null;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
pairs?: Paire[];
|
pairs?: Paire[];
|
||||||
markers?: MarqueurInfo[];
|
markers?: MarqueurInfo[];
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
let {
|
let {
|
||||||
guess = $bindable(null),
|
guess = $bindable(null),
|
||||||
real = null,
|
real = null,
|
||||||
|
distanceKm = null,
|
||||||
interactive = true,
|
interactive = true,
|
||||||
pairs = [],
|
pairs = [],
|
||||||
markers = [],
|
markers = [],
|
||||||
@@ -48,7 +50,7 @@
|
|||||||
let guessMarker: Marker | null = null;
|
let guessMarker: Marker | null = null;
|
||||||
let realMarker: Marker | null = null;
|
let realMarker: Marker | null = null;
|
||||||
let explorerMarkers: Marker[] = [];
|
let explorerMarkers: Marker[] = [];
|
||||||
let raf = 0;
|
let observateurTaille: ResizeObserver | null = null;
|
||||||
|
|
||||||
const SRC_LIGNE = 'jwe-ligne';
|
const SRC_LIGNE = 'jwe-ligne';
|
||||||
const SRC_PAIRES = 'jwe-paires';
|
const SRC_PAIRES = 'jwe-paires';
|
||||||
@@ -100,48 +102,60 @@
|
|||||||
id: 'jwe-ligne-line',
|
id: 'jwe-ligne-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: SRC_LIGNE,
|
source: SRC_LIGNE,
|
||||||
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
|
paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
|
||||||
});
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'jwe-ligne-label',
|
||||||
|
type: 'symbol',
|
||||||
|
source: SRC_LIGNE,
|
||||||
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'etiquette'],
|
||||||
|
'text-size': 14,
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-offset': [0, -1.4]
|
||||||
|
},
|
||||||
|
paint: { 'text-color': '#0b3d2e', 'text-halo-color': '#fffdf8', 'text-halo-width': 2 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const features: GeoJSON.Feature[] = [];
|
||||||
|
if (guess) {
|
||||||
|
features.push(ligneFeature(guess, real, null));
|
||||||
|
if (distanceKm !== null) {
|
||||||
|
// Étiquette de distance au milieu de la ligne guess ↔ réel
|
||||||
|
features.push(
|
||||||
|
pointFeature(
|
||||||
|
{ lat: (guess.lat + real.lat) / 2, lon: (guess.lon + real.lon) / 2 },
|
||||||
|
{ etiquette: `${distanceKm} km` }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const features: GeoJSON.Feature[] = guess ? [ligneFeature(guess, real, null)] : [];
|
|
||||||
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });
|
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });
|
||||||
|
|
||||||
// Marqueur vert = position réelle
|
// Marqueur vert = position réelle ; le marqueur corail du joueur RESTE
|
||||||
|
// à sa position : la comparaison visuelle guess ↔ réel est le cœur du jeu.
|
||||||
realMarker?.remove();
|
realMarker?.remove();
|
||||||
realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);
|
realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);
|
||||||
|
|
||||||
// Le marqueur du joueur « vole » vers la position réelle (sauf reduced-motion)
|
if (guess) {
|
||||||
if (guessMarker && guess) {
|
|
||||||
const from = { ...guess };
|
|
||||||
if (reducedMotion) {
|
|
||||||
guessMarker.setLngLat([real.lon, real.lat]);
|
|
||||||
} else {
|
|
||||||
const debut = performance.now();
|
|
||||||
const duree = 1400;
|
|
||||||
const step = (now: number) => {
|
|
||||||
if (!guessMarker) return;
|
|
||||||
const k = Math.min(1, (now - debut) / duree);
|
|
||||||
const e = 1 - Math.pow(1 - k, 3);
|
|
||||||
guessMarker.setLngLat([
|
|
||||||
from.lon + (real.lon - from.lon) * e,
|
|
||||||
from.lat + (real.lat - from.lat) * e
|
|
||||||
]);
|
|
||||||
if (k < 1) raf = requestAnimationFrame(step);
|
|
||||||
};
|
|
||||||
raf = requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
const bounds = new ml.LngLatBounds(
|
const bounds = new ml.LngLatBounds(
|
||||||
[Math.min(from.lon, real.lon), Math.min(from.lat, real.lat)],
|
[Math.min(guess.lon, real.lon), Math.min(guess.lat, real.lat)],
|
||||||
[Math.max(from.lon, real.lon), Math.max(from.lat, real.lat)]
|
[Math.max(guess.lon, real.lon), Math.max(guess.lat, real.lat)]
|
||||||
);
|
);
|
||||||
map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 });
|
// Le panneau résultat vient de remplacer la photo : synchroniser la
|
||||||
|
// taille du canvas AVANT de calculer le cadrage, sinon il est faussé.
|
||||||
|
// maxZoom 15 (et non 12) : pour un guess proche du lieu, les deux
|
||||||
|
// marqueurs doivent rester distinguables (juger « à quel point on est bon »).
|
||||||
|
map.resize();
|
||||||
|
map.fitBounds(bounds, { padding: 90, duration: reducedMotion ? 0 : 900, maxZoom: 15 });
|
||||||
} else {
|
} else {
|
||||||
map.easeTo({ center: [real.lon, real.lat], zoom: 11, duration: reducedMotion ? 0 : 900 });
|
map.easeTo({ center: [real.lon, real.lat], zoom: 11, duration: reducedMotion ? 0 : 900 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nettoyerRevelation(): void {
|
function nettoyerRevelation(): void {
|
||||||
cancelAnimationFrame(raf);
|
|
||||||
realMarker?.remove();
|
realMarker?.remove();
|
||||||
realMarker = null;
|
realMarker = null;
|
||||||
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
|
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
|
||||||
@@ -308,10 +322,15 @@
|
|||||||
syncExplorerMarkers();
|
syncExplorerMarkers();
|
||||||
if (real) dessinerRevelation();
|
if (real) dessinerRevelation();
|
||||||
});
|
});
|
||||||
|
// MapLibre ne détecte pas seul les changements de taille du conteneur
|
||||||
|
// (panneau résultat qui remplace la photo, rotation d'écran…).
|
||||||
|
observateurTaille = new ResizeObserver(() => map?.resize());
|
||||||
|
observateurTaille.observe(conteneur);
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
detruit = true;
|
detruit = true;
|
||||||
cancelAnimationFrame(raf);
|
observateurTaille?.disconnect();
|
||||||
|
observateurTaille = null;
|
||||||
map?.remove();
|
map?.remove();
|
||||||
map = null;
|
map = null;
|
||||||
};
|
};
|
||||||
@@ -319,7 +338,13 @@
|
|||||||
|
|
||||||
let ancienReal: Coordonnees | null = null;
|
let ancienReal: Coordonnees | null = null;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!map || !map.loaded()) return;
|
// Lecture inconditionnelle de `real` : au montage la carte n'existe pas
|
||||||
|
// encore, et un retour anticipé avant toute lecture réactive ferait que
|
||||||
|
// Svelte n'enregistre aucune dépendance — l'effet ne serait jamais relancé.
|
||||||
|
// (isStyleLoaded et non loaded : loaded() reste faux pendant le chargement
|
||||||
|
// des tuiles, ce qui ferait rater le dessin juste après un zoom/pan.)
|
||||||
|
void real;
|
||||||
|
if (!map || !map.isStyleLoaded()) return;
|
||||||
if (real && !ancienReal) dessinerRevelation();
|
if (real && !ancienReal) dessinerRevelation();
|
||||||
if (!real && ancienReal) nettoyerRevelation();
|
if (!real && ancienReal) nettoyerRevelation();
|
||||||
ancienReal = real;
|
ancienReal = real;
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { t } from '$lib/i18n/store.svelte';
|
||||||
|
import type { Viewer } from 'mapillary-js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mapillaryId: string;
|
||||||
|
credit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { mapillaryId, credit }: Props = $props();
|
||||||
|
|
||||||
|
let conteneur: HTMLDivElement;
|
||||||
|
let viewer: Viewer | null = null;
|
||||||
|
let etat = $state<'chargement' | 'pret' | 'erreur' | 'token'>('chargement');
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let detruit = false;
|
||||||
|
// Lazy load : mapillary-js (JS + CSS) n'est chargé que sur l'écran de jeu street.
|
||||||
|
void (async () => {
|
||||||
|
const token = env.PUBLIC_MAPILLARY_TOKEN;
|
||||||
|
if (!token) {
|
||||||
|
etat = 'token';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const [mod] = await Promise.all([
|
||||||
|
import('mapillary-js'),
|
||||||
|
import('mapillary-js/dist/mapillary.css')
|
||||||
|
]);
|
||||||
|
if (detruit) return;
|
||||||
|
// cover désactivé : la photo s'affiche directement, sans écran d'intro.
|
||||||
|
viewer = new mod.Viewer({
|
||||||
|
accessToken: token,
|
||||||
|
container: conteneur,
|
||||||
|
component: { cover: false }
|
||||||
|
});
|
||||||
|
// moveTo rejette si l'image a été retirée de Mapillary entre-temps.
|
||||||
|
await viewer.moveTo(mapillaryId);
|
||||||
|
if (detruit) return;
|
||||||
|
etat = 'pret';
|
||||||
|
} catch {
|
||||||
|
if (!detruit) etat = 'erreur';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
detruit = true;
|
||||||
|
viewer?.remove();
|
||||||
|
viewer = null;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<figure class="street-panel">
|
||||||
|
<div
|
||||||
|
bind:this={conteneur}
|
||||||
|
class="visionneuse"
|
||||||
|
role="application"
|
||||||
|
aria-label={t().jeu.streetAriaLabel}
|
||||||
|
></div>
|
||||||
|
{#if etat !== 'pret'}
|
||||||
|
<div class="voile">
|
||||||
|
<p>
|
||||||
|
{#if etat === 'chargement'}{t().jeu.streetChargement}
|
||||||
|
{:else if etat === 'token'}{t().jeu.streetToken}
|
||||||
|
{:else}{t().jeu.streetErreur}{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if credit && etat === 'pret'}
|
||||||
|
<figcaption>{credit}</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.street-panel {
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--oki-vert);
|
||||||
|
border-radius: var(--oki-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visionneuse {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voile {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--oki-blanc);
|
||||||
|
background: var(--oki-vert);
|
||||||
|
}
|
||||||
|
|
||||||
|
figcaption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(transparent, rgba(11, 61, 46, 0.85));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,6 +17,8 @@ const fr = {
|
|||||||
lieuDesc: 'Une rue, un bord de mer, une forêt… devine où tu es.',
|
lieuDesc: 'Une rue, un bord de mer, une forêt… devine où tu es.',
|
||||||
defi: 'Défi 5 rounds',
|
defi: 'Défi 5 rounds',
|
||||||
defiDesc: '5 lieux d’affilée, monuments et lieux mélangés.',
|
defiDesc: '5 lieux d’affilée, monuments et lieux mélangés.',
|
||||||
|
street: 'An lari a',
|
||||||
|
streetDesc: 'Navigue dans une rue en photo 360° et devine la commune.',
|
||||||
explorer: 'Aprann',
|
explorer: 'Aprann',
|
||||||
explorerDesc: 'Explore la carte librement et lis les fiches Wikipédia. Sans score.'
|
explorerDesc: 'Explore la carte librement et lis les fiches Wikipédia. Sans score.'
|
||||||
},
|
},
|
||||||
@@ -40,6 +42,10 @@ const fr = {
|
|||||||
indicesRestants: 'indices restants',
|
indicesRestants: 'indices restants',
|
||||||
plusDIndice: 'Plus d’indice disponible',
|
plusDIndice: 'Plus d’indice disponible',
|
||||||
poserMarqueur: 'Clique sur la carte pour placer ton marqueur',
|
poserMarqueur: 'Clique sur la carte pour placer ton marqueur',
|
||||||
|
streetChargement: 'Chargement de la vue de rue…',
|
||||||
|
streetErreur: 'Impossible de charger la photo Mapillary. Réessaie ou passe au lieu suivant.',
|
||||||
|
streetToken: 'Visionneuse indisponible : configuration Mapillary manquante.',
|
||||||
|
streetAriaLabel: 'Vue de rue interactive. Flèches pour avancer, glisser pour regarder autour.',
|
||||||
round: 'Manche',
|
round: 'Manche',
|
||||||
sur: 'sur',
|
sur: 'sur',
|
||||||
difficulte: {
|
difficulte: {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const gcf: Dict = {
|
|||||||
lieuDesc: 'On lari, on bodlanmè, on fowé… chwé kote ou yé.',
|
lieuDesc: 'On lari, on bodlanmè, on fowé… chwé kote ou yé.',
|
||||||
defi: 'Défi 5 rounds',
|
defi: 'Défi 5 rounds',
|
||||||
defiDesc: '5 koté youn dèyè ròt, moniman épi plas mélanjé.',
|
defiDesc: '5 koté youn dèyè ròt, moniman épi plas mélanjé.',
|
||||||
|
street: 'An lari a',
|
||||||
|
streetDesc: 'Mache nan on lari an foto 360° é chwé komin-lan.',
|
||||||
explorer: 'Aprann',
|
explorer: 'Aprann',
|
||||||
explorerDesc: 'Flanné asou kat-la é li fich Wikipédia yo. San pwen.'
|
explorerDesc: 'Flanné asou kat-la é li fich Wikipédia yo. San pwen.'
|
||||||
},
|
},
|
||||||
@@ -42,6 +44,10 @@ const gcf: Dict = {
|
|||||||
indicesRestants: 'endis ki rété',
|
indicesRestants: 'endis ki rété',
|
||||||
plusDIndice: 'Pa ni endis ankò',
|
plusDIndice: 'Pa ni endis ankò',
|
||||||
poserMarqueur: 'Kwiké asou kat-la pou mété makò-a',
|
poserMarqueur: 'Kwiké asou kat-la pou mété makò-a',
|
||||||
|
streetChargement: 'Nou ka chajé vi lari-a…',
|
||||||
|
streetErreur: 'Nou pa rivé chajé foto Mapillary-la. Éséyé ankò oben pasé o koté swivan.',
|
||||||
|
streetToken: 'Vi lari-a pa disponib : konfigirasyon Mapillary-la manké.',
|
||||||
|
streetAriaLabel: 'Vi lari-a. Flèch yo pou avansé, glissé pou gadté toutò.',
|
||||||
round: 'Mannèk',
|
round: 'Mannèk',
|
||||||
sur: 'asou',
|
sur: 'asou',
|
||||||
difficulte: {
|
difficulte: {
|
||||||
|
|||||||
@@ -0,0 +1,602 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "st-01",
|
||||||
|
"nom": "Rue Laurence Valmy, Pointe-à-Pitre",
|
||||||
|
"nom_creole": "Lari Pointe-à-Pitre",
|
||||||
|
"region": "GUADELOUPE",
|
||||||
|
"commune": "Pointe-à-Pitre",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 16.244455939903,
|
||||||
|
"lon": -61.530342688698
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "2941939632687980",
|
||||||
|
"credit": {
|
||||||
|
"username": "cartophyl",
|
||||||
|
"captured_at": "2015-07-07"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q335322",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île papillon, aile droite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Port de commerce principal de l’archipel"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 2.78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-02",
|
||||||
|
"nom": "Rue Achille René Boisneuf, Les Abymes",
|
||||||
|
"nom_creole": "Lari Les Abymes",
|
||||||
|
"region": "GUADELOUPE",
|
||||||
|
"commune": "Les Abymes",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 16.269078645766,
|
||||||
|
"lon": -61.506631441162
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "306781020957650",
|
||||||
|
"credit": {
|
||||||
|
"username": "ratzillas",
|
||||||
|
"captured_at": "2015-07-17"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q13636",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île papillon, aile droite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Commune la plus peuplée de l’archipel"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 70.43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-03",
|
||||||
|
"nom": "Place Renovation le Bourg, Le Gosier",
|
||||||
|
"nom_creole": "Lari Le Gosier",
|
||||||
|
"region": "GUADELOUPE",
|
||||||
|
"commune": "Le Gosier",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 16.205608856053,
|
||||||
|
"lon": -61.492154911837
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "919062132219342",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-01-02"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q918874",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île papillon, aile droite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Station balnéaire au sud de la Grande-Terre"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 45.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-04",
|
||||||
|
"nom": "Rue Emmanuel Robinet, Sainte-Anne",
|
||||||
|
"nom_creole": "Lari Sainte-Anne",
|
||||||
|
"region": "GUADELOUPE",
|
||||||
|
"commune": "Sainte-Anne",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 16.223896372356,
|
||||||
|
"lon": -61.384172350192
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "956408765176680",
|
||||||
|
"credit": {
|
||||||
|
"username": "ratzillas",
|
||||||
|
"captured_at": "2019-07-15"
|
||||||
|
},
|
||||||
|
"is_pano": true,
|
||||||
|
"wikidata_id": "Q386243",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île papillon, aile droite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Plages au sud de la Grande-Terre"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 80.96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-05",
|
||||||
|
"nom": "Route des Esclaves, Basse-Terre",
|
||||||
|
"nom_creole": "Lari Basse-Terre",
|
||||||
|
"region": "GUADELOUPE",
|
||||||
|
"commune": "Basse-Terre",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 15.992959593433,
|
||||||
|
"lon": -61.730437544596
|
||||||
|
},
|
||||||
|
"difficulte": "EXPERT",
|
||||||
|
"mapillary_id": "212605717097536",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-01-04"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q167024",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île papillon, aile gauche"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Chef-lieu au pied du volcan"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 4.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-06",
|
||||||
|
"nom": "Rue Victor Sévère, Fort-de-France",
|
||||||
|
"nom_creole": "Lari Fort-de-France",
|
||||||
|
"region": "MARTINIQUE",
|
||||||
|
"commune": "Fort-de-France",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 14.60615582786,
|
||||||
|
"lon": -61.069885806689
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "152801196814796",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2015-12-31"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q81621",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île aux fleurs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Chef-lieu, sur une grande baie"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 44.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-07",
|
||||||
|
"nom": "Rue des Barrières, Le Lamentin",
|
||||||
|
"nom_creole": "Lari Le Lamentin",
|
||||||
|
"region": "MARTINIQUE",
|
||||||
|
"commune": "Le Lamentin",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 14.615969225175,
|
||||||
|
"lon": -60.999773265489
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "1120779875836768",
|
||||||
|
"credit": {
|
||||||
|
"username": "Caramel",
|
||||||
|
"captured_at": "2024-04-29"
|
||||||
|
},
|
||||||
|
"is_pano": true,
|
||||||
|
"wikidata_id": "Q947069",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île aux fleurs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Banlieue du chef-lieu, zone aéroportuaire"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 62.73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-08",
|
||||||
|
"nom": "Rue Gabriel Péri, Saint-Pierre",
|
||||||
|
"nom_creole": "Lari Saint-Pierre",
|
||||||
|
"region": "MARTINIQUE",
|
||||||
|
"commune": "Saint-Pierre",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 14.738699893002,
|
||||||
|
"lon": -61.176412948112
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "1299527321706804",
|
||||||
|
"credit": {
|
||||||
|
"username": "jeremylavarenne",
|
||||||
|
"captured_at": "2025-08-31"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q589980",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île aux fleurs, côte caraïbe nord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Ville détruite par une éruption en 1902"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 38.69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-09",
|
||||||
|
"nom": "Rue Florent Holo, Le François",
|
||||||
|
"nom_creole": "Lari Le François",
|
||||||
|
"region": "MARTINIQUE",
|
||||||
|
"commune": "Le François",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 14.615696752255,
|
||||||
|
"lon": -60.902520891662
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "4102521129805870",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-01-01"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q282944",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île aux fleurs, côte atlantique"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Bourg face aux îlets du sud"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 54.43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-10",
|
||||||
|
"nom": "Passage des Fleurs, Sainte-Marie",
|
||||||
|
"nom_creole": "Lari Sainte-Marie",
|
||||||
|
"region": "MARTINIQUE",
|
||||||
|
"commune": "Sainte-Marie",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 14.783766654906,
|
||||||
|
"lon": -60.995666311379
|
||||||
|
},
|
||||||
|
"difficulte": "EXPERT",
|
||||||
|
"mapillary_id": "131479329019761",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2015-12-31"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q1758414",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île aux fleurs, côte atlantique nord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Musée de la banane et rhum"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 44.65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-11",
|
||||||
|
"nom": "Rue Francois Arago, Cayenne",
|
||||||
|
"nom_creole": "Lari Cayenne",
|
||||||
|
"region": "GUYANE",
|
||||||
|
"commune": "Cayenne",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 4.9388185867966,
|
||||||
|
"lon": -52.331808101487
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "259270559222687",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-08-18"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q44401",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Terre de forêt amazonienne"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Chef-lieu, sur l’île du même nom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 25.52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-12",
|
||||||
|
"nom": "Rue de l’Odéon, Kourou",
|
||||||
|
"nom_creole": "Lari Kourou",
|
||||||
|
"region": "GUYANE",
|
||||||
|
"commune": "Kourou",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 5.1615176393394,
|
||||||
|
"lon": -52.643325658226
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "198127629404270",
|
||||||
|
"credit": {
|
||||||
|
"username": "hacp",
|
||||||
|
"captured_at": "2022-09-07"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q273312",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Terre de forêt amazonienne"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Ville des fusées"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 2289.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-13",
|
||||||
|
"nom": "Avenue Albert Sarraut, Saint-Laurent-du-Maroni",
|
||||||
|
"nom_creole": "Lari Saint-Laurent-du-Maroni",
|
||||||
|
"region": "GUYANE",
|
||||||
|
"commune": "Saint-Laurent-du-Maroni",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 5.5060375294565,
|
||||||
|
"lon": -54.027135700884
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "147142287390967",
|
||||||
|
"credit": {
|
||||||
|
"username": "pma",
|
||||||
|
"captured_at": "2021-01-18"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q319959",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Terre de forêt amazonienne, à l’ouest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Ville du fleuve Maroni, ancien bagne"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 4320.77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-14",
|
||||||
|
"nom": "Rue Maurice BOCS, Matoury",
|
||||||
|
"nom_creole": "Lari Matoury",
|
||||||
|
"region": "GUYANE",
|
||||||
|
"commune": "Matoury",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 4.8496318387901,
|
||||||
|
"lon": -52.328783153736
|
||||||
|
},
|
||||||
|
"difficulte": "EXPERT",
|
||||||
|
"mapillary_id": "582210316504280",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-08-18"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q816739",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Terre de forêt amazonienne"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Banlieue sud du chef-lieu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 146.68
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-15",
|
||||||
|
"nom": "Rue de la CANNE À SUCRE, Remire-Montjoly",
|
||||||
|
"nom_creole": "Lari Remire-Montjoly",
|
||||||
|
"region": "GUYANE",
|
||||||
|
"commune": "Remire-Montjoly",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": 4.9037937340773,
|
||||||
|
"lon": -52.273335996076
|
||||||
|
},
|
||||||
|
"difficulte": "EXPERT",
|
||||||
|
"mapillary_id": "247810083801809",
|
||||||
|
"credit": {
|
||||||
|
"username": "kaartcam",
|
||||||
|
"captured_at": "2016-08-18"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q996222",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Terre de forêt amazonienne"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Plages à l’est du chef-lieu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 46.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-16",
|
||||||
|
"nom": "Rue de Paris, Saint-Denis",
|
||||||
|
"nom_creole": "Lari Saint-Denis",
|
||||||
|
"region": "REUNION",
|
||||||
|
"commune": "Saint-Denis",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": -20.8832861,
|
||||||
|
"lon": 55.4501
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "2262788723930767",
|
||||||
|
"credit": {
|
||||||
|
"username": "AllWays360",
|
||||||
|
"captured_at": "2022-10-06"
|
||||||
|
},
|
||||||
|
"is_pano": true,
|
||||||
|
"wikidata_id": "Q47045",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île intense de l’océan Indien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Chef-lieu, au nord"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 142.33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-17",
|
||||||
|
"nom": "Rue du Cimetiere, Saint-Pierre",
|
||||||
|
"nom_creole": "Lari Saint-Pierre",
|
||||||
|
"region": "REUNION",
|
||||||
|
"commune": "Saint-Pierre",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": -21.338988428747,
|
||||||
|
"lon": 55.478221620359
|
||||||
|
},
|
||||||
|
"difficulte": "MOYEN",
|
||||||
|
"mapillary_id": "143202321798356",
|
||||||
|
"credit": {
|
||||||
|
"username": "sigstpierre",
|
||||||
|
"captured_at": "2022-11-09"
|
||||||
|
},
|
||||||
|
"is_pano": true,
|
||||||
|
"wikidata_id": "Q249884",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île intense de l’océan Indien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Capitale du sud, front de mer animé"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 96.82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-18",
|
||||||
|
"nom": "Rue Aristide Briand, Le Tampon",
|
||||||
|
"nom_creole": "Lari Le Tampon",
|
||||||
|
"region": "REUNION",
|
||||||
|
"commune": "Le Tampon",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": -21.277675635385,
|
||||||
|
"lon": 55.514338556402
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "1382811445417165",
|
||||||
|
"credit": {
|
||||||
|
"username": "vandecasteele",
|
||||||
|
"captured_at": "2019-12-16"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q13648",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île intense de l’océan Indien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Commune des hauts, dans le sud"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 182.09
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-19",
|
||||||
|
"nom": "Rue Elie Eudor, Saint-Paul",
|
||||||
|
"nom_creole": "Lari Saint-Paul",
|
||||||
|
"region": "REUNION",
|
||||||
|
"commune": "Saint-Paul",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": -21.008621906549,
|
||||||
|
"lon": 55.271355918998
|
||||||
|
},
|
||||||
|
"difficulte": "DIFFICILE",
|
||||||
|
"mapillary_id": "928324514646684",
|
||||||
|
"credit": {
|
||||||
|
"username": "sogefi",
|
||||||
|
"captured_at": "2018-10-14"
|
||||||
|
},
|
||||||
|
"is_pano": false,
|
||||||
|
"wikidata_id": "Q316887",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île intense de l’océan Indien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Côte ouest, premier site de peuplement"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 241.64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "st-20",
|
||||||
|
"nom": "Ccal de la Republique, Saint-André",
|
||||||
|
"nom_creole": "Lari Saint-André",
|
||||||
|
"region": "REUNION",
|
||||||
|
"commune": "Saint-André",
|
||||||
|
"coordonnees": {
|
||||||
|
"lat": -20.962268395233,
|
||||||
|
"lon": 55.651364657007
|
||||||
|
},
|
||||||
|
"difficulte": "EXPERT",
|
||||||
|
"mapillary_id": "519625869420978",
|
||||||
|
"credit": {
|
||||||
|
"username": "vandecasteele",
|
||||||
|
"captured_at": "2019-06-28"
|
||||||
|
},
|
||||||
|
"is_pano": true,
|
||||||
|
"wikidata_id": "Q1649370",
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"niveau": 1,
|
||||||
|
"texte": "Île intense de l’océan Indien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"niveau": 2,
|
||||||
|
"texte": "Côte est, pays de la canne"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"aire_commune_km2": 54.66
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import rawLieux from './data/lieux.json';
|
import rawLieux from './data/lieux.json';
|
||||||
|
import rawStreet from './data/street.json';
|
||||||
import type {
|
import type {
|
||||||
Categorie,
|
Categorie,
|
||||||
Difficulte,
|
Difficulte,
|
||||||
@@ -6,10 +7,15 @@ import type {
|
|||||||
GuessResult,
|
GuessResult,
|
||||||
Lieu,
|
Lieu,
|
||||||
Region,
|
Region,
|
||||||
RoundPlace
|
RoundPlace,
|
||||||
|
StreetLieu
|
||||||
} from '$lib/types';
|
} from '$lib/types';
|
||||||
|
|
||||||
const lieux = rawLieux as Lieu[];
|
const lieux = rawLieux as Lieu[];
|
||||||
|
const street = rawStreet as StreetLieu[];
|
||||||
|
|
||||||
|
/** Lieu « scorable » : les deux corpus partagent les champs du scoring. */
|
||||||
|
type Scoreable = Lieu | StreetLieu;
|
||||||
|
|
||||||
const REGIONS: Region[] = ['GUADELOUPE', 'MARTINIQUE', 'GUYANE', 'REUNION'];
|
const REGIONS: Region[] = ['GUADELOUPE', 'MARTINIQUE', 'GUYANE', 'REUNION'];
|
||||||
const CATEGORIES: Categorie[] = ['MONUMENT', 'LIEU'];
|
const CATEGORIES: Categorie[] = ['MONUMENT', 'LIEU'];
|
||||||
@@ -43,7 +49,13 @@ function parseCategorie(value: string | null): Categorie | null {
|
|||||||
return value && CATEGORIES.includes(value as Categorie) ? (value as Categorie) : null;
|
return value && CATEGORIES.includes(value as Categorie) ? (value as Categorie) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLieuById(id: string): Lieu | undefined {
|
/** Recherche dans les deux corpus (photos classiques + street Mapillary). */
|
||||||
|
export function getLieuById(id: string): Scoreable | undefined {
|
||||||
|
return lieux.find((l) => l.id === id) ?? street.find((l) => l.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recherche limitée au corpus photo classique (défi, proxy photo). */
|
||||||
|
export function getLieuPhotoById(id: string): Lieu | undefined {
|
||||||
return lieux.find((l) => l.id === id);
|
return lieux.find((l) => l.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +86,28 @@ export function getRandomLieu(options: {
|
|||||||
return pool[Math.floor(Math.random() * pool.length)];
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sélection aléatoire d'un lieu street (mode « An lari a », corpus Mapillary). */
|
||||||
|
export function getRandomStreet(options: {
|
||||||
|
region?: string | null;
|
||||||
|
exclude?: string | null;
|
||||||
|
}): StreetLieu | null {
|
||||||
|
const region = parseRegion(options.region ?? null);
|
||||||
|
const excluded = new Set(
|
||||||
|
(options.exclude ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
let pool = street.filter((l) => !excluded.has(l.id));
|
||||||
|
if (region) pool = pool.filter((l) => l.region === region);
|
||||||
|
// Si tout a été exclu, on réessaie sans les exclusions (le jeu doit continuer).
|
||||||
|
if (pool.length === 0) {
|
||||||
|
pool = region ? street.filter((l) => l.region === region) : street;
|
||||||
|
}
|
||||||
|
if (pool.length === 0) return null;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}
|
||||||
|
|
||||||
/** Largeurs (px) servies par le proxy photo /api/photo/[id]?w=. */
|
/** Largeurs (px) servies par le proxy photo /api/photo/[id]?w=. */
|
||||||
export const PHOTO_LARGEURS = [400, 800, 1200] as const;
|
export const PHOTO_LARGEURS = [400, 800, 1200] as const;
|
||||||
|
|
||||||
@@ -103,6 +137,27 @@ export function sanitizeRound(lieu: Lieu): RoundPlace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anti-triche, mode street : ne garde que les champs publics.
|
||||||
|
* Seul `mapillaryId` part au client (la visionneuse doit bien appeler Mapillary) ;
|
||||||
|
* coords/commune/région/wikidata_id ne partent jamais avant le guess.
|
||||||
|
* Limitation documentée (architecture.md §10) : l'image_key est publique côté
|
||||||
|
* Mapillary, un joueur techniquement équipé peut retrouver la position — même
|
||||||
|
* propriété que GeoGuessr/Street View, acceptable en solo sans leaderboard.
|
||||||
|
*/
|
||||||
|
export function sanitizeRoundStreet(lieu: StreetLieu): RoundPlace {
|
||||||
|
return {
|
||||||
|
id: lieu.id,
|
||||||
|
nom: lieu.nom,
|
||||||
|
nom_creole: lieu.nom_creole,
|
||||||
|
categorie: 'LIEU',
|
||||||
|
difficulte: lieu.difficulte,
|
||||||
|
mapillaryId: lieu.mapillary_id,
|
||||||
|
credit: `© ${lieu.credit.username} / Mapillary (CC BY-SA 4.0)`,
|
||||||
|
indices: lieu.indices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rayon effectif (km) de la courbe de score (audit A1).
|
* Rayon effectif (km) de la courbe de score (audit A1).
|
||||||
* Les communes guyanaises sont immenses (Saint-Laurent-du-Maroni ≈ 4 321 km²) :
|
* Les communes guyanaises sont immenses (Saint-Laurent-du-Maroni ≈ 4 321 km²) :
|
||||||
@@ -111,7 +166,7 @@ export function sanitizeRound(lieu: Lieu): RoundPlace {
|
|||||||
* au moins celui du disque de même aire que la commune : sqrt(aire / π).
|
* au moins celui du disque de même aire que la commune : sqrt(aire / π).
|
||||||
* Champ optionnel : sans lui, on retombe sur le rayon de difficulté.
|
* Champ optionnel : sans lui, on retombe sur le rayon de difficulté.
|
||||||
*/
|
*/
|
||||||
export function rayonEffectifKm(lieu: Lieu): number {
|
export function rayonEffectifKm(lieu: Scoreable): number {
|
||||||
const base = RAYONS[lieu.difficulte];
|
const base = RAYONS[lieu.difficulte];
|
||||||
const aire = lieu.aire_commune_km2;
|
const aire = lieu.aire_commune_km2;
|
||||||
if (typeof aire === 'number' && Number.isFinite(aire) && aire > 0) {
|
if (typeof aire === 'number' && Number.isFinite(aire) && aire > 0) {
|
||||||
@@ -126,7 +181,7 @@ export function rayonEffectifKm(lieu: Lieu): number {
|
|||||||
* bonus temps ≤ +20 % ; total plafonné à 5000.
|
* bonus temps ≤ +20 % ; total plafonné à 5000.
|
||||||
*/
|
*/
|
||||||
export function scoreGuess(
|
export function scoreGuess(
|
||||||
lieu: Lieu,
|
lieu: Scoreable,
|
||||||
guess: { lat: number; lon: number },
|
guess: { lat: number; lon: number },
|
||||||
hintsUsed: number,
|
hintsUsed: number,
|
||||||
timeMs?: number
|
timeMs?: number
|
||||||
@@ -154,7 +209,7 @@ export function scoreGuess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Réponse « Passer » : la vérité est révélée, score 0, pas de distance. */
|
/** Réponse « Passer » : la vérité est révélée, score 0, pas de distance. */
|
||||||
export function skipResult(lieu: Lieu): GuessResult {
|
export function skipResult(lieu: Scoreable): GuessResult {
|
||||||
return {
|
return {
|
||||||
distanceKm: null,
|
distanceKm: null,
|
||||||
score: 0,
|
score: 0,
|
||||||
|
|||||||
+28
-2
@@ -44,6 +44,27 @@ export interface Lieu {
|
|||||||
aire_commune_km2?: number;
|
aire_commune_km2?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lieu du mode street (« An lari a ») — serveur uniquement.
|
||||||
|
* La photo statique est remplacée par une image Mapillary (CC BY-SA 4.0).
|
||||||
|
* `wikidata_id` pointe vers la COMMUNE (phase éducative : fiche de la commune).
|
||||||
|
*/
|
||||||
|
export interface StreetLieu {
|
||||||
|
id: string;
|
||||||
|
nom: string;
|
||||||
|
nom_creole: string;
|
||||||
|
region: Region;
|
||||||
|
commune: string;
|
||||||
|
coordonnees: Coordonnees;
|
||||||
|
difficulte: Difficulte;
|
||||||
|
mapillary_id: string;
|
||||||
|
credit: { username: string; captured_at: string };
|
||||||
|
is_pano: boolean;
|
||||||
|
wikidata_id: string | null;
|
||||||
|
indices: Indice[];
|
||||||
|
aire_commune_km2?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** Lieu SANITISÉ envoyé par GET /api/round (aucune coordonnée, commune, région, wikidata_id). */
|
/** Lieu SANITISÉ envoyé par GET /api/round (aucune coordonnée, commune, région, wikidata_id). */
|
||||||
export interface RoundPlace {
|
export interface RoundPlace {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -51,7 +72,12 @@ export interface RoundPlace {
|
|||||||
nom_creole: string;
|
nom_creole: string;
|
||||||
categorie: Categorie;
|
categorie: Categorie;
|
||||||
difficulte: Difficulte;
|
difficulte: Difficulte;
|
||||||
photo: Photo;
|
/** Modes photo (monument/lieu/defi). Absent en mode street. */
|
||||||
|
photo?: Photo;
|
||||||
|
/** Mode street : identifiant d'image Mapillary à charger dans la visionneuse. */
|
||||||
|
mapillaryId?: string;
|
||||||
|
/** Crédit affiché au résultat en mode street (« © auteur / Mapillary (CC BY-SA 4.0) »). */
|
||||||
|
credit?: string;
|
||||||
indices: Indice[];
|
indices: Indice[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,4 +121,4 @@ export interface WikiSummary {
|
|||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameMode = 'monument' | 'lieu' | 'defi';
|
export type GameMode = 'monument' | 'lieu' | 'defi' | 'street';
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
const modes: { id: GameMode; emoji: string }[] = [
|
const modes: { id: GameMode; emoji: string }[] = [
|
||||||
{ id: 'monument', emoji: '🏛️' },
|
{ id: 'monument', emoji: '🏛️' },
|
||||||
{ id: 'lieu', emoji: '📍' },
|
{ id: 'lieu', emoji: '📍' },
|
||||||
{ id: 'defi', emoji: '⛰️' }
|
{ id: 'defi', emoji: '⛰️' },
|
||||||
|
{ id: 'street', emoji: '🛣️' }
|
||||||
];
|
];
|
||||||
|
|
||||||
function jouer(): void {
|
function jouer(): void {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { getLieuById, sanitizeRound } from '$lib/server/lieux';
|
import { getLieuPhotoById, sanitizeRound } from '$lib/server/lieux';
|
||||||
import { idsPourSeed, seedValide } from '$lib/server/tirage';
|
import { idsPourSeed, seedValide } from '$lib/server/tirage';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const GET: RequestHandler = ({ params }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const rounds = idsPourSeed(params.seed).map((id) => {
|
const rounds = idsPourSeed(params.seed).map((id) => {
|
||||||
const lieu = getLieuById(id);
|
const lieu = getLieuPhotoById(id);
|
||||||
if (!lieu) throw new Error(`Tirage incohérent : id inconnu (${id})`);
|
if (!lieu) throw new Error(`Tirage incohérent : id inconnu (${id})`);
|
||||||
return sanitizeRound(lieu);
|
return sanitizeRound(lieu);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { getLieuById, PHOTO_LARGEURS } from '$lib/server/lieux';
|
import { getLieuPhotoById, PHOTO_LARGEURS } from '$lib/server/lieux';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE; contact OKI)';
|
const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE; contact OKI)';
|
||||||
@@ -22,7 +22,7 @@ const enVol = new Map<string, Promise<Buffer | null>>();
|
|||||||
* - conversion WebP + redimensionnement à la largeur demandée.
|
* - conversion WebP + redimensionnement à la largeur demandée.
|
||||||
*/
|
*/
|
||||||
async function chargerPhoto(id: string, w: Largeur): Promise<Buffer | null> {
|
async function chargerPhoto(id: string, w: Largeur): Promise<Buffer | null> {
|
||||||
const lieu = getLieuById(id);
|
const lieu = getLieuPhotoById(id);
|
||||||
if (!lieu) return null;
|
if (!lieu) return null;
|
||||||
const res = await fetch(lieu.photo.url, { headers: { 'User-Agent': USER_AGENT } });
|
const res = await fetch(lieu.photo.url, { headers: { 'User-Agent': USER_AGENT } });
|
||||||
if (!res.ok) throw new Error(`commons ${res.status}`);
|
if (!res.ok) throw new Error(`commons ${res.status}`);
|
||||||
@@ -59,7 +59,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 404 immédiat si l'id est inconnu (avant tout fetch amont).
|
// 404 immédiat si l'id est inconnu (avant tout fetch amont).
|
||||||
if (!getLieuById(id)) {
|
if (!getLieuPhotoById(id)) {
|
||||||
return new Response('Lieu inconnu', { status: 404 });
|
return new Response('Lieu inconnu', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { getRandomLieu, sanitizeRound } from '$lib/server/lieux';
|
import { getRandomLieu, getRandomStreet, sanitizeRound, sanitizeRoundStreet } from '$lib/server/lieux';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
/** GET /api/round?region=&categorie=&exclude=id1,id2 → lieu aléatoire SANITISÉ. */
|
/**
|
||||||
|
* GET /api/round?region=&categorie=&exclude=id1,id2 → lieu aléatoire SANITISÉ.
|
||||||
|
* Avec `mode=street` : pioche dans le corpus Mapillary (catégorie ignorée,
|
||||||
|
* réponse avec `mapillaryId` au lieu de `photo`).
|
||||||
|
*/
|
||||||
export const GET: RequestHandler = ({ url }) => {
|
export const GET: RequestHandler = ({ url }) => {
|
||||||
|
if (url.searchParams.get('mode') === 'street') {
|
||||||
|
const lieu = getRandomStreet({
|
||||||
|
region: url.searchParams.get('region'),
|
||||||
|
exclude: url.searchParams.get('exclude')
|
||||||
|
});
|
||||||
|
if (!lieu) {
|
||||||
|
return json({ erreur: 'Aucun lieu street disponible pour ces critères' }, { status: 404 });
|
||||||
|
}
|
||||||
|
return json(sanitizeRoundStreet(lieu), {
|
||||||
|
headers: { 'Cache-Control': 'no-store' }
|
||||||
|
});
|
||||||
|
}
|
||||||
const lieu = getRandomLieu({
|
const lieu = getRandomLieu({
|
||||||
region: url.searchParams.get('region'),
|
region: url.searchParams.get('region'),
|
||||||
categorie: url.searchParams.get('categorie'),
|
categorie: url.searchParams.get('categorie'),
|
||||||
|
|||||||
@@ -252,12 +252,12 @@
|
|||||||
<div class="resultat-scroll">
|
<div class="resultat-scroll">
|
||||||
<ResultPanel
|
<ResultPanel
|
||||||
{result}
|
{result}
|
||||||
creditPhoto={place.photo.credit}
|
creditPhoto={place.photo?.credit}
|
||||||
labelBouton={defiTermine ? t().resultat.voirBilan : t().resultat.suivant}
|
labelBouton={defiTermine ? t().resultat.voirBilan : t().resultat.suivant}
|
||||||
onsuivant={defiTermine ? montrerBilan : suivant}
|
onsuivant={defiTermine ? montrerBilan : suivant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if place.photo}
|
||||||
<PhotoPanel photo={place.photo} />
|
<PhotoPanel photo={place.photo} />
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
@@ -266,6 +266,7 @@
|
|||||||
<GameMap
|
<GameMap
|
||||||
bind:guess
|
bind:guess
|
||||||
real={result ? result.coordonnees : null}
|
real={result ? result.coordonnees : null}
|
||||||
|
distanceKm={result ? result.distanceKm : null}
|
||||||
interactive={!result}
|
interactive={!result}
|
||||||
onvalidate={() => void soumettre(false)}
|
onvalidate={() => void soumettre(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import GameMap from '$lib/components/GameMap.svelte';
|
import GameMap from '$lib/components/GameMap.svelte';
|
||||||
import CarteRegions, { type MarqueurCarte } from '$lib/components/CarteRegions.svelte';
|
import CarteRegions, { type MarqueurCarte } from '$lib/components/CarteRegions.svelte';
|
||||||
import PhotoPanel from '$lib/components/PhotoPanel.svelte';
|
import PhotoPanel from '$lib/components/PhotoPanel.svelte';
|
||||||
|
import StreetPanel from '$lib/components/StreetPanel.svelte';
|
||||||
import ResultPanel from '$lib/components/ResultPanel.svelte';
|
import ResultPanel from '$lib/components/ResultPanel.svelte';
|
||||||
import TimerRing from '$lib/components/TimerRing.svelte';
|
import TimerRing from '$lib/components/TimerRing.svelte';
|
||||||
import { t } from '$lib/i18n/store.svelte';
|
import { t } from '$lib/i18n/store.svelte';
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
if (regionParam) params.set('region', regionParam);
|
if (regionParam) params.set('region', regionParam);
|
||||||
if (mode === 'monument') params.set('categorie', 'MONUMENT');
|
if (mode === 'monument') params.set('categorie', 'MONUMENT');
|
||||||
if (mode === 'lieu') params.set('categorie', 'LIEU');
|
if (mode === 'lieu') params.set('categorie', 'LIEU');
|
||||||
|
if (mode === 'street') params.set('mode', 'street');
|
||||||
if (joues.length > 0) params.set('exclude', joues.join(','));
|
if (joues.length > 0) params.set('exclude', joues.join(','));
|
||||||
const res = await fetch(`/api/round?${params}`);
|
const res = await fetch(`/api/round?${params}`);
|
||||||
if (!res.ok) throw new Error(String(res.status));
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
@@ -246,12 +248,14 @@
|
|||||||
<div class="resultat-scroll">
|
<div class="resultat-scroll">
|
||||||
<ResultPanel
|
<ResultPanel
|
||||||
{result}
|
{result}
|
||||||
creditPhoto={place.photo.credit}
|
creditPhoto={place.photo?.credit ?? place.credit}
|
||||||
labelBouton={estDefi && defiTermine ? t().resultat.voirBilan : t().resultat.suivant}
|
labelBouton={estDefi && defiTermine ? t().resultat.voirBilan : t().resultat.suivant}
|
||||||
onsuivant={estDefi && defiTermine ? montrerBilan : suivant}
|
onsuivant={estDefi && defiTermine ? montrerBilan : suivant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if place.mapillaryId}
|
||||||
|
<StreetPanel mapillaryId={place.mapillaryId} credit={place.credit} />
|
||||||
|
{:else if place.photo}
|
||||||
<PhotoPanel photo={place.photo} />
|
<PhotoPanel photo={place.photo} />
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
@@ -260,6 +264,7 @@
|
|||||||
<GameMap
|
<GameMap
|
||||||
bind:guess
|
bind:guess
|
||||||
real={result ? result.coordonnees : null}
|
real={result ? result.coordonnees : null}
|
||||||
|
distanceKm={result ? result.distanceKm : null}
|
||||||
interactive={!result}
|
interactive={!result}
|
||||||
onvalidate={() => void soumettre(false)}
|
onvalidate={() => void soumettre(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { error } from '@sveltejs/kit';
|
|||||||
import type { GameMode } from '$lib/types';
|
import type { GameMode } from '$lib/types';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
const MODES: GameMode[] = ['monument', 'lieu', 'defi'];
|
const MODES: GameMode[] = ['monument', 'lieu', 'defi', 'street'];
|
||||||
|
|
||||||
export const load: PageLoad = ({ params }) => {
|
export const load: PageLoad = ({ params }) => {
|
||||||
if (!MODES.includes(params.mode as GameMode)) {
|
if (!MODES.includes(params.mode as GameMode)) {
|
||||||
|
|||||||
@@ -54,3 +54,34 @@ Chaque fichier est sous licence libre (Creative Commons, CC0 ou domaine public)
|
|||||||
(redimensionnement à 1200 px de large assuré par Wikimedia).
|
(redimensionnement à 1200 px de large assuré par Wikimedia).
|
||||||
- Photos « domaine public » : *La Pelée vue du Carbet.jpg* (Jurassi), *ILES et BAGNE 037.JPG* (davric), *Piton des Neiges1.JPG* (Dunog).
|
- Photos « domaine public » : *La Pelée vue du Carbet.jpg* (Jurassi), *ILES et BAGNE 037.JPG* (davric), *Piton des Neiges1.JPG* (Dunog).
|
||||||
- Photos CC0 (pas d'attribution requise, auteur crédité par courtoisie) : les deux clichés de Thérèse Gaigé (Anse Dufour, Anse Noire).
|
- Photos CC0 (pas d'attribution requise, auteur crédité par courtoisie) : les deux clichés de Thérèse Gaigé (Anse Dufour, Anse Noire).
|
||||||
|
|
||||||
|
## Images Mapillary — mode street « An lari a »
|
||||||
|
|
||||||
|
Les 20 images du corpus `app/src/lib/server/data/street.json` sont servies par la plateforme
|
||||||
|
Mapillary via la visionneuse mapillary-js (licence MIT). Chaque image reste la propriété de son
|
||||||
|
auteur et est publiée sous **CC BY-SA 4.0** (contributeurs Mapillary) — vérifiée via l'API Mapillary
|
||||||
|
(`creator.username`) par `app/scripts/validate-street.mjs`. Attribution affichée dans le jeu :
|
||||||
|
« © <auteur> / Mapillary (CC BY-SA 4.0) » + logo Mapillary intégré à la visionneuse.
|
||||||
|
|
||||||
|
| # | Lieu | Région | Image Mapillary | Auteur | Date de capture | 360° |
|
||||||
|
|---|------|--------|-----------------|--------|-----------------|------|
|
||||||
|
| 1 | Rue Laurence Valmy, Pointe-à-Pitre | GUADELOUPE | [2941939632687980](https://www.mapillary.com/app/?pKey=2941939632687980) | cartophyl | 2015-07-07 | non |
|
||||||
|
| 2 | Rue Achille René Boisneuf, Les Abymes | GUADELOUPE | [306781020957650](https://www.mapillary.com/app/?pKey=306781020957650) | ratzillas | 2015-07-17 | non |
|
||||||
|
| 3 | Place Renovation le Bourg, Le Gosier | GUADELOUPE | [919062132219342](https://www.mapillary.com/app/?pKey=919062132219342) | kaartcam | 2016-01-02 | non |
|
||||||
|
| 4 | Rue Emmanuel Robinet, Sainte-Anne | GUADELOUPE | [956408765176680](https://www.mapillary.com/app/?pKey=956408765176680) | ratzillas | 2019-07-15 | oui |
|
||||||
|
| 5 | Route des Esclaves, Basse-Terre | GUADELOUPE | [212605717097536](https://www.mapillary.com/app/?pKey=212605717097536) | kaartcam | 2016-01-04 | non |
|
||||||
|
| 6 | Rue Victor Sévère, Fort-de-France | MARTINIQUE | [152801196814796](https://www.mapillary.com/app/?pKey=152801196814796) | kaartcam | 2015-12-31 | non |
|
||||||
|
| 7 | Rue des Barrières, Le Lamentin | MARTINIQUE | [1120779875836768](https://www.mapillary.com/app/?pKey=1120779875836768) | Caramel | 2024-04-29 | oui |
|
||||||
|
| 8 | Rue Gabriel Péri, Saint-Pierre | MARTINIQUE | [1299527321706804](https://www.mapillary.com/app/?pKey=1299527321706804) | jeremylavarenne | 2025-08-31 | non |
|
||||||
|
| 9 | Rue Florent Holo, Le François | MARTINIQUE | [4102521129805870](https://www.mapillary.com/app/?pKey=4102521129805870) | kaartcam | 2016-01-01 | non |
|
||||||
|
| 10 | Passage des Fleurs, Sainte-Marie | MARTINIQUE | [131479329019761](https://www.mapillary.com/app/?pKey=131479329019761) | kaartcam | 2015-12-31 | non |
|
||||||
|
| 11 | Rue Francois Arago, Cayenne | GUYANE | [259270559222687](https://www.mapillary.com/app/?pKey=259270559222687) | kaartcam | 2016-08-18 | non |
|
||||||
|
| 12 | Rue de l’Odéon, Kourou | GUYANE | [198127629404270](https://www.mapillary.com/app/?pKey=198127629404270) | hacp | 2022-09-07 | non |
|
||||||
|
| 13 | Avenue Albert Sarraut, Saint-Laurent-du-Maroni | GUYANE | [147142287390967](https://www.mapillary.com/app/?pKey=147142287390967) | pma | 2021-01-18 | non |
|
||||||
|
| 14 | Rue Maurice BOCS, Matoury | GUYANE | [582210316504280](https://www.mapillary.com/app/?pKey=582210316504280) | kaartcam | 2016-08-18 | non |
|
||||||
|
| 15 | Rue de la CANNE À SUCRE, Remire-Montjoly | GUYANE | [247810083801809](https://www.mapillary.com/app/?pKey=247810083801809) | kaartcam | 2016-08-18 | non |
|
||||||
|
| 16 | Rue de Paris, Saint-Denis | REUNION | [2262788723930767](https://www.mapillary.com/app/?pKey=2262788723930767) | AllWays360 | 2022-10-06 | oui |
|
||||||
|
| 17 | Rue du Cimetiere, Saint-Pierre | REUNION | [143202321798356](https://www.mapillary.com/app/?pKey=143202321798356) | sigstpierre | 2022-11-09 | oui |
|
||||||
|
| 18 | Rue Aristide Briand, Le Tampon | REUNION | [1382811445417165](https://www.mapillary.com/app/?pKey=1382811445417165) | vandecasteele | 2019-12-16 | non |
|
||||||
|
| 19 | Rue Elie Eudor, Saint-Paul | REUNION | [928324514646684](https://www.mapillary.com/app/?pKey=928324514646684) | sogefi | 2018-10-14 | non |
|
||||||
|
| 20 | Ccal de la Republique, Saint-André | REUNION | [519625869420978](https://www.mapillary.com/app/?pKey=519625869420978) | vandecasteele | 2019-06-28 | oui |
|
||||||
|
|||||||
@@ -106,3 +106,19 @@ Règles :
|
|||||||
|
|
||||||
- **ADR-001 v1 — Défi par lien à seed** (`/defi/<seed>`) : `src/lib/server/tirage.ts` (hash cyrb53 → PRNG mulberry32, Fisher-Yates déterministe, contraintes ≥1 MONUMENT / ≥1 LIEU / ≥2 régions par échanges déterministes — zéro `Math.random`, déterminisme inter-machines). `GET /api/defi/[seed]` → 5 rounds sanitisés identiques pour tous (`no-store`, seed alphanumérique 4–40). Accueil : « Créer un lien de défi » (seed 8 chars sans ambiguïté). Bilan : « Copier le lien du défi ». La comparaison des scores reste humaine (image canvas) — zéro compte, zéro état serveur, zéro leaderboard. La v2 temps réel (PlaySocketJS) reste conditionnée au gate de traction de l'ADR (≥30 parties/sem. ×4 sem. + ≥5 demandes explicites).
|
- **ADR-001 v1 — Défi par lien à seed** (`/defi/<seed>`) : `src/lib/server/tirage.ts` (hash cyrb53 → PRNG mulberry32, Fisher-Yates déterministe, contraintes ≥1 MONUMENT / ≥1 LIEU / ≥2 régions par échanges déterministes — zéro `Math.random`, déterminisme inter-machines). `GET /api/defi/[seed]` → 5 rounds sanitisés identiques pour tous (`no-store`, seed alphanumérique 4–40). Accueil : « Créer un lien de défi » (seed 8 chars sans ambiguïté). Bilan : « Copier le lien du défi ». La comparaison des scores reste humaine (image canvas) — zéro compte, zéro état serveur, zéro leaderboard. La v2 temps réel (PlaySocketJS) reste conditionnée au gate de traction de l'ADR (≥30 parties/sem. ×4 sem. + ≥5 demandes explicites).
|
||||||
- **ADR-002 — Carte SVG légère hors-jeu** : pipeline `app/scripts/build-carte-regions.mjs` (geo.api.gouv.fr → mapshaper 7 % → `src/lib/map/regions-geo.json`, 91,8 KB gz ≤ 150 KB) ; composant `CarteRegions.svelte` from scratch (SVG, projection équirectangulaire par inset, 2×2 responsive, `<button>` par région avec `aria-label` FR/créole, SSR sans JS, prop `marqueurs` pour le récap guess→réel, attribution © OSM/données publiques). Utilisée sur l'accueil (sélection de région) et les bilans défi — MapLibre reste strictement aux écrans de jeu (devinette, résultat, explorer). Attribution : contours © geo.api.gouv.fr / données publiques.
|
- **ADR-002 — Carte SVG légère hors-jeu** : pipeline `app/scripts/build-carte-regions.mjs` (geo.api.gouv.fr → mapshaper 7 % → `src/lib/map/regions-geo.json`, 91,8 KB gz ≤ 150 KB) ; composant `CarteRegions.svelte` from scratch (SVG, projection équirectangulaire par inset, 2×2 responsive, `<button>` par région avec `aria-label` FR/créole, SSR sans JS, prop `marqueurs` pour le récap guess→réel, attribution © OSM/données publiques). Utilisée sur l'accueil (sélection de région) et les bilans défi — MapLibre reste strictement aux écrans de jeu (devinette, résultat, explorer). Attribution : contours © geo.api.gouv.fr / données publiques.
|
||||||
|
|
||||||
|
## 9. Correctif UX résultat (2026-07-17)
|
||||||
|
|
||||||
|
- **Carte résultat façon GeoGuessr** : à la révélation, le marqueur du joueur **reste à sa position** (corail) et la position réelle apparaît (vert), reliées par une ligne pointillée avec **étiquette de distance au milieu** (`distanceKm`) ; la carte cadre les deux points (`fitBounds`, instantané si `prefers-reduced-motion`). Remplace l'ancien « vol » du marqueur vers la position réelle, qui détruisait la comparaison visuelle. Prop `distanceKm` ajoutée à `GameMap` (alimentée par la réponse serveur, jamais calculée côté client).
|
||||||
|
|
||||||
|
## 10. ADR-003 — Mode street « An lari a » via Mapillary (2026-07-18)
|
||||||
|
|
||||||
|
5e mode de jeu : le joueur voit une **photo street-level navigable** (visionneuse Mapillary) au lieu d'une photo statique, et devine la commune. Mécanique carte + guess + score **inchangée**.
|
||||||
|
|
||||||
|
- **Pourquoi Mapillary** : seule source de street-level imagery couvrant correctement les 4 régions (bonne couverture Gwada/Martinique/Réunion, littoral guyanais) ; images **CC BY-SA 4.0**, lib `mapillary-js` **MIT**, API gratuite (token client requis, rate-limité par app). **Exception documentée** à « zéro service tiers » (Mapillary = plateforme Meta), limitée à ce mode ; alternative souveraine future = Panoramax (reco R4 audit passe 2), couverture DOM encore insuffisante à cette date.
|
||||||
|
- **Corpus curé, pas de tirage aléatoire dans l'API** : `app/src/lib/server/data/street.json` (20 lieux, 5/région), produit par `app/scripts/curate-street.mjs` (bbox < 0,01° ou grille de recherches par rayon autour de la **mairie** — le centroïde geo.api tombe souvent hors du centre-ville ; tri qualité/récence, préférence 360°), revérifié par `app/scripts/validate-street.mjs` (image existe, `computed_geometry` < 100 m, crédit auteur, sitelink frwiki). La vérité terrain (`coordonnees`) est **figée** dans le corpus (dérive possible de `computed_geometry` côté Mapillary).
|
||||||
|
- **Phase éducative par commune** : `wikidata_id` = QID de la **commune** (un coin de rue n'a pas vocation à avoir sa page) → `WikiFiche` réutilisé tel quel, aucune variante Cas B nouvelle. `aire_commune_km2` renseigné (équité Guyane, A1).
|
||||||
|
- **Anti-triche** : `/api/round?mode=street` + `sanitizeRoundStreet()` — seuls `id, nom, nom_creole, categorie, difficulte, mapillaryId, credit, indices` partent ; coords/commune/région/wikidata_id jamais avant le guess. **Limitation assumée** : l'`image_key` est publique (la visionneuse appelle Mapillary depuis le navigateur) → un joueur techniquement équipé peut retrouver la position via l'API Mapillary (même propriété que GeoGuessr/Street View ; acceptable en solo sans leaderboard). La visionneuse n'affiche **pas de carte** (mapillary-js n'en a pas par défaut), donc aucun spoiler visuel.
|
||||||
|
- **Visionneuse** : `StreetPanel.svelte`, lazy load façon MapLibre (`import('mapillary-js')` + CSS — chunk séparé ~1 MB, 0 occurrence dans le bundle initial), `component.cover: false`, `moveTo()` avec état d'erreur (image retirée → message + round suivant possible). Token `PUBLIC_MAPILLARY_TOKEN` via `$env/dynamic/public` (pattern `PUBLIC_TILES_URL`) : **token client**, conçu par Mapillary pour être exposé côté navigateur.
|
||||||
|
- **Réutilisation sans modification** : `GameMap`, `ResultPanel`, `WikiFiche`, scoring (`scoreGuess`/`skipResult` acceptent `Lieu | StreetLieu`), timer, indices, bouton « Passer ». `RoundPlace.photo` devient optionnel (`mapillaryId`/`credit` ajoutés) ; lookup unifié `getLieuById` (2 corpus) pour `/api/guess`, `getLieuPhotoById` conservé pour le défi et le proxy photo.
|
||||||
|
- **Hors scope v1** : street dans `/defi/[seed]` et `/explorer`, cache SW des tuiles Mapillary (les requêtes `graph.mapillary.com`/FBCDN passent en direct).
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,8 @@
|
|||||||
# JWE v2 — Guide de déploiement auto-hébergé
|
# JWE v2 — Guide de déploiement auto-hébergé
|
||||||
|
|
||||||
Cible : un petit VPS (1 vCPU / 1 Go RAM suffit) sous Debian/Ubuntu, derrière nginx ou Caddy. Aucun service propriétaire, aucune clé API.
|
Cible : un petit VPS (1 vCPU / 1 Go RAM suffit) sous Debian/Ubuntu, derrière nginx ou Caddy. Aucun service propriétaire, aucune clé API payante.
|
||||||
|
|
||||||
|
> **Exception documentée (mode street)** : le 5e mode « An lari a » utilise la visionneuse Mapillary (images CC BY-SA 4.0, lib MIT) et requiert un **token client Mapillary gratuit** — voir §2.1. Sans ce token, les 4 autres modes fonctionnent normalement ; seul le mode street affiche un message d'indisponibilité.
|
||||||
|
|
||||||
## 1. Application SvelteKit
|
## 1. Application SvelteKit
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ WorkingDirectory=/srv/jwe/app
|
|||||||
Environment=PORT=3000
|
Environment=PORT=3000
|
||||||
Environment=ORIGIN=https://jwe.o-k-i.net
|
Environment=ORIGIN=https://jwe.o-k-i.net
|
||||||
Environment=PUBLIC_TILES_URL=https://tiles.o-k-i.net/styles/oki/style.json
|
Environment=PUBLIC_TILES_URL=https://tiles.o-k-i.net/styles/oki/style.json
|
||||||
|
Environment=PUBLIC_MAPILLARY_TOKEN=MLY|xxxxxxxx|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
ExecStart=/usr/bin/node build
|
ExecStart=/usr/bin/node build
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
||||||
@@ -39,6 +42,16 @@ Restart=on-failure
|
|||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 2.1 Token Mapillary (mode street « An lari a »)
|
||||||
|
|
||||||
|
1. Créez un compte sur [mapillary.com](https://www.mapillary.com) puis une application sur le [dashboard développeurs](https://www.mapillary.com/dashboard/developers) : récupérez le **client token** (format `MLY|…|…`).
|
||||||
|
2. Exposez-le via `PUBLIC_MAPILLARY_TOKEN` (service systemd ci-dessus, ou `app/.env` en développement — voir `app/.env.example`). C'est un **token client** : conçu pour être visible côté navigateur, rate-limité par application — ce n'est pas un secret.
|
||||||
|
3. Vérifiez périodiquement le corpus street (images parfois retirées par leurs auteurs) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app && node scripts/validate-street.mjs # lit le token depuis .env
|
||||||
|
```
|
||||||
|
|
||||||
## 2. Reverse proxy (exemple nginx)
|
## 2. Reverse proxy (exemple nginx)
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
# JWE — ADR-001 & ADR-002 (format audit OKI)
|
|
||||||
|
|
||||||
Deux décisions d'architecture rédigées selon le format du prompt d'audit post-exécution : critères bloquants, preuves exigées, rapport tabulaire, interdits. Elles **ne modifient pas** le build v1 en cours ; elles cadrent la phase 2 et une optimisation perf.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# ADR-001 — Mode Défi : seed-link en v1, PlaySocketJS en phase 2 conditionnée
|
|
||||||
|
|
||||||
- **Statut** : Accepté (v1) / À étudier (v2, gate explicite)
|
|
||||||
- **Date** : 2026-07-17
|
|
||||||
- **Décideurs** : Architecte OKI
|
|
||||||
- **Références** : PlaySocketJS (MIT, rooms, CRDT, server-authoritative, battle-tested >1,3 M duels sur 1 instance Node) ; Geoguess/Geoguess (modèle de room sans compte, max 5 joueurs) ; prompt v2 §A5 (pas de comptes, pas de leaderboard serveur).
|
|
||||||
|
|
||||||
## Contexte
|
|
||||||
|
|
||||||
Le prompt v2 interdit comptes et leaderboard serveur. L'usage réel visé (OKI, AKILPA, salles de classe, événements) crée pourtant un besoin : **jouer les mêmes 5 lieux que d'autres et comparer les scores**. Deux horizons : maintenant (zéro serveur) et plus tard (temps réel, si le projet prouve sa traction).
|
|
||||||
|
|
||||||
## Décision
|
|
||||||
|
|
||||||
**v1 — Défi asynchrone par lien à seed partagée.**
|
|
||||||
- L'URL de défi contient une seed opaque (ex. `/defi/<seed>`) ; le serveur (ou la fonction SvelteKit) dérive de la seed les mêmes 5 lieux pour tous les joueurs (PRNG seedé côté serveur, jamais côté client).
|
|
||||||
- Aucune coordonnée dans l'URL ni dans le payload initial (anti-triche §6 du prompt v2 maintenu).
|
|
||||||
- Fin de partie : l'image de partage canvas (déjà prévue prompt v2 §4.4) affiche le score ; la comparaison se fait humainement (screenshot dans le groupe WhatsApp/Signal). Zéro serveur d'état, zéro compte, cohérent doctrine.
|
|
||||||
|
|
||||||
**v2 — Duel temps réel via PlaySocketJS, UNIQUEMENT si le gate §"Conditions" est franchi.**
|
|
||||||
- Modèle : rooms sans compte à la Geoguess (nom de room partagé, 2–5 joueurs), état synchronisé par PlaySocketJS en mode **server-authoritative** (le serveur valide les guesses et calcule les scores — jamais le client).
|
|
||||||
- Hébergement : 1 instance Node sur le VPS OKI existant, dans l'enveloppe 40–50 €/mois. Aucune dépendance Google/Firebase (contraire à Geoguess — voir annexe).
|
|
||||||
|
|
||||||
## Options considérées
|
|
||||||
|
|
||||||
| Option | Souveraineté | Coût serveur | Effort | Anti-triche | Verdict |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| Seed-link asynchrone (v1) | Totale | Nul | S | Serveur dérive les lieux, scores non comparés en ligne | **Retenu v1** |
|
|
||||||
| PlaySocketJS rooms (v2) | Bonne (MIT, self-host) | 1 instance Node | M | Server-authoritative natif | **Retenu v2 si gate** |
|
|
||||||
| Socket.io maison | Bonne | 1 instance Node | M–L | À réimplémenter | Rejeté (réinvention) |
|
|
||||||
| Supabase/Firebase Realtime | Mauvaise (cloud propriétaire) | Abonnement | M | Faible côté client | Rejeté (doctrine) |
|
|
||||||
|
|
||||||
## Conditions de déclenchement de la v2 (gate — bloquant)
|
|
||||||
|
|
||||||
La v2 n'est étudiée que si TOUTES les conditions suivantes sont remplies et documentées dans le rapport de gate :
|
|
||||||
1. Traction mesurée : ≥ 30 parties/semaine pendant 4 semaines consécutives (mesure locale anonymisée, pas d'analytics tiers).
|
|
||||||
2. Demande explicite : ≥ 5 retours d'utilisateurs ou partenaires (OKI, AKILPA, enseignants) demandant le défi en direct.
|
|
||||||
3. Capacité d'hébergement confirmée sur le VPS OKI sans dépasser l'enveloppe 40–50 €/mois.
|
|
||||||
|
|
||||||
## Vérifications (obligatoires, preuves à l'appui)
|
|
||||||
|
|
||||||
- **V1 (bloquant, v1)** — Déterminisme : la même URL de défi produit les mêmes 5 lieux, dans le même ordre, sur deux machines différentes. Preuve : double exécution filmée ou logs serveur.
|
|
||||||
- **V2 (bloquant, v1)** — Anti-fuite : l'URL et les payloads initiaux ne contiennent ni coordonnées, ni commune, ni `wikidata_id` décodable. Preuve : inspection réseau + revue de l'URL.
|
|
||||||
- **V3 (bloquant, v1)** — L'image de partage n'affiche que score/pseudo libre, jamais les réponses des autres joueurs.
|
|
||||||
- **V4 (bloquant, v2)** — POC PlaySocketJS : room à 3 clients, déconnexion/reconnexion d'un joueur sans perte d'état (CRDT), score validé côté serveur (tentative de triche client rejetée — preuve par requête forgée).
|
|
||||||
- **V5 (v2)** — Licence : `npm view playsocketjs license` = MIT, consigné dans le rapport.
|
|
||||||
- **V6 (v2)** — Charge : 10 rooms simultanées × 5 joueurs sur le VPS sans saturation (preuve : log ressources).
|
|
||||||
|
|
||||||
## Rapport (format obligatoire)
|
|
||||||
|
|
||||||
```
|
|
||||||
| ID | Vérification | Méthode / Preuve | PASS/FAIL | Correctif appliqué |
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interdits
|
|
||||||
|
|
||||||
- Aucun compte utilisateur, aucun leaderboard global, aucun stockage de pseudos au-delà de la room.
|
|
||||||
- Aucune dépendance Google/Firebase/Supabase pour le temps réel.
|
|
||||||
- Aucune copie de code Geoguess/Geoguess (Vue.js) : référence de design uniquement.
|
|
||||||
- La v2 ne doit pas régresser le hors-scope du prompt v2 pour le solo.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# ADR-002 — Carte SVG/topojson légère pour les écrans hors-jeu (perf 4G)
|
|
||||||
|
|
||||||
- **Statut** : Accepté
|
|
||||||
- **Date** : 2026-07-17
|
|
||||||
- **Références** : composant carte SVG/topojson d'OpenGuessr Education (inspiration uniquement — licence MIT + Commons Clause = non-commercial, **aucune copie de code**) ; geo.api.gouv.fr (contours de communes GeoJSON, gratuit, sans clé — testé OK) ; prompt v2 §4.1 (lazy-load MapLibre) et §4.5 (perf).
|
|
||||||
|
|
||||||
## Contexte
|
|
||||||
|
|
||||||
MapLibre GL JS pèse ~800 kB de JS avant la première tile. L'accueil, le récapitulatif final et les fiches « Aprann » n'ont pas besoin d'une carte interactive complète. Sur 4G caribéen, charger MapLibre sur ces écrans détruit le budget < 3 s du prompt v2.
|
|
||||||
|
|
||||||
## Décision
|
|
||||||
|
|
||||||
- Les écrans **hors-jeu** (accueil 4 régions, mini-carte du récapitulatif, vignette de fiche lieu) utilisent une **carte SVG statique** générée au build depuis un topojson des communes des 4 régions, projection précalculée, aucune tile, aucun JS carte.
|
|
||||||
- MapLibre reste réservé aux écrans **de jeu** (devinette, résultat, explorer interactif) et reste lazy-loadé.
|
|
||||||
- Pipeline données : contours communes via `geo.api.gouv.fr/communes?codeRegion=01|02|03|04&fields=nom,code,contour&format=json&geometry=contour` → simplification (mapshaper, seuil à calibrer) → topojson versionné dans le repo → composant Svelte SVG from scratch (MIT).
|
|
||||||
|
|
||||||
## Vérifications
|
|
||||||
|
|
||||||
- **V1 (bloquant)** — Budget poids : topojson simplifié des 4 régions ≤ 150 kB gzippé ; preuve : taille du fichier + `gzip -9` consignés. Si dépassement : simplifier davantage ou découper par région avec import dynamique.
|
|
||||||
- **V2 (bloquant)** — LCP accueil < 2,5 s en throttling 4G (Lighthouse mobile, 3 runs, médiane consignée).
|
|
||||||
- **V3** — Rendu sans JS : la carte SVG s'affiche en SSG avec JS désactivé ; chaque région est un `<a>` ou `<button>` réel (accessibilité clavier, lecteur d'écran : nom de région en FR + créole).
|
|
||||||
- **V4** — Attribution données : © contributeurs OpenStreetMap / réutilisation données publiques mentionnée dans le footer (licence ODbL pour les contours dérivés OSM le cas échéant).
|
|
||||||
- **V5** — Zéro code copié d'OpenGuessr Education : revue de provenance, le composant est écrit from scratch (licence MIT du projet).
|
|
||||||
|
|
||||||
## Rapport (format obligatoire)
|
|
||||||
|
|
||||||
```
|
|
||||||
| ID | Vérification | Méthode / Preuve | PASS/FAIL | Correctif appliqué |
|
|
||||||
```
|
|
||||||
|
|
||||||
## Interdits
|
|
||||||
|
|
||||||
- Ne pas charger MapLibre (ni son CSS) sur l'accueil ou le récapitulatif.
|
|
||||||
- Ne pas embarquer de tuiles raster de substitution (le SVG est vectoriel pur).
|
|
||||||
- Ne pas copier le code d'OpenGuessr Education (Commons Clause).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Annexe — Étude Geoguess/Geoguess (référence, 2026-07-17)
|
|
||||||
|
|
||||||
- **Projet** : clone GeoGuessr open source, solo + multijoueur par rooms (nom de room partagé, ≤ 5 joueurs), PWA, cartes custom GeoJSON, licence **MIT**.
|
|
||||||
- **Stack** : Vue.js + Google Maps StreetView + Firebase → **non réutilisable** (doctrine zéro Google ; Vue ≠ Svelte).
|
|
||||||
- **Signal fort** : leur README admet que la démo publique est limitée par le **prix de l'API Google** et renvoie vers l'auto-déploiement avec clé personnelle — justification directe du zéro propriétaire OKI, à citer dans l'ADR d'architecture initial.
|
|
||||||
- **À retenir** : (1) le modèle de room sans compte (base UX de l'ADR-001 v2) ; (2) le repo **GeoGuess-Maps**, liste de cartes communautaires — modèle pour les futurs map-packs JWE contribués par la communauté OKI/AKILPA via PR, sans CMS.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Mission : JWE — Audit post-exécution, vérifications obligatoires & recommandations
|
|
||||||
|
|
||||||
## À exécuter par chaque agent à la fin de sa tâche, avant de la marquer « terminée »
|
|
||||||
|
|
||||||
La refonte JWE a été produite à partir du prompt de mission v2 (Svelte 5 + SvelteKit 2, MapLibre, SQLite, 4 régions : Guadeloupe, Martinique, Guyane, La Réunion). Cet audit est la seconde passe. **Aucune tâche n'est terminée sans son rapport de vérification** (format §6). Un FAIL non résolu bloque la livraison. Si le correctif dépasse le scope de l'agent, l'agent produit une RECOMMANDATION argumentée au lieu de coder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Faits établis (à lire avant tout — ils corrigent le prompt v2)
|
|
||||||
|
|
||||||
1. **Le QID d'exemple du prompt v2 était faux** : Fort Delgrès = `Q3077840` (vérifié), pas `Q2216838`. Ses coordonnées officielles sont 15°59′19″N, 61°43′23″O (≈ 15.9886, -61.7231), pas (16.0036, -61.7319). Si le seed ou l'exemple a été copié tel quel, corriger.
|
|
||||||
2. **Principe absolu : fetch, pas recall.** Tout QID, toute coordonnée, toute URL photo, toute licence doit être vérifié par une requête réelle, jamais depuis la mémoire du modèle.
|
|
||||||
3. **Les communes de Guyane sont immenses** (Maripasoula ≈ 18 000 km², Saint-Laurent-du-Maroni ≈ 4 800 km²). Un radius de score fixe par difficulté y est injuste.
|
|
||||||
4. **La Réunion est à ~12 000 km des Antilles** : un `maxBounds` global couvrant les 4 régions rend une carte inutilisable (95 % d'océan), et un zoom automatique sur la région du lieu au début du round révèle la réponse.
|
|
||||||
5. **Un lieu sans page Wikipedia a rarement une photo sur Wikimedia Commons** : le piège « 2-3 lieux sans article frwiki avec photo libre » exige des photos propres (collectif OKI) ou Panoramax.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Agent Contenu — vérifications données
|
|
||||||
|
|
||||||
- **C1 — QID valides (bloquant).** Pour CHAQUE lieu avec `wikidata_id` non null :
|
|
||||||
```
|
|
||||||
curl "https://www.wikidata.org/w/api.php?action=wbgetentities&ids=<QID>&props=sitelinks|labels&languages=fr&format=json" -H "User-Agent: JWE-OKI/1.0"
|
|
||||||
```
|
|
||||||
Le sitelink `frwiki` doit exister et correspondre au lieu (comparer le label). Preuve : tableau `lieu → QID → sitelink frwiki → OK/KO` dans le rapport.
|
|
||||||
- **C2 — Les « null » sont de vrais null (bloquant).** Pour chaque lieu à `wikidata_id: null` intentionnel, prouver par une recherche API (`action=query&list=search` sur frwiki) qu'aucun article frwiki n'existe. Sinon le lieu ne teste pas le Cas B.
|
|
||||||
- **C3 — Coordonnées précises.** Écart < 500 m avec la source officielle (Wikidata `P625`, IGN, geo.api.gouv.fr). Corriger le seed Fort Delgrès (voir §0.1).
|
|
||||||
- **C4 — Photos sans fuite.** (a) Noms de fichiers opaques (`uuid.webp`, jamais `fort-delgres.webp` — le nom ne doit pas révéler la réponse dans les devtools). (b) EXIF strippés : preuve `exiftool <fichier>` sans GPS ni appareil. (c) Licence vérifiable : page de description Commons atteignable, crédit et licence exacts dans le JSON.
|
|
||||||
- **C5 — Photos des lieux « null ».** Source documentée : photo propre OKI (crédit collectif) ou Panoramax (instance + ID + licence). Si introuvable : remplacer le lieu, ne jamais garder une photo sans licence claire.
|
|
||||||
|
|
||||||
## 2. Agent Architecte — vérifications décisions
|
|
||||||
|
|
||||||
- **A1 — Score juste en Guyane (bloquant).** Vérifier que la formule utilise un radius effectif : `radius_eff = max(radius_difficulté, rayon_commune)` avec `rayon_commune ≈ sqrt(aire_commune / π)`. L'aire peut être précalculée dans le JSON du lieu (champ `aire_commune_km2`) ou dérivée du contour geo.api.gouv.fr. Test à produire : un guess à 40 km du chef-lieu de Maripasoula ne doit pas être quasi-nul.
|
|
||||||
- **A2 — Pas de fuite de région (bloquant).** Au début d'un round, la caméra ne doit révéler ni la région ni la commune du lieu. Si la solution retenue est la devinette en deux temps (choix de la région via 4 boutons → zoom → placement du marqueur), vérifier que le scoring combine les deux étapes et que c'est documenté dans l'ADR. Si une autre solution a été codée, vérifier l'absence de fuite et l'usage mobile.
|
|
||||||
- **A3 — Chaîne Wikipedia exacte.** `wbgetentities(props=sitelinks)` → titre frwiki → `fr.wikipedia.org/api/rest_v1/page/summary/{titre}`. Cache ≥ 24 h (pas de « temps réel »). User-Agent explicite côté proxy. Fallback de langue documenté : fr → en. **Cas intermédiaire tranché et documenté** : QID existant mais pas d'article frwiki → recommandé : afficher le Cas B variante « pas encore d'article en français — et si tu le traduisais/créais ? ».
|
|
||||||
- **A4 — Décision Strapi vs SvelteKit.** L'ADR doit appliquer le critère du prompt (contributeurs non-devs ?). En l'absence de contributeurs non-devs aujourd'hui, la cohérence doctrine OKI = SvelteKit seul + JSON versionné + script de validation au commit. Si Strapi a été retenu, l'ADR doit justifier par des contributeurs réels identifiés.
|
|
||||||
- **A5 — Hors scope respecté.** Pas de comptes utilisateurs, pas de leaderboard serveur, pas de multijoueur. Scores en `localStorage`. Tout écart = FAIL ou RECOMMANDATION de suppression.
|
|
||||||
|
|
||||||
## 3. Agent Back-end — vérifications (si applicable)
|
|
||||||
|
|
||||||
- **B1 — Endpoint lieu aléatoire sans fuite (bloquant).** Preuve `curl` : la réponse ne contient ni `coordonnees`, ni `commune`, ni indice de région ; URL photo opaque.
|
|
||||||
- **B2 — Score calculé côté serveur (bloquant).** Le client envoie `{lieu_id, lat, lon, région_choisie?, indices_utilisés, temps}` ; le serveur calcule (formule A1) et renvoie score + distance + coordonnées réelles. Le client ne calcule rien.
|
|
||||||
- **B3 — Proxy Wikipedia.** Cache effectif (headers ou store, TTL ≥ 24 h), User-Agent conforme, **aucun texte d'article stocké en base** (contrainte §2.3 du prompt v2).
|
|
||||||
- **B4 — Pas de dépendance propriétaire.** Audit `package.json` : zéro clé API payante, zéro Google/Mapbox, télémétrie désactivée.
|
|
||||||
|
|
||||||
## 4. Agent Front-end — vérifications
|
|
||||||
|
|
||||||
- **F1 — Carte.** Bounds effectifs, aucune fuite de région (voir A2), MapLibre lazy-loadé (vérifier le chunk réseau : pas de JS MapLibre sur l'accueil), boutons zoom visibles, navigation clavier.
|
|
||||||
- **F2 — Cas A / Cas B (bloquant).** Les deux écrans de résultat sont **visuellement distincts** (couleur, icône), le message de lacune est présent en FR **et** en créole, et le CTA « Kontribyé sou Wikipédia » ouvre bien l'éditeur/recherche pré-remplie ou le guide OKI. Captures à l'appui.
|
|
||||||
- **F3 — Cas intermédiaire géré.** QID présent mais pas d'article frwiki : pas d'écran vide ni d'erreur console ; comportement conforme à la décision A3.
|
|
||||||
- **F4 — Écran résultat enrichi.** Si le contour de commune est intégré (geo.api.gouv.fr, gratuit sans clé : `https://geo.api.gouv.fr/communes?code=<insee>&fields=nom,contour&format=json&geometry=contour`), vérifier l'affichage du polygone et le respect de `prefers-reduced-motion` sur l'animation du marqueur.
|
|
||||||
- **F5 — Doctrine OKI.** Une seule action primaire par écran ; bouton Valider en zone du pouce sur mobile ; timer non anxiogène ; micro-interactions en créole ; style carte non-Google.
|
|
||||||
- **F6 — Performance 4G caribéen.** Premier écran < 3 s en throttling 4G ; photos WebP/AVIF avec `srcset` et lazy loading ; poids d'un round (hors tiles) mesuré et indiqué dans le rapport.
|
|
||||||
|
|
||||||
## 5. Agent QA — passe finale
|
|
||||||
|
|
||||||
- **Q1 — Rejouer les bloquants.** C1, C2, A1, A2, B1, B2, F2 au minimum, avec preuves.
|
|
||||||
- **Q2 — Partie complète** dans chaque mode (Konnèt moniman, Kote mwen ye ?, Défi 5 rounds, Aprann), mobile et desktop, dont au moins un round tombant sur un lieu Cas B et un lieu guyanais.
|
|
||||||
- **Q3 — Checklist OKI du prompt v2 §4 (Agent Intégration & QA)** intégralement passée.
|
|
||||||
- **Q4 — Scope creep.** Signaler toute feature absente du prompt v2 (auth, comptes, partage réseau social, analytics…).
|
|
||||||
- **Q5 — Core Web Vitals** mobile 4G mesurés (LCP, INP, CLS) et consignés.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Format du rapport de vérification (obligatoire, un par agent)
|
|
||||||
|
|
||||||
```
|
|
||||||
| ID | Vérification | Méthode / Preuve (commande, requête, capture) | PASS/FAIL | Correctif appliqué |
|
|
||||||
|----|--------------|-----------------------------------------------|-----------|--------------------|
|
|
||||||
```
|
|
||||||
|
|
||||||
Suivi d'une section **Recommandations** pour tout ce qui dépasse le scope, triée : `bloquant` / `important` / `confort`, avec pour chacune : problème, solution proposée, effort estimé (S/M/L), fichier(s) concerné(s).
|
|
||||||
|
|
||||||
Commits atomiques préfixés : `verify:` (preuves), `fix:` (correctifs), `docs:` (ADR, rapport).
|
|
||||||
|
|
||||||
## 7. Interdits pendant l'audit
|
|
||||||
|
|
||||||
- Aucune nouvelle feature (l'audit corrige et recommande, il n'étend pas).
|
|
||||||
- Aucun refactor hors correctif de FAIL.
|
|
||||||
- Aucun PASS sans preuve exécutable ou capture.
|
|
||||||
- Ne pas « réparer » une donnée (QID, coordonnée, licence) depuis la mémoire : requête réelle ou suppression du lieu.
|
|
||||||
Reference in New Issue
Block a user