Aller au contenu

Outils cartographiques

Chaque outil agit sur la carte qui le contient et repose sur vos propres primitives shadcn. Un outil qui appelle l'API, comme le profil d'altitude, fait une requête facturée par appel.

npx shadcn@latest registry add @unmap=https://registry.unmap.dev/r/{name}.json
npx shadcn@latest add @unmap/measure

La ligne React enregistre le registre une fois par projet, ce qui permet ensuite de nommer les éléments @unmap/<item> ; shadcn-vue n'a pas de registry add et prend directement l'URL de l'élément. Les fichiers arrivent dans components/ui/ à côté des vôtres ; rien n'est remplacé sans confirmation.

Parcourir tous les éléments sur registry.unmap.dev
ÉlémentRôle
measureCliquez sur la carte pour mesurer une distance le long du grand cercle, avec une étiquette en direct sur la carte et les boutons Annuler et Effacer sur votre Button. Mètres et kilomètres, en anglais ou en français.
feature-inspectorLes entités sous un clic, ou dans la vue, listées dans vos Card et Table. Cliquez sur une ligne pour faire ressortir l'entité et lire ses propriétés.
time-sliderVotre Slider qui pilote une fenêtre temporelle sur des couches de la carte, avec lecture, pause, pas à pas et boucle. Fonctionne à côté d'une layer-legend sur la même couche.
drawDessine et modifie des points, des lignes, des polygones et des cercles sur vos ToggleGroup, Button et Tooltip. Les formes reviennent en GeoJSON et résistent à un changement de style.
compare-swipeDeux cartes dans un même cadre, avec un séparateur à faire glisser et une caméra partagée : avant et après, ou un style contre un autre.
elevation-profileL'altitude du sol le long d'un itinéraire, d'une ligne tracée ou de vos propres coordonnées, sur votre Chart, avec la distance, le dénivelé positif et négatif, le point le plus bas et le plus haut. Survolez le graphique ou la ligne et l'autre suit.

Mesurer des distances

measure ajoute un point là où le lecteur clique et mesure la ligne qui les relie le long du grand cercle, selon la forme de la Terre : de Vancouver à Halifax, on lit environ 4 430 km. Tant que le pointeur est sur la carte, une ligne pointillée le rejoint et l'étiquette sur la carte affiche le total. La ligne est tracée par-dessus la carte dans votre --primary, avec les boutons Annuler et Effacer sur votre Button shadcn :

import { Map } from "@/components/ui/map";
import { Measure } from "@/components/ui/measure";
 
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11}>
  <Measure lang="fr" onChange={({ points, metres }) => console.log(points.length, metres)} />
</Map>

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-measure-01

Projets React. Installe components/unmap-map-measure.tsx ; affichez <MapMeasure apiKey={key} />.

Ouvrir la page de l'exemple
  • Sous 1 km, les distances sont en mètres ; au-delà, en kilomètres avec deux, une ou aucune décimale selon la grandeur. lang="fr" les écrit à la française, 1,25 km et 4 430 km, avec une espace insécable entre les milliers et avant l'unité, et nomme les boutons en français. format remplace la mise en forme.
  • Un double-clic ou un double-toucher ajoute un seul point, et ne zoome pas : le zoom au double-clic de la carte est suspendu pendant la mesure. Annuler retire le dernier point et Effacer les retire tous. Quand la carte a le focus, Échap masque la ligne pointillée jusqu'à ce que le pointeur bouge, et Retour arrière annule le dernier point.
  • Une ligne qui franchit l'antiméridien (180 degrés de longitude) reste une seule ligne continue.
  • onChange (Vue : @change) reçoit les points et leur longueur en mètres après chaque changement. active={false} (Vue : :active="false") arrête l'ajout de points et garde la mesure sur la carte.
  • La mesure résiste à un changement de style et quitte la carte quand le composant est démonté. Ses couches sont marquées comme celles d'un outil, si bien que l'inspecteur d'entités ne les liste jamais. La mesure se fait dans le navigateur et n'envoie aucune requête.
  • L'inspecteur et l'outil de mesure répondent tous deux aux clics sur la carte. N'en exécutez qu'un à la fois et désactivez l'autre avec enabled={false} ou active={false}.
  • Le composant affiche une simple rangée : placez-le dans votre propre Card s'il flotte sur la carte.
