Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 1 addition & 67 deletions demo/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const interactiveMap = new InteractiveMap('map', {
transformRequest: transformTileRequest,
enableZoomControls: true,
readMapText: true,
// enableFullscreen: true,
enableFullscreen: true,
// hasExitButton: true,
// markers: [{
// id: 'location',
Expand Down Expand Up @@ -245,72 +245,6 @@ interactiveMap.on('datasets:ready', function () {
let selectedFeatureIds = []

interactiveMap.on('draw:ready', function () {
// interactiveMap.addButton('drawPolygon', {
// label: 'Draw polygon',
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
// 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"/>',
// isPressed: false,
// mobile: { slot: 'right-top' },
// tablet: { slot: 'right-top' },
// desktop: { slot: 'right-top' },
// onClick: function (e) {
// e.target.setAttribute('aria-pressed', true)
// drawPlugin.newPolygon(crypto.randomUUID(), {
// stroke: '#e6c700',
// fill: 'rgba(255, 221, 0, 0.1)'
// })
// }
// })
// interactiveMap.addButton('drawLine', {
// label: 'Draw line',
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
// 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"/>',
// isPressed: false,
// mobile: { slot: 'right-top' },
// tablet: { slot: 'right-top' },
// desktop: { slot: 'right-top' },
// onClick: function (e) {
// e.target.setAttribute('aria-pressed', true)
// drawPlugin.newLine(crypto.randomUUID(), {
// stroke: { outdoor: '#99704a', dark: '#ffffff' }
// })
// }
// })
// interactiveMap.addButton('editFeature', {
// label: 'Edit feature',
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
// 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"/>',
// isDisabled: true,
// mobile: { slot: 'right-top' },
// tablet: { slot: 'right-top' },
// desktop: { slot: 'right-top' },
// onClick: function (e) {
// if (e.target.getAttribute('aria-disabled') === 'true') {
// return
// }
// interactPlugin.disable()
// drawPlugin.editFeature(selectedFeatureId)
// }
// })
// interactiveMap.addButton('deleteFeature', {
// label: 'Delete feature',
// group: { name: 'drawing-tools', label: 'Drawing tools', order: 2 },
// 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"/>',
// isDisabled: true,
// mobile: { slot: 'right-top' },
// tablet: { slot: 'right-top' },
// desktop: { slot: 'right-top' },
// onClick: function (e) {
// if (e.target.getAttribute('aria-disabled') === 'true') {
// return
// }
// drawPlugin.deleteFeature(selectedFeatureId)
// interactiveMap.toggleButtonState('drawPolygon', 'disabled', false)
// interactiveMap.toggleButtonState('drawLine', 'disabled', false)
// interactiveMap.toggleButtonState('editFeature', 'disabled', true)
// interactiveMap.toggleButtonState('deleteFeature', 'disabled', true)
// }
// })
drawPlugin.addFeature({
id: 'test1234',
type: 'Feature',
Expand Down
9 changes: 8 additions & 1 deletion demo/js/planning.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,20 @@ interactiveMap.on('map:stylechange', function (e) {
updateKeyColours(e.mapStyleId)
})

interactiveMap.on('app:panelopened', (e) => {
// console.log('app:panelopened', e)
})

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

interactiveMap.on('interact:markerchange', function (e) {
console.log(e)
interactiveMap.addPanel('info', {
label: 'Info',
html: '<p>Some info</p>',
visibleGeometry: {type: 'Feature', geometry: {type: 'Point', coordinates: e.coords}}
})
})

interactiveMap.on('draw:ready', function () {
Expand Down
6 changes: 5 additions & 1 deletion plugins/interact/src/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export function attachEvents ({
// (e.g. finishing a draw gesture) fires before this handler is live.
let clickReady = false
const clickReadyTimer = setTimeout(() => { clickReady = true }, 0)
const handleMapClick = (e) => { if (clickReady) { handleInteraction(e) } }
const handleMapClick = (e) => {
if (clickReady) {
handleInteraction(e)
}
}
const handleSelectAtTarget = () => handleInteraction(mapState.crossHair.getDetail())

const handleSelectDone = () => {
Expand Down
20 changes: 17 additions & 3 deletions providers/beta/esri/src/esriProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import esriConfig from '@arcgis/core/config.js'
import EsriMap from '@arcgis/core/Map.js'
import MapView from '@arcgis/core/views/MapView.js'
import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js'
import Point from '@arcgis/core/geometry/Point.js'
import { defaults, supportedShortcuts } from './defaults.js'
import { attachAppEvents } from './appEvents.js'
import { attachMapEvents } from './mapEvents.js'
import { getAreaDimensions, getCardinalMove, getPaddedExtent } from './utils/spatial.js'
import { getAreaDimensions, getCardinalMove, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
import { queryVectorTileFeatures } from './utils/query.js'
import { getExtentFromFlatCoords, getPointFromFlatCoords, getBboxFromGeoJSON } from './utils/coords.js'
import { cleanDOM } from './utils/esriFixes.js'
Expand Down Expand Up @@ -118,9 +119,11 @@ export default class EsriProvider {
// Side-effects
// ==========================

setView ({ center, zoom }) {
setView({ center, zoom }) {
this.view.animation?.destroy()
this.view.goTo({ center, zoom, duration: defaults.animationDuration })
const point = center ? new Point({ x: center[0], y: center[1], spatialReference: { wkid: 27700 }}) : this.view.center
const target = { center: point, zoom: zoom ?? this.view.zoom }
this.view.goTo({ ...target, duration: defaults.animationDuration })
}

zoomIn (zoomDelta) {
Expand Down Expand Up @@ -203,4 +206,15 @@ export default class EsriProvider {
const mapPoint = this.view.toMap(point)
return [mapPoint.x, mapPoint.y]
}

/**
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
*
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry.
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates).
* @returns {boolean}
*/
isGeometryObscured (geojson, panelRect) {
return isGeometryObscured(geojson, panelRect, this.view)
}
}
1 change: 1 addition & 0 deletions providers/beta/esri/src/utils/coords.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const collectCoords = (obj, acc) => {
obj.geometries.forEach(g => collectCoords(g, acc))
} else {
const flatten = (coords) => {
if (!Array.isArray(coords)) { return }
if (typeof coords[0] === 'number') acc.push(coords)
else coords.forEach(flatten)
}
Expand Down
48 changes: 47 additions & 1 deletion providers/beta/esri/src/utils/spatial.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Extent from '@arcgis/core/geometry/Extent.js'
import Point from '@arcgis/core/geometry/Point.js'
import { getBboxFromGeoJSON } from './coords.js'

// -----------------------------------------------------------------------------
// Internal (not exported)
Expand Down Expand Up @@ -124,8 +126,52 @@ const getPaddedExtent = (view, padding = DEFAULT_PADDING) => {
}


/**
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
* Used to decide whether to pan/zoom when a panel opens over a visibleGeometry target.
*
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates)
* @param {import('@arcgis/core/views/MapView.js').default} view - ESRI MapView instance
* @returns {boolean}
*/
const isGeometryObscured = (geojson, panelRect, view) => {
if (!view?.container) {
return false
}

const containerRect = view.container.getBoundingClientRect()
const extent = getBboxFromGeoJSON(geojson)

const corners = [
view.toScreen(new Point({ x: extent.xmin, y: extent.ymin, spatialReference: extent.spatialReference })),
view.toScreen(new Point({ x: extent.xmin, y: extent.ymax, spatialReference: extent.spatialReference })),
view.toScreen(new Point({ x: extent.xmax, y: extent.ymin, spatialReference: extent.spatialReference })),
view.toScreen(new Point({ x: extent.xmax, y: extent.ymax, spatialReference: extent.spatialReference }))
]

const screenMinX = Math.min(...corners.map(c => c.x))
const screenMaxX = Math.max(...corners.map(c => c.x))
const screenMinY = Math.min(...corners.map(c => c.y))
const screenMaxY = Math.max(...corners.map(c => c.y))

// Convert panelRect from viewport coords to view-container-relative coords
const panelLeft = panelRect.left - containerRect.left
const panelTop = panelRect.top - containerRect.top
const panelRight = panelRect.right - containerRect.left
const panelBottom = panelRect.bottom - containerRect.top

return (
screenMinX < panelRight &&
screenMaxX > panelLeft &&
screenMinY < panelBottom &&
screenMaxY > panelTop
)
}

export {
getAreaDimensions,
getCardinalMove,
getPaddedExtent
getPaddedExtent,
isGeometryObscured
}
55 changes: 55 additions & 0 deletions providers/beta/esri/src/utils/spatial.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { isGeometryObscured } from './spatial.js'

jest.mock('@arcgis/core/geometry/Extent.js', () =>
jest.fn().mockImplementation((opts) => ({ ...opts, type: 'extent' }))
)

jest.mock('@arcgis/core/geometry/Point.js', () =>
jest.fn().mockImplementation((opts) => ({ ...opts, type: 'point' }))
)

jest.mock('./coords.js', () => ({
getBboxFromGeoJSON: jest.fn(() => ({
xmin: 100, ymin: 200, xmax: 500, ymax: 600,
spatialReference: { wkid: 27700 },
type: 'extent'
}))
}))

describe('isGeometryObscured', () => {
const geojson = { type: 'Feature', geometry: { type: 'Point', coordinates: [300, 400] }, properties: {} }

// Container sits at viewport origin so container-relative coords equal viewport coords
const makeView = (toScreenFn) => ({
container: {
getBoundingClientRect: jest.fn(() => ({ left: 0, top: 0, right: 1000, bottom: 800 }))
},
toScreen: jest.fn(toScreenFn)
})

// Panel occupies the right 400px of the viewport
const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }

test('returns false when view has no container', () => {
expect(isGeometryObscured(geojson, panelRect, null)).toBe(false)
expect(isGeometryObscured(geojson, panelRect, {})).toBe(false)
})

test('returns true when geometry screen bbox overlaps the panel rect', () => {
// Corners project into the panel (x: 650 is between panelLeft 600 and panelRight 1000)
const view = makeView(() => ({ x: 650, y: 400 }))
expect(isGeometryObscured(geojson, panelRect, view)).toBe(true)
})

test('returns false when geometry screen bbox does not overlap the panel rect', () => {
// Corners project to x: 300, entirely left of panelLeft (600)
const view = makeView(() => ({ x: 300, y: 400 }))
expect(isGeometryObscured(geojson, panelRect, view)).toBe(false)
})

test('projects all four bbox corners', () => {
const view = makeView(() => ({ x: 300, y: 400 }))
isGeometryObscured(geojson, panelRect, view)
expect(view.toScreen).toHaveBeenCalledTimes(4)
})
})
13 changes: 12 additions & 1 deletion providers/maplibre/src/maplibreProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DEFAULTS, supportedShortcuts } from './defaults.js'
import { cleanCanvas, applyPreventDefaultFix } from './utils/maplibreFixes.js'
import { attachMapEvents } from './mapEvents.js'
import { attachAppEvents } from './appEvents.js'
import { getAreaDimensions, getCardinalMove, getBboxFromGeoJSON, getResolution, getPaddedBounds } from './utils/spatial.js'
import { getAreaDimensions, getCardinalMove, getBboxFromGeoJSON, isGeometryObscured, getResolution, getPaddedBounds } from './utils/spatial.js'
import { createMapLabelNavigator } from './utils/labels.js'
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
import { queryFeatures } from './utils/queryFeatures.js'
Expand Down Expand Up @@ -337,4 +337,15 @@ export default class MapLibreProvider {
const { lng, lat } = this.map.unproject([point.x, point.y])
return [lng, lat]
}

/**
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
*
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry.
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates).
* @returns {boolean}
*/
isGeometryObscured (geojson, panelRect) {
return isGeometryObscured(geojson, panelRect, this.map)
}
}
15 changes: 14 additions & 1 deletion providers/maplibre/src/maplibreProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { attachAppEvents } from './appEvents.js'
import { createMapLabelNavigator } from './utils/labels.js'
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
import { queryFeatures } from './utils/queryFeatures.js'
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds } from './utils/spatial.js'
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds, isGeometryObscured } from './utils/spatial.js'

jest.mock('./defaults.js', () => ({
DEFAULTS: { animationDuration: 400, coordinatePrecision: 7 },
Expand All @@ -20,6 +20,7 @@ jest.mock('./utils/spatial.js', () => ({
getAreaDimensions: jest.fn(() => '400m by 750m'),
getCardinalMove: jest.fn(() => 'north'),
getBboxFromGeoJSON: jest.fn(() => [-1, 50, 1, 52]),
isGeometryObscured: jest.fn(() => true),
getResolution: jest.fn(() => 10),
getPaddedBounds: jest.fn(() => [[0, 0], [1, 1]])
}))
Expand Down Expand Up @@ -170,6 +171,18 @@ describe('MapLibreProvider', () => {
expect(map.fitBounds).toHaveBeenCalledWith([-1, 50, 1, 52], { duration: 400 })
})

test('isGeometryObscured delegates to spatial utility with map instance', async () => {
const p = makeProvider()
await doInitMap(p)
const geojson = { type: 'Feature', geometry: { type: 'Point', coordinates: [1, 52] }, properties: {} }
const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }

const result = p.isGeometryObscured(geojson, panelRect)

expect(isGeometryObscured).toHaveBeenCalledWith(geojson, panelRect, map)
expect(result).toBe(true)
})

test('getCenter, getZoom, getBounds return formatted values', async () => {
const p = makeProvider()
await doInitMap(p)
Expand Down
40 changes: 40 additions & 0 deletions providers/maplibre/src/utils/spatial.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,50 @@ const getPaddedBounds = (LngLatBounds, map) => {
*/
const getBboxFromGeoJSON = (geojson) => turfBbox(geojson)

/**
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
* Used to decide whether to pan/zoom when a panel opens over a visibleGeometry target.
*
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates)
* @param {object} map - MapLibre map instance
* @returns {boolean}
*/
const isGeometryObscured = (geojson, panelRect, map) => {
const containerRect = map.getContainer().getBoundingClientRect()
const [west, south, east, north] = getBboxFromGeoJSON(geojson)

const corners = [
map.project([west, south]),
map.project([west, north]),
map.project([east, south]),
map.project([east, north])
]

const screenMinX = Math.min(...corners.map(c => c.x))
const screenMaxX = Math.max(...corners.map(c => c.x))
const screenMinY = Math.min(...corners.map(c => c.y))
const screenMaxY = Math.max(...corners.map(c => c.y))

// Convert panelRect from viewport coords to map-container-relative coords
const panelLeft = panelRect.left - containerRect.left
const panelTop = panelRect.top - containerRect.top
const panelRight = panelRect.right - containerRect.left
const panelBottom = panelRect.bottom - containerRect.top

return (
screenMinX < panelRight &&
screenMaxX > panelLeft &&
screenMinY < panelBottom &&
screenMaxY > panelTop
)
}

export {
getAreaDimensions,
getCardinalMove,
getBboxFromGeoJSON,
isGeometryObscured,
spatialNavigate,
getResolution,
getPaddedBounds,
Expand Down
Loading