Get started
Show a map

Edit an itinerary on the map

Edit an itinerary on the map

MapView draws a map. EditorMapView draws the map an itinerary is edited on. It owns the MapClient the studio's tools dispatch against. useMapLanguage and useTilesAccessRequired ship beside it. It is an opt-in subpath, @mountaya/elements-react/editor, because it brings a renderer and a drawing engine.

Mount the editor map

onClientReady is the one required prop. Everything a studio dispatches goes through the MapClient it hands back. A map that never reports one is a map no tool can reach, and onError and onAccessRequired are where you hear why. Both are attached as the map is built, before any client is handed out. onAccessRequired carries the status and the address of a 401, a 402 or a 403 on a tile, a sprite, a glyph or a style, and onError carries every other failure the map reports.

import { EditorMapView } from "@mountaya/elements-react/editor";

<EditorMapView
  ariaLabel="Route editor"
  client={client}
  onClientReady={holdMapClient}
  onMapReady={holdMap}
/>;

client is the SDK client the map fetches its specifications through, so pass the instance you already cache. ariaLabel names the container as a region landmark, and the localized string is yours. Markers draw from the provider's markerIcons, so an editor inside MountayaElementsProvider matches its chrome, and a markerIcons of the map's own wins over it. The camera is kept in the page's #view hash only when you set withUrlHash.

ViewerMapView is the view-only twin. It takes the same props and hands back a MapClient that draws, moves the camera and records, and refuses every edit, for a map that shows an itinerary a reader cannot change.

Add MapLibre's stylesheet

import "maplibre-gl/dist/maplibre-gl.css";

This package cannot pull it in for you. maplibre-gl is a peer brought by @mountaya/elements, so the copy to name is the one npm resolved for you. Add the line once at your entry point.

Skipping it throws nothing and blanks nothing. What breaks is everything MapLibre draws in DOM. The attribution credit disappears and popups lose their frame. Markers anchor from their top-left corner, so waypoints sit visibly off the line.

Attach a video recorder

A studio that records a flight and one that does not are the same map. What differs is whether anything is attached, so video takes a factory rather than a boolean. Pass nothing and no recorder is constructed.

<EditorMapView client={client} onClientReady={holdMapClient} video={attachRecorder} />;

The factory receives the MountayaMap, whose raw and events are public, so a recorder needs nothing past the published surface.

The hooks that keep a studio in step

These are @mountaya/studio-react/map's, except useMapLanguage(client, language) and useTilesAccessRequired, which ship with this entry. Each studio hook takes the map client, or null while none is mounted, and reads everything else from one context, StudioMapContext, under the same StudioProvider the studio's surfaces read. The layers a studio draws its itineraries and collections with are the studio's own, mounted for you.

useStudioMapBindings(client, options?) binds a map with every one of them, in the order MountayaStudio mounts them on its own map: call it once per map. Its options carry the answers only you have: the DEM, the URL seam, the camera limits and the collaboration sessions. Each hook below stays published for a map that binds less. One that draws the itinerary and takes no edits mounts the selection and the sync alone.

Keeps in stepHooks
The map's own pictureuseStudioMapSync, useMapLanguage, useStudioOverlay, useStudioEnrichment, useStudioPoiLayers, useStudioTimestamp
What the reader points atuseStudioItinerarySelection, useStudioZoneSelection, useStudioContextMenu
EditinguseStudioSegmentEditing, useStudioWaypointEditing, useStudioZoneEditing, useStudioSplitPicking, useStudioZoneOriginPicking, useStudioSegmentViaPrompt, useStudioSuggestionPicking
CollaborationuseStudioPeerDrafts, PeerNameResolver
GatesuseTilesAccessRequired

useStudioPeerDrafts paints what a peer is dragging right now, throttled at LIVE_SEGMENT_THROTTLE_MS. A refused dispatch reaches the reader through useStudioFailureAlert of ./headless. StudioSeams and PeerDraftSink are the two interfaces a collaborative host fills in. Both are optional, so a studio mounted with neither still edits and mirrors nothing.

Put draggable handles on the map

The handles are drawn by the map client EditorMapView builds, MapLibreClient from @mountaya/elements/editor. That entry is the one part of the package that needs a drawing library. terra-draw, terra-draw-maplibre-gl-adapter and @maplibre/maplibre-gl-directions are optional peers, reachable through it and no other entry. An application that only renders a map never installs them. Install the SDK has the matrix.

MapClient verbDraws
beginSegmentEditA segment's line, freehand or routed
beginWaypointEditA waypoint, dropped or picked off a POI
beginZoneEditA zone: circle, sector, sensor, polygon or freehand
beginBoundaryEditA rectangular boundary box

The first three have a published session and published rules waiting in @mountaya/studio. The boundary edit has neither, and a host reads its box off onBoundaryDraftChange.

Open an edit session

A session is store state, and arming an edit turns that state into handles on a canvas. The store is the one every studio surface reads. A draft reaches the list, the profile and the table. None of them knows an engine exists.

const stopListening = mapClient.onSegmentDraftChange((draft) => {
  if (draft.phase === "settled") {
    keepVias(draft.vias);
  }
});

mapClient.beginSegmentEdit({ color: "#e9501e", featureId: segmentId, mode: "linestring" });

// Later, when the edit ends.
mapClient.endSegmentEdit();
stopListening();

A draft arrives live for each frame of a drag and settled once the drag ends, so a host that records one undo step per gesture reads the settled ones. A colour is #rrggbb or #rgb, and anything else is refused before the edit opens.

Route a segment as it is drawn

SegmentRouteFn is the socket a routed segment routes through. It is injected when the session opens, so an engine routes without knowing what routes. It takes the activity slug, the vias and a signal, and answers in the shape the engine renders: the routed line, its distance and duration, and the analysis when the router computed it. It answers null when no route forms and SEGMENT_ROUTE_REFUSED when the reader declined the route, which the engine renders as a reverted via.

The studio routes through its own, over the Data API's directions query. A host that opens a session on an EditorMapView of its own passes the router it has. SegmentAnalyzeFn is the freehand twin: it reads the drawn line as it is, and answers it with each point's altitude and the analysis the profile draws.

Test against a MapClient double

A suite driving these hooks needs a MapClient double, and two ship from the package that declares the interface.

import { createMapClientSpy, createMapClientStub } from "@mountaya/elements/testing";

createMapClientStub no-ops every command and returns the neutral empty every reader's type demands. createMapClientSpy wraps each method in a spy factory you supply, so it names no test runner. Both are full object literals. A method added to the interface fails to compile here rather than going missing at runtime. Testing covers the rest.

Next steps