DéveloppeursDocsFournisseurs
Yeria
Documentation

MapView

Carte géographique : marqueurs, cadrage, surcouches.

Statut : brouillon. Rupture nette avec la v1 ; aucune donnée en production, aucun enjeu de migration.

Description

Le composant MapView affiche des données géographiques sur une carte interactive. Le format de transport s'organise autour d'une pile de couches nommées — marqueurs, formes, cartes de chaleur, sources de tuiles personnalisées ou GeoJSON brut — que le moteur de rendu compose en une seule vue, avec un panneau d'affichage des couches optionnel.

Philosophie de conception

  • Le backend décrit les données et l'intention. Le backend précise ce qu'il faut afficher (marqueurs, formes, ordre des couches, visibilité initiale, actions au clic) et quel cadrage adopter au départ. Le moteur de rendu maîtrise l'apparence (icônes, animations, gestes) et traduit la spécification en widgets natifs (flutter_map Marker/Polyline/Polygon/CircleMarker sur mobile, couches mapbox-gl / leaflet sur le web).
  • Une seule convention de coordonnées, partout. Tous les points géographiques sont des objets { lat: number, lon: number }. Les coordonnées sous forme de tableau ([lon, lat] ou [lat, lon]) ne sont acceptées dans aucun champ, y compris à l'intérieur des charges utiles des couches GeoJSON — voir Convention de coordonnées.
  • Les couches sont un concept de premier plan — et l'unique chemin de données sur le fil. Même une carte à marqueur unique se décrit sous la forme layers: [{ type: 'markers', markers: [...] }]. Le SDK fournit des méthodes de commodité (addMarker, addPolygon, …) qui composent ces couches en coulisses, afin que le code du backend reste concis.
  • GeoJSON est une porte de sortie, pas le cas nominal. Un backend qui produit déjà du GeoJSON (PostGIS, Overpass, etc.) peut le brancher via le type de couche geojson, mais les types typés markers / shapes existent pour éviter aux auteurs backend toute expertise SIG dans les cas courants.

Démarrage rapide

javascript
1import { YeriaApp } from '@numerum-tech/yeriasdk';
2
3const yeriaApp = new YeriaApp({ appId: 'my-app' });
4
5// `addMarker` is a convenience method — under the hood it appends to a
6// default markers layer, creating it on first call. The wire payload that
7// reaches the renderer is always shaped as `{ layers: [...] }`.
8const map = yeriaApp.createMapView('stores-map', 'Partner Stores')
9  .setIntro('Find our partner stores')
10  .setViewport({ center: { lat: 48.8566, lon: 2.3522 }, zoom: 12 })
11  .setControls({ zoom: true, userLocation: true, layerToggle: true })
12  .addMarker({
13    id: 'paris',
14    location: { lat: 48.8566, lon: 2.3522 },
15    title: 'Paris Centre',
16    description: 'Open 9–19',
17    icon: 'store',
18    action: { method: 'GET', url: '/api/stores/paris' }
19  });
20
21return yeriaApp.serve(map);

Modèle de premier niveau

ts
1interface MapView {
2  id: string;
3  type: 'Map';
4  content: MapContent;
5  metadata?: ViewMetadata;
6  process?: ProcessContext;
7}
8
9interface MapContent {
10  // Header
11  title: string;
12  intro?: string;
13
14  // Camera
15  viewport?: MapViewport;
16  basemap?: 'streets' | 'satellite' | 'terrain' | 'dark' | 'auto'; // default 'auto'
17
18  // Data — always layered
19  layers: MapLayer[];
20
21  // UI
22  controls?: MapControls;
23  emptyMessage?: string;      // shown if no layer ends up drawable
24
25  // Input mode (location picker)
26  mode?: 'view' | 'pick';     // default 'view'
27  pick?: MapPickConfig;       // required when mode === 'pick'
28}

layers[] est l'unique chemin de données — même une carte à marqueur unique se décrit comme une couche markers comportant une seule entrée. Les méthodes de commodité du SDK (addMarker, addPolygon, …) créent ou complètent des couches par défaut afin que le code du backend reste concis. Une vue qui ne produit aucune couche dessinable en mode: 'view' exige emptyMessage ; en mode: 'pick', le marqueur placé par l'utilisateur constitue la donnée.

Convention de coordonnées

Tout point géographique de cette spécification s'écrit :

