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 presentaccuracy?, speed?, bearing?, altitude? // null when unreportedThe 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).
| Field | Unit | Notes |
|---|---|---|
lat, lng | degrees (WGS-84) | always present |
accuracy | metres (horizontal, 1σ) | null when the OS did not report |
speed | metres/second | null when the OS did not report (e.g. stationary) |
bearing | degrees, [0, 360) clockwise from true north | null when the OS did not report; direction of travel, not device heading |
altitude | metres above WGS-84 ellipsoid | null when the OS did not report or the vertical accuracy was invalid |
timestamp | wall-clock | OS provider’s fix time, not the time Beekon received it |
Raw passthrough
Section titled “Raw passthrough”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.
Reading locations
Section titled “Reading locations”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" ) }}Task { for await loc in await Beekon.shared.locations { let acc = loc.accuracy.map { String($0) } ?? "—" let spd = loc.speed.map { String($0) } ?? "—" print("\(loc.lat), \(loc.lng) acc=\(acc)m speed=\(spd)m/s") }}Beekon.shared.locations is overloaded: as a property it’s the live AsyncStream<Location>; called as locations(from:to:) it’s a historical-range fetch — see Persistence & history.
Beekon.instance.locations.listen((Location loc) { print('${loc.lat}, ${loc.lng} acc=${loc.accuracy ?? "—"}m speed=${loc.speed ?? "—"}m/s');});The Flutter stream only emits while the Flutter engine is alive — for fixes captured during process death, query history.
const unsubscribe = Beekon.onLocation((loc) => { const acc = loc.accuracy ?? '—'; const spd = loc.speed ?? '—'; console.log(`${loc.lat}, ${loc.lng} acc=${acc}m speed=${spd}m/s`);});onLocation only emits while the JS engine is alive. For fixes captured while JS was asleep, use history.
Filtering on top
Section titled “Filtering on top”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 */ }for await loc in await Beekon.shared.locations where (loc.accuracy ?? .infinity) <= 25 { // render}Beekon.instance.locations .where((loc) => (loc.accuracy ?? double.infinity) <= 25) .listen((loc) { /* render */ });Beekon.onLocation((loc) => { if ((loc.accuracy ?? Infinity) <= 25) { // render }});The persistence layer still records the raw fix — filtering happens in your stream consumer, not in storage.
Live stream vs history
Section titled “Live stream vs history”Two separate read paths:
- Live stream —
Beekon.locations(Android, iOS, Flutter) orBeekon.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)(orBeekon.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.