MapStore Service
  • C# 99.2%
  • Dockerfile 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
njooris 91cfcf8a4c Merge pull request 'feature/pictures' (#7) from feature/pictures into master
Reviewed-on: #7
Reviewed-by: njooris <njooris@student.42lyon.fr>
2026-11-12 15:38:43 +00:00
.devcontainer fix: changed port and endpoint route to secure internal endpoints 2026-10-26 17:37:11 +01:00
Api feat: public endpoint to get list of solutions in all languages, and removed language dependency on internal endpoint 2026-11-12 15:27:36 +01:00
Migrations feat: avatars 2026-11-10 17:14:57 +01:00
Models feat: public endpoint to give solutions 2026-11-10 18:28:21 +01:00
Properties fix: changed port and endpoint route to secure internal endpoints 2026-10-26 17:37:11 +01:00
Services feat: public endpoint to get list of solutions in all languages, and removed language dependency on internal endpoint 2026-11-12 15:27:36 +01:00
.env.example feat: GeoJsonStorage service class 2026-10-26 10:42:06 +01:00
.gitignore feat: Map Creator and endpoint 2026-10-26 13:59:03 +01:00
appsettings.Development.json feat: avatars 2026-11-10 17:14:57 +01:00
appsettings.json feat: avatars 2026-11-10 17:14:57 +01:00
Dockerfile chore: update devcontainer 2026-10-14 23:01:29 +02:00
MapStore.csproj chore: update devcontainer 2026-10-14 23:01:29 +02:00
nuget.config.example chore: small fixes 2026-10-21 11:36:11 +02:00
Program.cs feat: avatars 2026-11-10 17:14:57 +01:00
README.md feat: public endpoint to get list of solutions in all languages, and removed language dependency on internal endpoint 2026-11-12 15:27:36 +01:00

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 vers MapTranslations : un nom de feature ne peut pas exister dans une langue non déclarée par la carte.
  • OwnerId est prévu pour de futures cartes créées par les utilisateurs, ce qui n'est pas implémenté.
  • Scale vaut world, continent, country ou region. Il est stocké en int pour que l'ordre suive la taille de l'échelle.
  • scale et tags sont des identifiants stables : leur traduction est à la charge du front.
  • AvatarType est le type MIME de l'avatar (image/png, image/jpeg ou image/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 name de chaque feature, sans alias ni featureId ;
  • 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-Type envoyé 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-cache et Last-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

  1. Validation de la structure : FeatureCollection, scale et tags valides, géométries autorisées, taille et nombre de features plafonnés.
  2. Validation des langues : les langues de la carte sont les clés de map. Chacune doit avoir un name de carte, et chaque feature doit avoir un name dans chacune de ces langues, et dans aucune autre. Sinon l'import est refusé (400).
  3. Anonymisation :
    • l'ordre des features est mélangé et les ids 1..n sont attribués dans ce nouvel ordre, pour que ni l'ordre ni les ids ne trahissent l'ordre alphabétique de la source ;
    • properties est vidé et les membres map, scale et tags retirés ;
    • l'id numérique est placé au niveau de la feature, ce qui permet d'utiliser directement setFeatureState dans MapLibre.
  4. Nettoyage des alias : trim, valeurs vides et doublons du name retirés.
  5. É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 OwnerId est prévu dans le schéma.

Mise en place (dev)

  1. Copier .env.example en .env.
  2. Renommer nuget.config.example en nuget.config. Il reste ignoré par git, car il contient les identifiants Forgejo.
  3. Ouvrir le projet dans le devcontainer (.devcontainer/compose.yml démarre aussi Postgres).