ts
1interface GeoPoint {
2  lat: number;       // -90 to 90
3  lon: number;       // -180 to 180
4  altitude?: number; // meters above sea level
5  precision?: number;// horizontal accuracy in meters (sensor data)
6}
  • Toujours un objet, jamais un tableau. [lat, lon] comme [lon, lat] sont rejetés par le validateur du SDK.
  • La conversion côté moteur de rendu tient en une ligne :
  • Dart / latlong2 : LatLng(p.lat as double, p.lon as double)
  • Mapbox GL / Leaflet : [p.lon, p.lat] (longitude en premier, convention GeoJSON)
  • flutter_map_geojson : le type de couche geojson du SDK émet du GeoJSON déjà converti ; les moteurs de rendu qui empruntent ce chemin ne manipulent [lon, lat] qu'à l'intérieur des charges utiles GeoJSON — le reste de la spécification demeure en {lat, lon}.

Marqueurs

ts
1interface MapMarker {
2  id: string;
3  location: GeoPoint;
4
5  // Display
6  title?: string;
7  description?: string;
8  icon?: string;          // catalog name OR full URL OR data: URI
9  color?: string;         // hex string, e.g. '#1A73E8'; falls back to renderer default
10  size?: 'sm' | 'md' | 'lg'; // default 'md'
11  selected?: boolean;     // renderer should highlight; default false
12
13  // Interaction
14  action?: ActionRef;     // see "Actions"
15  popup?: MarkerPopup;    // overrides the default title+description popup
16
17  // Free-form
18  meta?: Record<string, unknown>;
19}
20
21interface MarkerPopup {
22  title?: string;         // defaults to marker.title
23  body?: string;          // markdown allowed
24  image?: string;         // URL
25  actions?: ActionRef[];  // buttons inside the popup
26}

Les moteurs de rendu doivent interpréter icon ainsi : (a) un nom du catalogue (p. ex. 'store', 'home', 'pin') s'il correspond à une entrée de leur catalogue ; (b) une URL si la valeur commence par http(s):// ou data: ; (c) à défaut, le repère par défaut du moteur de rendu, les valeurs inconnues étant ignorées.

Formes (géométrie typée)

shapes remplace le chemin polygone-par-overlays de la v1. Les formes sont dessinées au-dessus du fond de carte et sous les marqueurs (sauf indication contraire du zIndex de la couche).

ts
1type MapShape = PolygonShape | CircleShape | PolylineShape | RectangleShape;
2
3interface ShapeBase {
4  id: string;
5  config?: ShapeStyle;
6  action?: ActionRef;     // optional click handler
7  meta?: Record<string, unknown>;
8}
9
10interface PolygonShape extends ShapeBase {
11  type: 'Polygon';
12  points: GeoPoint[];     // 3+ points; the polygon auto-closes
13}
14
15interface CircleShape extends ShapeBase {
16  type: 'Circle';
17  center: GeoPoint;
18  radius: number;         // meters
19}
20
21interface PolylineShape extends ShapeBase {
22  type: 'Polyline';
23  points: GeoPoint[];     // 2+ points
24}
25
26interface RectangleShape extends ShapeBase {
27  type: 'Rectangle';
28  sw: GeoPoint;           // south-west corner
29  ne: GeoPoint;           // north-east corner
30}
31
32interface ShapeStyle {
33  fillColor?: string;
34  fillOpacity?: number;   // 0–1
35  strokeColor?: string;
36  strokeOpacity?: number; // 0–1
37  strokeWidth?: number;   // px
38  dashed?: boolean;
39}

Cadrage

ts
1interface MapViewport {
2  // Pick ONE of (center+zoom) or (bounds) or (fitMarkers)
3  center?: GeoPoint;
4  zoom?: number;          // typical 0–22
5  bounds?: { sw: GeoPoint; ne: GeoPoint };
6  fitMarkers?: boolean;   // if true, renderer fits viewport to all visible markers + shapes; takes precedence
7
8  // Constraints
9  minZoom?: number;
10  maxZoom?: number;
11
12  // 3D — renderer may ignore if unsupported (must NOT throw)
13  bearing?: number;       // 0–360, default 0
14  pitch?: number;         // 0–60, default 0
15}

Ordre de résolution au moment du rendu : fitMarkersboundscenter+zoom. Si rien n'est fourni et qu'il existe au moins un marqueur ou une forme, le moteur de rendu ajuste le cadrage aux données ; sinon il affiche une région par défaut raisonnable, à sa discrétion.

