From e4a0bfd045ed59fb9de76daf2a071855cb348c1a Mon Sep 17 00:00:00 2001 From: sucupira Date: Sat, 18 Jul 2026 20:15:03 -0400 Subject: [PATCH] feat(app): mode street 'An lari a' - visionneuse mapillary-js lazy, /api/round?mode=street, i18n FR/GCF (ADR-003) --- app/.env.example | 7 + app/package-lock.json | 296 +++++++++++++++++++++- app/package.json | 1 + app/src/lib/components/StreetPanel.svelte | 115 +++++++++ app/src/lib/i18n/fr.ts | 6 + app/src/lib/i18n/gcf.ts | 6 + app/src/lib/server/lieux.ts | 65 ++++- app/src/lib/types.ts | 30 ++- app/src/routes/+page.svelte | 3 +- app/src/routes/api/defi/[seed]/+server.ts | 4 +- app/src/routes/api/photo/[id]/+server.ts | 6 +- app/src/routes/api/round/+server.ts | 20 +- app/src/routes/defi/[seed]/+page.svelte | 4 +- app/src/routes/jeu/[mode]/+page.svelte | 8 +- app/src/routes/jeu/[mode]/+page.ts | 2 +- 15 files changed, 546 insertions(+), 27 deletions(-) create mode 100644 app/.env.example create mode 100644 app/src/lib/components/StreetPanel.svelte diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..6472048 --- /dev/null +++ b/app/.env.example @@ -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= diff --git a/app/package-lock.json b/app/package-lock.json index c915c24..f460e66 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,6 +8,7 @@ "name": "jwe", "version": "2.0.0", "dependencies": { + "mapillary-js": "^4.1.2", "maplibre-gl": "^5.1.0", "sharp": "^0.35.3" }, @@ -2586,6 +2587,12 @@ "dev": true, "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": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2609,6 +2616,24 @@ "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": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.2.tgz", @@ -2631,6 +2656,12 @@ "dev": true, "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2638,6 +2669,12 @@ "dev": true, "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": { "version": "0.8.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", @@ -2836,6 +2873,12 @@ "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": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -2884,6 +2927,15 @@ "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -3221,6 +3273,11 @@ "dev": true, "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": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -3245,6 +3302,16 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", @@ -3329,6 +3396,14 @@ "dev": true, "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": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", @@ -3631,6 +3706,16 @@ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3721,7 +3806,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -3754,6 +3838,11 @@ "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": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3867,6 +3956,15 @@ "dev": true, "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -3983,6 +4081,15 @@ "dev": true, "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": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4000,6 +4107,63 @@ "dev": true, "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": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", @@ -4100,6 +4264,35 @@ "dev": true, "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", @@ -4121,6 +4314,15 @@ "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": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -4260,6 +4462,12 @@ "license": "MIT", "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": { "version": "3.94.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", @@ -4382,7 +4590,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", - "dev": true, "funding": [ { "type": "github", @@ -4489,6 +4696,21 @@ "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": { "version": "8.5.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", @@ -4570,7 +4792,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -4638,7 +4859,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", - "dev": true, "license": "MIT", "dependencies": { "quickselect": "^2.0.0" @@ -4648,7 +4868,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", - "dev": true, "license": "ISC" }, "node_modules/rc": { @@ -4862,6 +5081,24 @@ "dev": true, "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": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -5117,6 +5354,11 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -5284,6 +5526,12 @@ "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": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5346,8 +5594,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -5419,6 +5666,22 @@ "dev": true, "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": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -5573,6 +5836,16 @@ "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": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", @@ -5580,6 +5853,15 @@ "dev": true, "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": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz", diff --git a/app/package.json b/app/package.json index 9eb05ad..728a656 100644 --- a/app/package.json +++ b/app/package.json @@ -22,6 +22,7 @@ "vite": "^6.0.0" }, "dependencies": { + "mapillary-js": "^4.1.2", "maplibre-gl": "^5.1.0", "sharp": "^0.35.3" } diff --git a/app/src/lib/components/StreetPanel.svelte b/app/src/lib/components/StreetPanel.svelte new file mode 100644 index 0000000..a47656c --- /dev/null +++ b/app/src/lib/components/StreetPanel.svelte @@ -0,0 +1,115 @@ + + +
+
+ {#if etat !== 'pret'} +
+

+ {#if etat === 'chargement'}{t().jeu.streetChargement} + {:else if etat === 'token'}{t().jeu.streetToken} + {:else}{t().jeu.streetErreur}{/if} +

+
+ {/if} + {#if credit && etat === 'pret'} +
{credit}
+ {/if} +
+ + diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 57df26f..c6aef7f 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -17,6 +17,8 @@ const fr = { lieuDesc: 'Une rue, un bord de mer, une forêt… devine où tu es.', defi: 'Défi 5 rounds', 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', explorerDesc: 'Explore la carte librement et lis les fiches Wikipédia. Sans score.' }, @@ -40,6 +42,10 @@ const fr = { indicesRestants: 'indices restants', plusDIndice: 'Plus d’indice disponible', 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', sur: 'sur', difficulte: { diff --git a/app/src/lib/i18n/gcf.ts b/app/src/lib/i18n/gcf.ts index 4ecc24c..dfcabdd 100644 --- a/app/src/lib/i18n/gcf.ts +++ b/app/src/lib/i18n/gcf.ts @@ -19,6 +19,8 @@ const gcf: Dict = { lieuDesc: 'On lari, on bodlanmè, on fowé… chwé kote ou yé.', defi: 'Défi 5 rounds', 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', explorerDesc: 'Flanné asou kat-la é li fich Wikipédia yo. San pwen.' }, @@ -42,6 +44,10 @@ const gcf: Dict = { indicesRestants: 'endis ki rété', plusDIndice: 'Pa ni endis ankò', 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', sur: 'asou', difficulte: { diff --git a/app/src/lib/server/lieux.ts b/app/src/lib/server/lieux.ts index 9af7fae..820e7e2 100644 --- a/app/src/lib/server/lieux.ts +++ b/app/src/lib/server/lieux.ts @@ -1,4 +1,5 @@ import rawLieux from './data/lieux.json'; +import rawStreet from './data/street.json'; import type { Categorie, Difficulte, @@ -6,10 +7,15 @@ import type { GuessResult, Lieu, Region, - RoundPlace + RoundPlace, + StreetLieu } from '$lib/types'; 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 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; } -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); } @@ -74,6 +86,28 @@ export function getRandomLieu(options: { 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=. */ 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). * 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 / π). * 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 aire = lieu.aire_commune_km2; 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. */ export function scoreGuess( - lieu: Lieu, + lieu: Scoreable, guess: { lat: number; lon: number }, hintsUsed: 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. */ -export function skipResult(lieu: Lieu): GuessResult { +export function skipResult(lieu: Scoreable): GuessResult { return { distanceKm: null, score: 0, diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index e2e23b7..2841f77 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -44,6 +44,27 @@ export interface Lieu { 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). */ export interface RoundPlace { id: string; @@ -51,7 +72,12 @@ export interface RoundPlace { nom_creole: string; categorie: Categorie; 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[]; } @@ -95,4 +121,4 @@ export interface WikiSummary { thumbnail: string | null; } -export type GameMode = 'monument' | 'lieu' | 'defi'; +export type GameMode = 'monument' | 'lieu' | 'defi' | 'street'; diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 490087a..b48bace 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -13,7 +13,8 @@ const modes: { id: GameMode; emoji: string }[] = [ { id: 'monument', emoji: '🏛️' }, { id: 'lieu', emoji: '📍' }, - { id: 'defi', emoji: '⛰️' } + { id: 'defi', emoji: '⛰️' }, + { id: 'street', emoji: '🛣️' } ]; function jouer(): void { diff --git a/app/src/routes/api/defi/[seed]/+server.ts b/app/src/routes/api/defi/[seed]/+server.ts index d59add6..ff7915e 100644 --- a/app/src/routes/api/defi/[seed]/+server.ts +++ b/app/src/routes/api/defi/[seed]/+server.ts @@ -1,5 +1,5 @@ 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 type { RequestHandler } from './$types'; @@ -17,7 +17,7 @@ export const GET: RequestHandler = ({ params }) => { ); } 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})`); return sanitizeRound(lieu); }); diff --git a/app/src/routes/api/photo/[id]/+server.ts b/app/src/routes/api/photo/[id]/+server.ts index f5e063f..88fa742 100644 --- a/app/src/routes/api/photo/[id]/+server.ts +++ b/app/src/routes/api/photo/[id]/+server.ts @@ -1,5 +1,5 @@ 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'; 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>(); * - conversion WebP + redimensionnement à la largeur demandée. */ async function chargerPhoto(id: string, w: Largeur): Promise { - const lieu = getLieuById(id); + const lieu = getLieuPhotoById(id); if (!lieu) return null; const res = await fetch(lieu.photo.url, { headers: { 'User-Agent': USER_AGENT } }); 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). - if (!getLieuById(id)) { + if (!getLieuPhotoById(id)) { return new Response('Lieu inconnu', { status: 404 }); } diff --git a/app/src/routes/api/round/+server.ts b/app/src/routes/api/round/+server.ts index 9a74a91..0495cd7 100644 --- a/app/src/routes/api/round/+server.ts +++ b/app/src/routes/api/round/+server.ts @@ -1,9 +1,25 @@ 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'; -/** 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 }) => { + 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({ region: url.searchParams.get('region'), categorie: url.searchParams.get('categorie'), diff --git a/app/src/routes/defi/[seed]/+page.svelte b/app/src/routes/defi/[seed]/+page.svelte index f64796d..6e3c1e9 100644 --- a/app/src/routes/defi/[seed]/+page.svelte +++ b/app/src/routes/defi/[seed]/+page.svelte @@ -252,12 +252,12 @@
- {:else} + {:else if place.photo} {/if} diff --git a/app/src/routes/jeu/[mode]/+page.svelte b/app/src/routes/jeu/[mode]/+page.svelte index 4436340..a05c1bd 100644 --- a/app/src/routes/jeu/[mode]/+page.svelte +++ b/app/src/routes/jeu/[mode]/+page.svelte @@ -4,6 +4,7 @@ import GameMap from '$lib/components/GameMap.svelte'; import CarteRegions, { type MarqueurCarte } from '$lib/components/CarteRegions.svelte'; import PhotoPanel from '$lib/components/PhotoPanel.svelte'; + import StreetPanel from '$lib/components/StreetPanel.svelte'; import ResultPanel from '$lib/components/ResultPanel.svelte'; import TimerRing from '$lib/components/TimerRing.svelte'; import { t } from '$lib/i18n/store.svelte'; @@ -62,6 +63,7 @@ if (regionParam) params.set('region', regionParam); if (mode === 'monument') params.set('categorie', 'MONUMENT'); if (mode === 'lieu') params.set('categorie', 'LIEU'); + if (mode === 'street') params.set('mode', 'street'); if (joues.length > 0) params.set('exclude', joues.join(',')); const res = await fetch(`/api/round?${params}`); if (!res.ok) throw new Error(String(res.status)); @@ -246,12 +248,14 @@
- {:else} + {:else if place.mapillaryId} + + {:else if place.photo} {/if} diff --git a/app/src/routes/jeu/[mode]/+page.ts b/app/src/routes/jeu/[mode]/+page.ts index 35625a7..5febadc 100644 --- a/app/src/routes/jeu/[mode]/+page.ts +++ b/app/src/routes/jeu/[mode]/+page.ts @@ -2,7 +2,7 @@ import { error } from '@sveltejs/kit'; import type { GameMode } from '$lib/types'; import type { PageLoad } from './$types'; -const MODES: GameMode[] = ['monument', 'lieu', 'defi']; +const MODES: GameMode[] = ['monument', 'lieu', 'defi', 'street']; export const load: PageLoad = ({ params }) => { if (!MODES.includes(params.mode as GameMode)) {