<script setup lang="ts">
import { ref } from "vue";
import { UnmapMap } from "@/components/ui/map";
import { UnmapMeasure } from "@/components/ui/measure";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY;
const metres = ref(0);
</script>
 
<template>
  <UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
    <UnmapMeasure lang="fr" @change="metres = $event.metres" />
  </UnmapMap>
</template>

Inspecter les entités

feature-inspector liste ce que la carte a dessiné sous un clic, dans vos Card et Table shadcn : chaque entité à quelques pixels près, la plus haute d'abord, avec la couche qui l'a dessinée. La plus haute est sélectionnée : ses propriétés sont listées et sa forme est surlignée sur la carte dans votre --primary. Cliquez sur une ligne pour choisir une autre entité, ou sur la ligne sélectionnée pour la désélectionner. La liste et les propriétés forment deux tableaux, chacun nommé par la région qui l'entoure, si bien qu'un lecteur d'écran les annonce :

import { Map } from "@/components/ui/map";
import { FeatureInspector } from "@/components/ui/feature-inspector";
 
<Map apiKey={key} center={[-75.69, 45.42]} zoom={14}>
  <FeatureInspector lang="fr" className="absolute top-2 left-2 w-80" onSelect={(feature) => console.log(feature)} />
</Map>

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-inspect-01

Projets React. Installe components/unmap-map-inspect.tsx ; affichez <MapInspect apiKey={key} />.

Ouvrir la page de l'exemple
  • Par défaut, toutes les couches sont interrogées : les routes, les lieux et l'eau du fond de carte, et vos propres données. layers limite la recherche à certains identifiants de couches du style ; un identifiant qui se termine par * désigne toutes les couches qui commencent par le reste, et un identifiant absent du style est ignoré. Les couches des outils, comme une mesure ou le surlignage lui-même, ne sont jamais listées.
  • scope="view" liste plutôt ce qui est dessiné dans toute la vue, de nouveau après chaque déplacement, jusqu'à limit lignes (50 par défaut). La sélection est conservée d'un rafraîchissement à l'autre.
  • Une entité coupée entre plusieurs tuiles n'est listée qu'une fois. Les entités sans identifiant qui portent les mêmes propriétés dans la même couche se fondent en une seule ligne.
  • Avec un identifiant, le surlignage couvre l'entité entière dans toutes les tuiles chargées. Les polygones sont remplis, non tracés, si bien qu'aucune jointure de tuile ne les traverse ; les morceaux sont dessinés par tuile et peuvent se chevaucher légèrement au bord des tuiles. Sans identifiant, ou depuis une source qui promeut une propriété en identifiant (promoteId), le surlignage couvre le morceau dessiné là où vous avez cliqué.
  • L'inspecteur lit ce que la carte a déjà dessiné : il n'envoie aucune requête. enabled={false} (Vue : :enabled="false") ignore les clics et vide la liste, la sélection et le surlignage. lang="fr" écrit la carte en français et nomme les entités par name:fr quand elles en ont un. Le surlignage résiste à un changement de style et disparaît au démontage. onSelect (Vue : @select) reçoit l'entité sélectionnée, ou null.
  • Le surlignage est calculé à la sélection : il reste dessiné si un curseur ou une légende filtre ensuite cette entité. Cliquez de nouveau pour l'actualiser.
  • L'inspecteur et l'outil measure répondent tous deux aux clics sur la carte. N'en exécutez qu'un à la fois.
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapFeatureInspector } from "@/components/ui/feature-inspector";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY;
</script>
 
<template>
  <UnmapMap :api-key="apiKey" :center="[-75.69, 45.42]" :zoom="14">
    <UnmapFeatureInspector lang="fr" class="absolute top-2 left-2 w-80" scope="view" :limit="20" />
  </UnmapMap>
</template>

Curseur temporel

time-slider place votre Slider shadcn sur un nombre porté par vos entités, une année ou un instant en millisecondes, et n'affiche que les entités de la fenêtre choisie, avec des boutons de lecture, de pause et de pas à pas :

import { Map } from "@/components/ui/map";
import { GeoJSONLayer } from "@/components/ui/geojson-layer";
import { TimeSlider } from "@/components/ui/time-slider";
 
