Moveet

June 10, 2026 · View on GitHub

CI License: MIT Node.js TypeScript Docker

A real-time vehicle fleet simulator that runs vehicles on actual road networks with A* pathfinding, realistic motion physics, BPR traffic congestion, time-of-day patterns, geofencing, incident-based rerouting, session recording, and a custom browser-side map rendering engine — no map tile provider required.


Contents


Features

🗺 Road-network agnosticIngests any GeoJSON/OSM-derived road graph — swap the file to simulate a different city
🌐 Network CLIapps/network pipeline: download OSM data from Geofabrik, extract a bbox, filter road classes, export GeoJSON, validate topology, and diff versions — one prepare command does it all
🔀 A* pathfindingHaversine heuristic over bidirectional road segments; respects turn restrictions, roundabouts, and road-class access rules; incident-aware route cache
🚗 Vehicle typesFive types (car, truck, motorcycle, ambulance, bus) with distinct speed profiles, acceleration curves, road restrictions, and special behaviours (e.g. ambulances ignore heat-zone penalties)
🚦 Traffic realismBPR congestion model (flow/capacity), time-of-day rush-hour/night demand multipliers, traffic-signal intersection delays, surface-smoothness speed factors
🎨 Custom map rendererD3 SVG scene with a Mercator projection (1×–15× zoom, pan); dedicated layers for roads, vehicles, POIs, heat-zone contours, incident markers, geofences, breadcrumb trails, and dispatch routes — no Leaflet or Mapbox
📡 Real-time WebSocket100 ms batched broadcast with backpressure handling; streams vehicle positions, routes, heat zones, incidents, geofence events, fleet events, and replay frames
🔥 Heat zonesContour density map (green → red, 50 thresholds) derived from road-network intersection density
🔲 GeofencingDraw custom polygons on the map; monitor vehicles crossing zone boundaries; enter/exit events broadcast in real time
⚠️ Incidents & reroutingOperator-created road incidents trigger live A* rerouting for all affected vehicles
🎬 Recording & replayNDJSON session recording; replay with pause, seek, and 1×/2×/4× speed controls and interpolated progress bar
🚘 Breadcrumb trailsPer-vehicle position history rendered as fading path overlays on the map
🚦 Fleet managementGroup vehicles into named, colour-coded fleets; assign/unassign at runtime
🔍 POI + road searchTypeahead combining road names and points of interest; dispatches selected vehicles to result
🖥 Operator UIIcon-rail sidebar (Vehicles · Fleets · Incidents · Geofences · Recordings · Visibility · Speed · Adapter) + bottom dock with live and replay controls
🔌 Adapter pluginsHot-swappable source and sink plugins; configure via env vars or REST API at runtime

Quick Start

Prerequisites

  • Node.js ≥ 24, npm ≥ 9 (workspace root)
  • Docker (optional)

Run locally

git clone https://github.com/ivannovazzi/moveet.git
cd moveet
npm install
npm run dev          # starts all three services via Turborepo
ServiceURL
Dashboardhttp://localhost:5012
Simulator APIhttp://localhost:5010
Adapter APIhttp://localhost:5011

Or start services individually:

npm run dev:sim      # simulator only  :5010
npm run dev:ui       # UI only         :5012
npm run dev:adapter  # adapter only    :5011

To prepare a road network for a new city:

cd apps/network
npm run dev -- prepare nairobi   # or any region in regions.json

Architecture

flowchart TD
    NET["<b>apps/network</b><br/>OSM CLI pipeline<br/>(offline, one-time)"]
    UI["<b>apps/ui</b><br/>React 19 · D3 · Vite<br/>:5012"]
    SIM["<b>apps/simulator</b><br/>Express · ws · Turf.js<br/>:5010"]
    ADP["<b>apps/adapter</b><br/>Express · plugin manager<br/>:5011"]
    EXT["External system<br/><i>GraphQL · Kafka · REST · …</i>"]

    NET -- "GeoJSON road network" --> SIM
    UI -- "REST + WebSocket" --> SIM
    SIM -- "GET /vehicles<br/>POST /sync" --> ADP
    ADP -- "source / sink plugins" --> EXT

Network is an offline CLI that turns raw OpenStreetMap data into a simulator-ready GeoJSON road network. Run it once per city; the output drops straight into apps/simulator/data/.

Simulator is the core — it builds a routable graph from GeoJSON, runs vehicles with per-vehicle interval timers, and serves a REST API + WebSocket feed. It works completely standalone.

UI is a React app that renders everything in an SVG canvas using D3 with a Mercator projection. It has no map-tile dependency — roads, routes, heat-zone contours, POIs, incidents, geofences, breadcrumb trails, and vehicles are all drawn from GeoJSON/API data.

Adapter is optional — only needed when you want to push data to an external fleet management system. It hot-swaps source and sink plugins at runtime via its own REST API.

Simulator internals

