@la-trace/map-sdk
v0.1.1
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.
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.
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.