<Map apiKey={key} center={[-114.07, 51.05]} zoom={6}>
  <GeoJSONLayer id="wells" data={wells} />
  <TimeSlider lang="fr" layers={["wells-*"]} property="year" min={1950} max={2025} interval={500} loop />
</Map>

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-timeline-01

Projets React. Installe components/unmap-map-timeline.tsx ; affichez <MapTimeline apiKey={key} />.

Ouvrir la page de l'exemple
  • Une valeur seule affiche un pas : 2001 avec step={1} affiche les entités dont year est d'au moins 2001 et inférieur à 2002. cumulative affiche plutôt tout ce qui précède la fin du pas. Une paire, comme defaultValue={[1990, 2000]}, affiche les entités comprises entre les deux, bornes incluses, et garde sa largeur pendant la lecture.
  • La lecture avance d'un pas toutes les interval millisecondes (1 000 par défaut) et s'arrête à la fin ; loop la fait recommencer. Appuyer sur lecture à la fin repart du début.
  • L'état est non contrôlé avec defaultValue, ou contrôlé avec value et onValueChange (Vue : v-model). format écrit l'étiquette, par exemple une date à partir de millisecondes.
  • La fenêtre s'ajoute au filtre propre de chaque couche, dans la même syntaxe, à côté des valeurs qu'une layer-legend décoche sur la même couche : elles partagent un même registre de clauses de filtre par carte, si bien qu'aucune n'annule l'autre et que les clauses ne s'imbriquent jamais. Les entités sans la propriété sont masquées tant que le curseur s'applique. Il s'applique de nouveau après un changement de style et quand une couche nommée est ajoutée plus tard.
  • Au démontage, la fenêtre est retirée : aucune entité ne reste masquée sans curseur pour la faire revenir. Le filtrage se fait dans le navigateur : un pas n'est pas une requête.
  • Les valeurs sont des nombres, une année ou des millisecondes depuis 1970. Sur un filtre en syntaxe d'expression (ou sans filtre), une année stockée en texte se compare quand même ; sur un filtre en syntaxe héritée, elle ne correspond jamais. Les dates ISO en chaîne ne sont pas prises en charge.
  • Une valeur contrôlée doit être renvoyée depuis onValueChange (Vue : update:modelValue, donc v-model), sinon rien ne bouge et la lecture n'avance jamais.
  • Dans Vue, transmettez l'étiquette à chaque poignée dans votre Slider.vue, par exemple <SliderThumb :aria-labelledby="$attrs['aria-labelledby']" />, car reka-ui ne nomme pas les poignées à partir de la racine.
<script setup lang="ts">
import { ref } from "vue";
import { UnmapMap } from "@/components/ui/map";
import { UnmapGeojsonLayer } from "@/components/ui/geojson-layer";
import { UnmapTimeSlider } from "@/components/ui/time-slider";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY;
const wells = { type: "FeatureCollection", features: [] };
const year = ref<number | [number, number]>(1990);
</script>
 
<template>
  <UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="6">
    <UnmapGeojsonLayer id="wells" :data="wells" />
    <UnmapTimeSlider v-model="year" lang="fr" :layers="['wells-*']" property="year" :min="1950" :max="2025" cumulative />
  </UnmapMap>
</template>

Dessiner sur la carte

draw ajoute à la carte une barre d'outils de dessin : points, lignes, polygones et cercles, et un outil de sélection pour les déplacer, les remodeler et les supprimer. Le dessin repose sur terra-draw et la barre d'outils utilise vos propres ToggleGroup, Button et Tooltip. Ce que vous dessinez revient sous forme de FeatureCollection GeoJSON.

import { useState } from "react";
import { Map } from "@/components/ui/map";
import { Draw, EMPTY_DRAWING } from "@/components/ui/draw";
 
export function Sketch() {
  const [shapes, setShapes] = useState(EMPTY_DRAWING);
  return (
    <Map apiKey={key} style="base" center={[-114.07, 51.05]} zoom={13}>
      <Draw value={shapes} onValueChange={setShapes} />
    </Map>
  );
}

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-draw-01

Projets React. Installe components/unmap-map-draw.tsx ; affichez <MapDraw apiKey={key} />.

Ouvrir la page de l'exemple

La même chose en Vue :