flowchart LR
    GJ[GeoJSON<br/>road network] --> RN[RoadNetwork<br/>graph + A*]
    RN --> VM[VehicleManager<br/>movement · routing · types]
    VM --> SC[SimulationController<br/>start · stop · options]
    SC --> RM[RecordingManager]
    SC --> RP[ReplayManager]
    SC --> IM[IncidentManager<br/>rerouting]
    SC --> FM[FleetManager]
    SC --> GF[GeoFenceManager<br/>enter / exit events]
    SC --> TM[TrafficManager<br/>BPR · time-of-day]
    SC --> WS[WebSocket<br/>broadcaster]

Network CLI

apps/network is a standalone CLI that turns raw OpenStreetMap data into a simulator-ready GeoJSON road network. It requires a locally installed osmium-tool (≥ 1.14) and runs entirely offline after the initial Geofabrik download.

One-command setup

cd apps/network
npm run dev -- prepare nairobi        # interactive wizard if region omitted
npm run dev -- prepare --output apps/simulator/data/network.geojson

The prepare command runs the full pipeline: download → extract → filter → export → validate.

Individual commands

CommandDescription
network downloadDownload country PBF from Geofabrik (cached after first run)
network extractClip a bounding box from the country PBF using osmium
network filterKeep only drivable road classes from the extracted PBF
network exportConvert filtered PBF to GeoJSON via osmium
network validateRun topology checks: orphan nodes, duplicate edges, disconnected components
network diff <old> <new>Compare two network GeoJSON files and report changes
network prepare [region]Full pipeline in one step

Regions are defined in regions.json (covers major cities globally). Pass --bbox w,s,e,n for a custom area or --geofabrik <path> for a Geofabrik sub-path.


Simulator API

Base URL: http://localhost:5010

Simulation control

MethodPathDescription
GET/statusSimulation state (running, ready, interval)
POST/startStart simulation (accepts options body)
POST/stopStop simulation
POST/resetReset to initial state
GET/optionsGet current simulation options
POST/optionsUpdate simulation options

Vehicles & routing

MethodPathDescription
GET/vehiclesList all vehicle DTOs
POST/directionDispatch one or more vehicles to a destination
GET/directionsGet active direction assignments
POST/find-nodeSnap a lat/lng to the nearest graph node
POST/find-roadSnap a lat/lng to the nearest road edge
POST/searchFull-text POI search

Map data

MethodPathDescription
GET/networkFull road-network GeoJSON
GET/roadsRoad segments GeoJSON
GET/poisPoints of interest
GET/heatzonesCurrent heat zone features
POST/heatzonesRegenerate heat zones

Fleets

MethodPathDescription
GET/fleetsList all fleets
POST/fleetsCreate a fleet
DELETE/fleets/:idDelete a fleet
POST/fleets/:id/assignAssign vehicles to a fleet
POST/fleets/:id/unassignUnassign vehicles from a fleet

Incidents

MethodPathDescription
GET/incidentsList active incidents
POST/incidentsCreate an incident (triggers rerouting)
DELETE/incidents/:idClear an incident
POST/incidents/randomCreate a random incident

Geofences

MethodPathDescription
GET/geofencesList all geofence zones
POST/geofencesCreate a geofence (GeoJSON polygon + metadata)
GET/geofences/:idGet a geofence
PUT/geofences/:idUpdate a geofence
DELETE/geofences/:idDelete a geofence
PATCH/geofences/:id/toggleEnable / disable a geofence

Health

MethodPathDescription
GET/healthUptime and subsystem status

Recording & replay

MethodPathDescription
POST/recording/startStart recording the session
POST/recording/stopStop recording and save NDJSON file
GET/recordingsList saved recordings
POST/replay/startLoad and start a recording replay
POST/replay/pausePause replay
POST/replay/resumeResume replay
POST/replay/stopStop replay, return to live mode
POST/replay/seekSeek to a timestamp (ms)
POST/replay/speedSet playback speed multiplier
GET/replay/statusCurrent replay state

WebSocket Events

Connect to ws://localhost:5010. On connect the server sends a status and options snapshot.

EventDirectionPayload
vehiclesserver → clientArray of VehicleDTO (position, speed, heading, fleetId)
statusserver → clientSimulationStatus (running, ready, interval)
optionsserver → clientCurrent StartOptions
heatzonesserver → clientHeatZoneFeature[]
directionserver → clientActive dispatch assignment
waypoint:reachedserver → clientVehicle reached a waypoint
route:completedserver → clientVehicle completed its full route
resetserver → clientSimulation was reset
fleet:createdserver → clientNew fleet
fleet:deletedserver → clientFleet removed
fleet:assignedserver → clientVehicles assigned to fleet
incident:createdserver → clientNew incident + affected vehicles
incident:clearedserver → clientIncident resolved
vehicle:reroutedserver → clientVehicle rerouted around incident
geofence:eventserver → clientVehicle entered or exited a geofence zone

Adapter Plugins

Base URL: http://localhost:5011

Runtime configuration API

MethodPathDescription
GET/configCurrent source + sinks config
POST/config/sourceSwap the active source plugin
POST/config/sinksReplace the active sink list
DELETE/config/sinks/:typeRemove one sink
GET/vehiclesVehicles from the current source
GET/fleetsFleets from the current source
POST/syncPush a position update through all sinks
GET/healthHealth check

