orbit-studio / src /composables /useGeolocation.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
3.3 kB
// Asking the device where it is.
//
// Two exports, for two kinds of caller. `currentPosition` is the primitive: a
// plain promise, for the places that are not components — `sceneSync` resolving the
// sky view's observer, and CesiumController. `useGeolocation` wraps it for the
// places that are, adding the one thing a component needs and a promise cannot give
// it: a reactive flag saying a fix is in flight.
//
// The state is module-scoped rather than per-caller because there is one device.
// Two components asking must see the same answer about whether it is busy.
//
// Deliberately not VueUse's `useGeolocation`, which wraps `watchPosition` and hands
// coordinates back as refs: every caller here wants one fix, awaited, with a refusal
// it can act on, and a standing position watch would cost battery and leave the
// platform's location indicator lit for a value read once.
//
// Note that this needs a secure context: over plain http on a LAN address the api is
// either absent or refuses, so `pnpm dev:host` cannot exercise it.
import { readonly, type Ref, ref } from "vue";
import type { CesiumController } from "../modules/CesiumController";
import type { Observer } from "../modules/skyGeometry";
// Long, because a cold fix genuinely takes this long. Cutting it short would turn a
// slow success into a false failure, which is why the wait is worth showing in the
// ui rather than shortening.
const TIMEOUT_MS = 10_000;
/**
* The device's position, or undefined when the user declines, the fix fails, or it
* times out.
*
* `navigator.geolocation` predates promises and reports failure through a second
* callback, which is awkward for a caller that has to decide whether to carry on —
* the sky view will not open without an observer, so it has to await an answer
* rather than fire and forget. Going through here rather than calling the api
* directly is also what makes a refusal reportable: the second callback is easy to
* leave out, and a geolocation button that silently does nothing when permission is
* declined is indistinguishable from a broken one.
*/
export async function currentPosition(): Promise<Observer | undefined> {
if (!navigator.geolocation) {
return undefined;
}
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
({ coords }) => resolve({ lat: coords.latitude, lon: coords.longitude }),
(error) => {
console.warn(`Geolocation unavailable: ${error.message}`);
resolve(undefined);
},
{ timeout: TIMEOUT_MS },
);
});
}
const pending = ref(false);
/** A location request a component can drive, and say something about while it waits. */
export function useGeolocation(cc: CesiumController): { pending: Readonly<Ref<boolean>>; locate: () => Promise<void> } {
async function locate(): Promise<void> {
if (pending.value) {
return;
}
pending.value = true;
try {
// Through the controller rather than writing the store from here: it owns the
// ground station write and the name that goes on it, so there is still exactly
// one writer of ground stations.
await cc.setGroundStationFromGeolocation();
} finally {
pending.value = false;
}
}
return { pending: readonly(pending), locate };
}