Geolocation
A 100% self-hosted geolocation module: capture latitude/longitude through the browser API, compute distance/trajectory, and plot maps — with no paid or external API. The default map is a tile-free SVG plot (no tile images, no requests). It mirrors the geo module of tempest-fastapi-sdk, so types and math match between client and server.
Why tile-free?
Any image map (OpenStreetMap, Mapbox, Google) downloads tiles from an external server. That clashes with the "nothing external" requirement. So TrajectoryMap draws the projected trajectory in SVG (Web Mercator) with no background — zero requests. If you host your own tile server, you can enable the optional Leaflet layer by passing tileUrl.
When to use
- Capture the user's position (once or continuously) via
navigator.geolocation. - Record and display a trajectory (run, delivery, trail).
- Compute distance between points or estimate travel time offline.
- Plot everything without depending on a paid/external map.
Types — Coordinate, TrackPoint, TravelEstimate
Types mirror the FastAPI SDK schemas (snake_case preserved on TravelEstimate so a response deserializes directly):
import type { Coordinate, TrackPoint, TravelEstimate, TravelMode } from "tempest-react-sdk";
const sp: Coordinate = { latitude: -23.5505, longitude: -46.6333 };
// TrackPoint = Coordinate + capture time (+ optional accuracy)
const point: TrackPoint = { ...sp, timestamp: Date.now(), accuracy: 12 };
// TravelMode = "car" | "motorcycle" | "bus"
const mode: TravelMode = "car";
Utility validators: isValidLatitude, isValidLongitude, isCoordinate (type guard), clampLatitude, normalizeLongitude (antimeridian wrap).
Distance and trajectory — haversineKm, pathLengthKm, bearingDeg
Pure math, no network, identical to the server:
import { haversineKm, pathLengthKm, bearingDeg } from "tempest-react-sdk";
const sp = { latitude: -23.5505, longitude: -46.6333 };
const rio = { latitude: -22.9068, longitude: -43.1729 };
haversineKm(sp, rio); // ≈ 360.9 (km, great-circle)
// Total length of a trajectory (sum of legs):
pathLengthKm([sp, { latitude: -23.2, longitude: -45 }, rio]);
// Initial bearing (degrees, 0 = north, clockwise):
bearingDeg(sp, rio); // ≈ 65 (north-east)
Framing a set of points — boundingBox, boundsCenter, expandBounds
To center a map on a track, or decide the zoom that covers it:
import { boundingBox, boundsCenter, expandBounds } from "tempest-react-sdk";
const bounds = boundingBox([sp, rio]); // { north, south, east, west } | null
if (bounds) {
boundsCenter(bounds); // the midpoint, to center on
expandBounds(bounds, 0.1); // 10% of slack, so the route does not touch the edge
}
boundingBox returns null for an empty list — there is no frame around no
points, and returning a zeroed box would put the map in the Atlantic.
expandBounds exists because framing exactly on the extremes puts the first and
last point against the frame — the margin is what makes the path fit visually.
The raw math
haversineKm is the ready-made formula, but its two pieces come out on their
own for callers building a calculation of their own: EARTH_RADIUS_KM (6371,
the mean radius the formula assumes) and toRadians(degrees). And
unprojectMercator is projectMercator's counterpart — it converts screen
coordinates back to lat/long, which is what a click on the map needs.
import { EARTH_RADIUS_KM, projectMercator, toRadians, unprojectMercator } from "tempest-react-sdk";
const xy = projectMercator({ latitude: -23.55, longitude: -46.63 });
unprojectMercator(xy); // back to the lat/long pair
EARTH_RADIUS_KM * toRadians(1); // ≈ 111.2 km per degree of latitude
Offline travel estimate — estimateTravel
No network: great-circle distance × circuity factor, duration from a mode-adjusted average speed. Mirrors estimate_travel from the FastAPI SDK.
import { estimateTravel } from "tempest-react-sdk";
const est = estimateTravel(sp, rio, "car");
// { mode: "car", distance_km: ~469, duration_minutes: ~562, source: "heuristic" }
// A bus is ~1.6× slower than a car over the same leg:
estimateTravel(sp, rio, "bus").duration_minutes;
// Tune the factors:
estimateTravel(sp, rio, "car", { circuityFactor: 1.4, carSpeedKmh: 60 });
Default factors
circuityFactor = 1.3 (roads are ~30% longer than the straight line), carSpeedKmh = 50, duration multipliers car: 1.0 · motorcycle: 0.95 · bus: 1.6.
They are exported too — DEFAULT_CIRCUITY_FACTOR, DEFAULT_CAR_SPEED_KMH and
DEFAULT_MODE_DURATION_FACTORS — so a settings screen can show the value it
is about to override instead of restating it and drifting.
Real routing (opt-in) — createOSRMBackend
When you host an OSRM and want real street routing, build a backend injecting your server URL. It is the only piece that makes a request — and only when you create it.
import { createOSRMBackend } from "tempest-react-sdk";
const backend = createOSRMBackend({ baseUrl: "https://osrm.internal" });
const est = await backend.route(sp, rio, { mode: "car" });
// { …, source: "osrm" }
This makes a network request
The SDK ships no endpoint. Point baseUrl at a routing engine you host. For a zero-network estimate, use estimateTravel.
Any engine (Valhalla, GraphHopper) works: implement the RoutingBackend interface (route(origin, destination, { mode }) => Promise<TravelEstimate>).
Live tracking — usePositionTracker
Records a trajectory from watchPosition, filtering jitter and accumulating distance. Lifecycle tied to the component (the watch is cleared on unmount).
import { usePositionTracker, TrajectoryMap } from "tempest-react-sdk";
export function LiveRun() {
const { points, lastPoint, distanceKm, isTracking, start, stop, clear } =
usePositionTracker({ minDistanceKm: 0.01, positionOptions: { enableHighAccuracy: true } });
return (
<div>
<button onClick={isTracking ? stop : start}>
{isTracking ? "Stop" : "Track"}
</button>
<button onClick={clear}>Clear</button>
<span>{distanceKm.toFixed(2)} km · {points.length} points</span>
<TrajectoryMap points={points} current={lastPoint} height={360} />
</div>
);
}
minDistanceKm(default0.005= 5 m) discards samples that are too close — filters GPS jitter while standing still.maxPointscaps the retained array (total distance keeps counting).autoStart: truestarts tracking on mount.
Imperative version — createPositionTracker
Outside React (services), the controller exposes start/stop/clear + points/distanceKm/status getters:
import { createPositionTracker } from "tempest-react-sdk";
const tracker = createPositionTracker({
minDistanceKm: 0.01,
onUpdate: (pts) => console.log(pts.length),
});
tracker.start();
// …later
tracker.stop();
console.log(tracker.distanceKm);
Map plot — TrajectoryMap
By default, a tile-free SVG plot: it projects the points (Web Mercator), auto-fits the bounds, and draws the polyline + markers (green start, pulsing current) + grid + a km scale bar. Zero dependency, zero network.
import { TrajectoryMap } from "tempest-react-sdk";
<TrajectoryMap
points={points}
current={lastPoint}
height={360}
showGrid
showScale
/>;
Real tiles (opt-in) with Leaflet
If you host your own tiles, pass tileUrl — the component loads Leaflet on demand (dynamic import) and mounts a real tile layer. Requires leaflet installed in the app and its stylesheet imported once.
// npm install leaflet
import "leaflet/dist/leaflet.css";
import { TrajectoryMap } from "tempest-react-sdk";
<TrajectoryMap
points={points}
tileUrl="https://tiles.YOUR-SERVER/{z}/{x}/{y}.png"
tileAttribution="© Your map data"
height={420}
/>;
Leaflet is an optional peer
Without tileUrl, Leaflet never enters the bundle (the import is lazy). If tileUrl is passed and leaflet is not installed, the component shows a message instead of crashing.
| Prop | Type | Default | Description |
|---|---|---|---|
points |
readonly Coordinate[] |
— | Trajectory to draw. |
current |
Coordinate \| null |
— | Current-position marker (pulsing). |
height |
number |
320 |
Viewport height in px. |
padding |
number |
24 |
Inner padding in px. |
showGrid / showScale |
boolean |
true |
Grid and scale bar (SVG mode). |
strokeColor |
string |
primary token | Path color. |
tileUrl |
string |
— | {z}/{x}/{y} template of your tile server → enables Leaflet. |
tileAttribution |
string |
— | Attribution over the tiles. |
Recap
- Capture:
usePositionTracker/createPositionTrackerrecord a trajectory vianavigator.geolocation, filtering jitter — all in the browser. - Math:
haversineKm,pathLengthKm,bearingDeg,estimateTravel— offline, mirroringtempest-fastapi-sdk. - Map:
TrajectoryMapplots tile-free SVG by default;tileUrlenables Leaflet only with a server you host. - Real routing:
createOSRMBackendis opt-in and points at your OSRM — the only piece that touches the network.
See also
- Utility hooks —
useGeolocation(single fix / low-level watch) - HTTP — send the trajectory to the backend
- Offline — persist the trajectory locally