Packages & adapters

Kinesis.js ships a tiny core plus opt-in adapters and wrappers. This page covers each package, how to write your own adapter, Web Worker mode, custom styling and performance tuning.

OpenLayers

@kinesisjs/openlayers manages an OpenLayers Feature<Point> per point, with optional fading trails and gap visualisation. It can attach to an existing layer and scope itself to a set of feature ids so it co-exists with the rest of your map.

openlayers.tsts
import { Tracker } from '@kinesisjs/core';
import { OpenLayersAdapter, createVehicleStyle } from '@kinesisjs/openlayers';

const adapter = new OpenLayersAdapter(map, {
style: createVehicleStyle({ icon: '/car.png', iconScale: 0.7 }),
trail: { enabled: true, maxPoints: 80, width: 4, opacity: 0.45 },
warningOpacity: 0.35,
});

const tracker = new Tracker({ adapter, interpolation: 'adaptive' });
tracker.start();

Leaflet

@kinesisjs/leaflet manages an L.Marker per point with heading-aware rotation baked into the icon. Remember Leaflet's coordinate order is [lat, lng] — the adapter converts your { lng, lat } positions for you — and import its stylesheet.

SSR

Leaflet references window at import time. In an SSR setup, import the Leaflet map component only on the client.

MapLibre GL

@kinesisjs/maplibre renders differently from the other two adapters — and the difference is the point. OpenLayers and Leaflet get one object per point (a feature, a marker); MapLibre draws the whole fleet from a single GeoJSON source in one GPU pass. The adapter keeps plain features in a fixed array, mutates them in place, and uploads once per tick with a single setData — a thousand points cost one upload and zero DOM nodes.

Color, heading rotation and opacity aren't written to markers one by one either: they live in feature properties and are applied on the GPU through the layer's data-driven expressions. The default arrow icon is generated at runtime as an SDF sprite — no sprite sheet to host, and one sprite serves every per-point color via icon-color.

maplibre.tsts
import { Map as MapLibreMap } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { Tracker } from '@kinesisjs/core';
import { MapLibreAdapter, createVehicleStyle } from '@kinesisjs/maplibre';

const map = new MapLibreMap({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [29, 41], // [lng, lat] — same order as core, nothing is swapped
zoom: 11,
});

const tracker = new Tracker({
adapter: new MapLibreAdapter(map, {
  style: createVehicleStyle({
    speedColorBands: [
      { max: 30,  color: '#22c55e' },
      { max: 80,  color: '#eab308' },
      { max: 130, color: '#ef4444' },
    ],
  }),
}),
interpolation: 'adaptive',
});
tracker.start();
SSR

maplibre-gl touches window at import time and needs a WebGL context — keep the map and adapter in a browser-only code path. You don't need to wait for map.on('load'): the adapter installs its source and layers once the style is ready and flushes anything ingested in the meantime.

Angular

@kinesisjs/angular exposes a standalone kinesisMap directive plus a kinesisTracker() factory. Inputs accept a Signal or Observable; teardown is automatic via DestroyRef.

live-map.component.tsts
@Component({
standalone: true,
imports: [KinesisMapDirective],
template: `<div kinesisMap [positions]="positions" [interpolation]="'adaptive'"
                [worker]="true" [trail]="trail" class="map"></div>`,
})
export class LiveMapComponent {
positions = inject(FeedService).positions; // Signal<Position[]>
trail = { enabled: true, maxPoints: 80, width: 4, opacity: 0.45 };
}

React, Vue & other frameworks

Planned

Dedicated @kinesisjs/react, @kinesisjs/vue and Svelte wrappers are not published yet — they're planned for v1.0+ (roadmap). Nothing blocks you today: the core is framework-agnostic, so you can integrate with the pattern below now.

Because the core is framework-agnostic, you can use it directly from any framework today — construct a Tracker in an effect (or the framework's mount hook), call tracker.ingest(positions) from your feed, and dispose it on unmount. A future wrapper will only wrap this same lifecycle.

route-aware (OSRM)

@kinesisjs/route-aware is a drop-in CustomInterpolator that snaps positions to the road graph using OSRM. It prefetches polylines, caches them in an LRU, guards against detours, and always falls back to linear — never blocking the tick loop.

route-aware.tsts
import { Tracker } from '@kinesisjs/core';
import { OpenLayersAdapter } from '@kinesisjs/openlayers';
import { OSRMInterpolator } from '@kinesisjs/route-aware';

const tracker = new Tracker({
adapter: new OpenLayersAdapter(map),
interpolation: new OSRMInterpolator({
  baseUrl: 'https://your-osrm.example.com', // self-host for production
  profile: 'driving',
  cacheSize: 500,
  maxDetourFactor: 2.5,
}),
});
tracker.start();

Web Worker mode

At thousands of points, the tick loop competes with the map's own pan/zoom rendering. Move it off-thread with one flag. The real tracker runs in the worker; the adapter stays on the main thread for DOM/map writes.

worker.tsts
const tracker = new Tracker({ adapter, worker: true });
// or point at a hosted worker file:
const hosted = new Tracker({
adapter,
worker: { url: new URL('./kinesis.worker.js', import.meta.url) },
});
CaveatDetail
Custom interpolatorsNot supported — functions can't cross the worker boundary.
Fade animationsDegrade to snapping (no rAF on the worker).
getStats()Lags ~30 ticks behind.
markCompleted / removeVehicleOptimistic.

Custom styling

Pass a style provider — a function of the TrailPoint — to colour or size markers from live data such as speed bands or a per-point meta.color.

styling.tsts
const adapter = new OpenLayersAdapter(map, {
style: createVehicleStyle({
  speedColorBands: [
    { max: 30,  color: '#22c55e' },
    { max: 80,  color: '#eab308' },
    { max: 130, color: '#ef4444' },
  ],
}),
});

Performance tuning

  • Match renderLagMs to your feed period. Too low and motion stutters; too high and it lags reality.
  • Raise ingestThrottle if a bursty feed sends duplicate positions.
  • Enable worker: true only for very high point counts — at typical sizes a 0.15 ms tick is already ~1% of the frame budget.
  • Cap trails with maxPoints to keep memory flat.
  • Watch getStats() — droppedTicksLast60s and tickHistoryP99 tell you when to shed work.

Write your own adapter

The contract is small. Implement TrackAdapter — create, update and remove a feature per id, plus teardown — and you have a new map binding.

class MyAdapter implements TrackAdapter { addVehicle(id: string, p: TrailPoint): void { /* draw on your map */ } updatePosition(id: string, p: TrailPoint): void { /* feature.setCoordinates */ } removeVehicle(id: string): void { /* remove from layer */ } destroy(): void { /* cleanup */ } }