Skip to content

Locations

A Location is exactly seven fields. lat, lng, and timestamp are always present — the OS cannot return a fix without them. The other four are optional: null means “the OS did not measure this field” (e.g. a stationary device returns no bearing; an indoor fix returns no altitude). Beekon never substitutes 0 for absence — 0 always means “the OS reported zero.”

lat, lng, timestamp, // always present
accuracy?, speed?, bearing?, altitude? // null when unreported

The fields are identical on every platform. All numeric fields are Double end-to-end; the optional fields surface as Double? (Kotlin/Swift), double? (Dart), number | null (TypeScript).

FieldUnitNotes
lat, lngdegrees (WGS-84)always present
accuracymetres (horizontal, 1σ)null when the OS did not report
speedmetres/secondnull when the OS did not report (e.g. stationary)
bearingdegrees, [0, 360) clockwise from true northnull when the OS did not report; direction of travel, not device heading
altitudemetres above WGS-84 ellipsoidnull when the OS did not report or the vertical accuracy was invalid
timestampwall-clockOS provider’s fix time, not the time Beekon received it

This is non-negotiable in v1: Beekon does not modify locations. No Kalman filter, no outlier rejection, no speed clamp, no minimum-accuracy filter. Whatever the OS provider produces is what you get on the stream and in history.

The single exception is value-level normalisation in the Location constructor: invalid floats (NaN, Infinity, bearing outside [0, 360)) are coerced to null. This rejects values that cannot be meaningful — it never invents a value to fill in for an absent measurement. Cross-platform behaviour is locked by beekon_flutter/test/normalisation_vectors_test.dart (and the equivalent test on each native platform).

A consequence: adjacent fixes can disagree dramatically (urban canyon, tunnel, cold start). That’s the OS doing its honest best — surface it to users via the accuracy field rather than hiding it.

The live stream is hot — it emits the latest gated fix to all subscribers. On Android the buffer is SharedFlow with replay = 1, DROP_OLDEST, capacity 64 (so a slow consumer drops old emissions but never blocks the producer). iOS uses AsyncStream with equivalent semantics.

LaunchedEffect(Unit) {
Beekon.locations.collect { loc ->
Log.d(
"beekon",
"${loc.lat}, ${loc.lng} acc=${loc.accuracy ?: "—"}m speed=${loc.speed ?: "—"}m/s"
)
}
}

The most common shape: drop low-accuracy fixes when rendering on a map.

Beekon.locations
// `accuracy` is nullable — drop fixes that don't carry one,
// since you can't reason about their reliability for map rendering.
.filter { (it.accuracy ?: Double.MAX_VALUE) <= 25.0 }
.collect { /* render */ }

The persistence layer still records the raw fix — filtering happens in your stream consumer, not in storage.

Two separate read paths:

  • Live streamBeekon.locations (Android, iOS, Flutter) or Beekon.onLocation(cb) (RN). Real-time, replay-1. Drops emissions older than the last seen one. Only flows while the host process is alive (for Flutter / RN: while the Dart / JS engine is alive).
  • history(from, to) (or Beekon.shared.locations(from:to:) on iOS) — durable. Reads the native database (Room/SQLite on Android, GRDB/SQLite on iOS). Survives process death and termination. See Persistence & history.

In practice: render the live stream while the user is on a tracking screen; query history when they open a “trips” view.