Skip to content

Commit c54fbce

Browse files
authored
Visible geometry option added to addPanel options (#151)
* visibleGeometry option added to panel options
1 parent e89e1ce commit c54fbce

File tree

18 files changed

+757
-78
lines changed

18 files changed

+757
-78
lines changed

demo/js/index.js

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ const interactiveMap = new InteractiveMap('map', {
126126
transformRequest: transformTileRequest,
127127
enableZoomControls: true,
128128
readMapText: true,
129-
// enableFullscreen: true,
129+
enableFullscreen: true,
130130
// hasExitButton: true,
131131
// markers: [{
132132
// id: 'location',
@@ -245,72 +245,6 @@ interactiveMap.on('datasets:ready', function () {
245245
let selectedFeatureIds = []
246246

247247
interactiveMap.on('draw:ready', function () {
248-
// interactiveMap.addButton('drawPolygon', {
249-
// label: 'Draw polygon',
250-
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
251-
// iconSvgContent: '<path d="M19.5 7v10M4.5 7v10M7 19.5h10M7 4.5h10"/><path d="M22 18v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zm0-15v3a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 18v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1zM7 3v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1z"/>',
252-
// isPressed: false,
253-
// mobile: { slot: 'right-top' },
254-
// tablet: { slot: 'right-top' },
255-
// desktop: { slot: 'right-top' },
256-
// onClick: function (e) {
257-
// e.target.setAttribute('aria-pressed', true)
258-
// drawPlugin.newPolygon(crypto.randomUUID(), {
259-
// stroke: '#e6c700',
260-
// fill: 'rgba(255, 221, 0, 0.1)'
261-
// })
262-
// }
263-
// })
264-
// interactiveMap.addButton('drawLine', {
265-
// label: 'Draw line',
266-
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
267-
// iconSvgContent: '<path d="M5.706 16.294L16.294 5.706"/><path d="M21 2v3c0 .549-.451 1-1 1h-3c-.549 0-1-.451-1-1V2c0-.549.451-1 1-1h3c.549 0 1 .451 1 1zM6 17v3c0 .549-.451 1-1 1H2c-.549 0-1-.451-1-1v-3c0-.549.451-1 1-1h3c.549 0 1 .451 1 1z"/>',
268-
// isPressed: false,
269-
// mobile: { slot: 'right-top' },
270-
// tablet: { slot: 'right-top' },
271-
// desktop: { slot: 'right-top' },
272-
// onClick: function (e) {
273-
// e.target.setAttribute('aria-pressed', true)
274-
// drawPlugin.newLine(crypto.randomUUID(), {
275-
// stroke: { outdoor: '#99704a', dark: '#ffffff' }
276-
// })
277-
// }
278-
// })
279-
// interactiveMap.addButton('editFeature', {
280-
// label: 'Edit feature',
281-
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
282-
// iconSvgContent: '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>',
283-
// isDisabled: true,
284-
// mobile: { slot: 'right-top' },
285-
// tablet: { slot: 'right-top' },
286-
// desktop: { slot: 'right-top' },
287-
// onClick: function (e) {
288-
// if (e.target.getAttribute('aria-disabled') === 'true') {
289-
// return
290-
// }
291-
// interactPlugin.disable()
292-
// drawPlugin.editFeature(selectedFeatureId)
293-
// }
294-
// })
295-
// interactiveMap.addButton('deleteFeature', {
296-
// label: 'Delete feature',
297-
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
298-
// iconSvgContent: '<path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
299-
// isDisabled: true,
300-
// mobile: { slot: 'right-top' },
301-
// tablet: { slot: 'right-top' },
302-
// desktop: { slot: 'right-top' },
303-
// onClick: function (e) {
304-
// if (e.target.getAttribute('aria-disabled') === 'true') {
305-
// return
306-
// }
307-
// drawPlugin.deleteFeature(selectedFeatureId)
308-
// interactiveMap.toggleButtonState('drawPolygon', 'disabled', false)
309-
// interactiveMap.toggleButtonState('drawLine', 'disabled', false)
310-
// interactiveMap.toggleButtonState('editFeature', 'disabled', true)
311-
// interactiveMap.toggleButtonState('deleteFeature', 'disabled', true)
312-
// }
313-
// })
314248
drawPlugin.addFeature({
315249
id: 'test1234',
316250
type: 'Feature',

demo/js/planning.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,20 @@ interactiveMap.on('map:stylechange', function (e) {
153153
updateKeyColours(e.mapStyleId)
154154
})
155155

156+
interactiveMap.on('app:panelopened', (e) => {
157+
// console.log('app:panelopened', e)
158+
})
156159

157160
interactiveMap.on('map:exit', function (e) {
158161
drawOptions = ['shape', 'square']
159162
})
160163

161164
interactiveMap.on('interact:markerchange', function (e) {
162-
console.log(e)
165+
interactiveMap.addPanel('info', {
166+
label: 'Info',
167+
html: '<p>Some info</p>',
168+
visibleGeometry: {type: 'Feature', geometry: {type: 'Point', coordinates: e.coords}}
169+
})
163170
})
164171

165172
interactiveMap.on('draw:ready', function () {

plugins/interact/src/events.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export function attachEvents ({
3939
// (e.g. finishing a draw gesture) fires before this handler is live.
4040
let clickReady = false
4141
const clickReadyTimer = setTimeout(() => { clickReady = true }, 0)
42-
const handleMapClick = (e) => { if (clickReady) { handleInteraction(e) } }
42+
const handleMapClick = (e) => {
43+
if (clickReady) {
44+
handleInteraction(e)
45+
}
46+
}
4347
const handleSelectAtTarget = () => handleInteraction(mapState.crossHair.getDetail())
4448

4549
const handleSelectDone = () => {

providers/beta/esri/src/esriProvider.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import esriConfig from '@arcgis/core/config.js'
44
import EsriMap from '@arcgis/core/Map.js'
55
import MapView from '@arcgis/core/views/MapView.js'
66
import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js'
7+
import Point from '@arcgis/core/geometry/Point.js'
78
import { defaults, supportedShortcuts } from './defaults.js'
89
import { attachAppEvents } from './appEvents.js'
910
import { attachMapEvents } from './mapEvents.js'
10-
import { getAreaDimensions, getCardinalMove, getPaddedExtent } from './utils/spatial.js'
11+
import { getAreaDimensions, getCardinalMove, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
1112
import { queryVectorTileFeatures } from './utils/query.js'
1213
import { getExtentFromFlatCoords, getPointFromFlatCoords, getBboxFromGeoJSON } from './utils/coords.js'
1314
import { cleanDOM } from './utils/esriFixes.js'
@@ -118,9 +119,11 @@ export default class EsriProvider {
118119
// Side-effects
119120
// ==========================
120121

121-
setView ({ center, zoom }) {
122+
setView({ center, zoom }) {
122123
this.view.animation?.destroy()
123-
this.view.goTo({ center, zoom, duration: defaults.animationDuration })
124+
const point = center ? new Point({ x: center[0], y: center[1], spatialReference: { wkid: 27700 }}) : this.view.center
125+
const target = { center: point, zoom: zoom ?? this.view.zoom }
126+
this.view.goTo({ ...target, duration: defaults.animationDuration })
124127
}
125128

126129
zoomIn (zoomDelta) {
@@ -203,4 +206,15 @@ export default class EsriProvider {
203206
const mapPoint = this.view.toMap(point)
204207
return [mapPoint.x, mapPoint.y]
205208
}
209+
210+
/**
211+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
212+
*
213+
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry.
214+
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates).
215+
* @returns {boolean}
216+
*/
217+
isGeometryObscured (geojson, panelRect) {
218+
return isGeometryObscured(geojson, panelRect, this.view)
219+
}
206220
}

providers/beta/esri/src/utils/coords.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const collectCoords = (obj, acc) => {
3333
obj.geometries.forEach(g => collectCoords(g, acc))
3434
} else {
3535
const flatten = (coords) => {
36+
if (!Array.isArray(coords)) { return }
3637
if (typeof coords[0] === 'number') acc.push(coords)
3738
else coords.forEach(flatten)
3839
}

providers/beta/esri/src/utils/spatial.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import Extent from '@arcgis/core/geometry/Extent.js'
2+
import Point from '@arcgis/core/geometry/Point.js'
3+
import { getBboxFromGeoJSON } from './coords.js'
24

35
// -----------------------------------------------------------------------------
46
// Internal (not exported)
@@ -124,8 +126,52 @@ const getPaddedExtent = (view, padding = DEFAULT_PADDING) => {
124126
}
125127

126128

129+
/**
130+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
131+
* Used to decide whether to pan/zoom when a panel opens over a visibleGeometry target.
132+
*
133+
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry
134+
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates)
135+
* @param {import('@arcgis/core/views/MapView.js').default} view - ESRI MapView instance
136+
* @returns {boolean}
137+
*/
138+
const isGeometryObscured = (geojson, panelRect, view) => {
139+
if (!view?.container) {
140+
return false
141+
}
142+
143+
const containerRect = view.container.getBoundingClientRect()
144+
const extent = getBboxFromGeoJSON(geojson)
145+
146+
const corners = [
147+
view.toScreen(new Point({ x: extent.xmin, y: extent.ymin, spatialReference: extent.spatialReference })),
148+
view.toScreen(new Point({ x: extent.xmin, y: extent.ymax, spatialReference: extent.spatialReference })),
149+
view.toScreen(new Point({ x: extent.xmax, y: extent.ymin, spatialReference: extent.spatialReference })),
150+
view.toScreen(new Point({ x: extent.xmax, y: extent.ymax, spatialReference: extent.spatialReference }))
151+
]
152+
153+
const screenMinX = Math.min(...corners.map(c => c.x))
154+
const screenMaxX = Math.max(...corners.map(c => c.x))
155+
const screenMinY = Math.min(...corners.map(c => c.y))
156+
const screenMaxY = Math.max(...corners.map(c => c.y))
157+
158+
// Convert panelRect from viewport coords to view-container-relative coords
159+
const panelLeft = panelRect.left - containerRect.left
160+
const panelTop = panelRect.top - containerRect.top
161+
const panelRight = panelRect.right - containerRect.left
162+
const panelBottom = panelRect.bottom - containerRect.top
163+
164+
return (
165+
screenMinX < panelRight &&
166+
screenMaxX > panelLeft &&
167+
screenMinY < panelBottom &&
168+
screenMaxY > panelTop
169+
)
170+
}
171+
127172
export {
128173
getAreaDimensions,
129174
getCardinalMove,
130-
getPaddedExtent
175+
getPaddedExtent,
176+
isGeometryObscured
131177
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { isGeometryObscured } from './spatial.js'
2+
3+
jest.mock('@arcgis/core/geometry/Extent.js', () =>
4+
jest.fn().mockImplementation((opts) => ({ ...opts, type: 'extent' }))
5+
)
6+
7+
jest.mock('@arcgis/core/geometry/Point.js', () =>
8+
jest.fn().mockImplementation((opts) => ({ ...opts, type: 'point' }))
9+
)
10+
11+
jest.mock('./coords.js', () => ({
12+
getBboxFromGeoJSON: jest.fn(() => ({
13+
xmin: 100, ymin: 200, xmax: 500, ymax: 600,
14+
spatialReference: { wkid: 27700 },
15+
type: 'extent'
16+
}))
17+
}))
18+
19+
describe('isGeometryObscured', () => {
20+
const geojson = { type: 'Feature', geometry: { type: 'Point', coordinates: [300, 400] }, properties: {} }
21+
22+
// Container sits at viewport origin so container-relative coords equal viewport coords
23+
const makeView = (toScreenFn) => ({
24+
container: {
25+
getBoundingClientRect: jest.fn(() => ({ left: 0, top: 0, right: 1000, bottom: 800 }))
26+
},
27+
toScreen: jest.fn(toScreenFn)
28+
})
29+
30+
// Panel occupies the right 400px of the viewport
31+
const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
32+
33+
test('returns false when view has no container', () => {
34+
expect(isGeometryObscured(geojson, panelRect, null)).toBe(false)
35+
expect(isGeometryObscured(geojson, panelRect, {})).toBe(false)
36+
})
37+
38+
test('returns true when geometry screen bbox overlaps the panel rect', () => {
39+
// Corners project into the panel (x: 650 is between panelLeft 600 and panelRight 1000)
40+
const view = makeView(() => ({ x: 650, y: 400 }))
41+
expect(isGeometryObscured(geojson, panelRect, view)).toBe(true)
42+
})
43+
44+
test('returns false when geometry screen bbox does not overlap the panel rect', () => {
45+
// Corners project to x: 300, entirely left of panelLeft (600)
46+
const view = makeView(() => ({ x: 300, y: 400 }))
47+
expect(isGeometryObscured(geojson, panelRect, view)).toBe(false)
48+
})
49+
50+
test('projects all four bbox corners', () => {
51+
const view = makeView(() => ({ x: 300, y: 400 }))
52+
isGeometryObscured(geojson, panelRect, view)
53+
expect(view.toScreen).toHaveBeenCalledTimes(4)
54+
})
55+
})

providers/maplibre/src/maplibreProvider.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DEFAULTS, supportedShortcuts } from './defaults.js'
77
import { cleanCanvas, applyPreventDefaultFix } from './utils/maplibreFixes.js'
88
import { attachMapEvents } from './mapEvents.js'
99
import { attachAppEvents } from './appEvents.js'
10-
import { getAreaDimensions, getCardinalMove, getBboxFromGeoJSON, getResolution, getPaddedBounds } from './utils/spatial.js'
10+
import { getAreaDimensions, getCardinalMove, getBboxFromGeoJSON, isGeometryObscured, getResolution, getPaddedBounds } from './utils/spatial.js'
1111
import { createMapLabelNavigator } from './utils/labels.js'
1212
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
1313
import { queryFeatures } from './utils/queryFeatures.js'
@@ -337,4 +337,15 @@ export default class MapLibreProvider {
337337
const { lng, lat } = this.map.unproject([point.x, point.y])
338338
return [lng, lat]
339339
}
340+
341+
/**
342+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
343+
*
344+
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry.
345+
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates).
346+
* @returns {boolean}
347+
*/
348+
isGeometryObscured (geojson, panelRect) {
349+
return isGeometryObscured(geojson, panelRect, this.map)
350+
}
340351
}

providers/maplibre/src/maplibreProvider.test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { attachAppEvents } from './appEvents.js'
44
import { createMapLabelNavigator } from './utils/labels.js'
55
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
66
import { queryFeatures } from './utils/queryFeatures.js'
7-
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds } from './utils/spatial.js'
7+
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds, isGeometryObscured } from './utils/spatial.js'
88

99
jest.mock('./defaults.js', () => ({
1010
DEFAULTS: { animationDuration: 400, coordinatePrecision: 7 },
@@ -20,6 +20,7 @@ jest.mock('./utils/spatial.js', () => ({
2020
getAreaDimensions: jest.fn(() => '400m by 750m'),
2121
getCardinalMove: jest.fn(() => 'north'),
2222
getBboxFromGeoJSON: jest.fn(() => [-1, 50, 1, 52]),
23+
isGeometryObscured: jest.fn(() => true),
2324
getResolution: jest.fn(() => 10),
2425
getPaddedBounds: jest.fn(() => [[0, 0], [1, 1]])
2526
}))
@@ -170,6 +171,18 @@ describe('MapLibreProvider', () => {
170171
expect(map.fitBounds).toHaveBeenCalledWith([-1, 50, 1, 52], { duration: 400 })
171172
})
172173

174+
test('isGeometryObscured delegates to spatial utility with map instance', async () => {
175+
const p = makeProvider()
176+
await doInitMap(p)
177+
const geojson = { type: 'Feature', geometry: { type: 'Point', coordinates: [1, 52] }, properties: {} }
178+
const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
179+
180+
const result = p.isGeometryObscured(geojson, panelRect)
181+
182+
expect(isGeometryObscured).toHaveBeenCalledWith(geojson, panelRect, map)
183+
expect(result).toBe(true)
184+
})
185+
173186
test('getCenter, getZoom, getBounds return formatted values', async () => {
174187
const p = makeProvider()
175188
await doInitMap(p)

providers/maplibre/src/utils/spatial.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,50 @@ const getPaddedBounds = (LngLatBounds, map) => {
196196
*/
197197
const getBboxFromGeoJSON = (geojson) => turfBbox(geojson)
198198

199+
/**
200+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
201+
* Used to decide whether to pan/zoom when a panel opens over a visibleGeometry target.
202+
*
203+
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry
204+
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates)
205+
* @param {object} map - MapLibre map instance
206+
* @returns {boolean}
207+
*/
208+
const isGeometryObscured = (geojson, panelRect, map) => {
209+
const containerRect = map.getContainer().getBoundingClientRect()
210+
const [west, south, east, north] = getBboxFromGeoJSON(geojson)
211+
212+
const corners = [
213+
map.project([west, south]),
214+
map.project([west, north]),
215+
map.project([east, south]),
216+
map.project([east, north])
217+
]
218+
219+
const screenMinX = Math.min(...corners.map(c => c.x))
220+
const screenMaxX = Math.max(...corners.map(c => c.x))
221+
const screenMinY = Math.min(...corners.map(c => c.y))
222+
const screenMaxY = Math.max(...corners.map(c => c.y))
223+
224+
// Convert panelRect from viewport coords to map-container-relative coords
225+
const panelLeft = panelRect.left - containerRect.left
226+
const panelTop = panelRect.top - containerRect.top
227+
const panelRight = panelRect.right - containerRect.left
228+
const panelBottom = panelRect.bottom - containerRect.top
229+
230+
return (
231+
screenMinX < panelRight &&
232+
screenMaxX > panelLeft &&
233+
screenMinY < panelBottom &&
234+
screenMaxY > panelTop
235+
)
236+
}
237+
199238
export {
200239
getAreaDimensions,
201240
getCardinalMove,
202241
getBboxFromGeoJSON,
242+
isGeometryObscured,
203243
spatialNavigate,
204244
getResolution,
205245
getPaddedBounds,

0 commit comments

Comments
 (0)