Contrôles

ts
1interface MapControls {
2  zoom?: boolean;            // zoom +/- buttons; default true
3  compass?: boolean;         // shown when bearing != 0; default true
4  userLocation?: boolean;    // "locate me" button; default false
5  layerToggle?: boolean;     // legend / show-hide panel; default true if any layer is `toggleable`
6  scale?: boolean;           // distance scale; default false
7  fullscreen?: boolean;      // default false
8  attribution?: string;      // override; renderer must always show some attribution
9}

Couches (composition multicouche)

Lorsque plusieurs jeux de données logiques partagent une même carte (p. ex. Boutiques, Zones de couverture, Trafic), utilisez layers[] :

ts
1type MapLayer =
2  | MarkersLayer
3  | ShapesLayer
4  | HeatmapLayer
5  | TilesLayer
6  | GeoJsonLayer;
7
8interface LayerBase {
9  id: string;                // stable id; clients persist toggle state by id
10  name?: string;             // shown in legend / toggle UI
11  legendIcon?: string;       // icon shown next to `name`
12  visible?: boolean;         // default true
13  toggleable?: boolean;      // user can show/hide; default true
14  zIndex?: number;           // higher = on top; default by insertion order
15  minZoom?: number;          // hide below this zoom
16  maxZoom?: number;          // hide above this zoom
17}
18
19interface MarkersLayer extends LayerBase {
20  type: 'markers';
21  markers: MapMarker[];
22  cluster?: boolean;         // default true when markers.length > 50
23  clusterRadius?: number;    // px; default 50
24}
25
26interface ShapesLayer extends LayerBase {
27  type: 'shapes';
28  shapes: MapShape[];
29}
30
31interface HeatmapLayer extends LayerBase {
32  type: 'heatmap';
33  points: Array<{ lat: number; lon: number; intensity?: number }>;
34  radius?: number;           // px; default 25
35  intensityMax?: number;     // default 1
36  colorRamp?: string[];      // hex stops, low-to-high; renderer default if omitted
37}
38
39interface TilesLayer extends LayerBase {
40  type: 'tiles';
41  url: string;               // {z}/{x}/{y} template
42  attribution: string;       // required by most providers
43  maxNativeZoom?: number;
44  opacity?: number;          // 0–1
45}
46
47interface GeoJsonLayer extends LayerBase {
48  type: 'geojson';
49  data: object;              // RFC 7946 FeatureCollection | Feature | Geometry
50  // Defaults applied to features that lack `properties.style`:
51  defaultMarkerIcon?: string;
52  defaultShapeStyle?: ShapeStyle;
53}

addMarker / addShape et consorts ajoutent-ils leurs éléments ?

Les méthodes de commodité acceptent toutes un argument final layerId optionnel qui désigne la couche cible :

AppelCible
addMarker(m) (sans layerId)Couche de marqueurs par défaut _default_markers. Créée automatiquement au premier appel avec name: undefined, toggleable: false.
addMarker(m, 'stores')Couche d'id: 'stores'. Doit déjà exister (créée via addLayer({...})). Lève LayerNotFoundError dans le cas contraire.
addPolygon(id, points, config?) (sans layerId)Couche de formes par défaut _default_shapes.
addPolygon(id, points, config?, 'zones')Couche nommée 'zones'. Même règle de préexistence.
Même schéma pour addMarkers, addCircle, addPolyline, addRectangle, addShape.

Une incompatibilité de type lève LayerTypeMismatchError au moment même de l'appel (p. ex. ajouter un marqueur à une couche de type shapes). Les erreurs se déclenchent sur le constructeur du SDK, non à serve() : les auteurs backend obtiennent ainsi un retour immédiat en développement.

Il n'existe pas de curseur caché de « couche courante » — chaque appel utilise la couche par défaut ou nomme explicitement sa cible.

javascript
1// Simple case — no layer ceremony
2map.addMarker({ id: 'home', location: { lat: 6.13, lon: 1.22 } });
3//  → goes into _default_markers (auto-created)
4
5// Multi-layer case — declare, then target
6map
7  .addLayer({ id: 'stores', type: 'markers', name: 'Boutiques', cluster: true })
8  .addLayer({ id: 'events', type: 'markers', name: 'Événements' });
9
10map.addMarker({ id: 'paris', location: { lat: 48.85, lon: 2.35 } }, 'stores');
11map.addMarker({ id: 'fest',  location: { lat: 43.60, lon: 1.44 } }, 'events');