Source plugins

flowchart LR
    SRC["Source plugin"] --> MGR["Plugin Manager"]
    subgraph Sources
        static["<b>static</b><br/>synthetic vehicles"]
        graphql_s["<b>graphql</b><br/>GraphQL query"]
        rest_s["<b>rest</b><br/>HTTP GET"]
        mysql["<b>mysql</b>"]
        postgres["<b>postgres</b>"]
    end
    Sources --> SRC
PluginKey config fields
staticcount (default 20)
graphqlurl, query, token, headers, vehiclePath, maxVehicles
resturl, token, headers, vehiclePath, maxVehicles
mysqlhost, port, user, password, database, query
postgreshost, port, user, password, database, query

Sink plugins

flowchart LR
    MGR["Plugin Manager"] --> SINK["Sink plugin(s)"]
    subgraph Sinks
        console["<b>console</b><br/>stdout"]
        graphql_k["<b>graphql</b><br/>GraphQL mutation"]
        rest_k["<b>rest</b><br/>HTTP POST"]
        redpanda["<b>redpanda</b><br/>Kafka / Redpanda"]
        redis["<b>redis</b>"]
        webhook["<b>webhook</b><br/>HTTP fire-and-forget"]
    end
    SINK --> Sinks

Multiple sinks run simultaneously. Configure via env vars or the runtime API:

# Env-var example: Redpanda + webhook
SOURCE_TYPE=graphql
SOURCE_CONFIG='{"url":"https://api.example.com/graphql","token":"..."}'

SINK_TYPES=redpanda,webhook
SINK_REDPANDA_CONFIG='{"brokers":"localhost:9092","topic":"fleet-updates"}'
SINK_WEBHOOK_CONFIG='{"url":"https://hooks.example.com/fleet"}'

Configuration

Simulator (apps/simulator/.env)

VariableDefaultDescription
PORT5010HTTP / WebSocket port
GEOJSON_PATH./data/network.geojsonPath to the road-network GeoJSON file
VEHICLE_COUNT70Number of vehicles to spawn
UPDATE_INTERVAL500Position broadcast interval (ms)
MIN_SPEED20Minimum vehicle speed (km/h)
MAX_SPEED60Maximum vehicle speed (km/h)
ACCELERATION5Acceleration rate (km/h per tick)
DECELERATION7Deceleration rate (km/h per tick)
TURN_THRESHOLD30Bearing change (°) that triggers slowdown
SPEED_VARIATION0.1Random speed jitter factor [0, 1]
HEATZONE_SPEED_FACTOR0.5Speed multiplier inside heat zones
ADAPTER_URL(empty)Enable adapter sync (e.g. http://localhost:5011)
SYNC_ADAPTER_TIMEOUT5000Adapter sync timeout (ms)

Adapter (apps/adapter/.env)

VariableDefaultDescription
PORT5011HTTP port
SOURCE_TYPEstaticActive source plugin
SOURCE_CONFIG{}JSON config for the source plugin
SINK_TYPES(empty)Comma-separated sink plugin names
SINK_<TYPE>_CONFIG{}JSON config per sink, e.g. SINK_REDPANDA_CONFIG

Testing

Tests use Vitest across all four packages. CI enforces 50 % coverage thresholds.

npm test                          # all packages via Turborepo
cd apps/simulator && npm test     # simulator
cd apps/ui && npm test            # UI
cd apps/adapter && npm test       # adapter
cd apps/network && npm test       # network CLI

Simulator test coverage includes: road-network graph, A* pathfinding, vehicle types and profiles, turn restrictions, BPR traffic manager, time-of-day clock, geofence manager, heat zones, fleet management, incident rerouting, recording/replay lifecycle, rate limiter, geospatial helpers, serializer, config validation, and SimulationController lifecycle.


Docker

Pull and run (no build needed)

curl -O https://raw.githubusercontent.com/ivannovazzi/moveet/main/docker-compose.ghcr.yml
docker compose -f docker-compose.ghcr.yml up

The simulator image does not bundle a road network: place a simulator-ready GeoJSON at ./apps/simulator/data/network.geojson (see Network CLI) or edit the volume in the compose file.

Open http://localhost:5012.

Images (published on every release via GitHub Container Registry):

ghcr.io/ivannovazzi/moveet-simulator
ghcr.io/ivannovazzi/moveet-adapter
ghcr.io/ivannovazzi/moveet-ui

Build from source

cd apps/simulator && docker compose up

Project Structure

PackagePathTechPort
networkapps/network/Node.js 24 · Commander · osmium-tool (local install)CLI
simulatorapps/simulator/Node.js 24 · Express 4 · ws 8 · Turf.js 75010
adapterapps/adapter/Node.js 24 · Express 45011
uiapps/ui/React 19 · D3 7 · Vite · TypeScript 5.8 · CSS Modules5012

Each package has its own README with deeper architecture notes.


Contributing

Please read CONTRIBUTING.md before opening a PR.

Security

See SECURITY.md for the vulnerability disclosure policy.

License

MIT © Ivan Novazzi