<script setup lang="ts">
import { ref } from "vue";
import { UnmapMap } from "@/components/ui/map";
import { UnmapDraw } from "@/components/ui/draw";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const shapes = ref({ type: "FeatureCollection" as const, features: [] });
</script>
 
<template>
  <UnmapMap :api-key="apiKey" map-style="base" :center="[-114.07, 51.05]" :zoom="13">
    <UnmapDraw v-model="shapes" />
  </UnmapMap>
</template>
  • Les types (DrawValue, DrawFeature) accompagnent le composant. En React, import type { DrawValue } from "@/components/ui/draw" ; en Vue, ils sont exportés par le fichier du composant, donc import type { DrawValue } from "@/components/ui/draw/UnmapDraw.vue" type votre propre état.
  • Aucun outil n'est actif au départ : la carte se déplace comme d'habitude. Choisissez un outil pour dessiner, et choisissez-le de nouveau pour le reposer. modes choisit les outils de dessin (point, linestring, polygon, circle) ; la sélection et la suppression sont toujours là.
  • Terminez une ligne avec un clic sur son dernier point, et un polygone avec un clic sur son premier point. Avec l'outil de sélection, faites glisser une forme pour la déplacer, un sommet pour la remodeler, un point milieu pour ajouter un sommet, et faites un clic droit sur un sommet pour le retirer. Le bouton de suppression retire la forme sélectionnée.
  • Les touches fonctionnent quand le canevas de la carte a le focus : cliquez d'abord sur la carte. Pendant le dessin, Échap abandonne la forme en cours et Entrée termine une ligne, un polygone ou un cercle. Avec l'outil de sélection, Échap désélectionne, et Suppr ou Retour arrière retire la forme sélectionnée (les portables Mac n'ont pas de touche Suppr).
  • onValueChange (Vue : v-model) se déclenche quand une forme est terminée ou supprimée, et en direct, à chaque étape, pendant qu'on fait glisser une forme ou qu'on modifie un sommet, pas seulement à la fin du geste. Sélectionner une forme ne le déclenche pas. Appliquez un anti-rebond avant toute opération coûteuse, comme une sauvegarde.
  • La valeur est semi-contrôlée. Transmettre un nouvel objet au contenu différent remplace les formes. Laisser value tel quel, le même objet, ne ramène pas le dessin en arrière : pour annuler un changement, transmettez un nouvel objet. L'ordre des clés de l'objet n'a pas d'importance : une valeur passée par un schéma ou une base de données compte toujours comme l'écho de ce qu'on vous a communiqué. defaultValue part de formes sans les contrôler.
  • Chaque entité garde properties.mode, l'outil qui l'a dessinée, dont le dessin a besoin pour la modifier de nouveau ; un cercle est un polygone avec radiusKilometers. Vos propres propriétés et identifiants sont conservés.
  • Une valeur que vous transmettez peut omettre mode : il est déduit de la géométrie. Les positions sont réduites à la longitude et à la latitude : l'altitude d'une position 3D issue d'un GPS ou de KML est retirée au chargement, et les coordonnées sont arrondies à 9 décimales. Une géométrie que le dessin ne sait pas modifier, comme un MultiPolygon, est ignorée avec un avertissement dans la console, et elle est absente de la valeur qui vous revient : gardez ces formes dans votre propre état.
  • Les formes prennent votre --primary, bordées de votre --background ; color en fixe une autre. Elles passent au-dessus des étiquettes de la carte volontairement, pour que les poignées restent à portée. En Vue, l'outil actif est marqué par aria-pressed, dans vos jetons d'accent.
  • Un changement de mode, de style ou de thème garde chaque forme terminée et l'outil choisi ; une forme en cours de dessin est abandonnée. Pour deux dessins sur une même carte, donnez à chacun son propre id.
  • Draw répond aussi aux clics sur la carte : n'utilisez qu'un seul outil à la fois parmi draw, measure et feature-inspector.
  • Tant que draw est monté, le menu contextuel du navigateur est désactivé sur le canevas de la carte, parce que le dessin se sert du clic droit pour retirer un sommet.
  • Dessiner ne fait aucune requête : les formes restent dans la page jusqu'à ce que vous les envoyiez quelque part.
  • Le poids est d'environ 35 Ko compressés (gzip) pour terra-draw, réduit aux cinq modes par le tree-shaking, et d'environ 3 Ko pour son adaptateur MapLibre. Il se charge avec le composant : gardez draw pour les pages qui en ont besoin.
  • lang="fr" affiche la barre d'outils en français, et position accepte n'importe quel coin.

Comparer deux cartes

compare-swipe place deux cartes dans un même cadre avec un séparateur entre elles : avant et après, clair et sombre, ou un style contre un autre. Les deux cartes partagent une seule caméra : déplacer, zoomer, tourner ou incliner l'une entraîne l'autre.

import { CompareSwipe } from "@/components/ui/compare-swipe";
 
<CompareSwipe
  apiKey={key}
  center={[-123.12, 49.28]}
  zoom={12}
  before={{ style: "base", mode: "light" }}
  after={{ style: "base", mode: "dark" }}
  className="h-96"
/>

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-compare-01

Projets React. Installe components/unmap-map-compare.tsx ; affichez <MapCompare apiKey={key} />.

Ouvrir la page de l'exemple

La même chose en Vue :

<script setup lang="ts">
import { ref } from "vue";
import { UnmapCompareSwipe } from "@/components/ui/compare-swipe";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const position = ref(50);
</script>
 
<template>
  <UnmapCompareSwipe
    v-model:position="position"
    :api-key="apiKey"
    :center="[-123.12, 49.28]"
    :zoom="12"
    :before="{ mapStyle: 'base', mode: 'light' }"
    :after="{ mapStyle: 'base', mode: 'dark' }"
    class="h-96"
  />
</template>

Une couche qui appartient à un seul côté va dans les children de ce côté, avec le mapId de ce côté :

<CompareSwipe
  apiKey={key}
  center={[-123.12, 49.28]}
  zoom={12}
  after={{ children: <GeoJSONLayer mapId="after" id="parks" data={parks} /> }}
/>
  • before s'affiche à gauche du séparateur et after à droite ; orientation="horizontal" place before au-dessus et after en dessous.
  • before et after prennent les propriétés de la carte pour ce côté : style, mode, thème, superpositions, langue. La clé, la caméra (center, zoom, bearing, pitch) et les limites de la caméra se fixent une seule fois, sur la comparaison. Réglez les limites avec maxBounds, minZoom et maxZoom à cet endroit, pour que les deux cartes les partagent.
  • Une couche d'un seul côté va dans les children de ce côté (Vue : l'emplacement #before ou #after), et elle a besoin de l'identifiant de la carte de ce côté : mapId="before" ou mapId="after" (Vue : map-id). Sans lui, la couche cherche la carte par défaut, qui n'est aucune des deux. beforeId et afterId renomment les deux cartes, et la couche prend le nouveau nom.
  • Le séparateur est un séparateur de fenêtre : faites-le glisser, ou donnez-lui le focus et utilisez les flèches (5 % par pression, step pour changer), Début et Fin. position et onPositionChange (Vue : v-model:position) le contrôlent, et defaultPosition fixe son point de départ.
  • Pour dessiner d'un seul côté, montez draw dans les children de ce côté, avec le mapId de ce côté. Du côté after, placez-le dans un coin de droite (position="bottom-right" ; les boutons de zoom occupent le coin supérieur droit), pour que le séparateur ne passe pas sur la barre d'outils en se déplaçant.
  • onViewportChange (Vue : @viewport-change) signale la caméra partagée une fois par mouvement.
  • Les boutons de zoom et de boussole s'affichent sur la carte qui occupe le coin supérieur droit : la carte after, ou la carte before en mode horizontal. Donnez à l'autre côté ses propres controls pour les y afficher aussi.
  • Le cadre fait au moins 320 px de haut. Réglez sa hauteur avec className (h-96), ou avec containerStyle en React et style en Vue, qui peuvent aussi fixer minHeight.
  • Le centre, le zoom, l'orientation et l'inclinaison sont synchronisés ; le roulis ne l'est pas.
  • Pincer avec un doigt de chaque côté du séparateur n'est pas pris en charge, parce que chaque carte ne voit qu'un doigt et qu'elles tirent en sens contraire. Pincez d'un seul côté. Un nouveau geste sur un côté arrête l'élan de l'autre côté, si bien que les deux ne se contrarient jamais.
  • Chaque carte charge son propre style et ses propres tuiles, et chaque tuile est une requête : une comparaison fait environ deux fois les requêtes d'une seule carte, et elle est facturée à la requête comme le reste.
  • lang="fr" nomme le séparateur en français pour les lecteurs d'écran.

Profil d'altitude

elevation-profile trace l'altitude du sol le long d'une ligne sur votre Chart shadcn : un itinéraire de @unmap/routing ou du panneau d'itinéraire, une ligne tracée à la main, ou vos propres coordonnées. Il échantillonne la ligne tous les interval mètres le long du grand cercle, demande les hauteurs à l'API d'altitude unmap, et affiche la distance, le dénivelé positif et négatif, le point le plus bas et le plus haut. Survolez le graphique et un repère suit la ligne sur la carte ; survolez la ligne et le graphique suit.

L'exemple ci-dessous calcule une sortie à vélo près de Canmore et fait flotter le profil dans votre Card au bas de la carte dès qu'il y a un itinéraire. Sur un téléphone, les deux cartes occupent toute la largeur ; à partir de sm, le formulaire d'itinéraire se place en haut à gauche et le profil en bas à droite, sans cacher l'attribution de la carte.

"use client";
 
import { useState } from "react";
import type { LngLat } from "@unmap/routing";
import { Card, CardContent } from "@/components/ui/card";
import { Map } from "@/components/ui/map";
import { RoutingPanel } from "@/components/ui/routing-panel";
import { ElevationProfile } from "@/components/ui/elevation-profile";
 
export function RideProfile() {
  const apiKey = process.env.NEXT_PUBLIC_UNMAP_KEY!;
  const [route, setRoute] = useState<LngLat[] | null>(null);
  return (
    <div className="h-[640px]">
      <Map apiKey={apiKey} style="outdoor" center={[-115.57, 51.18]} zoom={10}>
        <Card className="absolute inset-x-4 top-4 z-10 sm:inset-x-auto sm:top-6 sm:left-6 sm:w-80">
          <CardContent>
            <RoutingPanel apiKey={apiKey} mode="bicycle" onRoute={(next) => setRoute(next.coordinates)} />
          </CardContent>
        </Card>
        {route ? (
          <Card className="absolute inset-x-4 bottom-10 z-10 sm:inset-x-auto sm:right-6 sm:w-[28rem] motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-4 motion-safe:duration-200">
            <CardContent>
              <ElevationProfile apiKey={apiKey} line={route} />
            </CardContent>
          </Card>
        ) : null}
      </Map>
    </div>
  );
}

Essayez-le, puis ajoutez-le comme point de départ :

npx @unmap/cli add map-elevation-01

Projets React. Installe components/unmap-map-elevation.tsx ; affichez <MapElevation apiKey={key} />.

Ouvrir la page de l'exemple
  • line accepte des paires [lng, lat], une LineString GeoJSON, une Feature qui en contient une, ou une FeatureCollection comme la value de l'outil de dessin, dont la ligne la plus récente est profilée (les points et les polygones à côté sont ignorés). Toute autre valeur, ou moins de deux points, affiche une invite et n'envoie rien.
  • Chaque requête compte pour un appel et porte jusqu'à 500 points. L'espacement par défaut place au plus 500 échantillons sur la ligne et ne descend jamais sous 30 m, une limite d'espacement du composant indépendante de la résolution native du MNA : un profil coûte donc un appel. Un interval plus petit coûte un appel par tranche de 500 échantillons, et une ligne n'est jamais échantillonnée plus de 5 000 fois, soit 10 appels au plus. Chaque requête est facturée, y compris celle qu'on annule. Les nouvelles tentatives automatiques du client sont désactivées : une requête en échec n'est jamais renvoyée à votre insu. Le bouton Réessayer est le seul moyen de la renvoyer, et chaque clic compte pour un appel.
  • Une ligne qui change sans cesse, comme un sommet qu'on déplace, n'est envoyée qu'une fois immobile pendant 300 ms : un déplacement ne déclenche pas une requête à chaque mouvement. Une nouvelle ligne annule la requête en cours, et la même ligne passée de nouveau, même dans un nouveau tableau, n'envoie rien.
  • Pour afficher de nouveau une ligne sans la payer une deuxième fois, passez cache, par exemple une new Map() que vous conservez (Vue : :cache). Une ligne dont les échantillons s'y trouvent affiche son profil tout de suite, sans requête ni attente, et chaque profil obtenu y est ajouté. Videz-le quand la clé change.
  • Les hauteurs sont en mètres dans le datum du MNA actif. Avec la publication mondiale 6, les datums et modèles de terrain/surface sont mixtes. Là où un échantillon tombe hors de la zone couverte, le graphique laisse un vide au lieu de tracer zéro, et le dénivelé ne compte que les échantillons qui ont une hauteur. Une ligne sans aucune couverture affiche l'état « aucune donnée d'altitude ».
  • Le dénivelé positif et négatif est la somme brute des montées et des descentes entre échantillons consécutifs : il peut sembler élevé sur un terrain accidenté. Une défaillance affiche une alerte à la place du graphique : une clé refusée par l'API, une limite de débit, une ligne trop grande pour être envoyée, un service d'altitude qui n'a pas répondu, ou une requête rejetée par l'API. Une limite de débit ou une requête sans réponse offre un bouton Réessayer ; les autres cas n'en offrent pas, car renvoyer la même requête n'y changerait rien.
  • Le dénivelé positif est le chiffre principal, avec la distance, le dénivelé négatif, le point le plus bas et le plus haut à côté. Pendant le chargement d'une nouvelle ligne, le profil précédent reste affiché, estompé, et le premier chargement réserve la place du résultat : le panneau ne saute pas. Une ligne de source sous le graphique renvoie aux mentions et licences des fournisseurs du MNA actif ; laissez-la visible. Les lecteurs d'écran ignorent le graphique, lisent les statistiques et annoncent la distance et le dénivelé positif à l'arrivée d'un profil.
  • lang="fr" passe les libellés et les nombres en français. onProfile reçoit les échantillons et les statistiques une fois chargés (null s'il n'y en a pas), par exemple pour colorer une route-line selon l'altitude avec son gradient ; un échantillon hors de la zone couverte a une altitude null, alors ignorez-le ou comblez-le. onHover reçoit l'échantillon sous le pointeur.
  • Il n'ajoute qu'une ligne invisible de 16 px à survoler, et le repère, au-dessus de la carte : tracez la ligne elle-même avec route-line, le panneau d'itinéraire ou votre outil de dessin. Les deux reviennent après un changement de style. Le repère prend votre --primary (ou la couleur d'itinéraire du thème) ; le graphique prend --chart-1. Le graphique est votre chart shadcn, soit Recharts en React : installer l'élément apporte recharts 3, alors un projet dont le chart.tsx est encore sur Recharts 2 doit d'abord mettre son graphique à jour.
  • Le panneau d'itinéraire transmet chaque itinéraire qu'il trace à onRoute (Vue : @route), et c'est ainsi que l'exemple ci-dessus alimente le profil.