Mode sélection (saisie d'une position)

Pour les parcours qui exigent une position choisie par l'utilisateur :

ts
1interface MapPickConfig {
2  prompt?: string;                 // helper text above the map; e.g. "Tap to choose your delivery point"
3  initialLocation?: GeoPoint;
4  submitUrl: string;               // URL the picked location is POSTed to
5  submitMethod?: 'POST' | 'PUT';   // default POST
6  submitLabel?: string;            // confirm button label; default "Confirm"
7  payloadKey?: string;             // key under which the location is sent; default 'location'
8  bounds?: { sw: GeoPoint; ne: GeoPoint }; // restrict pickable area
9  snapToMarkers?: boolean;         // default false; if true, picking snaps to the nearest existing marker
10}

Lorsque mode === 'pick', le moteur de rendu :

  1. Affiche la carte avec les marqueurs et formes existants en guise de contexte.
  2. Laisse l'utilisateur poser ou déplacer un unique marqueur de sélection (initialisé sur pick.initialLocation, ou sur la position courante de l'utilisateur si controls.userLocation est actif).
  3. À la confirmation, envoie POST pick.submitUrl avec le corps { [pick.payloadKey]: { lat, lon }, processContext? }.

La primitive mobile de sélection existe déjà dans yeria-app/lib/presentation/shared/widgets/map_picker/gps_map_picker_widget.dart — la spécification ne fait que lui donner un format de transport.

Actions

ActionRef a la même forme que dans toutes les autres vues Yeria : le répartiteur d'actions du JsonRenderer achemine donc les clics sur la carte par un chemin unique :

ts
1interface ActionRef {
2  url: string;
3  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; // default GET
4  body?: Record<string, unknown>;
5  confirm?: { title: string; message: string; submitLabel?: string };
6}

L'action d'un marqueur et l'action d'une forme sont toutes deux prises en charge. En leur absence, toucher le marqueur affiche sa bulle (et rien ne se produit pour une forme).

Méthodes du SDK (JavaScript)

