From acc1d7cc1edbfbfead330f0cac393ea6d51b4ef1 Mon Sep 17 00:00:00 2001 From: evlist Date: Tue, 13 Dec 2022 20:59:39 +0100 Subject: [PATCH] (Re)Adding background geolocation. --- package-lock.json | 9 +++ package.json | 1 + src/components/gpx-record/GpxRecord.tsx | 63 +++++++++++++++- src/db/change-handler.ts | 26 ++++++- src/db/gpx.ts | 25 +++++++ src/db/lib.ts | 2 +- src/db/trkseg.ts | 30 +++++++- src/lib/background-geolocation.ts | 96 +++++++++++++++++++++++++ src/workers/dispatcher-worker.ts | 5 +- 9 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/lib/background-geolocation.ts diff --git a/package-lock.json b/package-lock.json index cb96885..c7a11c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@awesome-cordova-plugins/geolocation": "^6.2.0", + "@capacitor-community/background-geolocation": "^1.2.6", "@capacitor/android": "^3.4.3", "@capacitor/core": "^3.4.3", "@capacitor/ios": "^3.4.3", @@ -550,6 +551,14 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor-community/background-geolocation": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@capacitor-community/background-geolocation/-/background-geolocation-1.2.6.tgz", + "integrity": "sha512-zYe2gUf+CXoE7tbBzd6CNv3yW1SRCVAxVKg5t63zHbMQR2JfurVlfIEnXjLyc+strAMPavGwbIXEjiGv5jJvdA==", + "peerDependencies": { + "@capacitor/core": ">=3.0.0" + } + }, "node_modules/@capacitor/android": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.9.0.tgz", diff --git a/package.json b/package.json index c8c694a..3a725b5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@awesome-cordova-plugins/geolocation": "^6.2.0", + "@capacitor-community/background-geolocation": "^1.2.6", "@capacitor/android": "^3.4.3", "@capacitor/core": "^3.4.3", "@capacitor/ios": "^3.4.3", diff --git a/src/components/gpx-record/GpxRecord.tsx b/src/components/gpx-record/GpxRecord.tsx index 6ded8a8..3e04200 100644 --- a/src/components/gpx-record/GpxRecord.tsx +++ b/src/components/gpx-record/GpxRecord.tsx @@ -12,13 +12,54 @@ import Dialog from '../dialog'; import { useI18n } from '@solid-primitives/i18n'; import GpxChooser from '../gpx-chooser'; import { currentGpxId } from '../gpx-dialog'; +import { + startBackgroundGeolocation, + stopBackgroundGeolocation, +} from '../../lib/background-geolocation'; +import dispatch from '../../workers/dispatcher-main'; +import { emptyTrk } from '../../db/trk'; +import getUri from '../../lib/ids'; + +declare global { + var $lastValidLocationTime: number; +} const GpxRecord: Component<{}> = (props) => { const [t] = useI18n(); const [state, setState] = createSignal('stopped'); const [open, setOpen] = createSignal(false); + const [watcherId, setWatcherId] = createSignal(); const [gpxId, setGpxId] = createSignal(''); - const [trkId, setTrkId] = createSignal(''); + const [trksegId, setTrksegId] = createSignal(); + + const newLocationHandler = async (location: any) => { + console.log( + `Location filtering, elapsed time: ${ + location.time - globalThis.$lastValidLocationTime + }` + ); + if (location.time - globalThis.$lastValidLocationTime > 15000) { + globalThis.$lastValidLocationTime = location.time; + await dispatch({ + action: 'appendTrkpt', + params: { + trksegIdObj: trksegId(), + trkpt: { + $: { + lat: location.latitude, + lon: location.longitude, + }, + ele: location.altitude, + time: new Date(location.time).toISOString(), + extensions: { + speed: location.speed, + accuracy: location.accuracy, + }, + }, + }, + }); + } + }; const handleClickOpen = () => { if (state() === 'stopped') { @@ -29,22 +70,37 @@ const GpxRecord: Component<{}> = (props) => { const handleClose = () => { setOpen(false); }; - const startRecordingHandler = () => { + const startRecordingHandler = async () => { + const trkId = await dispatch({ + action: 'appendTrk', + params: { gpxId: gpxId(), trk: emptyTrk }, + }); + const trkIdObj = getUri('trk', trkId); + setTrksegId({ ...trkIdObj, trkseg: 0 }); + globalThis.$lastValidLocationTime = 0; + const id = await startBackgroundGeolocation(newLocationHandler, 20); + setWatcherId(id); setState('recording'); setOpen(false); }; const pauseRecordingHandler = () => { + stopBackgroundGeolocation(watcherId()); setState('paused'); setOpen(false); }; - const resumeRecordingHandler = () => { + const resumeRecordingHandler = async () => { + setTrksegId({ ...trksegId()!, trkseg: trksegId()!.trkseg + 1 }); + const id = await startBackgroundGeolocation(newLocationHandler, 20); + setWatcherId(id); setState('recording'); setOpen(false); }; const stopRecordingHandler = () => { + stopBackgroundGeolocation(watcherId()); setState('stopped'); setOpen(false); }; + return ( <>
@@ -81,6 +137,7 @@ const GpxRecord: Component<{}> = (props) => { color='primary' variant='contained' onClick={startRecordingHandler} + disabled={gpxId() === 'new'} sx={{ width: '100%', margin: '10px', diff --git a/src/db/change-handler.ts b/src/db/change-handler.ts index f1c6975..91be022 100644 --- a/src/db/change-handler.ts +++ b/src/db/change-handler.ts @@ -1,3 +1,4 @@ +import { slice } from 'lodash'; import { getWpt } from '../db/wpt'; import { returnAgain } from '../workers/dispatcher-worker'; import { getAllGpxes, getGpx } from './gpx'; @@ -31,14 +32,24 @@ const sendUpdate = async (params: any) => { }; const changeHandler = async (change: any) => { - console.log({ caller: 'ChangeHandler', change }); + console.log({ caller: 'ChangeHandler', change, watches: globalThis.watches }); const { id } = change; if (!globalThis.watches) { globalThis.watches = new Map(); } sendUpdate(globalThis.watches.get(id)); - const parentId = id.substring(0, id.lastIndexOf('/')); + const tokens = id.split('/').slice(0, -1); + if (['1wpt', '2rte', '3trk'].includes(tokens.at(-1))) { + tokens.splice(-1); + } + const parentId = tokens.join('/'); + console.log({ + caller: 'ChangeHandler', + change, + watches: globalThis.watches, + parentId, + }); if (parentId === 'gpx') { const gpxes = await getAllGpxes(); @@ -64,12 +75,23 @@ const changeHandler = async (change: any) => { export default changeHandler; export const getAndWatch = async (params: any) => { + console.log({ + caller: 'ChangeHandler / getAndWatch', + params, + watches: globalThis.watches, + }); const { method, _dispatchId, id, ...otherParams } = params; if (!globalThis.watches) { globalThis.watches = new Map(); } globalThis.watches.set(id, params); const returnValue = await methods[method](params); + console.log({ + caller: 'ChangeHandler / getAndWatch', + params, + returnValue, + watches: globalThis.watches, + }); return returnValue; }; diff --git a/src/db/gpx.ts b/src/db/gpx.ts index 7e7e50e..e4c8cf3 100644 --- a/src/db/gpx.ts +++ b/src/db/gpx.ts @@ -392,6 +392,31 @@ export const getGpx = async (params: any) => { return gpx; }; +export const getNextGpxTrkId = async (gpxId: string) => { + const docs = await getFamily(gpxId, { include_docs: true }); + const gpxIdObj = getUri('gpx', gpxId); + console.log({ caller: 'getNextGpxTrkId', gpxId, gpxIdObj, docs }); + let trkId = ''; + docs.rows.forEach((row: any) => { + if (row.doc.type === 'trk') { + trkId = row.doc._id; + } + }); + if (trkId !== '') { + const trkIdObj = getUri('trk', trkId); + return getUri('trk', { gpx: gpxIdObj.gpx, trk: trkIdObj.trk + 1 }); + } + return getUri('trk', { gpx: gpxIdObj.gpx, trk: 0 }); +}; + +export const appendTrk = async (params: any) => { + const { gpxId, trk } = params; + const trkId = await getNextGpxTrkId(gpxId); + console.log({ caller: 'appendTrk', gpxId, trk, trkId }); + await put(trkId, 'trk', (doc) => trk, {}); + return trkId; +}; + export const putGpx = async (params: any) => { let { id, gpx } = params; diff --git a/src/db/lib.ts b/src/db/lib.ts index 092d64c..3552e25 100644 --- a/src/db/lib.ts +++ b/src/db/lib.ts @@ -10,7 +10,7 @@ export const put = async ( update: (doc: any) => any, defaultDoc: any ) => { - var current; + let current; try { current = await db.get(_id); } catch { diff --git a/src/db/trkseg.ts b/src/db/trkseg.ts index aa13812..c9e26cb 100644 --- a/src/db/trkseg.ts +++ b/src/db/trkseg.ts @@ -1,7 +1,8 @@ -import getUri from '../lib/ids'; +import getUri, { intToTrkptId } from '../lib/ids'; import { appendToArray } from './gpx'; import { getFamily, put } from './lib'; import { putNewTrk } from './trk'; +import { emptyWpt } from './wpt'; const emptyTrkseg: Trkseg = { trkpt: undefined, @@ -46,3 +47,30 @@ export const getTrkseg = async (params: any) => { }); return trkseg; }; + +export const putTrkseg = async (trkSegId: string, trkSeg = emptyTrkseg) => { + await put( + trkSegId, + 'trkseg', + (doc) => { + return trkSeg; + }, + emptyTrkseg + ); + return trkSegId; +}; + +export const appendTrkpt = async (params: any) => { + const { trksegIdObj, trkpt } = params; + const idTrkseg = getUri('trkseg', trksegIdObj); + try { + await db.get(idTrkseg); + } catch { + await put(idTrkseg, 'trkseg', (doc) => doc, emptyTrkseg); + } + const trkptIdPart = intToTrkptId(new Date(trkpt.time as string).valueOf()); + const trkptIdObj = { ...trksegIdObj, trkpt: trkptIdPart }; + const trkptId = getUri('trkpt', trkptIdObj); + await put(trkptId, 'trkpt', (doc) => trkpt, emptyWpt); + return trkptId; +}; diff --git a/src/lib/background-geolocation.ts b/src/lib/background-geolocation.ts new file mode 100644 index 0000000..0e2a1c0 --- /dev/null +++ b/src/lib/background-geolocation.ts @@ -0,0 +1,96 @@ +import { BackgroundGeolocationPlugin } from '@capacitor-community/background-geolocation'; +import { registerPlugin } from '@capacitor/core'; + +const BackgroundGeolocation = registerPlugin( + 'BackgroundGeolocation' +); + +const backgroundGeolocationConfig = { + // If the "backgroundMessage" option is defined, the watcher will + // provide location updates whether the app is in the background or the + // foreground. If it is not defined, location updates are only + // guaranteed in the foreground. This is true on both platforms. + // On Android, a notification must be shown to continue receiving + // location updates in the background. This option specifies the text of + // that notification. + backgroundMessage: 'Cancel to prevent battery drain.', + + // The title of the notification mentioned above. Defaults to "Using + // your location". + backgroundTitle: 'Tracking You.', + + // Whether permissions should be requested from the user automatically, + // if they are not already granted. Defaults to "true". + requestPermissions: true, + + // If "true", stale locations may be delivered while the device + // obtains a GPS fix. You are responsible for checking the "time" + // property. If "false", locations are guaranteed to be up to date. + // Defaults to "false". + stale: false, + + // The minimum number of metres between subsequent locations. Defaults + // to 0. + distanceFilter: 10, +}; + +export const startBackgroundGeolocation = async ( + newLocationHandler: any, + distanceFilter: number +) => { + backgroundGeolocationConfig.distanceFilter = distanceFilter; + const locationHandler = (location: any, error: any) => { + console.log('com.dyomedea.dyomedea LOG', ' - Callback'); + if (error) { + if (error.code === 'NOT_AUTHORIZED') { + if ( + window.confirm( + 'This app needs your location, ' + + 'but does not have permission.\n\n' + + 'Open settings now?' + ) + ) { + // It can be useful to direct the user to their device's + // settings when location permissions have been denied. The + // plugin provides the 'openSettings' method to do exactly + // this. + BackgroundGeolocation.openSettings(); + } + } + return console.error('com.dyomedea.dyomedea LOG', ' - error: ', error); + } + console.log(location); + if (location !== undefined) { + newLocationHandler(location); + } + + return console.log('com.dyomedea.dyomedea LOG', ' - location: ', location); + }; + + var watcher_id; + + console.log('com.dyomedea.dyomedea LOG', ' - Adding the watcher'); + await BackgroundGeolocation.addWatcher( + backgroundGeolocationConfig, + locationHandler + ) + .then(function after_the_watcher_has_been_added(id) { + // When a watcher is no longer needed, it should be removed by calling + // 'removeWatcher' with an object containing its ID. + console.log('com.dyomedea.dyomedea LOG', ' - Watcher added'); + watcher_id = id; + /*BackgroundGeolocation.removeWatcher({ + id: watcher_id, + }); */ + }) + .catch((reason) => { + console.error('com.dyomedea.dyomedea LOG', ' - reason: ', reason); + }); + return watcher_id; +}; + +export const stopBackgroundGeolocation = (watcher_id: any) => { + BackgroundGeolocation.removeWatcher({ + id: watcher_id, + }); +}; diff --git a/src/workers/dispatcher-worker.ts b/src/workers/dispatcher-worker.ts index 5b2ec65..a408b3c 100644 --- a/src/workers/dispatcher-worker.ts +++ b/src/workers/dispatcher-worker.ts @@ -7,11 +7,12 @@ import { pruneAndSaveImportedGpx, getGpx, putGpx, + appendTrk, getAllGpxes, getAllGpxesWithSummary, } from '../db/gpx'; import { getTrk, putNewTrk } from '../db/trk'; -import { getTrkseg } from '../db/trkseg'; +import { getTrkseg, appendTrkpt } from '../db/trkseg'; import { getWpt, putWpt } from '../db/wpt'; //const self = globalThis as unknown as WorkerGlobalScope; @@ -29,8 +30,10 @@ onmessage = async function (e) { getAllGpxesWithSummary, getGpx, putGpx, + appendTrk, getTrk, getTrkseg, + appendTrkpt, getWpt, putWpt,