npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ingissa/navcore-simulator

v1.0.4

Published

Headless GPS simulator for NavCore. Works in Node.js, React Native, and CI.

Readme

@ingissa/navcore-simulator

Headless GPS simulation engine for the NavCore ecosystem. Works in Node.js (CI), React Native, and any JavaScript environment.


Contents


Installation

# Inside the go4exams monorepo — already available via workspace
# Import directly from the source tree:
import { SimulatorEngine, NoiseEngine } from '@ingissa/navcore-simulator'

Peer dependencies: @ingissa/navcore-core (already in monorepo). React Native hooks additionally require react and react-native, and are exported from @ingissa/navcore-simulator/react-native so Node.js CI never loads React Native runtime files.


Quick Start

import { SimulatorEngine, NoiseEngine, ScenarioInjector } from '@ingissa/navcore-simulator';

const engine = new SimulatorEngine({
  routeGeometry: myRoute,       // Coordinate[] from @ingissa/navcore-core
  speedKmh: 50,
  speedMultiplier: 4,           // 4× real-time
  noiseProfile: NoiseEngine.STANDARD,
  loop: false,
  onPositionUpdate: (pos) => {
    console.log(pos.coord, pos.bearing, pos.speed);
  },
  onRouteComplete: () => console.log('Route complete'),
});

engine.addScenario(ScenarioInjector.gpsBlackout(42, 5000));
engine.startRecording();
engine.start();

Core Classes

SimulatorEngine

The main orchestrator. Headless — no React, no Expo APIs. Runs in Node.js CI.

const engine = new SimulatorEngine(config: SimulatorConfig);

// Playback
engine.start()
engine.stop()
engine.pause()
engine.resume()
engine.seek(geometryIndex: number)    // jump to any route index, recalculates bearing
engine.setSpeed(1 | 2 | 4 | 8)

// Scenarios
engine.addScenario(scenario: Scenario)
engine.removeScenario(name: string)
engine.clearScenarios()

// State
engine.state        // 'idle' | 'running' | 'paused' | 'complete'
engine.position     // SimPosition | null
engine.progress     // 0–1

// Recording
engine.startRecording(sessionId?: string)
engine.stopRecording(): SimSession

// Cleanup
engine.destroy()    // clears all timers — no leaks

Timer internals: setInterval at 1000ms / speedMultiplier. Always cleaned up on destroy() and on natural completion.

Callback safety: All callbacks (onPositionUpdate, onRouteComplete, onScenarioStart, onScenarioEnd) are wrapped in try/catch — a throwing callback never crashes the engine.


RouteWalker

Manages geometry traversal. Extracted from the engine so it's independently testable.

const walker = new RouteWalker(geometry: Coordinate[], startIndex?: number);

walker.advance(steps: number): WalkerPosition      // fractional steps supported
walker.seek(index: number): WalkerPosition
walker.interpolate(index: number, fraction: number): Coordinate
walker.bearingAt(index: number): number            // forward-looking bearing

walker.totalDistanceM   // total route length in metres
walker.totalPoints      // geometry.length
walker.isComplete       // true when at last point
walker.currentIndex     // current floating-point index

NoiseEngine

GPS noise simulation using the Box-Muller transform. Zero external dependencies.

const engine = new NoiseEngine(profile: NoiseProfile);

engine.apply(coord: Coordinate): Coordinate   // returns noisy coord (never mutates input)
engine.setProfile(profile: NoiseProfile)      // hot-swap mid-simulation

// Static presets
NoiseEngine.NONE       // stdDevM: 0  — exact coordinates
NoiseEngine.CLEAN      // stdDevM: 3  — good urban GPS
NoiseEngine.STANDARD   // stdDevM: 12 — matches the original Go4Exams ±0.00011°
NoiseEngine.URBAN      // stdDevM: 25 — city canyon
NoiseEngine.DEGRADED   // stdDevM: 50 — bad conditions

Seeded mode: Add seed: number to any profile for deterministic output — same seed + same route = identical noise trace (critical for regression tests).

