element-portable/src/utils/beacon/geolocation.ts
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

136 lines
4.7 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { logger } from "matrix-js-sdk/src/logger";
// map GeolocationPositionError codes
// https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
export enum GeolocationError {
// no navigator.geolocation
Unavailable = "Unavailable",
// The acquisition of the geolocation information failed because the page didn't have the permission to do it.
PermissionDenied = "PermissionDenied",
// The acquisition of the geolocation failed because at least one internal source of position returned an internal error.
PositionUnavailable = "PositionUnavailable",
// The time allowed to acquire the geolocation was reached before the information was obtained.
Timeout = "Timeout",
// other unexpected failure
Default = "Default",
}
const GeolocationOptions = {
timeout: 10000,
maximumAge: 60000,
};
const isGeolocationPositionError = (error: unknown): error is GeolocationPositionError =>
typeof error === "object" && !!(error as GeolocationPositionError)["PERMISSION_DENIED"];
/**
* Maps GeolocationPositionError to our GeolocationError enum
*/
export const mapGeolocationError = (error: GeolocationPositionError | Error | unknown): GeolocationError => {
logger.error("Geolocation failed", error);
if (isGeolocationPositionError(error)) {
switch (error?.code) {
case error.PERMISSION_DENIED:
return GeolocationError.PermissionDenied;
case error.POSITION_UNAVAILABLE:
return GeolocationError.PositionUnavailable;
case error.TIMEOUT:
return GeolocationError.Timeout;
default:
return GeolocationError.Default;
}
} else if (error instanceof Error && error.message === GeolocationError.Unavailable) {
return GeolocationError.Unavailable;
} else {
return GeolocationError.Default;
}
};
const getGeolocation = (): Geolocation => {
if (!navigator.geolocation) {
throw new Error(GeolocationError.Unavailable);
}
return navigator.geolocation;
};
export type GenericPosition = {
latitude: number;
longitude: number;
altitude?: number;
accuracy?: number;
timestamp: number;
};
export type TimedGeoUri = {
geoUri: string;
timestamp: number;
};
export const genericPositionFromGeolocation = (geoPosition: GeolocationPosition): GenericPosition => {
const { latitude, longitude, altitude, accuracy } = geoPosition.coords;
return {
// safari reports geolocation timestamps as Apple Cocoa Core Data timestamp
// or ms since 1/1/2001 instead of the regular epoch
// they also use local time, not utc
// to simplify, just use Date.now()
timestamp: Date.now(),
latitude,
longitude,
altitude: altitude ?? undefined,
accuracy,
};
};
export const getGeoUri = (position: GenericPosition): string => {
const lat = position.latitude;
const lon = position.longitude;
const alt = Number.isFinite(position.altitude) ? `,${position.altitude}` : "";
const acc = Number.isFinite(position.accuracy) ? `;u=${position.accuracy}` : "";
return `geo:${lat},${lon}${alt}${acc}`;
};
export const mapGeolocationPositionToTimedGeo = (position: GeolocationPosition): TimedGeoUri => {
const genericPosition = genericPositionFromGeolocation(position);
return { timestamp: genericPosition.timestamp, geoUri: getGeoUri(genericPosition) };
};
/**
* Gets current position, returns a promise
* @returns Promise<GeolocationPosition>
*/
export const getCurrentPosition = async (): Promise<GeolocationPosition> => {
try {
const position = await new Promise((resolve: PositionCallback, reject) => {
getGeolocation().getCurrentPosition(resolve, reject, GeolocationOptions);
});
return position;
} catch (error) {
throw new Error(mapGeolocationError(error));
}
};
export type ClearWatchCallback = () => void;
export const watchPosition = (
onWatchPosition: PositionCallback,
onWatchPositionError: (error: GeolocationError) => void,
): ClearWatchCallback => {
try {
const onError = (error: GeolocationPositionError): void => onWatchPositionError(mapGeolocationError(error));
const watchId = getGeolocation().watchPosition(onWatchPosition, onError, GeolocationOptions);
const clearWatch = (): void => {
getGeolocation().clearWatch(watchId);
};
return clearWatch;
} catch (error) {
throw new Error(mapGeolocationError(error));
}
};