Configuration de la vue
setIntro(text)
Paragraphe d'introduction placé au-dessus de la carte.
setViewport(viewport)
viewport est un MapViewport ; passez { center, zoom } dans le cas simple.
setBasemap(name)
'streets' | 'satellite' | 'terrain' | 'dark' | 'auto'.
setControls(controls)
MapControls partiel ; fusionné avec les valeurs par défaut.
setEmptyMessage(text)
Affiché lorsqu'aucune couche n'est finalement dessinable.
Commodité — couche par défaut ou cible nommée via le layerId optionnel
addMarker(marker, layerId?) / addMarkers(markers, layerId?)
Ajoute dans layerId (qui doit exister) ou dans _default_markers (créée automatiquement au premier appel).
clearMarkers(layerId?)
Vide la couche de marqueurs visée (celle par défaut si l'argument est omis).
addPolygon(id, points, config?, layerId?)
Ajoute un Polygon dans layerId ou _default_shapes.
addCircle(id, center, radius, config?, layerId?)
Ajoute un Circle.
addPolyline(id, points, config?, layerId?)
Ajoute une Polyline.
addRectangle(id, sw, ne, config?, layerId?)
Ajoute un Rectangle.
addShape(shape, layerId?)
Ajoute une forme typée.
clearShapes(layerId?)
Vide la couche de formes visée.
Couches — composition explicite
addLayer(layer)
Empile une couche typée.
setLayers(layers)
Remplace la pile de couches.
clearLayers()
Vide la pile de couches (supprime aussi les couches par défaut implicites).
getLayer(id)
Renvoie la couche portant cet identifiant, ou undefined.
Sélection
setPickMode(config)
Positionne mode='pick' et pick=config.
Hérité
getContent() / serve() / toJSON() / setProcess(...) etc.
Provient de BaseView.

Matrice de conformité des moteurs de rendu

Les moteurs de rendu SHOULD publier leur niveau de conformité à cette matrice dans leur documentation. Un moteur de rendu est conforme s'il implémente chaque MUST, se dégrade proprement sur chaque MAY (une valeur inconnue reste sans effet et ne provoque jamais de plantage) et documente chaque SHOULD qu'il laisse de côté.

FonctionnalitéNiveau
title, introMUST
raccourci markersMUST
raccourci shapes (Polygon, Circle, Polyline)MUST
viewport.{center, zoom}MUST
viewport.fitMarkersMUST
viewport.boundsSHOULD
viewport.{minZoom, maxZoom}SHOULD
viewport.{bearing, pitch}MAY
basemapSHOULD (doit accepter la valeur, rendu au mieux)
controls.zoomMUST
controls.userLocationSHOULD
controls.compassSHOULD
controls.layerToggleSHOULD
controls.{scale, fullscreen, attribution}MAY
marker.{icon, color, size, selected}SHOULD (catalogue d'icônes) ; NE MUST PAS planter sur une valeur inconnue
marker.actionMUST
marker.popupSHOULD
shape.actionSHOULD
forme RectangleSHOULD
layers[] (markers, shapes)MUST
layers[] heatmapSHOULD
layers[] tilesSHOULD
layers[] geojsonSHOULD
layer.{visible, toggleable, zIndex, minZoom, maxZoom}MUST, pour les types de couches pris en charge
layer.cluster (markers)SHOULD
mode: 'pick'SHOULD
emptyMessageMUST

Règles de validation (côté SDK)

  • lat ∈ [-90, 90], lon ∈ [-180, 180]. Rejet dans le cas contraire.
  • Un Polygon exige points.length ≥ 3. Une Polyline exige points.length ≥ 2.
  • Circle.radius > 0.
  • Rectangle.sw.lat ≤ ne.lat, sw.lon ≤ ne.lon.
  • viewport.zoom ∈ [0, 22]. bearing ∈ [0, 360). pitch ∈ [0, 60].
  • Une MarkersLayer exige markers.length ≥ 1 ; une ShapesLayer exige shapes.length ≥ 1 ; une HeatmapLayer exige points.length ≥ 1.
  • Les valeurs d'id de couche doivent être uniques au sein de layers[].
  • mode === 'pick' exige pick.submitUrl.
  • Une vue en mode: 'view' dont layers[] est vide (ou ne contient que des couches sans rien à dessiner) exige emptyMessage. En mode: 'pick', les couches ne servent que de contexte facultatif : le marqueur placé par l'utilisateur constitue la donnée.

Exemple complet

json
1{
2  "id": "logistics",
3  "type": "Map",
4  "content": {
5    "title": "Logistique – Région du Centre",
6    "intro": "Aperçu en temps réel des points de livraison et zones de couverture.",
7    "basemap": "auto",
8    "viewport": { "fitMarkers": true, "minZoom": 6, "maxZoom": 18 },
9    "controls": {
10      "zoom": true, "compass": true, "userLocation": true,
11      "layerToggle": true, "scale": true
12    },
13    "layers": [
14      {
15        "id": "stores",
16        "type": "markers",
17        "name": "Boutiques",
18        "legendIcon": "store",
19        "cluster": true,
20        "markers": [
21          {
22            "id": "store-lome",
23            "location": { "lat": 6.1319, "lon": 1.2228 },
24            "title": "Boutique Lomé Centre",
25            "description": "Ouvert 8h–20h",
26            "icon": "store",
27            "color": "#1A73E8",
28            "action": { "method": "GET", "url": "/api/stores/lome" }
29          }
30        ]
31      },
32      {
33        "id": "coverage",
34        "type": "shapes",
35        "name": "Zone de livraison",
36        "visible": true,
37        "toggleable": true,
38        "zIndex": -1,
39        "shapes": [
40          {
41            "id": "delivery-zone",
42            "type": "Polygon",
43            "points": [
44              { "lat": 6.20, "lon": 1.18 },
45              { "lat": 6.20, "lon": 1.30 },
46              { "lat": 6.05, "lon": 1.30 },
47              { "lat": 6.05, "lon": 1.18 }
48            ],
49            "config": { "fillColor": "#34A853", "fillOpacity": 0.15, "strokeColor": "#34A853", "strokeWidth": 2 }
50          }
51        ]
52      },
53      {
54        "id": "traffic",
55        "type": "heatmap",
56        "name": "Trafic",
57        "visible": false,
58        "points": [
59          { "lat": 6.13, "lon": 1.22, "intensity": 0.9 },
60          { "lat": 6.14, "lon": 1.23, "intensity": 0.6 }
61        ]
62      }
63    ]
64  },
65  "metadata": { "version": "2.0.0", "createdAt": "2026-04-26T08:00:00.000Z" }
66}

Journal des modifications

Voir map-view-changelog.md — les différences majeures avec la v1 et la justification de chaque changement.