const deterministicProfile = { ...NoiseEngine.STANDARD, seed: 42 };

ScenarioInjector

Registers scenarios to fire at geometry indices. Called internally by SimulatorEngine on every tick.

const injector = new ScenarioInjector(onStart?, onEnd?);

injector.register(scenario: Scenario)
injector.remove(name: string)
injector.clear()
injector.tick(currentIndex: number): string | undefined  // returns active scenario name

// Static factories
ScenarioInjector.gpsBlackout(atIndex, durationMs)
ScenarioInjector.wrongSideRoad(atIndex, offsetM)
ScenarioInjector.uTurn(atIndex)
ScenarioInjector.speedSpike(atIndex, spikeKmh, durationMs?)
ScenarioInjector.accuracyDrop(atIndex, newAccuracyM, durationMs)

No double-fire: Each scenario fires exactly once per simulation run, even if tick() is called with the same index multiple times.


GpxParser

Zero-dependency GPX 1.1 parser. Pure string/regex — no DOMParser, no xml2js.

// Parse GPX string
const result = GpxParser.parse(gpxString: string): GpxParseResult

// Load from file (Node.js only)
const result = GpxParser.fromFile('/path/to/route.gpx')

// Load from base64 (React Native file picker)
const result = GpxParser.fromBase64(base64String)
interface GpxParseResult {
  coordinates: Coordinate[];      // [lng, lat] — NavCore convention
  timestamps:  number[];          // ms since epoch per point
  metadata: {
    name?:        string;
    totalPoints:  number;
    durationMs:   number;
    distanceM:    number;
    avgSpeedKmh:  number;
  };
}

Supports both <trkpt> (track points) and <wpt> (waypoints).


SessionRecorder

Records and exports simulation sessions.

const recorder = new SessionRecorder();
recorder.start(sessionId?: string)
recorder.stop(existing?: SimSession): SimSession

// Export formats
recorder.export('json')   // full SimSession as JSON
recorder.export('gpx')    // standard GPX 1.1 — re-importable by GpxParser
recorder.export('csv')    // timestamp,lng,lat,bearing,speed,accuracy

The GPX export is critical: a recorded session → GPX → GpxParserSimulatorEngine creates a full regression test loop.


FleetSimulator (Phase 2)

const fleet = FleetSimulator.spawn(5, {
  routeGeometry: myRoute,
  speedKmh: 50,
  noiseProfile: NoiseEngine.STANDARD,
  profiles: [
    { label: 'urban', noiseProfile: NoiseEngine.URBAN },
    { label: 'clean', noiseProfile: NoiseEngine.CLEAN },
  ],
});

fleet.start();
// ... wait for completion
const report = fleet.report();
// report.comparison.avgScoreByProfile
// report.comparison.deviationsBySegment (heatmap data)

@experimental — fully typed but not tested in Phase 1.


React Native

useSimulator

Drop-in replacement for useGpsSimulator:

import {
  NoiseEngine,
  ScenarioInjector,
  useSimulator,
} from '@ingissa/navcore-simulator/react-native';

// BEFORE
const simGps = useGpsSimulator(route, isNavigating, speedMultiplier);

// AFTER
const { position, controls, session, isRecording } = useSimulator({
  routeGeometry: route,
  active: isNavigating,
  config: {
    speedKmh: 50,
    speedMultiplier: 1,
    noiseProfile: NoiseEngine.STANDARD,
    loop: false,
  },
  scenarios: [
    ScenarioInjector.gpsBlackout(42, 5000),
    ScenarioInjector.wrongSideRoad(78, 18),
  ],
  onScenarioFired: (s) => console.log('Scenario:', s.name),
});

// position has the exact same shape as the old SimPosition
// controls: { pause, resume, seek, setSpeed }
// session: { start, stop }  — for recording

The engine is recreated only when routeGeometry changes (identity comparison). Destroyed automatically on unmount.


SimulatorControls

Self-contained control panel. Drop it into any screen:

