@la-trace/map-sdk
v0.4.0
Published
LaTrace cartography SDK — drop-in MapLibre-based map with LaTrace basemaps, smart pins and tracks.
Readme
@la-trace/map-sdk
Drop-in interactive map for LaTrace integrations: ship the same basemaps, typed POI markers, tracks and shapes the LaTrace product uses, in any web app, in a few lines.
Built on top of MapLibre GL JS. Framework-agnostic — works in vanilla, React, Vue, Svelte, Angular.
Two entry points
This package ships two independent APIs; pick the one that matches your need.
| | createLaTraceMap | createLaTraceExplore |
|---|---|---|
| What | A MapLibre map with La Trace basemaps, typed pins, tracks & shapes that you drive yourself. | The full La Trace Explore experience (map + search + filters + POI panels) embedded in an isolated iframe. |
| You provide | Pins/tracks/shapes, camera calls, your own UI around it. | POIs (setPois) + config overrides; the SDK renders the whole UX and emits events. |
| Peer dep | maplibre-gl (rendered in your page). | None (rendering happens inside the iframe). |
| style.css | Required (import '@la-trace/map-sdk/style.css'). | Not needed (styles live inside the iframe). |
createLaTraceMap is documented first (below); createLaTraceExplore has its
own section further down. Thanks to "sideEffects": false, importing only one
of them tree-shakes the other out of your bundle.
Features
- 3 LaTrace basemaps out of the box: Plan, Satellite, Relief — MapTiler-backed, MapLibre-rendered.
- Smart pin layer: collision-aware marker placement + density-sampled circles below the marker zoom threshold. Same algorithm as the LaTrace product.
- 179 LaTrace POI categories baked-in (Castle, Restaurant, Bakery, Beach, …) with the LaTrace droplet design and the matching color palette.
- Tracks for GPX-like content (LineString / MultiLineString / GeoJSON Features), with hover & click events.
- Shapes for zones / overlays (polygon, rectangle, circle by radius), interactive.
- 3D terrain toggle (MapTiler RGB DEM), one method call.
- Active POIs: mark a pin as selected — the SDK promotes it to a marker, runs an emphasized animation, and bypasses collision so it never disappears.
- Built-in MapNav control matching the LaTrace UI (zoom group, basemap picker with previews, 3D, GPS, fullscreen). Each section is individually toggleable, or you can drop the whole nav.
- Mobile bottom-sheet for the basemap picker, portaled into
document.bodyso it always sits above your app's overlays. - Typed events for everything (clicks, hovers, camera, basemap, terrain, active pins).
Install
npm install @la-trace/map-sdk maplibre-gl
# or pnpm add @la-trace/map-sdk maplibre-gl
# or yarn add @la-trace/map-sdk maplibre-glmaplibre-gl is a peer dependency.
Quick start
<div id="map" style="width: 100%; height: 480px;"></div>import '@la-trace/map-sdk/style.css';
import { createLaTraceMap, laTracePoiPin } from '@la-trace/map-sdk';
const map = createLaTraceMap({
container: '#map',
apiKey: 'your-latrace-api-key',
basemap: 'plan',
center: [2.3522, 48.8566],
zoom: 5,
});
map.on('ready', () => {
map.addPin(
laTracePoiPin({
id: 'eiffel',
lat: 48.8584,
lng: 2.2945,
label: 'Tour Eiffel',
type: 'Monument',
score: 100,
}),
);
});
map.on('pin:click', ({ pin }) => {
console.log('Clicked', pin.id);
map.setActivePins([pin.id]);
});The bundled stylesheet already embeds maplibre-gl.css, so the import
above is the only one you need.
API reference
createLaTraceMap(options)
| Option | Type | Default | Notes |
|---|---|---|---|
| container | string \| HTMLElement | required | CSS selector, element id, or DOM node. |
| apiKey | string | required | Your LaTrace API key (pass-through for now, future-gated). |
| basemap | 'plan' \| 'satellite' \| 'topo' | 'plan' | Initial style. |
| center | [lng, lat] | [2.3522, 48.8566] | Initial center. |
| zoom | number | 5 | Initial zoom. |
| minZoom / maxZoom | number | — | Optional clamps. |
| pins | PinClusteringOptions | see below | Marker clustering tuning. |
| mapNav | false \| MapNavOptions | {} (all on) | Built-in nav control config. |
PinClusteringOptions
| Field | Default | Notes |
|---|---|---|
| markerExclusionRadiusPx | 180 | Half-size of the no-collision square around each marker. |
| minMarkerZoom | 4.5 | Below this zoom, every pin is a circle (active pins still render as markers). |
| maxCirclePoints | 1500 | Cap on circles drawn after density sampling. |
| circleColor | '#0D1D27' | Default circle fill. Per-pin override via pin.circleColor. |
MapNavOptions
| Field | Default | Notes |
|---|---|---|
| fullscreen | true | Show the fullscreen button. |
| stylePicker | true | Show the basemap picker. |
| terrain3d | true | Show the 3D toggle. |
| gps | true | Show the GPS button. |
| zoom | true | Show the zoom group. |
| position | 'bottom-right' | MapLibre ControlPosition. |
LaTraceMap instance
// Pins
map.addPin(pin);
map.addPins(pins);
map.removePin(id);
map.clearPins();
map.setPinOptions({ circleColor: '#15803d' });
map.addLaTracePin({ id, lat, lng, type: 'Castle' });
// Active selection (force-promote to marker + emphasized variant)
map.setActivePin(id);
map.setActivePins([id1, id2]);
map.unsetActivePin(id);
map.clearActivePins();
map.getActivePins();
// Tracks (GeoJSON LineString / MultiLineString / Feature / FeatureCollection)
map.addTrack({ id, geometry, color, width, opacity });
map.updateTrack({ id, geometry, ... });
map.removeTrack(id);
map.clearTracks();
// Shapes
map.addShape({ kind: 'rectangle', id, sw, ne, fillColor, ... });
map.addShape({ kind: 'circle', id, center, radiusMeters, ... });
map.addShape({ kind: 'polygon', id, rings, ... });
map.updateShape(shape);
map.removeShape(id);
map.clearShapes();
// Camera
map.flyTo({ center, zoom, duration });
map.fitBounds([[w, s], [e, n]], padding);
map.setBasemap('satellite');
map.getBasemap();
map.setTerrain3d(true);
map.getTerrain3d();
map.toggleTerrain3d();
// Geolocation (drives MapLibre's GeolocateControl, blue pulsing dot)
map.triggerGeolocate();
// Lifecycle
map.destroy();
// Escape hatch — the underlying maplibregl.Map
map.raw;Events
map.on('ready', () => {});
map.on('pin:click', ({ pin, lngLat }) => {});
map.on('pin:mouseenter', ({ pin, lngLat }) => {});
map.on('pin:mouseleave', ({ pin, lngLat }) => {});
map.on('track:click', ({ track, lngLat }) => {});
map.on('track:mouseenter', ({ track, lngLat }) => {});
map.on('track:mouseleave', ({ track, lngLat }) => {});
map.on('shape:click', ({ shape, lngLat }) => {});
map.on('shape:mouseenter', ({ shape, lngLat }) => {});
map.on('shape:mouseleave', ({ shape, lngLat }) => {});
map.on('map:click', ({ lngLat, point }) => {});
map.on('map:move', (snapshot) => {}); // continuous
map.on('map:moveend', (snapshot) => {}); // settled
map.on('map:zoom', ({ zoom }) => {});
map.on('map:zoomend', ({ zoom }) => {});
map.on('basemap:change', ({ basemap }) => {});
map.on('terrain3d:change', ({ enabled }) => {});
map.on('activepins:change', ({ ids }) => {});MapCameraSnapshot (passed to map:move / map:moveend):
{ center: [lng, lat], zoom, bearing, pitch, bounds: { sw: [lng, lat], ne: [lng, lat] } }map.on(event, handler) returns an unsubscribe function. map.off(event, handler) works too.
Pin shape
interface Pin {
id: string; // stable, used for events / updates / removal
lat: number;
lng: number;
score?: number; // higher = wins collision priority
label?: string; // echoed back in events; SDK does not render it
circleColor?: string;
markerHtml?: string; // see security note below
markerHtmlEmphasized?: string;
anchor?: 'center' | 'top' | 'bottom' | 'left' | 'right';
data?: unknown; // arbitrary, echoed back
}A pin without markerHtml stays a circle forever — even if you call
setActivePin on it. To get the LaTrace droplet automatically, build the
pin via laTracePoiPin({ type }):
import { laTracePoiPin } from '@la-trace/map-sdk';
const pin = laTracePoiPin({
id: 'p1', lat: 48.85, lng: 2.34, score: 90,
type: 'Restaurant', // one of 179 LaTrace categories
});type is a LaTracePoiType string union — your editor will autocomplete
the full list.
La Trace Explore (createLaTraceExplore)
The Explore API embeds the complete La Trace Explore experience (map, search, filters, POI panels) into any web page or native webview. You mount it in a container, push your POIs and config overrides; the SDK renders the UX inside an isolated iframe and emits typed events back to your page.
Key model:
- Isolated iframe + typed bridge. The SDK mounts a La Trace app iframe and
exposes a typed JS API over a
postMessagebridge. You never write rawpostMessage. - Zero-storage. La Trace stores none of your POI data. You push it
(
setPois); search and filters run over that corpus in memory. - Config = base + overrides. The base config (theming, sections, wording,
rights) is resolved server-side from
configId. You only push targetedConfigOverrides on top. - No
style.css. UnlikecreateLaTraceMap, Explore needs no stylesheet import; all styling lives inside the iframe.
Install
npm install @la-trace/map-sdk
# maplibre-gl is NOT needed for createLaTraceExploreQuick start
<div id="map" style="width: 100%; height: 100vh;"></div>import { createLaTraceExplore } from '@la-trace/map-sdk';
// No stylesheet import needed for Explore.
const explore = createLaTraceExplore({
container: '#map',
apiKey: 'pk_live_xxx', // publishable key (pk_live_* / pk_test_*)
configId: '9c2f98f1-394a-41d9-a7ba-99de654b7e6d',
locale: 'fr',
config: { poiDetailMode: 'externalPreview' },
});
// createLaTraceExplore returns synchronously; the map is ready after `ready()`.
await explore.ready();
const result = explore.setPois(pois); // see PushResult below
if (result.rejected.length) console.warn('rejected POIs', result.rejected);
explore.on('pin:click', ({ poi }) => console.log('clicked', poi.id));ExploreOptions (required: container, apiKey, configId):
| Option | Type | Notes |
|---|---|---|
| container | string \| HTMLElement | CSS selector, id, or DOM node. |
| apiKey | string | Publishable key. Pass-through for now, server-validated later. |
| configId | string | Base config + stats key. |
| config | ConfigOverride | Targeted overrides (see Theming below). |
| locale | 'fr' \| 'en' \| 'nl' | Precedence: option > config.locale > browser > 'fr'. |
| initialView | { center?, zoom?, bbox? } | center is [lng, lat], bbox is [w, s, e, n]. |
| pois | Poi[] | POIs at boot (same as calling setPois after ready). |
| initialFilters | ActiveFilters | Pre-filtering at boot. |
| initialFavorites | string[] | Pre-filled hearts (rehydration). |
| state | ExploreState | Full state restore (deep-link). |
| allow | string[] | iframe permissions. Default ['geolocation','fullscreen']. |
Pushing POIs
You own the corpus; the SDK never stores it. Push methods return a
PushResult, so you always know what was accepted vs. rejected.
interface PushResult {
accepted: number;
rejected: Array<{ id?: string; reason: PushRejectReason }>;
}
type PushRejectReason =
| 'missing_id' | 'duplicate_id' | 'invalid_coords'
| 'unknown_category' | 'invalid_field';An invalid POI is rejected (never rendered wrong) and reported both in
PushResult.rejected and via the pois:rejected event. The rest of the batch
is still accepted. unknown_category is a warning: the POI is accepted with
a neutral marker but still listed in rejected.
A minimal Poi (see the full shape in the exported Poi type):
explore.setPois([
{
id: 'r-42', // opaque host id, the key everywhere (required)
coords: [2.3611, 48.8674], // [lng, lat] WGS84 (required)
category: 'restaurant', // mapped to a La Trace PoiType (required)
name: 'Chez X', // string or { fr, en, nl }
priceRange: '€€',
externalUrl: 'https://host.example/chez-x',
},
]);Push methods:
| Method | Effect |
|---|---|
| setPois(pois) | Replaces the whole displayed set. Diffed by id (no flicker). |
| addPois(pois) | Upsert by id. |
| updatePoi(poi) | Full replacement of one POI by id (not a partial patch). |
| removePois(ids) | Remove by id. |
| clearPois() | Remove all. |
| setLoading(loading) | Show the native skeleton while you fetch. |
Recommended corpus size: ≤ ~5 000 POIs. Beyond that, push by zone
(addPois/removePois on viewport:change / search:area).
"Search this area" is yours
In pushed-corpus mode the SDK never searches by area itself. When the user
clicks "Search this area", the map only reports the current viewport bbox via the
search:area event. You query your own backend and re-push:
explore.on('search:area', async ({ bbox }) => {
explore.setLoading(true);
try {
const pois = await myBackend.search(bbox); // [west, south, east, north]
explore.setPois(pois); // REPLACES the corpus
} finally {
explore.setLoading(false); // always, even on error
}
});setPois replaces the corpus (POIs absent from the list disappear);
addPois adds to it. The map does not recenter after a push — the user chose
that area, so the camera stays put. Call fitBounds(bbox) to recenter explicitly.
Instance methods
// Lifecycle
explore.ready(); // Promise<void>
explore.resize(); // force a resize if the container changed outside ResizeObserver
explore.destroy();
// Live config
explore.setConfigOverride(partialConfig);
explore.setLocale('en');
// Camera
explore.flyTo({ center: [lng, lat], zoom }, { durationMs: 600 });
explore.fitBounds([w, s, e, n], { padding: 40 });
explore.setCenter([lng, lat]);
explore.setZoom(12);
explore.getViewport(); // last-known cache, refreshed by `viewport:change` (not real-time)
// Selection / panels
explore.openPoi(poiId);
explore.closePanel();
explore.highlightPoi(poiId); // null to clear
// Search & filters
explore.search(query);
explore.clearSearch();
explore.setActiveFilters(filters);
explore.clearFilters();
explore.setRecentSearches(items);
// Favorites
explore.setFavorites(ids); // rehydrate full hearts
// State (deep-link / restore)
explore.getState(); // last-known cache
explore.applyState(state);
// Native / webview geolocation
explore.setUserLocation({ lng, lat, accuracy }); // null to clear
// Events
explore.on(event, handler);
explore.off(event, handler);Getters (
getViewport,getState) return the last-known cached state refreshed by events over the async bridge, not a real-time read. For a guaranteed-fresh value, listen to the matching event.
Events
explore.on('ready', () => {});
explore.on('pin:click', ({ poi }) => {});| Event | Payload | Usage |
|---|---|---|
| ready | {} | Init finished. |
| error | { code, message } | Auth / network / config / render. |
| pin:click | { poi } | Marker clicked. |
| pin:hover | { poiId \| null } | Map hover (enter/leave). |
| poi:view | { poiId } | Panel / preview shown. |
| gallery:open | { poiId } | Gallery opened. |
| pois:rejected | { rejected[] } | Invalid POIs at push time. |
| preview:open | { poiId } | externalPreview preview opened. |
| preview:close | { poiId, navigated } | Preview closed (dismiss vs. navigation). |
| external:open | { poiId, url } | Navigate to external page. |
| reservation:click | { poiId, url } | Reservation CTA. |
| search:query | { query } | Debounced typing. |
| search:submit | { query, resultCount } | Search submitted. |
| search:area | { bbox } | "Search this area" — you query and re-push (see below). |
| results:empty | { context } | No results ('search' \| 'filters' \| 'viewport'). |
| filter:change | { filters } | Active filters changed. |
| viewport:change | Viewport | Pan/zoom settled. |
| basemap:change | { basemap } | Basemap changed. |
| geolocate | { coords } | "My location". |
| state:change | ExploreState | State mutation (sync your URL). |
| back | {} | Back triggered inside the iframe (mobile). |
| favorite:toggle | { poiId, favorited } | Persist favorite host-side. |
| locale:change | { locale } | Language switch (re-push translated corpus if needed). |
Every poiId payload uses the id you pushed (pass-through), so events
wire straight into your analytics.
Theming & config overrides
config (at init) and setConfigOverride(...) (live) merge targeted overrides
on top of the base config resolved from configId. You only push what you
override.
const explore = createLaTraceExplore({
container: '#map',
apiKey: 'pk_live_xxx',
configId: '9c2f98f1-...',
config: {
poiDetailMode: 'externalPreview', // 'panel' | 'externalPreview'
theme: { // CSS tokens --lt-*
'--lt-action-primary-surface-default': '#FFDD33',
'--radius-md': '0',
},
fonts: { headings: { family: 'DIN 1451', url: 'https://cdn.example/din.woff2' } },
poiColors: { // SINGLE source for markers + filters
restaurant: { background: '#FFF3B0', text: '#FFDD33' },
wineshop: { background: '#E7DCEE', text: '#774192' },
},
poiIcons: { // custom marker logos, replacing the glyph
Restaurant: 'https://cdn.example/pins/resto.svg', // key = PoiType (exact case)
wineshop: 'https://cdn.example/pins/cave.png', // key = host category (case-insensitive)
},
wording: { // relabels the result counter
poiNounSingle: 'Résultat',
poiNounPlural: 'Résultats',
},
filters: [
{ key: 'category', label: "Type", type: 'category', options: [
{ value: 'restaurant', label: 'Restaurants', color: '#FFDD33' },
{ value: 'bar', label: 'Bars' },
] },
{ key: 'terrasse', label: 'Terrasse', type: 'toggle', group: 'essentials' },
],
search: { placeholder: 'OÙ ALLEZ-VOUS ?', geocoding: true, recentSearches: true },
favorites: { mode: 'host' }, // 'off' | 'stateless' | 'host'
units: 'metric', // 'metric' | 'imperial'
},
});Notable ConfigOverride fields: theme (--lt-* tokens), fonts,
poiColors (single source for marker + filter colors), poiIcons, filters,
sections, poiDetailMode, wording, ads, mapNav, ui (preset: 'bare'
for a naked map), basemaps, search, favorites, units, locale. See the
exported ConfigOverride type for the full shape.
poiIcons replaces the glyph at the centre of the marker with your own logo;
the pin's shape and colour stay driven by poiColors. Precedence:
poiType > category > La Trace glyph. Accepted values: an https URL (SVG or
PNG) or a data:image/svg+xml URI — a data URI of any other type is rejected,
as is http:. A rejected or broken URL silently falls back to the glyph, never an
empty pin.
wording.poiNounSingle / poiNounPlural relabel the result counter
("1 Résultat" / "128 Résultats"). The map picks the form from the total (plural
from 2); the fallback is key-by-key, so pushing only the plural leaves the
singular on La Trace i18n rather than printing "1 Résultats". These are raw
strings: re-push them on locale:change for a multilingual site.
Place search (createLaTraceGeocoder) — browser
Driving your own search bar (outside the Explore iframe)? Use the geocoder helper
instead of hand-rolling fetch + a prediction cache + bbox normalisation:
import { createLaTraceGeocoder } from '@la-trace/map-sdk';
const geocoder = createLaTraceGeocoder({
apiKey: 'pk_live_xxx',
apiBase: 'https://api.latrace.com/sdk/v1',
countries: 'fr,be', // optional — omit to let the server resolve scope from the key
});
const predictions = await geocoder.autocomplete('canal saint martin');
const location = await geocoder.geocode({ predictionId: predictions[0].id });
// location = { center: { lat, lng }, viewport?: { west, south, east, north }, formattedAddress }viewport is normalised to min/max internally (Photon can return the extent with
unordered corners). The Explore iframe already has its own internal search
(config.search.geocoding); this helper is only for a host-owned search UI.
Structured address — for a form that auto-fills city / department / region / country in separate fields, opt into the enriched response:
const geocoder = createLaTraceGeocoder({
apiKey: 'pk_live_xxx',
apiBase: 'https://api.latrace.com/sdk/v1',
fields: ['address'], // -> fills `location.address`
langs: ['fr', 'en', 'nl'], // -> resolves place names per locale
});
const location = await geocoder.geocode({ predictionId: predictions[0].id });
location.address?.city?.nl; // 'Gent'
location.address?.department?.fr; // 'Loire-Atlantique' (Photon `county`)
location.address?.postcode; // '9000' — NOT localized, a plain stringpostcode and countryCode are plain strings; city / department / region /
country are { fr?, en?, nl? } objects, and every locale key is optional. The
address travels through the prediction cache, so autocomplete → geocode costs
no second fetch.
nllimitation: our Photon index is builten,fr,de, so fornlwe return local names (Gent, Brussel) rather than true Dutch exonyms (Luik for Liège). Accurate for Flanders, inaccurate for Wallonia. Index rebuild pending.
Navigation deep-links (buildNavigationUrl)
Making an editorial static-map thumbnail clickable? buildNavigationUrl builds
the Google Maps URL that starts navigation to a POI. These are public Google
Maps URLs, not the Maps API: no key, no quota. The helper is pure (no fetch),
so it works server-side too.
import { buildNavigationUrl } from '@la-trace/map-sdk';
buildNavigationUrl(poi);
// -> 'https://www.google.com/maps/dir/?api=1&destination=48.8674%2C2.3611'
buildNavigationUrl({ lng: 2.3611, lat: 48.8674 }, { travelmode: 'walking' });<a href={buildNavigationUrl(poi)} target="_blank" rel="noopener">
<img src={staticMapSrc} alt={`Map — ${poi.name}`} />
</a>Google expects
destination=<lat>,<lng>— the reverse of the SDK's canonical[lng, lat]. The helper flips it for you; that inversion is the whole reason it exists. It never "fixes" a pair that looks swapped but is valid (48.8674is a legal longitude): the documented[lng, lat]order wins.
Static map images (signStaticMapUrl) — backend only
For editorial pages you can request a server-rendered map image with La Trace
markers and DA. An <img> cannot send an auth header, so the static-map
endpoint authenticates by query-param + a signed, expiring URL. Sign it on
your backend with the per-key signingSecret, then emit the resulting
<img src>.
// BACKEND ONLY — never import this in browser code.
import { signStaticMapUrl } from '@la-trace/map-sdk/static-map';
const url = signStaticMapUrl(process.env.LATRACE_SIGNING_SECRET, {
baseUrl: 'https://api.latrace.com/sdk/v1/static-map',
configId: '9c2f98f1-...',
key: 'pk_live_xxx',
center: [2.3611, 48.8674],
zoom: 15,
width: 640,
height: 360,
// 'lng,lat[,type[,color[,icon]]]' joined by ';', max 50. `type` is a La Trace
// PoiType (capitalised), `color` a hex fill, `icon` an https logo URL.
markers: '2.3611,48.8674,Restaurant,#c7fb0e',
scale: 2,
});
// -> <img src={url} />Warning: the
signingSecretis a real secret and is not the publishable key. It must stay on your backend, never ship to the browser, never bundle into client code. This helper lives on a separate entry point (@la-trace/map-sdk/static-map) and usesnode:cryptoprecisely so it cannot be pulled into a browser bundle by accident.
Security
HTML injection in custom markers
The SDK inserts pin.markerHtml, pin.markerHtmlEmphasized, and any
custom iconSvg you pass to renderLaTracePoiMarkerHtml via innerHTML.
If you build those strings from arbitrary user data, you are responsible
for escaping it.
The SDK exports an escapeHtml helper you can use for the common case:
import { escapeHtml } from '@la-trace/map-sdk';
const pin = {
id: place.id, lat: place.lat, lng: place.lng,
markerHtml: `<div class="poi">${escapeHtml(place.userSubmittedName)}</div>`,
};For richer untrusted content (HTML coming from a CMS, a third-party API, etc.), run it through your usual sanitizer (DOMPurify, sanitize-html, …) before handing it to the SDK.
MapTiler API key
The MapTiler API key used to fetch the LaTrace basemaps is embedded in the SDK bundle at build time. It is not a secret in the defense-in-depth sense — anyone inspecting the JavaScript can extract it — and that's expected for client-side maps. The key will be locked down to a list of LaTrace-approved origin domains via the MapTiler dashboard once integration partners are onboarded; until then, the same key is shared across all SDK consumers.
LaTrace API key (apiKey option)
The apiKey you pass to createLaTraceMap({ apiKey }) is stored but
not validated in this version. It will be checked server-side in a
future release, alongside the MapTiler-key origin whitelisting, so plan
your integration around providing a real key per environment now —
swapping it later won't require a code change on your side.
What is sent over the network
The SDK only talks to MapTiler (basemap tiles, optional terrain DEM tiles, optional geocoding) — no LaTrace endpoint is contacted by default. Anything you display via pins/tracks/shapes stays in the browser unless your own code uploads it elsewhere.
TypeScript
Everything is typed:
import type {
CreateLaTraceMapOptions,
Basemap,
Pin,
Track,
Shape,
PinClusteringOptions,
MapNavOptions,
LaTracePoiType,
MapCameraSnapshot,
// …
} from '@la-trace/map-sdk';The escape hatch map.raw is typed as maplibregl.Map.
Frameworks
The SDK is vanilla — no framework lock-in. Wrap it in whatever you use:
// React
import { useEffect, useRef } from 'react';
import { createLaTraceMap, type LaTraceMap } from '@la-trace/map-sdk';
export function Map({ apiKey }: { apiKey: string }) {
const ref = useRef<HTMLDivElement>(null);
const mapRef = useRef<LaTraceMap | null>(null);
useEffect(() => {
if (!ref.current) return;
const map = createLaTraceMap({ container: ref.current, apiKey });
mapRef.current = map;
return () => map.destroy();
}, [apiKey]);
return <div ref={ref} style={{ width: '100%', height: 480 }} />;
}Examples
Live demos covering the main integration patterns are maintained in a dedicated public repo: latrace-code/la-trace-sdk-examples.
| Example | Stack | What it shows |
|---|---|---|
| basic-vanilla | Vite + TS | Full-feature tour: typed POIs, custom markers, tracks, shapes, every event logged. |
| react-app | Vite + React + TS | Reusable <LaTraceMapView> component, sidebar-driven selection. |
| tracking-trip | Vite + TS | Animated GPX-style replay with follow-camera and scrubber. |
Clone the repo (or open one of the examples on StackBlitz / CodeSandbox
using the GitHub import URL) — each folder runs with pnpm install &&
pnpm dev, no monorepo setup required.
License
Proprietary — see LICENSE. Use is permitted as part of a LaTrace integration; redistribution requires a written agreement.
