diff --git a/docs/api.md b/docs/api.md
index c422448df..f6bad02e6 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -791,6 +791,8 @@ interactiveMap.showPanel('info-panel', { focus: false })
Hides a panel without removing it, preserving its content for when it is shown again.
+If focus is inside the panel, it moves to the element that opened the panel, or to the map if that element is no longer on the page. Otherwise focus stays where it is.
+
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `string` | Panel identifier to hide |
diff --git a/docs/api/context.md b/docs/api/context.md
index b24953454..63708de26 100644
--- a/docs/api/context.md
+++ b/docs/api/context.md
@@ -96,6 +96,8 @@ Current map state, including:
}
```
+`mapState.mapSizeScale` is the scale for the current map size (`small`: 1, `medium`: 1.5, `large`: 2). Multiply screen positions from `mapProvider.mapToScreen` by it to position elements over the map.
+
`mapState.isMapReady` is `true` once the map instance has been created (`map:ready`). The base map may still be loading at that point; check `mapProvider.isBaseMapReady()` before adding sources or layers to the underlying map.
---
diff --git a/plugins/datasets/src/initialise/DatasetsInit.jsx b/plugins/datasets/src/initialise/DatasetsInit.jsx
index c3c21b323..a9e314cc4 100755
--- a/plugins/datasets/src/initialise/DatasetsInit.jsx
+++ b/plugins/datasets/src/initialise/DatasetsInit.jsx
@@ -4,39 +4,28 @@ import { EVENTS } from '../../../../src/config/events.js'
import { initialiseDatasets } from './initialiseDatasets.js'
import { loadLayerAdapter } from '../adapters/loadLayerAdapter.js'
import { hasLayersMenu } from '../utils/hasLayersMenu.js'
-import { getPanelElementId } from '../../../../src/utils/getPanelElementId.js'
const LAYERS_PANEL_ID = 'datasetsLayers'
// Closes the Layers panel if its menu empties while it's open
-function useCloseEmptyLayersPanel ({ appConfig, appState, pluginConfig, pluginState, services }) {
- const { eventBus, focusMap } = services
+function useCloseEmptyLayersPanel ({ appState, pluginConfig, pluginState, services }) {
+ const { eventBus } = services
const hasMenu = hasLayersMenu({ pluginConfig, pluginState })
const isLayersPanelOpen = Boolean(appState.openPanels[LAYERS_PANEL_ID])
- const focusFrameRef = useRef(null)
useEffect(() => {
- if (hasMenu || !isLayersPanelOpen) {
- return
- }
- const panelEl = document.getElementById(getPanelElementId(appConfig.id, LAYERS_PANEL_ID))
- const hadFocus = Boolean(panelEl?.contains(document.activeElement))
- eventBus.emit(EVENTS.APP_HIDE_PANEL, LAYERS_PANEL_ID)
- if (hadFocus) {
- // Let the panel close and release modal focus/inert first, as Panel's handleClose does
- focusFrameRef.current = requestAnimationFrame(() => focusMap())
+ if (!hasMenu && isLayersPanelOpen) {
+ eventBus.emit(EVENTS.APP_HIDE_PANEL, LAYERS_PANEL_ID)
}
}, [hasMenu, isLayersPanelOpen])
-
- useEffect(() => () => cancelAnimationFrame(focusFrameRef.current), [])
}
-export function DatasetsInit ({ appConfig, appState, pluginConfig, pluginState, mapState, mapProvider, services }) {
+export function DatasetsInit ({ appState, pluginConfig, pluginState, mapState, mapProvider, services }) {
const { dispatch, datasetRegistry } = pluginState
const { eventBus, symbolRegistry, patternRegistry } = services
const isBaseMapReady = Boolean(mapProvider?.isBaseMapReady())
- useCloseEmptyLayersPanel({ appConfig, appState, pluginConfig, pluginState, services })
+ useCloseEmptyLayersPanel({ appState, pluginConfig, pluginState, services })
// Keep a ref to the latest pluginState so event handlers can access current data
const pluginStateRef = useRef(pluginState)
diff --git a/plugins/datasets/src/initialise/DatasetsInit.test.jsx b/plugins/datasets/src/initialise/DatasetsInit.test.jsx
index 413ecac45..c3488462c 100644
--- a/plugins/datasets/src/initialise/DatasetsInit.test.jsx
+++ b/plugins/datasets/src/initialise/DatasetsInit.test.jsx
@@ -55,7 +55,6 @@ const makeProps = (overrides = {}) => {
mapProvider: { isBaseMapReady: jest.fn().mockReturnValue(true) },
services: {
eventBus: makeEventBus(),
- focusMap: jest.fn(),
symbolRegistry: {},
patternRegistry: {}
},
@@ -80,20 +79,6 @@ describe('DatasetsInit', () => {
describe('menu panel management', () => {
const openPanelState = { openPanels: { datasetsLayers: {} } }
- let panelEl
-
- beforeEach(() => {
- jest.useFakeTimers()
- panelEl = document.createElement('div')
- panelEl.id = 'test-map-panel-datasets-layers'
- panelEl.tabIndex = -1
- document.body.appendChild(panelEl)
- })
-
- afterEach(() => {
- panelEl.remove()
- jest.useRealTimers()
- })
it('does not remove the panel or hide the button when there is no menu', () => {
const props = makeProps({ pluginConfig: { datasets: [] } })
@@ -103,28 +88,13 @@ describe('DatasetsInit', () => {
expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(EVENTS.APP_TOGGLE_BUTTON_STATE, expect.anything())
})
- it('closes the open panel and focuses the map when the menu empties with focus inside it', () => {
- const props = makeProps({ appState: openPanelState })
- const { eventBus, focusMap } = props.services
- const { rerender } = render()
- panelEl.focus()
-
- rerender()
- expect(eventBus.emit).toHaveBeenCalledWith(EVENTS.APP_HIDE_PANEL, 'datasetsLayers')
- expect(focusMap).not.toHaveBeenCalled()
- act(() => jest.runOnlyPendingTimers())
- expect(focusMap).toHaveBeenCalledTimes(1)
- })
-
- it('closes the open panel without moving focus when focus is outside it', () => {
+ it('closes the open panel when the menu empties', () => {
const props = makeProps({ appState: openPanelState })
- const { eventBus, focusMap } = props.services
const { rerender } = render()
+ expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(EVENTS.APP_HIDE_PANEL, 'datasetsLayers')
rerender()
- act(() => jest.runOnlyPendingTimers())
- expect(eventBus.emit).toHaveBeenCalledWith(EVENTS.APP_HIDE_PANEL, 'datasetsLayers')
- expect(focusMap).not.toHaveBeenCalled()
+ expect(props.services.eventBus.emit).toHaveBeenCalledWith(EVENTS.APP_HIDE_PANEL, 'datasetsLayers')
})
it('closes a panel that is open on load when there is no menu', () => {
@@ -142,16 +112,6 @@ describe('DatasetsInit', () => {
render()
expect(props.services.eventBus.emit).not.toHaveBeenCalledWith(EVENTS.APP_HIDE_PANEL, 'datasetsLayers')
})
-
- it('cancels a pending map focus on unmount', () => {
- const props = makeProps({ appState: openPanelState })
- const { rerender, unmount } = render()
- panelEl.focus()
- rerender()
- unmount()
- act(() => jest.runOnlyPendingTimers())
- expect(props.services.focusMap).not.toHaveBeenCalled()
- })
})
// ─── initialisation ──────────────────────────────────────────────────────────
diff --git a/plugins/draw/src/api/editFeature.js b/plugins/draw/src/api/editFeature.js
index 974897560..2543aa8b1 100644
--- a/plugins/draw/src/api/editFeature.js
+++ b/plugins/draw/src/api/editFeature.js
@@ -1,4 +1,3 @@
-import { MAP_SIZE_SCALES } from '../defaults.js'
import { validateGeometry } from '../validation/validateGeometry.js'
export const editFeature = ({ appState, appConfig, mapState, pluginConfig, pluginState, mapProvider, services }, featureId, options = {}) => {
@@ -34,7 +33,7 @@ export const editFeature = ({ appState, appConfig, mapState, pluginConfig, plugi
undoButtonId: `${appConfig.id}-draw-undo`,
isPanEnabled: appState.interfaceType !== 'keyboard',
interfaceType: appState.interfaceType,
- scale: MAP_SIZE_SCALES[mapState.mapSize],
+ scale: mapState.mapSizeScale,
featureId,
getSnapEnabled: () => draw.isSnapEnabled()
})
diff --git a/plugins/draw/src/api/editFeature.test.js b/plugins/draw/src/api/editFeature.test.js
index 9ec3a41c0..202973f4c 100644
--- a/plugins/draw/src/api/editFeature.test.js
+++ b/plugins/draw/src/api/editFeature.test.js
@@ -1,7 +1,5 @@
import { editFeature } from './editFeature.js'
-jest.mock('../defaults.js', () => ({ MAP_SIZE_SCALES: { medium: 1.5, large: 2 } }))
-
const makeContext = (overrides = {}) => {
const dispatch = jest.fn()
const eventBus = { emit: jest.fn() }
@@ -14,7 +12,7 @@ const makeContext = (overrides = {}) => {
const context = {
appState: { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' },
appConfig: { id: 'app' },
- mapState: { mapSize: 'medium' },
+ mapState: { mapSizeScale: 1.5 },
pluginConfig: { snapLayers: ['pc-layer'] },
pluginState: { dispatch },
mapProvider: { draw },
diff --git a/plugins/draw/src/events.js b/plugins/draw/src/events.js
index 19c49b9b4..c911e99ce 100644
--- a/plugins/draw/src/events.js
+++ b/plugins/draw/src/events.js
@@ -8,7 +8,6 @@
import { ADAPTER_EVENTS } from './adapterEvents.js'
import { validateGeometry } from './validation/validateGeometry.js'
import { MIN_VERTICES_REASONS } from './validation/rules.js'
-import { MAP_SIZE_SCALES } from './defaults.js'
import { stripInternalProperties } from './utils/stripInternalProperties.js'
const EDIT_VERTEX_MODE = 'edit_vertex'
@@ -68,7 +67,7 @@ const enterEditVertexMode = ({ draw, appState, appConfig, mapState, dispatch },
undoButtonId: `${appConfig?.id}-draw-undo`,
isPanEnabled: appState?.interfaceType !== 'keyboard',
interfaceType: appState?.interfaceType,
- scale: MAP_SIZE_SCALES[mapState?.mapSize],
+ scale: mapState?.mapSizeScale,
featureId,
getSnapEnabled: () => draw.isSnapEnabled()
})
diff --git a/plugins/draw/src/events.test.js b/plugins/draw/src/events.test.js
index e119b3dd0..36410a639 100644
--- a/plugins/draw/src/events.test.js
+++ b/plugins/draw/src/events.test.js
@@ -30,7 +30,7 @@ const setup = (overrides = {}) => {
const buttonConfig = { drawDone: {}, drawCancel: {}, drawUndo: {}, drawDeletePoint: {}, drawSnap: {}, ...overrides.buttonConfig }
const appState = { layoutRefs: { viewportRef: { current: 'viewport' } }, interfaceType: 'mouse' }
const appConfig = { id: 'app' }
- const mapState = { mapSize: 'medium' }
+ const mapState = { mapSizeScale: 1.5 }
const detach = attachEvents({ appState, appConfig, mapState, pluginState, mapProvider, buttonConfig, eventBus, hints })
return { draw, dispatch, pluginState, mapProvider, eventBus, hints, buttonConfig, detach }
diff --git a/plugins/draw/src/hooks/useSpatialList.js b/plugins/draw/src/hooks/useSpatialList.js
index 304f60186..8636da9b3 100644
--- a/plugins/draw/src/hooks/useSpatialList.js
+++ b/plugins/draw/src/hooks/useSpatialList.js
@@ -1,6 +1,5 @@
import { useEffect, useRef } from 'react'
import { EVENTS } from '../../../../src/config/events.js'
-import { scaleFactor } from '../../../../src/config/appConfig.js'
import { ADAPTER_EVENTS } from '../adapterEvents.js'
const EDIT_VERTEX_MODE = 'edit_vertex'
@@ -9,9 +8,9 @@ const DRAW_PROVIDER_ID = 'draw'
// Mirrors interact's own projectToScreen (plugins/interact/src/hooks/useSpatialList.js) —
// mapProvider.mapToScreen is the provider-agnostic projection every mapProvider implements,
// not any adapter-internal pixel conversion.
-const projectToScreen = (mapProvider, mapSize, coord) => {
+const projectToScreen = (mapProvider, mapSizeScale, coord) => {
const { x, y } = mapProvider.mapToScreen(coord)
- return { x: x * scaleFactor[mapSize], y: y * scaleFactor[mapSize] }
+ return { x: x * mapSizeScale, y: y * mapSizeScale }
}
// ids stay flat vertices-then-midpoints indices — matches the mode's own selectedVertexIndex
@@ -21,13 +20,13 @@ const projectToScreen = (mapProvider, mapSize, coord) => {
// N and M" label (and this interleave) only reads correctly for a single ring/part (the
// common case); a shape with holes or multiple parts would need ring-aware numbering to stay
// accurate, not done here.
-const buildVertexItems = (mapProvider, mapSize) => {
+const buildVertexItems = (mapProvider, mapSizeScale) => {
const { vertices, midpoints } = mapProvider.draw?.getVertexItems?.() ?? { vertices: [], midpoints: [] }
const toVertexItem = (coord, i) => ({
id: String(i),
label: `Point ${i + 1}`,
isMidpoint: false,
- ...projectToScreen(mapProvider, mapSize, coord)
+ ...projectToScreen(mapProvider, mapSizeScale, coord)
})
const toMidpointItem = (coord, i) => ({
id: String(vertices.length + i),
@@ -35,7 +34,7 @@ const buildVertexItems = (mapProvider, mapSize) => {
// "Insert point between N and M" was long enough to lose.
label: `Insert ${i + 1}-${((i + 1) % vertices.length) + 1}`,
isMidpoint: true,
- ...projectToScreen(mapProvider, mapSize, coord)
+ ...projectToScreen(mapProvider, mapSizeScale, coord)
})
const items = []
vertices.forEach((coord, i) => {
@@ -57,10 +56,10 @@ const buildVertexItems = (mapProvider, mapSize) => {
* meanwhile, restored automatically once the claim is released) rather than contributing
* additively like interact does.
*/
-function useVertexItemSync ({ pluginState, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus }) {
+function useVertexItemSync ({ pluginState, mapProvider, mapSizeScale, spatialListRegistry, itemsRef, eventBus }) {
useEffect(() => {
const rebuild = () => {
- itemsRef.current = pluginState.mode === EDIT_VERTEX_MODE ? buildVertexItems(mapProvider, mapSize) : []
+ itemsRef.current = pluginState.mode === EDIT_VERTEX_MODE ? buildVertexItems(mapProvider, mapSizeScale) : []
spatialListRegistry.notifyItemsChanged(DRAW_PROVIDER_ID)
}
rebuild()
@@ -74,7 +73,7 @@ function useVertexItemSync ({ pluginState, mapProvider, mapSize, spatialListRegi
eventBus.off(EVENTS.MAP_MOVE_END, rebuild)
draw.off(ADAPTER_EVENTS.UPDATE, rebuild)
}
- }, [pluginState.mode, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus])
+ }, [pluginState.mode, mapProvider, mapSizeScale, spatialListRegistry, itemsRef, eventBus])
// Registers once, for the lifetime of the hook — safe outside edit_vertex too, since an
// exclusive-capable provider only ever contributes while it holds the claim (see
@@ -170,11 +169,11 @@ function useConfirmHandler ({ eventBus, mapProvider, pluginState, activeIdRef, i
* @param {{ mapState: object, pluginState: object, services: object, mapProvider: object, spatialListRegistry: object, viewportRef: React.RefObject }} params
*/
export function useSpatialList ({ mapState, pluginState, services, mapProvider, spatialListRegistry, viewportRef }) {
- const { mapSize } = mapState
+ const { mapSizeScale } = mapState
const { eventBus, announce } = services
const itemsRef = useRef([])
const activeIdRef = useRef(null)
- useVertexItemSync({ pluginState, mapProvider, mapSize, spatialListRegistry, itemsRef, eventBus })
+ useVertexItemSync({ pluginState, mapProvider, mapSizeScale, spatialListRegistry, itemsRef, eventBus })
useActiveVertexHandler({ eventBus, mapProvider, pluginState, activeIdRef })
useConfirmHandler({ eventBus, mapProvider, pluginState, activeIdRef, itemsRef, viewportRef, announce })
}
diff --git a/plugins/draw/src/hooks/useSpatialList.test.js b/plugins/draw/src/hooks/useSpatialList.test.js
index 9ce9431e1..8984b3ad1 100644
--- a/plugins/draw/src/hooks/useSpatialList.test.js
+++ b/plugins/draw/src/hooks/useSpatialList.test.js
@@ -41,7 +41,7 @@ const makeMapProvider = (draw) => ({
const makeViewportRef = () => ({ current: { focus: jest.fn() } })
-const setup = ({ mode = 'edit_vertex', draw, eventBus, mapSize = 'small', registry, viewportRef, announce } = {}) => {
+const setup = ({ mode = 'edit_vertex', draw, eventBus, mapSizeScale = 1, registry, viewportRef, announce } = {}) => {
const eb = eventBus ?? makeEventBus()
const adapter = draw ?? makeDrawAdapter()
const mp = makeMapProvider(adapter)
@@ -53,7 +53,7 @@ const setup = ({ mode = 'edit_vertex', draw, eventBus, mapSize = 'small', regist
const spatialListRegistry = registry ?? createSpatialListRegistry({ eventBus: eb })
const { result, unmount, rerender } = renderHook(
({ mode }) => useSpatialList({
- mapState: { mapSize },
+ mapState: { mapSizeScale },
pluginState: { mode },
services: { eventBus: eb, announce: announceSpy },
mapProvider: mp,
@@ -97,7 +97,7 @@ describe('useSpatialList — lifecycle', () => {
const eb = makeEventBus()
const mp = { draw: null, mapToScreen: jest.fn() }
expect(() => renderHook(() => useSpatialList({
- mapState: { mapSize: 'small' },
+ mapState: { mapSizeScale: 1 },
pluginState: { mode: 'edit_vertex' },
services: { eventBus: eb },
mapProvider: mp,
diff --git a/plugins/interact/src/hooks/useInteractionHandlers.js b/plugins/interact/src/hooks/useInteractionHandlers.js
index 34a6897df..1ae269de6 100755
--- a/plugins/interact/src/hooks/useInteractionHandlers.js
+++ b/plugins/interact/src/hooks/useInteractionHandlers.js
@@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef } from 'react'
import { areAllContiguous, isContiguousWithAny } from '../utils/spatial.js'
import { getFeaturesAtPoint, findMatchingFeature, buildLayerConfigMap } from '../utils/featureQueries.js'
-import { scaleFactor } from '../../../../src/config/appConfig.js'
import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js'
/**
@@ -13,7 +12,7 @@ import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js'
*
* @param {Object} markers - markers object from mapState (has .items and .markerRefs)
* @param {{ x: number, y: number }} point - container-relative pixel coordinates
- * @param {number} scale - scaleFactor for the current mapSize (e.g. 1.5 for medium)
+ * @param {number} scale - mapState.mapSizeScale (e.g. 1.5 for medium)
* @returns {string|null}
*/
const findMarkerAtPoint = (markers, point, scale) => {
@@ -178,7 +177,7 @@ const logDebugFeatures = (coords, features) => {
* marker hit-testing is never performed.
*
* @param {Object} deps
- * @param {Object} deps.mapState - Map state including markers and mapSize
+ * @param {Object} deps.mapState - Map state including markers and mapSizeScale
* @param {Object} deps.pluginState - Plugin state including interactionModes, layers, etc.
* @param {Object} deps.services - Services including eventBus
* @param {Object} deps.mapProvider - Map provider instance for feature queries
@@ -210,7 +209,7 @@ const useHandleInteraction = ({ mapProvider, layers, interactionModes, multiSele
}
export const useInteractionHandlers = ({ mapState, pluginState, services, mapProvider }) => {
- const { markers, mapSize } = mapState
+ const { markers, mapSizeScale: scale } = mapState
const {
dispatch, layers, interactionModes, multiSelect, contiguous,
marker: markerOptions, tolerance, selectedFeatures, selectedMarkers,
@@ -218,7 +217,6 @@ export const useInteractionHandlers = ({ mapState, pluginState, services, mapPro
} = pluginState
const { eventBus } = services
const layerConfigMap = buildLayerConfigMap(layers)
- const scale = scaleFactor[mapSize] ?? 1
const processFeatureMatch = useCallback(({ feature, config }) => {
markers.remove('location')
diff --git a/plugins/interact/src/hooks/useInteractionHandlers.test.js b/plugins/interact/src/hooks/useInteractionHandlers.test.js
index f583ec9f6..ba9f9b91a 100644
--- a/plugins/interact/src/hooks/useInteractionHandlers.test.js
+++ b/plugins/interact/src/hooks/useInteractionHandlers.test.js
@@ -55,7 +55,8 @@ const makeMarkerEl = (rect) => ({
const setup = (pluginOverrides = {}, markerItems = [], markerRefs = new Map()) => {
const deps = {
mapState: {
- markers: { add: jest.fn(), remove: jest.fn(), items: markerItems, markerRefs }
+ markers: { add: jest.fn(), remove: jest.fn(), items: markerItems, markerRefs },
+ mapSizeScale: 1
},
pluginState: {
dispatch: jest.fn(),
@@ -346,7 +347,7 @@ it('does not check markers when selectMarker is not in interactionModes', () =>
it('emits selectionchange when features are selected', () => {
const deps = {
- mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() } },
+ mapState: { markers: { add: jest.fn(), remove: jest.fn(), items: [], markerRefs: new Map() }, mapSizeScale: 1 },
pluginState: {
selectedFeatures: [{ featureId: 'F1', layerId: 'l1', idProperty: 'id', properties: { name: 'A' }, geometry: { type: 'Point', coordinates: [0, 0] } }],
selectedMarkers: []
@@ -373,7 +374,7 @@ it('skips emission when selection remains empty after being cleared', () => {
// 1. First render with a feature (prev is null, emission happens)
const { rerender } = renderHook(
({ features }) => useInteractionHandlers({
- mapState: { markers: { items: [], markerRefs: new Map() } },
+ mapState: { markers: { items: [], markerRefs: new Map() }, mapSizeScale: 1 },
pluginState: { selectedFeatures: features, selectedMarkers: [] },
services: { eventBus },
mapProvider: { getFeatureGeometry: jest.fn(() => null) }
diff --git a/plugins/interact/src/hooks/useSpatialList.js b/plugins/interact/src/hooks/useSpatialList.js
index 319a2dae1..fade34e49 100644
--- a/plugins/interact/src/hooks/useSpatialList.js
+++ b/plugins/interact/src/hooks/useSpatialList.js
@@ -1,7 +1,6 @@
import { useEffect, useRef } from 'react'
import { EVENTS } from '../../../../src/config/events.js'
import { isStandaloneLabel } from '../../../../src/utils/symbolUtils.js'
-import { scaleFactor } from '../../../../src/config/appConfig.js'
import { buildLayerConfigMap } from '../utils/featureQueries.js'
import { getGeometryCenter } from '../utils/spatial.js'
@@ -21,14 +20,14 @@ const isInViewport = (el) => {
bottom > containerRect.top && top < containerRect.bottom
}
-// Screen position for a [lng, lat] point, scaled for the current mapSize — same correction
+// Screen position for a [lng, lat] point, scaled for the current map size — same correction
// useMarkersAPI.js's projectCoords applies (the map canvas itself is rendered at a different
-// resolution per mapSize; nothing to do with CSS transforms). mapProvider.mapToScreen is a
+// resolution per map size; nothing to do with CSS transforms). mapProvider.mapToScreen is a
// provider-agnostic method (implemented by every provider, including OpenLayers/Esri), so this
// helper works unchanged once OpenLayers' getVisibleFeatures stops being a stub.
-const projectToScreen = (mapProvider, mapSize, coords) => {
+const projectToScreen = (mapProvider, mapSizeScale, coords) => {
const { x, y } = mapProvider.mapToScreen(coords)
- return { x: x * scaleFactor[mapSize], y: y * scaleFactor[mapSize] }
+ return { x: x * mapSizeScale, y: y * mapSizeScale }
}
// x/y are the marker's own screen position — computed fresh here rather than read off
@@ -36,7 +35,7 @@ const projectToScreen = (mapProvider, mapSize, coords) => {
// current pan position. Used to position the corresponding accessible list item so touch/voice
// AT overlays (e.g. macOS Voice Control's "Show Numbers") land on the actual feature instead of
// stacking at the top-left of the viewport.
-const collectVisibleMarkers = (markers, mapProvider, mapSize) => {
+const collectVisibleMarkers = (markers, mapProvider, mapSizeScale) => {
const items = []
for (const marker of markers.items) {
if (!marker.label) { continue }
@@ -44,7 +43,7 @@ const collectVisibleMarkers = (markers, mapProvider, mapSize) => {
if (!isStandaloneLabel(marker) && el && isInViewport(el)) {
const item = { id: marker.id, label: marker.label, isMarker: true }
if (marker.coords) {
- Object.assign(item, projectToScreen(mapProvider, mapSize, marker.coords))
+ Object.assign(item, projectToScreen(mapProvider, mapSizeScale, marker.coords))
}
items.push(item)
}
@@ -52,7 +51,7 @@ const collectVisibleMarkers = (markers, mapProvider, mapSize) => {
return items
}
-const toFeatureItem = (feature, layerConfigMap, seenIds, mapProvider, mapSize) => {
+const toFeatureItem = (feature, layerConfigMap, seenIds, mapProvider, mapSizeScale) => {
const config = layerConfigMap[feature.layer?.id]
if (!config?.labelProperty) {
return null
@@ -84,19 +83,19 @@ const toFeatureItem = (feature, layerConfigMap, seenIds, mapProvider, mapSize) =
// geometry (e.g. OpenLayers' getVisibleFeatures, currently a stub returning [] — see PR notes).
const center = getGeometryCenter(feature)
if (center) {
- Object.assign(item, projectToScreen(mapProvider, mapSize, center))
+ Object.assign(item, projectToScreen(mapProvider, mapSizeScale, center))
}
return item
}
-const collectVisibleFeatures = (mapProvider, layers, mapSize) => {
+const collectVisibleFeatures = (mapProvider, layers, mapSizeScale) => {
const items = []
const seenIds = new Set()
const layerIds = layers.map(layer => layer.layerId)
const layerConfigMap = buildLayerConfigMap(layers)
const features = mapProvider.getVisibleFeatures(layerIds)
for (const feature of features) {
- const item = toFeatureItem(feature, layerConfigMap, seenIds, mapProvider, mapSize)
+ const item = toFeatureItem(feature, layerConfigMap, seenIds, mapProvider, mapSizeScale)
if (item) {
items.push(item)
}
@@ -118,7 +117,7 @@ const collectVisibleFeatures = (mapProvider, layers, mapSize) => {
* own doc comment for why: a single "whoever emits last wins" event doesn't scale once more
* than one plugin, e.g. draw mid-edit, needs to contribute to or take over the same list).
*/
-function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef }) {
+function useItemListSync ({ markers, mapSizeScale, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef }) {
const multiSelectRef = useRef(multiSelect)
multiSelectRef.current = multiSelect // always-current for the provider's getItems below, same convention as itemsRef
@@ -126,10 +125,10 @@ function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvi
const handleMoveEnd = () => {
const items = []
if (interactionModes?.includes('selectMarker')) {
- items.push(...collectVisibleMarkers(markers, mapProvider, mapSize))
+ items.push(...collectVisibleMarkers(markers, mapProvider, mapSizeScale))
}
if (interactionModes?.includes('selectFeature') && layers.length > 0) {
- items.push(...collectVisibleFeatures(mapProvider, layers, mapSize))
+ items.push(...collectVisibleFeatures(mapProvider, layers, mapSizeScale))
}
itemsRef.current = items
spatialListRegistry.notifyItemsChanged('interact')
@@ -141,7 +140,7 @@ function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvi
eventBus.off(EVENTS.MAP_MOVE_END, handleMoveEnd)
eventBus.off(EVENTS.MAP_DATA_CHANGE, handleMoveEnd)
}
- }, [markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef])
+ }, [markers, mapSizeScale, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef])
// Registers once, for the lifetime of the hook — getItems reads itemsRef/multiSelectRef
// fresh on every call, so there's never a need to re-register just because a prop changed;
@@ -240,12 +239,12 @@ function useSelectItemHandler ({ eventBus, dispatch, listboxActiveItemRef, multi
* @param {{ mapState: object, pluginState: object, services: object, mapProvider: object, spatialListRegistry: object }} params
*/
export function useSpatialList ({ mapState, pluginState, services, mapProvider, spatialListRegistry }) {
- const { markers, mapSize } = mapState
+ const { markers, mapSizeScale } = mapState
const { dispatch, interactionModes, layers, multiSelect } = pluginState
const { eventBus } = services
const listboxActiveItemRef = useRef(null)
const itemsRef = useRef([])
- useItemListSync({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef })
+ useItemListSync({ markers, mapSizeScale, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef })
useActiveItemHandler({ itemsRef, eventBus, dispatch, listboxActiveItemRef })
useSelectItemHandler({ eventBus, dispatch, listboxActiveItemRef, multiSelect })
}
diff --git a/plugins/interact/src/hooks/useSpatialList.test.js b/plugins/interact/src/hooks/useSpatialList.test.js
index ac5c413bb..f205c5e95 100644
--- a/plugins/interact/src/hooks/useSpatialList.test.js
+++ b/plugins/interact/src/hooks/useSpatialList.test.js
@@ -54,7 +54,7 @@ const makeMapProvider = (features = []) => ({
mapToScreen: jest.fn(() => ({ x: 10, y: 20 }))
})
-const setup = ({ interactionModes = [], markers, layers = [], mapProvider, eventBus, dispatch, multiSelect = false, mapSize = 'small' } = {}) => {
+const setup = ({ interactionModes = [], markers, layers = [], mapProvider, eventBus, dispatch, multiSelect = false, mapSizeScale = 1 } = {}) => {
const eb = eventBus ?? makeEventBus()
const mp = mapProvider ?? makeMapProvider()
const dp = dispatch ?? jest.fn()
@@ -65,7 +65,7 @@ const setup = ({ interactionModes = [], markers, layers = [], mapProvider, event
// spatialListRegistry.test.js.
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
const { result, unmount } = renderHook(() => useSpatialList({
- mapState: { markers: markers ?? makeMarkers(), mapSize },
+ mapState: { markers: markers ?? makeMarkers(), mapSizeScale },
pluginState: { interactionModes, layers, dispatch: dp, multiSelect },
services: { eventBus: eb },
mapProvider: mp,
@@ -97,7 +97,7 @@ describe('useSpatialList — lifecycle', () => {
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
const registerSpy = jest.spyOn(spatialListRegistry, 'registerItemProvider')
const { unmount } = renderHook(() => useSpatialList({
- mapState: { markers: makeMarkers(), mapSize: 'small' },
+ mapState: { markers: makeMarkers(), mapSizeScale: 1 },
pluginState: { interactionModes: [], layers: [], dispatch: jest.fn(), multiSelect: false },
services: { eventBus: eb },
mapProvider: makeMapProvider(),
@@ -252,19 +252,19 @@ describe('useSpatialList — selectMarker mode', () => {
container.remove()
})
- it('includes x/y screen position, scaled for mapSize, when the marker has coords', () => {
+ it('includes x/y screen position, scaled for mapSizeScale, when the marker has coords', () => {
const { el, container } = makeMarkerEl({ inViewport: true })
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true, coords: [-2.4, 54.5] }])
markers.markerRefs.set('m1', el)
const mp = makeMapProvider()
mp.mapToScreen.mockReturnValue({ x: 100, y: 200 })
- const { eb } = setup({ interactionModes: ['selectMarker'], markers, mapProvider: mp, mapSize: 'medium' })
+ const { eb } = setup({ interactionModes: ['selectMarker'], markers, mapProvider: mp, mapSizeScale: 1.5 })
act(() => eb.emit(MOVE_END, {}))
expect(mp.mapToScreen).toHaveBeenCalledWith([-2.4, 54.5])
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
- items: [markerItem({ id: 'm1', label: MARKER_LABEL, x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
+ items: [markerItem({ id: 'm1', label: MARKER_LABEL, x: 150, y: 300 })], multiselectable: false, label: 'Map features'
})
container.remove()
})
@@ -375,7 +375,7 @@ describe('useSpatialList — selectFeature mode: label resolution', () => {
})
})
- it('includes x/y at the bbox centre of the feature geometry, scaled for mapSize', () => {
+ it('includes x/y at the bbox centre of the feature geometry, scaled for mapSizeScale', () => {
const features = [{
layer: { id: 'roads' },
properties: { road_id: '1', road_name: 'High Street' },
@@ -384,12 +384,12 @@ describe('useSpatialList — selectFeature mode: label resolution', () => {
const mp = makeMapProvider(features)
mp.mapToScreen.mockReturnValue({ x: 100, y: 200 })
- const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp, mapSize: 'medium' })
+ const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp, mapSizeScale: 1.5 })
act(() => eb.emit(MOVE_END, {}))
expect(mp.mapToScreen).toHaveBeenCalledWith([2, 1]) // bbox centre of the polygon above
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
- items: [featureItem({ id: '1', label: 'High Street', x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
+ items: [featureItem({ id: '1', label: 'High Street', x: 150, y: 300 })], multiselectable: false, label: 'Map features'
})
})
diff --git a/plugins/map-styles/src/MapStyles.jsx b/plugins/map-styles/src/MapStyles.jsx
index 98acede2f..d15d646ef 100755
--- a/plugins/map-styles/src/MapStyles.jsx
+++ b/plugins/map-styles/src/MapStyles.jsx
@@ -1,7 +1,7 @@
import React from 'react'
import { EVENTS } from '../../../src/config/events.js'
import { textSizeSvgPath } from './config.js'
-import { scaleFactor } from '../../../src/config/appConfig.js'
+import { scaleFactor } from '../../../src/config/mapSizeScales.js'
export const MapStyles = ({ mapState, pluginConfig, services, mapProvider }) => {
const { mapStyle: currentMapStyle, mapSize: currentMapSize } = mapState
@@ -42,7 +42,7 @@ export const MapStyles = ({ mapState, pluginConfig, services, mapProvider }) =>