Skip to content

Commit e3bb11a

Browse files
committed
feat(map): improve device map for multiple locations
This changes the behaviour of the device map so that it no longer zooms to a level that includes all device locations, but only zooms on the clicked on. This was changed because some devices can have old, far distant locations (e.g. single-cell) which no longer is relevant. Now the device map centers and zooms on the most recent location and remembers which location is supposed to be displayed (by source). This also adds a button to center the device map on each individual location source. Finally, the tolerance for the GeoJSON object that renders the different location sources is lowered so that the hexagon is displayed at any zoom level. Fixes #381 See #378
1 parent 0e5e978 commit e3bb11a

File tree

11 files changed

+175
-35
lines changed

11 files changed

+175
-35
lines changed

src/MapApp.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export const MapApp = () => {
3636
// However, this app's sidebar should not overflow the viewport.
3737
const appHeight = `${window.innerHeight}px`
3838
document.documentElement.style.setProperty('--app-height', appHeight)
39-
console.debug(`[MapApp]`, 'appHeight:', appHeight)
4039
})
4140
return (
4241
<div id="layout">

src/component/AllDevicesMap/AllDevicesMap.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,12 @@ export const AllDevicesMap = () => {
104104
'click',
105105
'devices-dots',
106106
(e: MapMouseEvent & { features?: MapGeoJSONFeature[] }) => {
107-
const id = e.features?.[0]?.properties.id
107+
const { id, source } = e.features?.[0]?.properties ?? {}
108108
location.navigate({
109109
panel: `id:${id}`,
110+
deviceMap: {
111+
centerLocationSource: source,
112+
},
110113
})
111114
},
112115
)
@@ -139,7 +142,7 @@ export const AllDevicesMap = () => {
139142
({
140143
device: { id },
141144
location: {
142-
Resources: { 0: lat, 1: lng },
145+
Resources: { 0: lat, 1: lng, 6: source },
143146
},
144147
resources,
145148
}) => ({
@@ -153,6 +156,7 @@ export const AllDevicesMap = () => {
153156
resourceValues: resources
154157
.map(({ value, units }) => `${value} ${units ?? ''}`)
155158
.join('\n'),
159+
source,
156160
},
157161
}),
158162
),

src/component/KnownObjects/KnownObjects.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { Card as LocationCard, Icon as LocationIcon } from './Location.js'
1919
import { Card as PinnedCard, Icon as PinnedIcon } from './Pinned.js'
2020

21+
import { Center } from '#icons/LucideIcon.tsx'
2122
import './KnownObjects.css'
2223

2324
enum TabType {
@@ -126,8 +127,26 @@ export const KnownObjects = (props: {
126127
</Show>
127128
<Show when={isActive(TabType.Location) && hasLocations}>
128129
<For each={props.locations}>
129-
{(location) => (
130-
<DescribeInstance device={props.device} instance={location} />
130+
{(geoLocation) => (
131+
<DescribeInstance
132+
device={props.device}
133+
instance={geoLocation}
134+
actions={
135+
<button
136+
type="button"
137+
onClick={() => {
138+
location.navigate({
139+
deviceMap: {
140+
centerLocationSource: geoLocation.Resources[6],
141+
},
142+
})
143+
}}
144+
title="Center map on location"
145+
>
146+
<Center strokeWidth={1} />
147+
</button>
148+
}
149+
/>
131150
)}
132151
</For>
133152
</Show>

src/component/KnownObjects/Location.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ div.device-map .maplibregl-ctrl-scale {
2727
border-color: var(--chart-labels);
2828
color: var(--chart-labels);
2929
}
30+
31+
div.device-map .maplibregl-canvas-container {
32+
cursor: default;
33+
}

src/component/KnownObjects/Location.tsx

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { useNavigation } from '#context/Navigation.tsx'
12
import { useParameters } from '#context/Parameters.js'
2-
import { Center, Map, ZoomIn, ZoomOut } from '#icons/LucideIcon.js'
3+
import { Lock, Map, Unlock, ZoomIn, ZoomOut } from '#icons/LucideIcon.js'
34
import { createMap } from '#map/createMap.js'
45
import { geoJSONPolygonFromCircle } from '#map/geoJSONPolygonFromCircle.js'
5-
import { getLocationsBounds } from '#map/getLocationsBounds.js'
6+
import { getLocationsBounds } from '#map/getLocationsBounds.ts'
67
import { glyphFonts } from '#map/glyphFonts.js'
78
import {
89
defaultLocationSourceColor,
@@ -11,7 +12,13 @@ import {
1112
import { type Geolocation_14201 } from '@hello.nrfcloud.com/proto-map/lwm2m'
1213
import type { Map as MapLibreGlMap } from 'maplibre-gl'
1314
import { ScaleControl } from 'maplibre-gl'
14-
import { createEffect, createMemo, onCleanup } from 'solid-js'
15+
import {
16+
createEffect,
17+
createMemo,
18+
createSignal,
19+
onCleanup,
20+
Show,
21+
} from 'solid-js'
1522

1623
import './Location.css'
1724

@@ -29,11 +36,18 @@ const byAge = (loc1: Geolocation_14201, loc2: Geolocation_14201) =>
2936

3037
export const Card = (props: { locations: Geolocation_14201[] }) => {
3138
const parameters = useParameters()
39+
const location = useNavigation()
40+
const [locked, setLocked] = createSignal(true)
3241

3342
let ref!: HTMLDivElement
3443
let map: MapLibreGlMap
3544

36-
const bounds = createMemo(() => getLocationsBounds(props.locations))
45+
const centerLocation = createMemo(() =>
46+
props.locations.find(
47+
({ Resources }) =>
48+
Resources[6] === location.current()?.deviceMap?.centerLocationSource,
49+
),
50+
)
3751

3852
createEffect(() => {
3953
const mostRecent = props.locations.sort(byAge)[0]
@@ -42,7 +56,7 @@ export const Card = (props: { locations: Geolocation_14201[] }) => {
4256

4357
const {
4458
Resources: { 0: lat, 1: lng },
45-
} = mostRecent
59+
} = centerLocation() ?? mostRecent
4660

4761
map = createMap(
4862
ref,
@@ -65,18 +79,20 @@ export const Card = (props: { locations: Geolocation_14201[] }) => {
6579
const lat = Resources[0]
6680
const acc = Resources[3] ?? 500
6781
const src = Resources[6]
82+
6883
// Data for Hexagon
6984
const locationAreaSourceId = `center-circle-source-${src}`
70-
map.addSource(
71-
locationAreaSourceId,
72-
geoJSONPolygonFromCircle([lng, lat], acc, 6, Math.PI / 2),
73-
)
85+
map.addSource(locationAreaSourceId, {
86+
...geoJSONPolygonFromCircle([lng, lat], acc, 6, Math.PI / 2),
87+
// This will ensure that the polygon is drawn even at low zoom levels
88+
// See https://docs.mapbox.com/help/troubleshooting/working-with-large-geojson-data/#tolerance
89+
tolerance: 0.001,
90+
})
7491
// Render Hexagon
7592
map.addLayer({
7693
id: `center-circle-layer-${src}`,
7794
type: 'line',
78-
source: `center-circle-source-${src}`,
79-
layout: {},
95+
source: locationAreaSourceId,
8096
paint: {
8197
'line-color':
8298
locationSourceColors[src] ?? defaultLocationSourceColor,
@@ -105,16 +121,29 @@ export const Card = (props: { locations: Geolocation_14201[] }) => {
105121
},
106122
})
107123
}
124+
})
108125

109-
map.fitBounds(bounds(), {
110-
padding: 20,
111-
maxZoom: 16,
112-
})
126+
onCleanup(() => {
127+
map?.remove()
113128
})
114129
})
115130

116-
onCleanup(() => {
117-
map?.remove()
131+
createEffect(() => {
132+
if (centerLocation() === undefined) return
133+
map.fitBounds(getLocationsBounds([centerLocation()!]), {
134+
padding: 40,
135+
maxZoom: 16,
136+
})
137+
})
138+
139+
createEffect(() => {
140+
if (locked()) {
141+
map.scrollZoom.disable()
142+
map.dragPan.disable()
143+
} else {
144+
map.scrollZoom.enable()
145+
map.dragPan.enable()
146+
}
118147
})
119148

120149
return (
@@ -123,17 +152,28 @@ export const Card = (props: { locations: Geolocation_14201[] }) => {
123152
<button type="button" onClick={() => map?.zoomIn()}>
124153
<ZoomIn />
125154
</button>
126-
<button
127-
type="button"
128-
onClick={() =>
129-
map?.fitBounds(bounds(), {
130-
padding: 20,
131-
maxZoom: 16,
132-
})
155+
<Show
156+
when={locked()}
157+
fallback={
158+
<button
159+
type="button"
160+
onClick={() => {
161+
setLocked(true)
162+
}}
163+
>
164+
<Lock />
165+
</button>
133166
}
134167
>
135-
<Center />
136-
</button>
168+
<button
169+
type="button"
170+
onClick={() => {
171+
setLocked(false)
172+
}}
173+
>
174+
<Unlock />
175+
</button>
176+
</Show>
137177
<button type="button" onClick={() => map?.zoomOut()}>
138178
<ZoomOut />
139179
</button>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.lwm2m.instance header {
2+
display: flex;
3+
justify-content: space-between;
4+
}
5+
6+
.lwm2m.instance header .actions {
7+
display: flex;
8+
align-items: center;
9+
}

src/component/lwm2m/DescribeInstance.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import {
55
instanceTs,
66
type LwM2MObjectInstance,
77
} from '@hello.nrfcloud.com/proto-map/lwm2m'
8-
import { Show } from 'solid-js'
8+
import { Show, type JSX } from 'solid-js'
99
import { RelativeTime } from '../RelativeTime.js'
1010
import { ToggleButton } from '../ToggleButton.jsx'
1111
import { WhenToggled } from '../WhenToggled.jsx'
1212
import { DescribeObject } from './DescribeObject.js'
1313
import { DescribeResources } from './DescribeResources.js'
1414

15+
import './DescribeInstance.css'
16+
1517
export const DescribeInstance = (props: {
1618
instance: LwM2MObjectInstance
1719
device: Device
20+
actions?: JSX.Element
1821
}) => {
1922
const definition = definitions[props.instance.ObjectID]
2023
const ts = instanceTs(props.instance)
@@ -51,7 +54,10 @@ export const DescribeInstance = (props: {
5154
</RelativeTime>
5255
</small>
5356
</h2>
54-
<ToggleButton title="resources" id={toggleId} />
57+
<div class="actions">
58+
{props.actions}
59+
<ToggleButton title="resources" id={toggleId} />
60+
</div>
5561
</header>
5662
<WhenToggled id={toggleId}>
5763
<DescribeResources

src/context/navigation/decodeNavigation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ export const decode = (encoded?: string): Navigation | undefined => {
8282
}
8383
}
8484

85+
const deviceMapState = rest.find(
86+
(s) => s.split(':', 2)[0] === FieldKey.DeviceMap,
87+
)
88+
if (deviceMapState !== undefined) {
89+
const [, centerLocationSource] = deviceMapState.split(':', 3)
90+
nav.deviceMap = {
91+
centerLocationSource: centerLocationSource ?? 'none',
92+
}
93+
}
94+
8595
const helpState = rest.find((s) => s.split(':', 2)[0] === FieldKey.Tutorial)
8696
if (helpState !== undefined) {
8797
nav.tutorial = helpState.split(':', 2)[1] as string

src/context/navigation/encodeNavigation.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ void describe('encode() / decode()', () => {
107107
},
108108
))
109109

110+
void it('should encode the device map state', () =>
111+
assert.deepEqual(
112+
decode(
113+
encode({
114+
panel: 'world',
115+
deviceMap: {
116+
centerLocationSource: 'GNSS',
117+
},
118+
}),
119+
),
120+
{
121+
panel: 'world',
122+
search: [],
123+
pinnedResources: [],
124+
toggled: [],
125+
deviceMap: {
126+
centerLocationSource: 'GNSS',
127+
},
128+
},
129+
))
130+
110131
void it('should encode the tutorial state', () =>
111132
assert.deepEqual(
112133
decode(

src/context/navigation/encodeNavigation.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ export type NavigationMapState = {
1313
center: { lat: number; lng: number }
1414
zoom: number
1515
}
16+
17+
// Encode which location source is currently centered on the map
18+
export type DeviceMapState = {
19+
centerLocationSource: string
20+
}
1621
export type Navigation = {
1722
panel: string
1823
search: SearchTerm[]
1924
pinnedResources: PinnedResource[]
2025
map?: NavigationMapState
26+
deviceMap?: DeviceMapState
2127
tutorial?: keyof TutorialContent
2228
toggled: string[]
2329
query?: URLSearchParams
@@ -27,6 +33,7 @@ export enum FieldKey {
2733
Search = 's',
2834
PinnedResources = 'r',
2935
Map = 'm',
36+
DeviceMap = 'M',
3037
Tutorial = 'T',
3138
Toggled = 't',
3239
}
@@ -38,8 +45,16 @@ export const encode = (
3845
): string | undefined => {
3946
if (navigation === undefined) return ''
4047
const parts = []
41-
const { panel, search, pinnedResources, map, tutorial, toggled, query } =
42-
navigation
48+
const {
49+
panel,
50+
search,
51+
pinnedResources,
52+
map,
53+
deviceMap,
54+
tutorial,
55+
toggled,
56+
query,
57+
} = navigation
4358
let panelWithQuery = `${panel ?? ''}`
4459
if (query !== undefined) panelWithQuery += '?' + query.toString()
4560
parts.push(panelWithQuery)
@@ -69,6 +84,9 @@ export const encode = (
6984
`${FieldKey.Map}:${map.zoom}:${map.center.lat},${map.center.lng}`,
7085
)
7186
}
87+
if (deviceMap !== undefined) {
88+
parts.push(`${FieldKey.DeviceMap}:${deviceMap.centerLocationSource}`)
89+
}
7290
if (tutorial !== undefined) {
7391
parts.push(`${FieldKey.Tutorial}:${tutorial}`)
7492
}

0 commit comments

Comments
 (0)