Skip to content

Commit

Permalink
feat: make location sources switchable
Browse files Browse the repository at this point in the history
This adds the option to disable certain location sources.

See #930
  • Loading branch information
coderbyheart authored Dec 10, 2024
1 parent 72bdf4b commit bd6c177
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 89 deletions.
2 changes: 0 additions & 2 deletions src/components/fota/UpdateDevice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ export const UpdateDevice = ({
const upgradeType = bundleIdToType(firstBundle ?? '')
const firstTypeInUpgradePath = firstBundle?.split('*')[0]

console.log(firstTypeInUpgradePath, upgradeType)

const fotaSupported =
upgradeType !== null &&
fwTypes.find((type) => firstTypeInUpgradePath === type) !== undefined
Expand Down
45 changes: 22 additions & 23 deletions src/context/DeviceLocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ import type { TimeSpan } from '#api/api.js'
import { getObjectHistory } from '#api/getObjectHistory.js'
import { useDevice, type Device, type ListenerFn } from '#context/Device.js'
import { decodeMapState } from '#map/encodeMapState.js'
import {
toLocationSource,
type LocationSource,
} from '#map/LocationSourceLabels.js'
import { type LocationSource } from '#map/LocationSourceLabels.js'
import {
isGeolocation,
timeToDate,
Expand All @@ -19,7 +16,6 @@ import {
type Geolocation_14201,
type LwM2MObjectInstance,
} from '@hello.nrfcloud.com/proto-map/lwm2m'
import { isEqual } from 'lodash-es'
import { createContext, type ComponentChildren } from 'preact'
import { useContext, useEffect, useState } from 'preact/hooks'
import { useFingerprint } from './Fingerprint.js'
Expand Down Expand Up @@ -47,7 +43,7 @@ export const DeviceLocationContext = createContext<{
})

export const Provider = ({ children }: { children: ComponentChildren }) => {
const { onReported, device, reported } = useDevice()
const { onReported, device } = useDevice()
const [timeSpan, setTimeSpan] = useState<TimeSpan | undefined>(
isSSR
? undefined
Expand All @@ -71,6 +67,10 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
...l,
[instance.Resources[6]]: toGeoLocation(instance),
}))
console.debug(
`[DeviceLocation] Received location for ${device.id}`,
instance,
)
setTrail((t) =>
[instanceToTrail(device, instance.Resources), ...t].sort(byTs),
)
Expand All @@ -82,13 +82,6 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
}
}, [device])

useEffect(() => {
if (reported === undefined) return
const newLocations = locationsFromReported(reported)
if (isEqual(newLocations, locations)) return
setLocations(newLocations)
}, [reported])

useEffect(() => {
if (device === undefined) return
if (fingerprint === null) return
Expand Down Expand Up @@ -119,6 +112,22 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
)
.sort(byTs),
)
// Integrate newer locations into locations
setLocations((l) => {
for (const instance of partialInstances as Array<
LwM2MObjectInstance<Geolocation_14201>['Resources']
>) {
const src = instance[6] as LocationSource
if (
l[src] === undefined ||
l[src].ts.getTime() < instance[99] * 1000
) {
const geo = instanceToTrail(device, instance)
l[src] = geo
}
}
return l
})
})
})
}, [device, timeSpan, clustering])
Expand All @@ -143,16 +152,6 @@ export const Consumer = DeviceLocationContext.Consumer

export const useDeviceLocation = () => useContext(DeviceLocationContext)

const locationsFromReported = (
reported: Record<string, LwM2MObjectInstance>,
): Locations =>
Object.values(reported).reduce<Locations>((acc, instance) => {
if (isGeolocation(instance)) {
acc[toLocationSource(instance.Resources[6])] = toGeoLocation(instance)
}
return acc
}, {})