En Vue, le graphique est le chart de shadcn-vue, qui dessine avec Unovis : ajouter l'élément installe donc aussi les paquets d'Unovis. @profile et @hover sont les événements de onProfile et onHover.

<script setup lang="ts">
import { ref } from "vue";
import type { LngLat } from "@unmap/routing";
import { Card, CardContent } from "@/components/ui/card";
import { UnmapMap } from "@/components/ui/map";
import { UnmapRoutingPanel } from "@/components/ui/routing-panel";
import { UnmapElevationProfile } from "@/components/ui/elevation-profile";
 
const apiKey = import.meta.env.VITE_UNMAP_KEY;
const route = ref<LngLat[] | null>(null);
</script>
 
<template>
  <div class="h-[640px]">
    <UnmapMap :api-key="apiKey" map-style="outdoor" :center="[-115.57, 51.18]" :zoom="10">
      <Card class="absolute inset-x-4 top-4 z-10 sm:inset-x-auto sm:top-6 sm:left-6 sm:w-80">
        <CardContent>
          <UnmapRoutingPanel :api-key="apiKey" mode="bicycle" @route="route = $event.coordinates" />
        </CardContent>
      </Card>
      <Card
        v-if="route"
        class="absolute inset-x-4 bottom-10 z-10 sm:inset-x-auto sm:right-6 sm:w-[28rem] motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-4 motion-safe:duration-200"
      >
        <CardContent>
          <UnmapElevationProfile :api-key="apiKey" :line="route" />
        </CardContent>
      </Card>
    </UnmapMap>
  </div>
</template>