- C# 99.2%
- Dockerfile 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .devcontainer | ||
| Api | ||
| Migrations | ||
| Models | ||
| Properties | ||
| Services | ||
| .env.example | ||
| .gitignore | ||
| appsettings.Development.json | ||
| appsettings.json | ||
| Dockerfile | ||
| MapStore.csproj | ||
| nuget.config.example | ||
| Program.cs | ||
| README.md | ||
MapStore
Bibliothèque de cartes du jeu de géographie Terranaut.
- Le front liste les cartes disponibles et récupère un GeoJSON anonymisé, sans aucun nom, à afficher dans MapLibre.
- Le service Game récupère les solutions (featureId → noms dans toutes les langues) pour vérifier les réponses.
Les noms ne sont jamais présents dans le GeoJSON public : le front ne voit que des ids.
Déroulé d'une partie
Front → GET /maps choix de la carte
Front → POST /games { mapId } → Game
Game → GET /internal/maps/{id}/solutions → MapStore
Front → GET /maps/{id}/geojson affichage MapLibre
Game → "Trouve : Paris" → Front
Front → featureId cliqué → Game (vérification)
Stockage
Postgres
| Table | Clé | Colonnes |
|---|---|---|
Maps |
Id (int, auto-incrément) |
FeatureCount, OwnerId (null pour l'instant), CreatedAt, Scale (int, null pour les cartes importées avant son ajout), Tags (text[]), AvatarType (null si pas d'avatar) |
MapTranslations |
(MapId, Lang) |
Name, Description |
MapFeatureNames |
(MapId, FeatureId, Lang) |
Name (nom affiché), Aliases (text[], variantes acceptées) |
- Les langues disponibles d'une carte sont exactement ses lignes
MapTranslations. L'import refuse toute langue incomplète, donc une langue présente est garantie complète. MapFeatureNames (MapId, Lang)est une clé étrangère versMapTranslations: un nom de feature ne peut pas exister dans une langue non déclarée par la carte.OwnerIdest prévu pour de futures cartes créées par les utilisateurs, ce qui n'est pas implémenté.Scalevautworld,continent,countryouregion. Il est stocké en int pour que l'ordre suive la taille de l'échelle.scaleettagssont des identifiants stables : leur traduction est à la charge du front.AvatarTypeest le type MIME de l'avatar (image/png,image/jpegouimage/webp), détecté à l'upload.
Disque (volume Docker)
/data/maps/{mapId}.geojson FeatureCollection anonymisée
/data/avatars/{mapId} avatar de la carte (sans extension, type dans Maps.AvatarType)
Endpoints
Publics (routés par la gateway)
| Méthode | Route | Réponse |
|---|---|---|
| GET | /maps |
[{ id, featureCount, scale, tags, hasAvatar, translations: { fr: { name, description }, ... } }] |
| GET | /maps/{id} |
une carte (même format), 404 sinon |
| GET | /maps/{id}/geojson |
le GeoJSON anonymisé (application/geo+json) |
| GET | /maps/{id}/names |
{ en: [...], fr: [...] } : les noms possibles par langue, 404 si la carte n'existe pas |
| GET | /maps/{id}/avatar |
l'image de l'avatar, 404 si la carte n'existe pas ou n'a pas d'avatar |
GET /maps accepte deux filtres optionnels : scale (400 si la valeur n'existe pas) et tag, répétable. Une carte doit avoir tous les tags demandés, par exemple /maps?scale=country&tag=asia&tag=historic. La liste est triée de la plus grande échelle à la plus petite, puis par id ; les cartes sans échelle arrivent en dernier.
Internes (non routés par la gateway)
Les routes /internal/* ne sont pas exposées par la gateway et n'ont pas d'authentification.
| Méthode | Route | Réponse |
|---|---|---|
| GET | /internal/maps/{id}/solutions |
[{ featureId, names: { fr: { name, aliases }, ... } }], toutes les langues de la carte. 404 si la carte n'existe pas. |
| POST | /internal/maps |
body = fichier d'import, renvoie { id } |
| PATCH | /internal/maps/{id} |
body = { scale, tags }, renvoie 204. Un champ absent reste inchangé et tags remplace toute la liste. 400 si le body est invalide, 404 si la carte n'existe pas. |
| PUT | /internal/maps/{id}/avatar |
body = l'image brute (pas de multipart), renvoie 204. Remplace l'avatar existant. 400 si l'image est trop grande ou dans un format refusé, 404 si la carte n'existe pas. |
| DELETE | /internal/maps/{id}/avatar |
renvoie 204, 404 si la carte n'existe pas. |
Noms publics
GET /maps/{id}/names sert par exemple à l'autocomplétion. Pour ne pas donner les solutions :
- la réponse ne contient que le
namede chaque feature, sans alias nifeatureId; - chaque liste est triée par ordre alphabétique dans sa langue, jamais par
featureId. Sinon, l'ordre de la liste donnerait l'id de la feature dans le GeoJSON ; - les doublons sont retirés, donc une liste peut contenir moins de noms que
featureCount.
Les langues sont triées par code.
Avatars
- Formats acceptés : PNG, JPEG et WebP, 512 Ko maximum. Le format est détecté sur les premiers octets du fichier et le
Content-Typeenvoyé par le client est ignoré. Le SVG est refusé, parce qu'il peut contenir du script et que l'image est servie publiquement. - Le service ne redimensionne pas l'image : c'est au client de l'envoyer à la bonne taille.
- Le GET renvoie
Cache-Control: no-cacheetLast-Modified. Le client revalide donc à chaque fois (304 si l'avatar n'a pas changé) et voit tout de suite un remplacement. - Ordre d'écriture : fichier puis DB à l'upload, DB puis fichier à la suppression. La DB n'indique jamais un avatar dont le fichier n'existe pas.
Format d'import
Un GeoJSON valide. L'échelle et les tags sont à la racine, les métadonnées de la carte dans le membre map, et les noms de chaque feature dans properties.names :
{
"type": "FeatureCollection",
"scale": "continent",
"tags": ["europe", "historic"],
"map": {
"fr": { "name": "Europe", "description": "Pays et capitales d'Europe" },
"en": { "name": "Europe", "description": "European countries and capitals" }
},
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [2.35, 48.85] },
"properties": {
"names": {
"fr": { "name": "Paris" },
"en": { "name": "Paris", "aliases": ["City of Light"] }
}
}
}
]
}
scale est obligatoire et tags optionnel. Les tags sont nettoyés (trim, minuscules, doublons retirés), doivent respecter ^[a-z0-9-]+$ et sont limités à 20.
Géométries acceptées : Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon. Une même carte peut mélanger pays, villes et fleuves.
La conversion depuis une source externe (Natural Earth, OSM…) et la simplification des géométries (mapshaper) se font en amont, par script. Le service n'accepte que ce format.
Logique d'import
- Validation de la structure :
FeatureCollection,scaleettagsvalides, géométries autorisées, taille et nombre de features plafonnés. - Validation des langues : les langues de la carte sont les clés de
map. Chacune doit avoir unnamede carte, et chaque feature doit avoir unnamedans chacune de ces langues, et dans aucune autre. Sinon l'import est refusé (400). - Anonymisation :
- l'ordre des features est mélangé et les ids
1..nsont attribués dans ce nouvel ordre, pour que ni l'ordre ni les ids ne trahissent l'ordre alphabétique de la source ; propertiesest vidé et les membresmap,scaleettagsretirés ;- l'
idnumérique est placé au niveau de la feature, ce qui permet d'utiliser directementsetFeatureStatedans MapLibre.
- l'ordre des features est mélangé et les ids
- Nettoyage des alias : trim, valeurs vides et doublons du
nameretirés. - Écriture du fichier puis de la DB (en transaction). Si la DB échoue, il reste au pire un fichier orphelin, jamais une carte en base sans fichier.
Cartes immuables
Le contenu d'une carte n'est jamais modifié : pour le corriger, on la réimporte, ce qui crée un nouvel id. Une partie en cours ne peut donc pas voir ses featureId changer, et le GeoJSON peut être mis en cache sans limite de durée.
Seuls scale, tags (PATCH /internal/maps/{id}) et l'avatar restent modifiables : ils n'affectent ni les featureId ni le GeoJSON.
Hors périmètre
- Comparaison des réponses : normalisation de la casse, des accents et de la ponctuation (
U.S.A.=usa), tolérance aux fautes. C'est la responsabilité de Game, donc les alias ne contiennent que des variantes vraiment différentes (USA,Holland…). - Cartes utilisateurs : seul
OwnerIdest prévu dans le schéma.
Mise en place (dev)
- Copier
.env.exampleen.env. - Renommer
nuget.config.exampleennuget.config. Il reste ignoré par git, car il contient les identifiants Forgejo. - Ouvrir le projet dans le devcontainer (
.devcontainer/compose.ymldémarre aussi Postgres).