import { SimulatorControls } from '@ingissa/navcore-simulator/react-native';

<SimulatorControls
  simulator={controls}
  session={session}
  style="minimal"           // 'minimal' | 'full' | 'floating'
  showScenarioLog={true}
  showSessionRecorder={true}
  currentSpeed={1}
  engineState="running"
  isRecording={isRecording}
  onExportSession={(s) => shareFile(JSON.stringify(s))}
/>

| Style | Description | |---|---| | minimal | Horizontal bar: speed selector (1×/2×/4×/8×) + pause/resume | | full | Card: speed, pause/resume, scenario log, session recorder | | floating | Collapsible bottom sheet — does not obscure the map |

Uses Klaroways colour tokens (#081525, #00BF76, etc.) to match NavCoreTestScreen.


Noise Profiles

| Profile | stdDevM | Use case | |---|---|---| | NONE | 0 | Exact coordinates — CI tests | | CLEAN | 3 m | Good open-sky GPS | | STANDARD | 12 m | Matches original Go4Exams ±0.00011° | | URBAN | 25 m | Dense city canyon | | DEGRADED | 50 m | Tunnel exit / bad conditions | | CUSTOM | any | Provide customFn(index) → [lngOff, latOff] |


Scenario Types

| Type | Factory | Effect | |---|---|---| | GPS_BLACKOUT | gpsBlackout(idx, ms) | Pauses noise application (dead reckoning stress test) | | WRONG_SIDE_ROAD | wrongSideRoad(idx, offsetM) | Position offset perpendicular to route | | UTURN | uTurn(idx) | Fires at index — engine handles turn logic | | SPEED_SPIKE | speedSpike(idx, kmh) | Temporarily reports higher speed | | ACCURACY_DROP | accuracyDrop(idx, m, ms) | GPS accuracy degrades to m metres for ms | | CUSTOM | — | Provide params manually |


GPX Round-Trip Loop

real GPS trace
     ↓
  .gpx file
     ↓
 GpxParser.parse()
     ↓
 SimulatorEngine (replay with noise + scenarios)
     ↓
 SessionRecorder.export('gpx')
     ↓
 GpxParser.parse()  ←── CI assertion: reparsed.coordinates.length === session.positions.length
     ↓
 Next regression run

This loop is the foundation of the CI strategy. Every format change to SessionRecorder._toGpx() that breaks GpxParser.parse() will fail the integration test immediately.


CI / Regression Testing

cd packages/navcore-sdk
npx vitest run --reporter=verbose

Test files:

  • packages/simulator/src/core/__tests__/RouteWalker.test.ts
  • packages/simulator/src/core/__tests__/NoiseEngine.test.ts
  • packages/simulator/src/core/__tests__/SimulatorEngine.test.ts
  • packages/simulator/src/scenarios/__tests__/ScenarioInjector.test.ts
  • packages/simulator/src/replay/__tests__/GpxParser.test.ts
  • packages/simulator/src/__tests__/integration/simulator-pipeline.test.ts

All tests use vi.useFakeTimers() — no real waiting, no flakiness.


Migration from useGpsSimulator

The migration is a search-and-replace, not a rewrite.

1. Remove the inline hook and local type

Delete useGpsSimulator function (lines 114–174 in NavCoreTestScreen) and the local SimPosition interface.

2. Add import

import {
  useSimulator,
  SimulatorControls,
  NoiseEngine,
  ScenarioInjector,
} from '../../../../packages/navcore-sdk/packages/simulator/src';

3. Replace the hook call

// Before
const simGps = useGpsSimulator(route, isNavigating, speed);

// After
const { position: simGps, controls } = useSimulator({
  routeGeometry: route,
  active: isNavigating,
  config: { speedKmh: 50, noiseProfile: NoiseEngine.STANDARD },
});

4. Replace the speed UI

// Before: 40 lines of inline speedRow JSX + speed state
// After:
<SimulatorControls simulator={controls} style="minimal" />

The position object shape is identical to the original SimPositioncoord, bearing, speed, accuracy are all present.