const instanceToTrail = (
device: Device,
{
Expand Down
27 changes: 26 additions & 1 deletion src/context/MapState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type MapStateType,
type MapStyle,
} from '#map/encodeMapState.js'
import type { LocationSource } from '#map/LocationSourceLabels.js'
import { isSSR } from '#utils/isSSR.js'
import { createContext, type ComponentChildren } from 'preact'
import { useContext, useEffect, useState } from 'preact/hooks'
Expand All @@ -24,6 +25,8 @@ export const MapStateContext = createContext<{
hideHistory: () => void
clusterTrail: (enabled: boolean) => void
setZoom: (zoom: MapStateType['zoom']) => void
enableLocation: (location: LocationSource) => void
disableLocation: (location: LocationSource) => void
}>({
unlock: () => undefined,
lock: () => undefined,
Expand All @@ -35,6 +38,8 @@ export const MapStateContext = createContext<{
setCenter: () => undefined,
setZoom: () => undefined,
clusterTrail: () => undefined,
enableLocation: () => undefined,
disableLocation: () => undefined,
})

export const Provider = ({ children }: { children: ComponentChildren }) => {
Expand All @@ -56,7 +61,7 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
useEffect(() => {
if (state === undefined) return
const encodedState = encodeMapState(state)
if (document.location.hash.includes(encodedState)) return
if (document.location.hash?.slice(1) === encodedState) return
console.debug(`[MapContext] Syncing state`, encodedState)
document.location.hash = `#${encodedState}`
}, [state])
Expand Down Expand Up @@ -127,6 +132,26 @@ export const Provider = ({ children }: { children: ComponentChildren }) => {
zoom,
}))
},
disableLocation: (location) => {
setState((state) => ({
...state,
disabledLocations: [
...new Set([...(state?.disabledLocations ?? []), location]),
],
}))
},
enableLocation: (location) => {
setState((state) => {
const { disabledLocations, ...rest } = state ?? {}
const disabled = disabledLocations?.filter((l) => l !== location)
if (disabled?.length === 0)
return { ...rest, disabledLocations: undefined }
return {
...rest,
disabledLocations: disabled,
}
})
},
}}
>
{children}
Expand Down
5 changes: 0 additions & 5 deletions src/map/CenterOnMapLocations.css

This file was deleted.

44 changes: 0 additions & 44 deletions src/map/CenterOnMapLocations.tsx

This file was deleted.

4 changes: 4 additions & 0 deletions src/map/LocationSourceSelector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.location-source-selector.controls button.control.disabled {
background-color: #959595;
color: #3d3a3a;
}
65 changes: 65 additions & 0 deletions src/map/LocationSourceSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useDeviceLocation } from '#context/DeviceLocation.js'
import { useMapState } from '#context/MapState.js'
import {
LocationSource,
LocationSourceLabels,
} from '#map/LocationSourceLabels.js'
import cx from 'classnames'
import {
BlendIcon,
EyeOffIcon,
HexagonIcon,
SatelliteIcon,
WifiIcon,
} from 'lucide-preact'

import './LocationSourceSelector.css'

