6. Send live positions to JavaScript
A CoreLocation delegate and an Android listener that call a JS function with each new position.
import { PLATFORM } from "lucent:platform";import { CLLocation, CLLocationManager } from "lucent:ios/CoreLocation";import { CLLocation, CLLocationManager, type CLLocationManagerDelegate,} from "lucent:ios/CoreLocation";import { Location, LocationManager } from "lucent:android/android.location";import { Looper } from "lucent:android/android.os";import { appContext } from "lucent:android";import { main } from "lucent:thread";import { error } from "lucent:core";import type { Fix } from "./trip.lucent";
// --- iOS ---------------------------------------------------------------------… return { latitude: c.latitude, longitude: c.longitude, time: location.timestamp.getTime() };}
/** Forwards a manager's updates to a Lucent function. */class Updates implements CLLocationManagerDelegate { constructor(private readonly onFix: (fix: Fix) => void) {}
locationManager_didUpdateLocations(manager: CLLocationManager, locations: CLLocation[]): void { for (const location of locations) this.onFix(fromCLLocation(location)); }}
// Managers deliver to the thread that made them: made on the main thread, kept here.const managers = new Map<number, CLLocationManager>();
// --- Android -----------------------------------------------------------------
function fromLocation(location: Location): Fix {… };}
const listeners = new Map<number, (location: Location) => void>();
// --- The module --------------------------------------------------------------
let nextId = 1;
/** The last position the device knows, without waiting for a new one. */export async function lastFix(): Promise<Fix | null> { if (PLATFORM === "ios") {… return location ? fromLocation(location) : null; }}
/** Calls `onFix` with each new position, until `stop(id)`. */export async function watch(onFix: (fix: Fix) => void): Promise<number> { const id = nextId++; if (PLATFORM === "ios") { const updates = new Updates(onFix); await main(() => { const manager = new CLLocationManager(); manager.delegate = updates; managers.set(id, manager); manager.startUpdatingLocation(); }); } else { const manager = appContext().getSystemService(LocationManager); const looper = Looper.getMainLooper(); if (!manager || !looper) throw error("E_NO_LOCATION", "Location isn't available on this device"); const listener = (location: Location) => onFix(fromLocation(location)); listeners.set(id, listener); manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000n, 0, listener, looper); } return id;}
/** Stops the updates `watch` started, and lets go of its callback. */export async function stop(id: number): Promise<void> { if (PLATFORM === "ios") { const manager = managers.get(id); managers.delete(id); if (manager) await main(() => manager.stopUpdatingLocation()); } else { const listener = listeners.get(id); listeners.delete(id); if (listener) appContext().getSystemService(LocationManager)?.removeUpdates(listener); }}import { useEffect, useState } from "react";import { Text } from "react-native";import { lastFix } from "./src/location.lucent";import { Trip } from "./src/trip.lucent";
// A walk along the Seine, one fix every ten minutes.const walk = [ { latitude: 48.8584, longitude: 2.2945, time: 0 }, { latitude: 48.8637, longitude: 2.3023, time: 10 * 60 * 1000 }, { latitude: 48.8661, longitude: 2.3125, time: 20 * 60 * 1000 }, { latitude: 48.8638, longitude: 2.3252, time: 30 * 60 * 1000 }, { latitude: 48.8606, longitude: 2.3376, time: 40 * 60 * 1000 },];import { stop, watch } from "./src/location.lucent";import { Trip, type TripSummary } from "./src/trip.lucent";
const trip = new Trip();for (const fix of walk) trip.add(fix);
/** Adds a fix, skipping one that arrives late instead of failing the whole trip. */function record(fix: (typeof walk)[number]): void { try { trip.add(fix); } catch (e) { if ((e as { code?: string }).code !== "E_OUT_OF_ORDER") throw e; console.warn((e as Error).message); }}
record({ latitude: 48.86, longitude: 2.33, time: 5 * 60 * 1000 }); // late: skipped
export default function App() { const [shape, setShape] = useState<number | null>(null); const [here, setHere] = useState("Locating…");
useEffect(() => { lastFix().then( (fix) => setHere( fix ? `You are at ${fix.latitude.toFixed(4)}, ${fix.longitude.toFixed(4)}.` : "No position yet.", ), (e: Error) => setHere(e.message), ); }, []); const [summary, setSummary] = useState<TripSummary | null>(null);
useEffect(() => { const controller = new AbortController(); trip.simplify(100, controller.signal).then( (fixes) => setShape(fixes.length), () => {}, // aborted: the screen is gone ); return () => controller.abort(); const started = watch((fix) => { try { trip.add(fix); if (trip.count >= 2) setSummary(trip.summary()); } catch (e) { if ((e as { code?: string }).code !== "E_OUT_OF_ORDER") throw e; } }); return () => void started.then(stop); }, []);
const summary = trip.summary(); if (!summary) return <Text>Waiting for two positions…</Text>; return ( <Text> {summary.fixes} fixes, {(summary.meters / 1000).toFixed(2)} km, top speed{" "} {summary.topSpeed.toFixed(1)} m/s. {shape === null ? " Simplifying…" : ` ${shape} fixes shape the route.`} {here} {summary.topSpeed.toFixed(1)} m/s </Text> );}iOS: a delegate
Section titled “iOS: a delegate”Updates implements CLLocationManagerDelegate. Lucent makes an Objective-C object for it that forwards locationManager(_:didUpdateLocations:) to locationManager_didUpdateLocations: Swift's name, with its labels joined by _.
Android: a listener
Section titled “Android: a listener”LocationListener has one method to implement, so a plain function is one. removeUpdates takes the same function back, which is why watch keeps it in listeners.
To JavaScript
Section titled “To JavaScript”onFix is a JS function. Each update arrives on the Lucent thread and calls it; the call is posted to the JS thread. watch returns an id, and stop(id) removes the manager or listener, which lets go of the callback.
Run it
Section titled “Run it”With a permission granted, move the simulated location: the summary updates with each fix.