@defra/interactive-map 0.0.22-alpha → 0.0.24-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/images/light-raster-thumb.jpg +0 -0
- package/assets/images/outdoor-raster-thumb.jpg +0 -0
- package/assets/images/road-raster-thumb.jpg +0 -0
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +24 -2
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/interact.md +4 -11
- package/docs/plugins/search.md +70 -0
- package/docusaurus.config.cjs +2 -1
- package/package.json +23 -9
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/css/index.css +1 -21
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/api/editFeature.js +9 -4
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +68 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/Form/Form.jsx +11 -7
- package/plugins/search/src/components/Form/Form.test.jsx +23 -1
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/datasets.js +2 -2
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +7 -1
- package/plugins/search/src/events/fetchSuggestions.js +13 -10
- package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
- package/plugins/search/src/events/formHandlers.js +2 -4
- package/plugins/search/src/events/formHandlers.test.js +3 -3
- package/plugins/search/src/events/inputHandlers.js +1 -1
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -1
- package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
- package/plugins/search/src/index.js +3 -7
- package/plugins/search/src/reducer.js +10 -5
- package/plugins/search/src/reducer.test.js +2 -1
- package/plugins/search/src/search.scss +1 -0
- package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
- package/providers/beta/openlayers/dist/esm/index.js +1 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/dist/umd/index.js +2 -0
- package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/src/appEvents.js +50 -0
- package/providers/beta/openlayers/src/appEvents.test.js +140 -0
- package/providers/beta/openlayers/src/defaults.js +36 -0
- package/providers/beta/openlayers/src/index.js +21 -0
- package/providers/beta/openlayers/src/mapEvents.js +132 -0
- package/providers/beta/openlayers/src/mapEvents.test.js +197 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +234 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +331 -0
- package/providers/beta/openlayers/src/utils/spatial.js +142 -0
- package/providers/beta/openlayers/src/utils/spatial.test.js +117 -0
- package/providers/beta/openlayers/src/utils/zoom.js +26 -0
- package/providers/beta/openlayers/src/utils/zoom.test.js +23 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/index.js +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +112 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +179 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +22 -19
- package/providers/maplibre/src/utils/patternImages.test.js +146 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/rollup.esm.mjs +14 -3
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +98 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -1
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +163 -94
- package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/AppProvider.test.jsx +5 -0
- package/src/App/store/MapProvider.test.jsx +8 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/announcer.test.js +5 -7
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +71 -14
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/webpack.dev.mjs +6 -1
- package/webpack.umd.mjs +1 -0
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import OlMap from 'ol/Map.js'
|
|
2
|
+
import View from 'ol/View.js'
|
|
3
|
+
import TileLayer from 'ol/layer/Tile.js'
|
|
4
|
+
import { defaults as defaultInteractions } from 'ol/interaction/defaults.js'
|
|
5
|
+
import proj4 from 'proj4'
|
|
6
|
+
import { register } from 'ol/proj/proj4.js'
|
|
7
|
+
import { supportedShortcuts, DEFAULTS } from './defaults.js'
|
|
8
|
+
import { getViewResolutionConfig, ZOOM_ALIGNMENT } from './utils/zoom.js'
|
|
9
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
10
|
+
import { attachAppEvents, createTileSource } from './appEvents.js'
|
|
11
|
+
import { getAreaDimensions, getCardinalMove, getExtentFromGeoJSON, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
|
|
12
|
+
|
|
13
|
+
const CRS = 'EPSG:27700'
|
|
14
|
+
|
|
15
|
+
// OL view padding is [top, right, bottom, left]; app passes { top, right, bottom, left }
|
|
16
|
+
const toPaddingArray = (padding) => {
|
|
17
|
+
if (!padding) {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
const { top = 0, right = 0, bottom = 0, left = 0 } = padding
|
|
21
|
+
return [top, right, bottom, left]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Register British National Grid with proj4 so OL can use it
|
|
25
|
+
proj4.defs(CRS, '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs')
|
|
26
|
+
register(proj4)
|
|
27
|
+
|
|
28
|
+
export default class OpenLayersProvider {
|
|
29
|
+
constructor ({ mapProviderConfig = {}, events, eventBus }) {
|
|
30
|
+
this.events = events
|
|
31
|
+
this.eventBus = eventBus
|
|
32
|
+
this.capabilities = {
|
|
33
|
+
supportedShortcuts,
|
|
34
|
+
supportsMapSizes: false
|
|
35
|
+
}
|
|
36
|
+
Object.assign(this, mapProviderConfig)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initMap (config) {
|
|
40
|
+
const { container, padding, mapStyle, mapSize, center, zoom, bounds, minZoom, maxZoom, transformRequest } = config
|
|
41
|
+
this.mapStyleId = mapStyle?.id
|
|
42
|
+
this.mapSize = mapSize
|
|
43
|
+
const { events, eventBus } = this
|
|
44
|
+
|
|
45
|
+
const source = createTileSource(mapStyle.url, transformRequest)
|
|
46
|
+
const tileLayer = new TileLayer({ source })
|
|
47
|
+
|
|
48
|
+
const viewResolutions = getViewResolutionConfig(this.zoomAlignment ?? ZOOM_ALIGNMENT.UK)
|
|
49
|
+
|
|
50
|
+
const view = new View({
|
|
51
|
+
projection: CRS,
|
|
52
|
+
center,
|
|
53
|
+
zoom: zoom ?? viewResolutions.defaultMinZoom,
|
|
54
|
+
minZoom: Math.max(minZoom ?? viewResolutions.defaultMinZoom, viewResolutions.defaultMinZoom),
|
|
55
|
+
maxZoom: maxZoom ?? viewResolutions.maxZoom,
|
|
56
|
+
resolutions: viewResolutions.resolutions,
|
|
57
|
+
constrainResolution: false,
|
|
58
|
+
padding: toPaddingArray(padding)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const map = new OlMap({
|
|
62
|
+
target: container,
|
|
63
|
+
layers: [tileLayer],
|
|
64
|
+
view,
|
|
65
|
+
controls: [],
|
|
66
|
+
interactions: defaultInteractions({ doubleClickZoom: false })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (bounds) {
|
|
70
|
+
map.once('rendercomplete', () => {
|
|
71
|
+
view.fit(bounds, { size: map.getSize(), duration: 0 })
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.mapEventHandles = attachMapEvents({
|
|
76
|
+
map,
|
|
77
|
+
source,
|
|
78
|
+
events,
|
|
79
|
+
eventBus,
|
|
80
|
+
getZoom: this.getZoom.bind(this),
|
|
81
|
+
getCenter: this.getCenter.bind(this),
|
|
82
|
+
getBounds: this.getBounds.bind(this),
|
|
83
|
+
getResolution: this.getResolution.bind(this)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
this.appEventHandles = attachAppEvents({
|
|
87
|
+
tileLayer,
|
|
88
|
+
transformRequest,
|
|
89
|
+
events,
|
|
90
|
+
eventBus
|
|
91
|
+
}) || []
|
|
92
|
+
|
|
93
|
+
this.map = map
|
|
94
|
+
this.view = view
|
|
95
|
+
this.tileLayer = tileLayer
|
|
96
|
+
this.source = source
|
|
97
|
+
|
|
98
|
+
// MAP_READY is synchronous — OL map is immediately interactive after construction
|
|
99
|
+
eventBus.emit(events.MAP_READY, {
|
|
100
|
+
map: this.map,
|
|
101
|
+
mapStyleId: this.mapStyleId,
|
|
102
|
+
mapSize: this.mapSize,
|
|
103
|
+
crs: this.crs
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destroyMap () {
|
|
108
|
+
this.mapEventHandles?.remove()
|
|
109
|
+
this.appEventHandles?.remove()
|
|
110
|
+
|
|
111
|
+
this.mapEventHandles = null
|
|
112
|
+
this.appEventHandles = null
|
|
113
|
+
|
|
114
|
+
if (this.map) {
|
|
115
|
+
this.map.setTarget(null)
|
|
116
|
+
this.map = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.view = null
|
|
120
|
+
this.tileLayer = null
|
|
121
|
+
this.source = null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==========================
|
|
125
|
+
// Side-effects
|
|
126
|
+
// ==========================
|
|
127
|
+
|
|
128
|
+
setView ({ center, zoom }) {
|
|
129
|
+
this.view.animate({
|
|
130
|
+
center: center ?? this.view.getCenter(),
|
|
131
|
+
zoom: zoom ?? this.view.getZoom(),
|
|
132
|
+
duration: DEFAULTS.animationDuration
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
zoomIn (zoomDelta) {
|
|
137
|
+
this.view.animate({
|
|
138
|
+
zoom: this.view.getZoom() + zoomDelta,
|
|
139
|
+
duration: DEFAULTS.animationDuration
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
zoomOut (zoomDelta) {
|
|
144
|
+
this.view.animate({
|
|
145
|
+
zoom: this.view.getZoom() - zoomDelta,
|
|
146
|
+
duration: DEFAULTS.animationDuration
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
panBy (offset) {
|
|
151
|
+
const center = this.view.getCenter()
|
|
152
|
+
const resolution = this.view.getResolution()
|
|
153
|
+
// Pixel x/y → easting increases right, northing increases up (flip y)
|
|
154
|
+
this.view.animate({
|
|
155
|
+
center: [center[0] + offset[0] * resolution, center[1] - offset[1] * resolution],
|
|
156
|
+
duration: DEFAULTS.animationDuration
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fitToBounds (bounds) {
|
|
161
|
+
const extent = Array.isArray(bounds) ? bounds : getExtentFromGeoJSON(bounds)
|
|
162
|
+
this.view.fit(extent, { duration: DEFAULTS.animationDuration })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setPadding (padding) {
|
|
166
|
+
this.view.padding = toPaddingArray(padding)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ==========================
|
|
170
|
+
// Read-only getters
|
|
171
|
+
// ==========================
|
|
172
|
+
|
|
173
|
+
getCenter () {
|
|
174
|
+
const center = this.view.getCenter()
|
|
175
|
+
const p = DEFAULTS.coordinatePrecision
|
|
176
|
+
return [
|
|
177
|
+
Math.round(center[0] * Math.pow(10, p)) / Math.pow(10, p),
|
|
178
|
+
Math.round(center[1] * Math.pow(10, p)) / Math.pow(10, p)
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getZoom () {
|
|
183
|
+
return this.view.getZoom()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getBounds () {
|
|
187
|
+
const extent = this.view.calculateExtent(this.map.getSize())
|
|
188
|
+
return extent.map(n => Math.round(n * 100) / 100)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getFeaturesAtPoint (_point, _options) {
|
|
192
|
+
// Raster tiles have no queryable features
|
|
193
|
+
return []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getVisibleFeatures (_layerIds) {
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ==========================
|
|
201
|
+
// Spatial helpers
|
|
202
|
+
// ==========================
|
|
203
|
+
|
|
204
|
+
getAreaDimensions () {
|
|
205
|
+
return getAreaDimensions(getPaddedExtent(this.map))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getCardinalMove (from, to) {
|
|
209
|
+
return getCardinalMove(from, to)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getResolution () {
|
|
213
|
+
return this.view.getResolution()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
mapToScreen (coords) {
|
|
217
|
+
if (!this.map) {
|
|
218
|
+
return { x: 0, y: 0 }
|
|
219
|
+
}
|
|
220
|
+
const pixel = this.map.getPixelFromCoordinate(coords)
|
|
221
|
+
if (!pixel) {
|
|
222
|
+
return { x: 0, y: 0 }
|
|
223
|
+
}
|
|
224
|
+
return { x: pixel[0], y: pixel[1] }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
screenToMap (point) {
|
|
228
|
+
return this.map.getCoordinateFromPixel([point.x, point.y])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
isGeometryObscured (geojson, panelRect) {
|
|
232
|
+
return isGeometryObscured(geojson, panelRect, this.map)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Mock-prefixed variables are allowed in jest.mock factories by babel-plugin-jest-hoist
|
|
2
|
+
import OpenLayersProvider from './openlayersProvider.js'
|
|
3
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
4
|
+
import { attachAppEvents, createTileSource } from './appEvents.js'
|
|
5
|
+
import { getExtentFromGeoJSON, isGeometryObscured } from './utils/spatial.js'
|
|
6
|
+
|
|
7
|
+
const mockAnimate = jest.fn()
|
|
8
|
+
const mockGetCenter = jest.fn(() => [400000.126, 300000.455])
|
|
9
|
+
const mockGetZoom = jest.fn(() => 7)
|
|
10
|
+
const mockGetResolution = jest.fn(() => 56)
|
|
11
|
+
const mockCalculateExtent = jest.fn(() => [1.005, 2.014, 3.994, 4.0])
|
|
12
|
+
const mockViewFit = jest.fn()
|
|
13
|
+
const mockViewInstance = {
|
|
14
|
+
animate: mockAnimate,
|
|
15
|
+
getCenter: mockGetCenter,
|
|
16
|
+
getZoom: mockGetZoom,
|
|
17
|
+
getResolution: mockGetResolution,
|
|
18
|
+
calculateExtent: mockCalculateExtent,
|
|
19
|
+
fit: mockViewFit,
|
|
20
|
+
padding: null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mockMapOnce = jest.fn()
|
|
24
|
+
const mockMapGetSize = jest.fn(() => [800, 600])
|
|
25
|
+
const mockMapSetTarget = jest.fn()
|
|
26
|
+
const mockMapGetPixel = jest.fn(() => [100, 200])
|
|
27
|
+
const mockMapGetCoord = jest.fn(() => [400000, 300000])
|
|
28
|
+
const mockMapInstance = {
|
|
29
|
+
once: mockMapOnce,
|
|
30
|
+
getSize: mockMapGetSize,
|
|
31
|
+
setTarget: mockMapSetTarget,
|
|
32
|
+
getPixelFromCoordinate: mockMapGetPixel,
|
|
33
|
+
getCoordinateFromPixel: mockMapGetCoord,
|
|
34
|
+
getView: jest.fn(() => mockViewInstance)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mockSource = {}
|
|
38
|
+
const mockTileLayer = { setSource: jest.fn() }
|
|
39
|
+
const mockMapEventHandlesRemove = jest.fn()
|
|
40
|
+
const mockAppEventHandlesRemove = jest.fn()
|
|
41
|
+
|
|
42
|
+
jest.mock('ol/Map.js', () => ({ __esModule: true, default: jest.fn(() => mockMapInstance) }))
|
|
43
|
+
jest.mock('ol/View.js', () => ({ __esModule: true, default: jest.fn(() => mockViewInstance) }))
|
|
44
|
+
jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayer) }))
|
|
45
|
+
jest.mock('ol/interaction/defaults.js', () => ({ __esModule: true, defaults: jest.fn(() => []) }))
|
|
46
|
+
jest.mock('proj4', () => {
|
|
47
|
+
const fn = jest.fn()
|
|
48
|
+
fn.defs = jest.fn()
|
|
49
|
+
return { __esModule: true, default: fn }
|
|
50
|
+
})
|
|
51
|
+
jest.mock('ol/proj/proj4.js', () => ({ __esModule: true, register: jest.fn() }))
|
|
52
|
+
jest.mock('./mapEvents.js', () => ({
|
|
53
|
+
__esModule: true,
|
|
54
|
+
attachMapEvents: jest.fn(() => ({ remove: mockMapEventHandlesRemove }))
|
|
55
|
+
}))
|
|
56
|
+
jest.mock('./appEvents.js', () => ({
|
|
57
|
+
__esModule: true,
|
|
58
|
+
createTileSource: jest.fn(() => mockSource),
|
|
59
|
+
attachAppEvents: jest.fn(() => ({ remove: mockAppEventHandlesRemove }))
|
|
60
|
+
}))
|
|
61
|
+
jest.mock('./utils/spatial.js', () => ({
|
|
62
|
+
__esModule: true,
|
|
63
|
+
getAreaDimensions: jest.fn(() => '1 mile by 2 miles'),
|
|
64
|
+
getCardinalMove: jest.fn(() => 'north 100m'),
|
|
65
|
+
getExtentFromGeoJSON: jest.fn(() => [10, 20, 30, 40]),
|
|
66
|
+
getPaddedExtent: jest.fn(() => [0, 0, 800, 600]),
|
|
67
|
+
isGeometryObscured: jest.fn(() => false)
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
const events = {
|
|
71
|
+
MAP_READY: 'map:ready',
|
|
72
|
+
MAP_SET_STYLE: 'map:setstyle',
|
|
73
|
+
MAP_STYLE_CHANGE: 'map:stylechange'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultInitConfig = {
|
|
77
|
+
container: document.createElement('div'),
|
|
78
|
+
padding: null,
|
|
79
|
+
mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}' },
|
|
80
|
+
mapSize: null,
|
|
81
|
+
center: [400000, 300000],
|
|
82
|
+
zoom: 7,
|
|
83
|
+
bounds: null,
|
|
84
|
+
minZoom: null,
|
|
85
|
+
maxZoom: null,
|
|
86
|
+
transformRequest: null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function makeProvider (mapProviderConfig = {}) {
|
|
90
|
+
const eventBus = { emit: jest.fn(), on: jest.fn(), off: jest.fn() }
|
|
91
|
+
const provider = new OpenLayersProvider({ events, eventBus, mapProviderConfig })
|
|
92
|
+
return { provider, eventBus }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('OpenLayersProvider', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
jest.clearAllMocks()
|
|
98
|
+
mockViewInstance.padding = null
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('constructor', () => {
|
|
102
|
+
it('sets events and eventBus on the instance', () => {
|
|
103
|
+
const eventBus = { emit: jest.fn() }
|
|
104
|
+
const provider = new OpenLayersProvider({ events, eventBus, mapProviderConfig: {} })
|
|
105
|
+
expect(provider.events).toBe(events)
|
|
106
|
+
expect(provider.eventBus).toBe(eventBus)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('sets capabilities with supportsMapSizes false and supportedShortcuts array', () => {
|
|
110
|
+
const { provider } = makeProvider()
|
|
111
|
+
expect(provider.capabilities.supportsMapSizes).toBe(false)
|
|
112
|
+
expect(Array.isArray(provider.capabilities.supportedShortcuts)).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('spreads mapProviderConfig properties onto instance', () => {
|
|
116
|
+
const { provider } = makeProvider({ zoomAlignment: 'world', customProp: 'hello' })
|
|
117
|
+
expect(provider.zoomAlignment).toBe('world')
|
|
118
|
+
expect(provider.customProp).toBe('hello')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('initMap', () => {
|
|
123
|
+
it('creates tile source, OL objects, and emits MAP_READY', async () => {
|
|
124
|
+
const { provider, eventBus } = makeProvider()
|
|
125
|
+
await provider.initMap(defaultInitConfig)
|
|
126
|
+
expect(createTileSource).toHaveBeenCalledWith(defaultInitConfig.mapStyle.url, null)
|
|
127
|
+
expect(attachMapEvents).toHaveBeenCalled()
|
|
128
|
+
expect(attachAppEvents).toHaveBeenCalled()
|
|
129
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({
|
|
130
|
+
map: mockMapInstance,
|
|
131
|
+
mapStyleId: 'myStyle'
|
|
132
|
+
}))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('calls map.once rendercomplete when bounds are provided', async () => {
|
|
136
|
+
const { provider } = makeProvider()
|
|
137
|
+
let renderCallback
|
|
138
|
+
mockMapOnce.mockImplementation((event, cb) => { renderCallback = cb })
|
|
139
|
+
const bounds = [100, 200, 300, 400]
|
|
140
|
+
await provider.initMap({ ...defaultInitConfig, bounds })
|
|
141
|
+
expect(mockMapOnce).toHaveBeenCalledWith('rendercomplete', expect.any(Function))
|
|
142
|
+
renderCallback()
|
|
143
|
+
expect(mockViewFit).toHaveBeenCalledWith(bounds, { size: [800, 600], duration: 0 })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('does not call map.once when no bounds', async () => {
|
|
147
|
+
const { provider } = makeProvider()
|
|
148
|
+
await provider.initMap(defaultInitConfig)
|
|
149
|
+
expect(mockMapOnce).not.toHaveBeenCalled()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('destroyMap', () => {
|
|
154
|
+
it('removes event handles, nulls map target and all references', async () => {
|
|
155
|
+
const { provider } = makeProvider()
|
|
156
|
+
await provider.initMap(defaultInitConfig)
|
|
157
|
+
jest.clearAllMocks()
|
|
158
|
+
|
|
159
|
+
provider.destroyMap()
|
|
160
|
+
|
|
161
|
+
expect(mockMapEventHandlesRemove).toHaveBeenCalled()
|
|
162
|
+
expect(mockAppEventHandlesRemove).toHaveBeenCalled()
|
|
163
|
+
expect(mockMapSetTarget).toHaveBeenCalledWith(null)
|
|
164
|
+
expect(provider.map).toBeNull()
|
|
165
|
+
expect(provider.view).toBeNull()
|
|
166
|
+
expect(provider.tileLayer).toBeNull()
|
|
167
|
+
expect(provider.source).toBeNull()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not throw when called before initMap', () => {
|
|
171
|
+
const { provider } = makeProvider()
|
|
172
|
+
expect(() => provider.destroyMap()).not.toThrow()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('view control methods', () => {
|
|
177
|
+
let provider
|
|
178
|
+
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
;({ provider } = makeProvider())
|
|
181
|
+
await provider.initMap(defaultInitConfig)
|
|
182
|
+
jest.clearAllMocks()
|
|
183
|
+
mockViewInstance.padding = null
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('setView animates to given center and zoom', () => {
|
|
187
|
+
provider.setView({ center: [500000, 400000], zoom: 8 })
|
|
188
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ center: [500000, 400000], zoom: 8 }))
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('setView falls back to current center and zoom when not supplied', () => {
|
|
192
|
+
mockGetCenter.mockReturnValue([400000, 300000])
|
|
193
|
+
mockGetZoom.mockReturnValue(7)
|
|
194
|
+
provider.setView({})
|
|
195
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ center: [400000, 300000], zoom: 7 }))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('zoomIn increments zoom by delta', () => {
|
|
199
|
+
mockGetZoom.mockReturnValue(5)
|
|
200
|
+
provider.zoomIn(2)
|
|
201
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ zoom: 7 }))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('zoomOut decrements zoom by delta', () => {
|
|
205
|
+
mockGetZoom.mockReturnValue(9)
|
|
206
|
+
provider.zoomOut(3)
|
|
207
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ zoom: 6 }))
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('panBy animates center offset scaled by resolution', () => {
|
|
211
|
+
mockGetCenter.mockReturnValue([400000, 300000])
|
|
212
|
+
mockGetResolution.mockReturnValue(56)
|
|
213
|
+
provider.panBy([10, 20])
|
|
214
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({
|
|
215
|
+
center: [400000 + 10 * 56, 300000 - 20 * 56]
|
|
216
|
+
}))
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('fitToBounds passes an array extent directly to view.fit', () => {
|
|
220
|
+
provider.fitToBounds([1, 2, 3, 4])
|
|
221
|
+
expect(getExtentFromGeoJSON).not.toHaveBeenCalled()
|
|
222
|
+
expect(mockViewFit).toHaveBeenCalledWith([1, 2, 3, 4], expect.any(Object))
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('fitToBounds converts GeoJSON to extent via getExtentFromGeoJSON', () => {
|
|
226
|
+
const geojson = { type: 'Point', coordinates: [0, 51] }
|
|
227
|
+
provider.fitToBounds(geojson)
|
|
228
|
+
expect(getExtentFromGeoJSON).toHaveBeenCalledWith(geojson)
|
|
229
|
+
expect(mockViewFit).toHaveBeenCalledWith([10, 20, 30, 40], expect.any(Object))
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('setPadding converts object to OL [top, right, bottom, left] array', () => {
|
|
233
|
+
provider.setPadding({ top: 10, right: 20, bottom: 30, left: 40 })
|
|
234
|
+
expect(mockViewInstance.padding).toEqual([10, 20, 30, 40])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('setPadding fills missing keys with zero', () => {
|
|
238
|
+
provider.setPadding({ top: 10 })
|
|
239
|
+
expect(mockViewInstance.padding).toEqual([10, 0, 0, 0])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('setPadding with null sets padding to undefined', () => {
|
|
243
|
+
provider.setPadding(null)
|
|
244
|
+
expect(mockViewInstance.padding).toBeUndefined()
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe('getter methods', () => {
|
|
249
|
+
let provider
|
|
250
|
+
|
|
251
|
+
beforeEach(async () => {
|
|
252
|
+
;({ provider } = makeProvider())
|
|
253
|
+
await provider.initMap(defaultInitConfig)
|
|
254
|
+
jest.clearAllMocks()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('getCenter rounds coordinates to 2 decimal places', () => {
|
|
258
|
+
mockGetCenter.mockReturnValue([400000.126, 300000.455])
|
|
259
|
+
expect(provider.getCenter()).toEqual([400000.13, 300000.46])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('getZoom delegates to view.getZoom', () => {
|
|
263
|
+
mockGetZoom.mockReturnValue(9)
|
|
264
|
+
expect(provider.getZoom()).toBe(9)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('getBounds rounds extent values to 2 decimal places', () => {
|
|
268
|
+
mockCalculateExtent.mockReturnValue([1.126, 2.456, 3.994, 4.0])
|
|
269
|
+
expect(provider.getBounds()).toEqual([1.13, 2.46, 3.99, 4])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('getFeaturesAtPoint returns empty array', () => {
|
|
273
|
+
expect(provider.getFeaturesAtPoint()).toEqual([])
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('getVisibleFeatures returns empty array', () => {
|
|
277
|
+
expect(provider.getVisibleFeatures()).toEqual([])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('getResolution delegates to view.getResolution', () => {
|
|
281
|
+
mockGetResolution.mockReturnValue(28)
|
|
282
|
+
expect(provider.getResolution()).toBe(28)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('spatial helper methods', () => {
|
|
287
|
+
let provider
|
|
288
|
+
|
|
289
|
+
beforeEach(async () => {
|
|
290
|
+
;({ provider } = makeProvider())
|
|
291
|
+
await provider.initMap(defaultInitConfig)
|
|
292
|
+
jest.clearAllMocks()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('getAreaDimensions delegates to spatial utils', () => {
|
|
296
|
+
expect(provider.getAreaDimensions()).toBe('1 mile by 2 miles')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('getCardinalMove delegates to spatial utils', () => {
|
|
300
|
+
expect(provider.getCardinalMove([0, 0], [100, 100])).toBe('north 100m')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('mapToScreen returns pixel as {x, y}', () => {
|
|
304
|
+
mockMapGetPixel.mockReturnValue([150, 250])
|
|
305
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 150, y: 250 })
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('mapToScreen returns {0, 0} when getPixelFromCoordinate returns null', () => {
|
|
309
|
+
mockMapGetPixel.mockReturnValue(null)
|
|
310
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 0, y: 0 })
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('mapToScreen returns {0, 0} when map is not set', () => {
|
|
314
|
+
provider.map = null
|
|
315
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 0, y: 0 })
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('screenToMap delegates to map.getCoordinateFromPixel', () => {
|
|
319
|
+
mockMapGetCoord.mockReturnValue([400000, 300000])
|
|
320
|
+
expect(provider.screenToMap({ x: 100, y: 200 })).toEqual([400000, 300000])
|
|
321
|
+
expect(mockMapGetCoord).toHaveBeenCalledWith([100, 200])
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('isGeometryObscured delegates to spatial utils with map instance', () => {
|
|
325
|
+
const geojson = { type: 'Point', coordinates: [0, 51] }
|
|
326
|
+
const panelRect = { left: 0, top: 0, right: 100, bottom: 100 }
|
|
327
|
+
provider.isGeometryObscured(geojson, panelRect)
|
|
328
|
+
expect(isGeometryObscured).toHaveBeenCalledWith(geojson, panelRect, mockMapInstance)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import turfBbox from '@turf/bbox'
|
|
2
|
+
import { transformExtent } from 'ol/proj.js'
|
|
3
|
+
|
|
4
|
+
// In EPSG:27700 coordinates are projected meters — distances are Pythagorean, no geodesy needed
|
|
5
|
+
|
|
6
|
+
const formatDimension = (meters) => {
|
|
7
|
+
const WHOLE_MILE_THRESHOLD = 10
|
|
8
|
+
const MILE_THRESHOLD = 0.5
|
|
9
|
+
const METERS_PER_MILE = 1609.344
|
|
10
|
+
|
|
11
|
+
const miles = meters / METERS_PER_MILE
|
|
12
|
+
|
|
13
|
+
if (miles < MILE_THRESHOLD) {
|
|
14
|
+
return `${Math.round(meters)}m`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (miles < WHOLE_MILE_THRESHOLD) {
|
|
18
|
+
const value = Number.parseFloat(miles.toFixed(1))
|
|
19
|
+
const units = value === 1 ? 'mile' : 'miles'
|
|
20
|
+
return `${value} ${units}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rounded = Math.round(miles)
|
|
24
|
+
return `${rounded} miles`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns "400m by 1.4 miles" for the visible (padded) map area.
|
|
29
|
+
* extent: [xmin, ymin, xmax, ymax] in EPSG:27700 meters
|
|
30
|
+
*/
|
|
31
|
+
const getAreaDimensions = (extent) => {
|
|
32
|
+
if (!extent) {
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
const [xmin, ymin, xmax, ymax] = extent
|
|
36
|
+
const widthMeters = xmax - xmin
|
|
37
|
+
const heightMeters = ymax - ymin
|
|
38
|
+
return `${formatDimension(heightMeters)} by ${formatDimension(widthMeters)}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns "north 400m, east 750m" for moves between two EPSG:27700 [easting, northing] coords.
|
|
43
|
+
*/
|
|
44
|
+
const getCardinalMove = (from, to) => {
|
|
45
|
+
const THRESHOLD_METERS = 1
|
|
46
|
+
|
|
47
|
+
const dEasting = to[0] - from[0]
|
|
48
|
+
const dNorthing = to[1] - from[1]
|
|
49
|
+
|
|
50
|
+
const moves = []
|
|
51
|
+
|
|
52
|
+
if (Math.abs(dNorthing) > THRESHOLD_METERS) {
|
|
53
|
+
moves.push(`${dNorthing > 0 ? 'north' : 'south'} ${formatDimension(Math.abs(dNorthing))}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (Math.abs(dEasting) > THRESHOLD_METERS) {
|
|
57
|
+
moves.push(`${dEasting > 0 ? 'east' : 'west'} ${formatDimension(Math.abs(dEasting))}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return moves.join(', ')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get a flat bbox [west, south, east, north] in WGS84 from any GeoJSON object.
|
|
65
|
+
*/
|
|
66
|
+
const getBboxFromGeoJSON = (geojson) => turfBbox(geojson)
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a flat extent [xmin, ymin, xmax, ymax] in EPSG:27700 from any GeoJSON object.
|
|
70
|
+
* GeoJSON is always WGS84, so this transforms the bbox.
|
|
71
|
+
*/
|
|
72
|
+
const getExtentFromGeoJSON = (geojson) => {
|
|
73
|
+
const wgs84Bbox = turfBbox(geojson)
|
|
74
|
+
return transformExtent(wgs84Bbox, 'EPSG:4326', 'EPSG:27700')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the visible (padded) extent [xmin, ymin, xmax, ymax] in EPSG:27700.
|
|
79
|
+
* Accounts for view padding by converting padded pixel corners to map coordinates.
|
|
80
|
+
*/
|
|
81
|
+
const getPaddedExtent = (map) => {
|
|
82
|
+
const size = map.getSize()
|
|
83
|
+
const view = map.getView()
|
|
84
|
+
const padding = view.padding || [0, 0, 0, 0] // [top, right, bottom, left]
|
|
85
|
+
const [top, right, bottom, left] = padding
|
|
86
|
+
|
|
87
|
+
const swPixel = [left, size[1] - bottom]
|
|
88
|
+
const nePixel = [size[0] - right, top]
|
|
89
|
+
|
|
90
|
+
const sw = map.getCoordinateFromPixel(swPixel)
|
|
91
|
+
const ne = map.getCoordinateFromPixel(nePixel)
|
|
92
|
+
|
|
93
|
+
if (!sw || !ne) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [sw[0], sw[1], ne[0], ne[1]]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
|
|
102
|
+
* GeoJSON coords are expected in WGS84; they are transformed to EPSG:27700 for screen projection.
|
|
103
|
+
*/
|
|
104
|
+
const isGeometryObscured = (geojson, panelRect, map) => {
|
|
105
|
+
const containerRect = map.getTargetElement().getBoundingClientRect()
|
|
106
|
+
const [west, south, east, north] = getBboxFromGeoJSON(geojson)
|
|
107
|
+
|
|
108
|
+
const corners = [[west, south], [west, north], [east, south], [east, north]].map(coord => {
|
|
109
|
+
return map.getPixelFromCoordinate(coord)
|
|
110
|
+
}).filter(Boolean)
|
|
111
|
+
|
|
112
|
+
if (!corners.length) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const screenMinX = Math.min(...corners.map(c => c[0]))
|
|
117
|
+
const screenMaxX = Math.max(...corners.map(c => c[0]))
|
|
118
|
+
const screenMinY = Math.min(...corners.map(c => c[1]))
|
|
119
|
+
const screenMaxY = Math.max(...corners.map(c => c[1]))
|
|
120
|
+
|
|
121
|
+
const panelLeft = panelRect.left - containerRect.left
|
|
122
|
+
const panelTop = panelRect.top - containerRect.top
|
|
123
|
+
const panelRight = panelRect.right - containerRect.left
|
|
124
|
+
const panelBottom = panelRect.bottom - containerRect.top
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
screenMinX < panelRight &&
|
|
128
|
+
screenMaxX > panelLeft &&
|
|
129
|
+
screenMinY < panelBottom &&
|
|
130
|
+
screenMaxY > panelTop
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
getAreaDimensions,
|
|
136
|
+
getCardinalMove,
|
|
137
|
+
getBboxFromGeoJSON,
|
|
138
|
+
getExtentFromGeoJSON,
|
|
139
|
+
getPaddedExtent,
|
|
140
|
+
isGeometryObscured,
|
|
141
|
+
formatDimension
|
|
142
|
+
}
|