export const LocationSourceSelector = () => {
const { enableLocation, disableLocation, state } = useMapState()
const { locations } = useDeviceLocation()
return (
<div class="location-source-selector controls horizontal me-3 mt-2 d-flex flex-row ">
{[
LocationSource.GNSS,
LocationSource.WIFI,
LocationSource.MCELL,
LocationSource.SCELL,
].map((src) => {
const disabled =
locations[src] === undefined ||
(state?.disabledLocations?.includes(src) ?? false)
const enabled = !disabled
return (
<button
type="button"
title={`${enabled ? 'Disable' : 'Enable'} ${LocationSourceLabels.get(src)}`}
onClick={() => {
if (enabled) {
disableLocation(src)
} else {
enableLocation(src)
}
}}
class={cx(`d-flex flex-row align-items-center control`, {
disabled: !enabled,
})}
>
{enabled && (
<span>
{src === LocationSource.SCELL && <HexagonIcon />}
{src === LocationSource.MCELL && <BlendIcon />}
{src === LocationSource.WIFI && <WifiIcon />}
{src === LocationSource.GNSS && <SatelliteIcon />}
</span>
)}
{!enabled && <EyeOffIcon />}
{LocationSourceLabels.has(src) && (
<span class="ms-2 label">{LocationSourceLabels.get(src)}</span>
)}
</button>
)
})}
</div>
)
}
34 changes: 20 additions & 14 deletions src/map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { useDeviceLocation, type TrailPoint } from '#context/DeviceLocation.js'
import { useMapState } from '#context/MapState.js'
import { useParameters } from '#context/Parameters.js'
import { CenterOnLatest } from '#map/CenterOnLatest.js'
import { CenterOnMapLocations } from '#map/CenterOnMapLocations.js'
import { HistoryControls } from '#map/HistoryControls.js'
import { LocationSourceSelector } from '#map/LocationSourceSelector.js'
import { LockInfo } from '#map/LockInfo.js'
import { MapZoomControls } from '#map/MapZoomControls.js'
import { MapStyle } from '#map/encodeMapState.js'
import { MapStyle, type MapStateType } from '#map/encodeMapState.js'
import { mapStyle as mapStyleLight } from '#map/style-light.js'
import { mapStyle as mapStyleDark } from '#map/style.js'
import { transformRequest } from '#map/transformRequest.js'
import type { GeoLocation } from '#proto/lwm2m.js'
import { byTsReverse } from '#utils/byTs.js'
import { formatDistanceToNow } from 'date-fns'
import { MapPinOff } from 'lucide-preact'
import maplibregl from 'maplibre-gl'
Expand All @@ -20,6 +21,7 @@ import {
locationSourceColors,
locationSourceColorsDark,
LocationSourceLabels,
type LocationSource,
} from './LocationSourceLabels.js'
import { addHexagon } from './addHexagon.js'
import { defaultColor } from './defaultColor.js'
Expand Down Expand Up @@ -169,7 +171,9 @@ export const Map = ({
const layerIds: string[] = []
const sourceIds: string[] = []

for (const location of Object.values(locations)) {
for (const location of Object.values(locations)
.sort(byTsReverse)
.filter(removeHidden(mapState.state))) {
const { lng, lat, acc, src, ts } = location
const locationCenterSourceId = `${location.src} - source - center`
const locationSourceLabel = `${location.src} - location - source - label`
Expand Down Expand Up @@ -248,32 +252,31 @@ export const Map = ({
}
}
}
}, [mapInstance, locations])
}, [mapInstance, locations, mapState.state])

// Trail
const clustering = mapState.state?.cluster ?? false

useEffect(() => {
if (mapInstance === undefined) return

const trailBySource = trail.reduce<Record<string, TrailPoint[]>>(
(acc, location) => {
const trailBySource = trail
.filter(removeHidden(mapState.state))
.reduce<Record<string, TrailPoint[]>>((acc, location) => {
if (acc[location.src] === undefined) {
acc[location.src] = [location]
} else {
acc[location.src]!.push(location)
}
return acc
},
{},
)
}, {})

const layerIds: string[] = []
const sourceIds: string[] = []

const locationPointIds = Object.values(locations).map((location) =>
hashLocation(location),
)
const locationPointIds = Object.values(locations)
.filter(removeHidden(mapState.state))
.map((location) => hashLocation(location))

for (const [src, trail] of Object.entries(trailBySource)) {
for (const point of trail) {
Expand Down Expand Up @@ -373,7 +376,7 @@ export const Map = ({
}
}
}
}, [mapInstance, trail])
}, [mapInstance, trail, mapState.state])

return (
<>
Expand All @@ -390,7 +393,7 @@ export const Map = ({
{mapInstance !== undefined && (
<>
<div class="locationControls">
<CenterOnMapLocations map={mapInstance} />
<LocationSourceSelector />
<HistoryControls />
</div>
<div class="mapControls controls vertical">
Expand All @@ -411,3 +414,6 @@ export const Map = ({

const hashLocation = (location: GeoLocation): string =>
`${location.lat}, ${location.lng}, ${location.acc}, ${location.src}, ${location.ts.getTime()}`

const removeHidden = (state?: MapStateType) => (point: { src: string }) =>
state?.disabledLocations?.includes?.(point.src as LocationSource) !== true
Loading

0 comments on commit bd6c177

Please sign in to comment.