@defra/interactive-map 0.0.23-alpha → 0.0.25-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/docs/api/map-style-config.md +39 -2
- package/docs/plugins/search.md +70 -0
- package/package.json +17 -2
- 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/scale-bar/dist/css/index.css +31 -1
- package/plugins/interact/src/hooks/useAttachEvents.test.js +3 -0
- package/plugins/search/dist/css/index.css +250 -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/Form/Form.jsx +11 -7
- package/plugins/search/src/components/Form/Form.test.jsx +23 -1
- 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/utils/parseOsNamesResults.js +3 -3
- 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 +39 -0
- package/providers/beta/openlayers/src/appEvents.test.js +117 -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 +198 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +249 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +386 -0
- package/providers/beta/openlayers/src/utils/spatial.js +146 -0
- package/providers/beta/openlayers/src/utils/spatial.test.js +119 -0
- package/providers/beta/openlayers/src/utils/tileLayers.js +133 -0
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +251 -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/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +17 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +17 -0
- package/providers/maplibre/src/utils/maplibreFixes.js +1 -1
- package/providers/maplibre/src/utils/patternImages.js +14 -9
- package/providers/maplibre/src/utils/patternImages.test.js +2 -1
- package/rollup.esm.mjs +14 -3
- package/src/App/hooks/useFeatureItems.test.js +1 -0
- package/src/App/hooks/useLayoutMeasurements.test.js +0 -1
- package/src/App/hooks/useMarkersAPI.test.js +1 -0
- package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
- package/src/App/layout/layout.module.scss +1 -0
- package/src/App/store/AppProvider.test.jsx +5 -0
- package/src/App/store/MapProvider.test.jsx +8 -1
- package/src/services/announcer.test.js +5 -7
- package/src/types.js +11 -1
- package/webpack.dev.mjs +14 -5
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! For license information please see index.js.LICENSE.txt */
|
|
2
|
+
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("defra",[],t):"object"==typeof exports?exports.defra=t():(e.defra=e.defra||{},e.defra.openLayersProvider=t())}(this,()=>(()=>{"use strict";var e,t,r={},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((t,r)=>(o.f[r](e,t),t),[])),o.u=e=>"im-openlayers-provider.js",o.miniCssF=e=>{},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="defra.openLayersProvider:",o.l=(r,n,i,a)=>{if(e[r])e[r].push(n);else{var u,c;if(void 0!==i)for(var f=document.getElementsByTagName("script"),p=0;p<f.length;p++){var s=f[p];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+i){u=s;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",o.nc&&u.setAttribute("nonce",o.nc),u.setAttribute("data-webpack",t+i),u.src=r),e[r]=[n];var l=(t,n)=>{u.onerror=u.onload=null,clearTimeout(d);var o=e[r];if(delete e[r],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach(e=>e(n)),t)return t(n)},d=setTimeout(l.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=l.bind(null,u.onerror),u.onload=l.bind(null,u.onload),c&&document.head.appendChild(u)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={57:0};o.f.j=(t,r)=>{var n=o.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var i=new Promise((r,o)=>n=e[t]=[r,o]);r.push(n[2]=i);var a=o.p+o.u(t),u=new Error;o.l(a,r=>{if(o.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var i=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;u.message="Loading chunk "+t+" failed.\n("+i+": "+a+")",u.name="ChunkLoadError",u.type=i,u.request=a,n[1](u)}},"chunk-"+t,t)}};var t=(t,r)=>{var n,i,[a,u,c]=r,f=0;if(a.some(t=>0!==e[t])){for(n in u)o.o(u,n)&&(o.m[n]=u[n]);c&&c(o)}for(t&&t(r);f<a.length;f++)i=a[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0},r=this.webpackChunkdefra_DefraMap=this.webpackChunkdefra_DefraMap||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var i={};function a(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var a=n&&n.prototype instanceof f?n:f,p=Object.create(a.prototype);return u(p,"_invoke",function(r,n,o){var i,a,u,f=0,p=o||[],s=!1,l={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,a=0,u=e,l.n=r,c}};function d(r,n){for(a=r,u=n,t=0;!s&&f&&!o&&t<p.length;t++){var o,i=p[t],d=l.p,v=i[2];r>3?(o=v===n)&&(u=i[(a=i[4])?5:(a=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(a=0,l.v=n,l.n=i[1]):d<v&&(o=r<3||i[0]>n||n>v)&&(i[4]=r,i[5]=n,l.n=v,a=0))}if(o||r>1)return c;throw s=!0,n}return function(o,p,v){if(f>1)throw TypeError("Generator is already running");for(s&&1===p&&d(p,v),a=p,u=v;(t=a<2?e:u)||!s;){i||(a?a<3?(a>1&&(l.n=-1),d(a,u)):l.n=u:l.v=u);try{if(f=2,i){if(a||(o="next"),t=i[o]){if(!(t=t.call(i,u)))throw TypeError("iterator result is not an object");if(!t.done)return t;u=t.value,a<2&&(a=0)}else 1===a&&(t=i.return)&&t.call(i),a<2&&(u=TypeError("The iterator does not provide a '"+o+"' method"),a=1);i=e}else if((t=(s=l.n<0)?u:r.call(n,l))!==c)break}catch(t){i=e,a=1,u=t}finally{f=1}}return{value:t,done:s}}}(r,o,i),!0),p}var c={};function f(){}function p(){}function s(){}t=Object.getPrototypeOf;var l=[][n]?t(t([][n]())):(u(t={},n,function(){return this}),t),d=s.prototype=f.prototype=Object.create(l);function v(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,s):(e.__proto__=s,u(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=s,u(d,"constructor",s),u(s,"constructor",p),p.displayName="GeneratorFunction",u(s,o,"GeneratorFunction"),u(d),u(d,o,"Generator"),u(d,n,function(){return this}),u(d,"toString",function(){return"[object Generator]"}),(a=function(){return{w:i,m:v}})()}function u(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}u=function(e,t,r,n){function i(t,r){u(e,t,function(e){return this._invoke(t,r,e)})}t?o?o(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(i("next",0),i("throw",1),i("return",2))},u(e,t,r,n)}function c(e,t,r,n,o,i,a){try{var u=e[i](a),c=u.value}catch(e){return void r(e)}u.done?t(c):Promise.resolve(c).then(n,o)}function f(){var e,t,r=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).zoomAlignment;return{checkDeviceCapabilities:function(){return{isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}},load:(e=a().m(function e(){var t,n;return a().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(142).then(o.bind(o,272));case 1:return t=e.v.default,n={zoomAlignment:r,crs:"EPSG:27700"},e.a(2,{MapProvider:t,mapProviderConfig:n})}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){c(i,n,o,a,u,"next",e)}function u(e){c(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})}}return o.d(i,{default:()=>f}),i.default})());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './utils/tileLayers.js'
|
|
2
|
+
|
|
3
|
+
export { createTileSource, createVectorTileLayer, createOGCVectorTileLayer }
|
|
4
|
+
|
|
5
|
+
export function attachAppEvents ({ mapProvider, layer, layerType, transformRequest, events, eventBus, map }) {
|
|
6
|
+
const handleSetMapStyle = async (mapStyle) => {
|
|
7
|
+
if (layerType === 'raster') {
|
|
8
|
+
const source = createTileSource(mapStyle.url, transformRequest)
|
|
9
|
+
layer.setSource(source)
|
|
10
|
+
} else if (mapStyle.type === 'ogc-vt') {
|
|
11
|
+
const { layer: newLayer } = await createOGCVectorTileLayer(mapStyle.url, transformRequest)
|
|
12
|
+
map.getLayers().setAt(0, newLayer)
|
|
13
|
+
} else {
|
|
14
|
+
const { layer: newLayer } = await createVectorTileLayer(mapStyle.url, transformRequest)
|
|
15
|
+
map.getLayers().setAt(0, newLayer)
|
|
16
|
+
}
|
|
17
|
+
eventBus.emit(events.MAP_STYLE_CHANGE, { mapStyleId: mapStyle.id })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const handleSetPixelRatio = (pixelRatio) => {
|
|
21
|
+
map.setPixelRatio(pixelRatio)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const handleSizeChange = ({ mapSize }) => {
|
|
25
|
+
mapProvider.mapSize = mapSize
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
eventBus.on(events.MAP_SET_STYLE, handleSetMapStyle)
|
|
29
|
+
eventBus.on(events.MAP_SET_PIXEL_RATIO, handleSetPixelRatio)
|
|
30
|
+
eventBus.on(events.MAP_SIZE_CHANGE, handleSizeChange)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
remove () {
|
|
34
|
+
eventBus.off(events.MAP_SET_STYLE, handleSetMapStyle)
|
|
35
|
+
eventBus.off(events.MAP_SET_PIXEL_RATIO, handleSetPixelRatio)
|
|
36
|
+
eventBus.off(events.MAP_SIZE_CHANGE, handleSizeChange)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { attachAppEvents } from './appEvents.js'
|
|
2
|
+
|
|
3
|
+
import { createTileSource, createVectorTileLayer } from './utils/tileLayers.js'
|
|
4
|
+
|
|
5
|
+
const mockSourceInstance = {}
|
|
6
|
+
const mockVectorTileLayerInstance = {}
|
|
7
|
+
|
|
8
|
+
jest.mock('./utils/tileLayers.js', () => ({
|
|
9
|
+
__esModule: true,
|
|
10
|
+
createTileSource: jest.fn(() => mockSourceInstance),
|
|
11
|
+
createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayerInstance, source: {} }))
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const events = {
|
|
15
|
+
MAP_SET_STYLE: 'map:setstyle',
|
|
16
|
+
MAP_STYLE_CHANGE: 'map:stylechange',
|
|
17
|
+
MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
|
|
18
|
+
MAP_SIZE_CHANGE: 'map:sizechange'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeMap () {
|
|
22
|
+
return { getLayers: jest.fn(() => ({ setAt: jest.fn() })), setPixelRatio: jest.fn() }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeProvider () {
|
|
26
|
+
return { mapSize: 'small' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('attachAppEvents', () => {
|
|
30
|
+
describe('raster', () => {
|
|
31
|
+
function makeSetup () {
|
|
32
|
+
jest.clearAllMocks()
|
|
33
|
+
const layer = { setSource: jest.fn() }
|
|
34
|
+
const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
|
|
35
|
+
const mapProvider = makeProvider()
|
|
36
|
+
const handles = attachAppEvents({ mapProvider, layer, layerType: 'raster', transformRequest: null, events, eventBus, map: makeMap() })
|
|
37
|
+
const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
|
|
38
|
+
return { layer, eventBus, handles, handlerFor, mapProvider }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
it('subscribes to MAP_SET_STYLE on the event bus', () => {
|
|
42
|
+
const { eventBus } = makeSetup()
|
|
43
|
+
expect(eventBus.on).toHaveBeenCalledWith(events.MAP_SET_STYLE, expect.any(Function))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('on MAP_SET_STYLE: creates a tile source and sets it on the layer', async () => {
|
|
47
|
+
const { layer, handlerFor } = makeSetup()
|
|
48
|
+
await handlerFor(events.MAP_SET_STYLE)({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle' })
|
|
49
|
+
expect(createTileSource).toHaveBeenCalledWith('https://new.tiles.com/{z}/{x}/{y}', null)
|
|
50
|
+
expect(layer.setSource).toHaveBeenCalledWith(mockSourceInstance)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
|
|
54
|
+
const { eventBus, handlerFor } = makeSetup()
|
|
55
|
+
await handlerFor(events.MAP_SET_STYLE)({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle' })
|
|
56
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, { mapStyleId: 'newStyle' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('remove() unsubscribes all handlers', () => {
|
|
60
|
+
const { eventBus, handles, handlerFor } = makeSetup()
|
|
61
|
+
handles.remove()
|
|
62
|
+
expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SET_STYLE, handlerFor(events.MAP_SET_STYLE))
|
|
63
|
+
expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SET_PIXEL_RATIO, handlerFor(events.MAP_SET_PIXEL_RATIO))
|
|
64
|
+
expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SIZE_CHANGE, handlerFor(events.MAP_SIZE_CHANGE))
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('vector', () => {
|
|
69
|
+
function makeSetup () {
|
|
70
|
+
jest.clearAllMocks()
|
|
71
|
+
const setAt = jest.fn()
|
|
72
|
+
const map = { getLayers: jest.fn(() => ({ setAt })), setPixelRatio: jest.fn() }
|
|
73
|
+
const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
|
|
74
|
+
const mapProvider = makeProvider()
|
|
75
|
+
const handles = attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
|
|
76
|
+
const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
|
|
77
|
+
return { map, setAt, eventBus, handles, handlerFor, mapProvider }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('on MAP_SET_STYLE: creates a vector tile layer and places it at index 0', async () => {
|
|
81
|
+
const { setAt, handlerFor } = makeSetup()
|
|
82
|
+
await handlerFor(events.MAP_SET_STYLE)({ url: 'https://example.com/styles', id: 'newVts' })
|
|
83
|
+
expect(createVectorTileLayer).toHaveBeenCalledWith('https://example.com/styles', null)
|
|
84
|
+
expect(setAt).toHaveBeenCalledWith(0, mockVectorTileLayerInstance)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
|
|
88
|
+
const { eventBus, handlerFor } = makeSetup()
|
|
89
|
+
await handlerFor(events.MAP_SET_STYLE)({ url: 'https://example.com/styles', id: 'newVts' })
|
|
90
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, { mapStyleId: 'newVts' })
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('pixel ratio and map size', () => {
|
|
95
|
+
function makeSetup () {
|
|
96
|
+
jest.clearAllMocks()
|
|
97
|
+
const map = makeMap()
|
|
98
|
+
const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
|
|
99
|
+
const mapProvider = makeProvider()
|
|
100
|
+
attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
|
|
101
|
+
const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
|
|
102
|
+
return { map, mapProvider, handlerFor }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
it('on MAP_SET_PIXEL_RATIO: calls map.setPixelRatio with the new ratio', () => {
|
|
106
|
+
const { map, handlerFor } = makeSetup()
|
|
107
|
+
handlerFor(events.MAP_SET_PIXEL_RATIO)(3)
|
|
108
|
+
expect(map.setPixelRatio).toHaveBeenCalledWith(3)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('on MAP_SIZE_CHANGE: updates mapProvider.mapSize', () => {
|
|
112
|
+
const { mapProvider, handlerFor } = makeSetup()
|
|
113
|
+
handlerFor(events.MAP_SIZE_CHANGE)({ mapSize: 'large' })
|
|
114
|
+
expect(mapProvider.mapSize).toBe('large')
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// OS Maps API WMTS tile grid for EPSG:27700 — used by the tile source only
|
|
2
|
+
export const TILE_GRID_RESOLUTIONS = [896, 448, 224, 112, 56, 28, 14, 7, 3.5, 1.75, 0.875, 0.4375, 0.21875, 0.109375]
|
|
3
|
+
|
|
4
|
+
// World (ESRI-derived) LOD resolutions for EPSG:27700. When used in the OL View,
|
|
5
|
+
// view.zoom matches ESRI SDK zoom levels exactly. OL maps between view resolution
|
|
6
|
+
// and the nearest OS tile grid level automatically when loading tiles.
|
|
7
|
+
export const WORLD_LOD_RESOLUTIONS = [
|
|
8
|
+
156543.03392800014, 78271.51696400007, 39135.758482000034, 19567.879241000017,
|
|
9
|
+
9783.939620500008, 4891.969810250004, 2445.984905125002, 1222.992452562501,
|
|
10
|
+
611.4962262812505, 305.74811314062526, 152.87405657031263, 76.43702828515632,
|
|
11
|
+
38.21851414257816, 19.10925707128908, 9.55462853564454, 4.77731426782227,
|
|
12
|
+
2.388657133911135, 1.1943285669555674, 0.5971642834777837, 0.29858214173889186,
|
|
13
|
+
0.14929107086944593, 0.07464553543472296, 0.03732276771736148, 0.01866138385868074
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// ESRI LOD 6 (~2446 m/px) matches the ESRI SDK's minimum zoom for EPSG:27700
|
|
17
|
+
export const WORLD_DEFAULT_MIN_ZOOM = 6
|
|
18
|
+
|
|
19
|
+
// ESRI LOD 20 (~0.149 m/px) is the last level that maps to a unique OS tile (zoom 13).
|
|
20
|
+
// Past this point OL upscales the same zoom-13 tiles with no additional detail.
|
|
21
|
+
export const WORLD_DEFAULT_MAX_ZOOM = 20
|
|
22
|
+
|
|
23
|
+
// Top-left corner of the OS National Grid extent
|
|
24
|
+
export const TILE_GRID_ORIGIN = [-238375.0, 1376256.0]
|
|
25
|
+
|
|
26
|
+
export const TILE_SIZE = 256
|
|
27
|
+
|
|
28
|
+
export const DEFAULTS = {
|
|
29
|
+
animationDuration: 300,
|
|
30
|
+
coordinatePrecision: 2
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Zoom level tolerance for min/max checks (fractional zoom)
|
|
34
|
+
export const ZOOM_TOLERANCE = 0.01
|
|
35
|
+
|
|
36
|
+
export const supportedShortcuts = ['showKeyboardHelp', 'selectControl', 'moveLarge', 'nudgeMap', 'zoomLarge', 'nudgeZoom']
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default function createOpenLayersProvider ({ zoomAlignment } = {}) {
|
|
2
|
+
return {
|
|
3
|
+
checkDeviceCapabilities: () => ({
|
|
4
|
+
isSupported: !!document.createElement('canvas').getContext,
|
|
5
|
+
error: 'Canvas is not supported in this browser'
|
|
6
|
+
}),
|
|
7
|
+
load: async () => {
|
|
8
|
+
const MapProvider = (await import(/* webpackChunkName: "im-openlayers-provider" */ './openlayersProvider.js')).default
|
|
9
|
+
|
|
10
|
+
const mapProviderConfig = {
|
|
11
|
+
zoomAlignment,
|
|
12
|
+
crs: 'EPSG:27700'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
MapProvider,
|
|
17
|
+
mapProviderConfig
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { debounce } from '../../../../src/utils/debounce.js'
|
|
2
|
+
import { throttle } from '../../../../src/utils/throttle.js'
|
|
3
|
+
import { ZOOM_TOLERANCE } from './defaults.js'
|
|
4
|
+
|
|
5
|
+
const DEBOUNCE_IDLE_TIME = 500
|
|
6
|
+
const MOVE_THROTTLE_TIME = 10
|
|
7
|
+
const DRAG_TOLERANCE = 6
|
|
8
|
+
|
|
9
|
+
function attachLoadEvents ({ source, map, emit, eventBus, events, on }) {
|
|
10
|
+
let loadedEmitted = false
|
|
11
|
+
on(source, 'tileloadend', () => {
|
|
12
|
+
if (!loadedEmitted) {
|
|
13
|
+
loadedEmitted = true
|
|
14
|
+
eventBus.emit(events.MAP_LOADED)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
map.once('rendercomplete', () => emit(events.MAP_FIRST_IDLE))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function attachMoveEvents ({ map, view, emit, eventBus, events, on, debouncers }) {
|
|
21
|
+
let moving = false
|
|
22
|
+
|
|
23
|
+
const emitMoveEnd = debounce(() => {
|
|
24
|
+
moving = false
|
|
25
|
+
emit(events.MAP_MOVE_END)
|
|
26
|
+
}, DEBOUNCE_IDLE_TIME)
|
|
27
|
+
debouncers.push(emitMoveEnd)
|
|
28
|
+
|
|
29
|
+
const emitMove = throttle(() => emit(events.MAP_MOVE), MOVE_THROTTLE_TIME)
|
|
30
|
+
|
|
31
|
+
on(view, 'change', () => {
|
|
32
|
+
if (!moving) {
|
|
33
|
+
moving = true
|
|
34
|
+
eventBus.emit(events.MAP_MOVE_START)
|
|
35
|
+
}
|
|
36
|
+
emitMoveEnd()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Drive MAP_MOVE from postrender so resolution reads the actual animated value each frame,
|
|
40
|
+
// matching MapLibre/ESRI smooth scale bar behaviour
|
|
41
|
+
on(map, 'postrender', () => {
|
|
42
|
+
if (moving) {
|
|
43
|
+
emitMove()
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function attachClickEvents ({ map, eventBus, events, on }) {
|
|
49
|
+
let pointerDownPixel = null
|
|
50
|
+
|
|
51
|
+
on(map, 'pointerdown', (e) => {
|
|
52
|
+
pointerDownPixel = e.pixel
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
on(map, 'click', (e) => {
|
|
56
|
+
if (pointerDownPixel) {
|
|
57
|
+
const dx = e.pixel[0] - pointerDownPixel[0]
|
|
58
|
+
const dy = e.pixel[1] - pointerDownPixel[1]
|
|
59
|
+
if (Math.hypot(dx, dy) > DRAG_TOLERANCE) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
eventBus.emit(events.MAP_CLICK, {
|
|
64
|
+
point: { x: e.pixel[0], y: e.pixel[1] },
|
|
65
|
+
coords: e.coordinate
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function attachMapEvents ({
|
|
71
|
+
map,
|
|
72
|
+
source,
|
|
73
|
+
events,
|
|
74
|
+
eventBus,
|
|
75
|
+
getZoom,
|
|
76
|
+
getCenter,
|
|
77
|
+
getBounds,
|
|
78
|
+
getResolution
|
|
79
|
+
}) {
|
|
80
|
+
let destroyed = false
|
|
81
|
+
const listeners = []
|
|
82
|
+
const debouncers = []
|
|
83
|
+
|
|
84
|
+
const view = map.getView()
|
|
85
|
+
const viewMinZoom = view.getMinZoom()
|
|
86
|
+
const viewMaxZoom = view.getMaxZoom()
|
|
87
|
+
|
|
88
|
+
const getMapState = () => {
|
|
89
|
+
if (destroyed) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const zoom = getZoom()
|
|
93
|
+
return {
|
|
94
|
+
center: getCenter(),
|
|
95
|
+
bounds: getBounds(),
|
|
96
|
+
resolution: getResolution(),
|
|
97
|
+
zoom,
|
|
98
|
+
isAtMaxZoom: zoom + ZOOM_TOLERANCE >= viewMaxZoom,
|
|
99
|
+
isAtMinZoom: zoom - ZOOM_TOLERANCE <= viewMinZoom
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const emit = (name) => {
|
|
104
|
+
const state = getMapState()
|
|
105
|
+
if (state) {
|
|
106
|
+
eventBus.emit(name, state)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const on = (target, type, handler) => {
|
|
111
|
+
target.on(type, handler)
|
|
112
|
+
listeners.push([target, type, handler])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
attachLoadEvents({ source, map, emit, eventBus, events, on })
|
|
116
|
+
attachMoveEvents({ map, view: map.getView(), emit, eventBus, events, on, debouncers })
|
|
117
|
+
attachClickEvents({ map, eventBus, events, on })
|
|
118
|
+
|
|
119
|
+
on(map, 'postrender', () => eventBus.emit(events.MAP_RENDER))
|
|
120
|
+
|
|
121
|
+
const emitDataChange = debounce(() => emit(events.MAP_DATA_CHANGE), DEBOUNCE_IDLE_TIME)
|
|
122
|
+
debouncers.push(emitDataChange)
|
|
123
|
+
on(source, 'tileloadend', emitDataChange)
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
remove () {
|
|
127
|
+
destroyed = true
|
|
128
|
+
debouncers.forEach(d => d.cancel())
|
|
129
|
+
listeners.forEach(([target, type, handler]) => target.un(type, handler))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Non-passthrough debounce: captures fn without calling it so tests can control when it fires
|
|
2
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
3
|
+
|
|
4
|
+
const mockDebounceFns = []
|
|
5
|
+
|
|
6
|
+
jest.mock('../../../../src/utils/debounce.js', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
debounce: (fn) => {
|
|
9
|
+
const wrapper = jest.fn()
|
|
10
|
+
wrapper.cancel = jest.fn()
|
|
11
|
+
mockDebounceFns.push({ fn, wrapper })
|
|
12
|
+
return wrapper
|
|
13
|
+
}
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
// Passthrough throttle: calls fn immediately (no cancel needed, not added to debouncers)
|
|
17
|
+
jest.mock('../../../../src/utils/throttle.js', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
throttle: (fn) => jest.fn(fn)
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
const events = {
|
|
23
|
+
MAP_LOADED: 'map:loaded',
|
|
24
|
+
MAP_FIRST_IDLE: 'map:firstidle',
|
|
25
|
+
MAP_MOVE_START: 'map:movestart',
|
|
26
|
+
MAP_MOVE: 'map:move',
|
|
27
|
+
MAP_MOVE_END: 'map:moveend',
|
|
28
|
+
MAP_CLICK: 'map:click',
|
|
29
|
+
MAP_RENDER: 'map:render',
|
|
30
|
+
MAP_DATA_CHANGE: 'map:datachange'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeTarget () {
|
|
34
|
+
const handlers = {}
|
|
35
|
+
return {
|
|
36
|
+
on: jest.fn((type, handler) => { (handlers[type] = handlers[type] || []).push(handler) }),
|
|
37
|
+
once: jest.fn((type, handler) => { (handlers[type] = handlers[type] || []).push(handler) }),
|
|
38
|
+
un: jest.fn(),
|
|
39
|
+
trigger: (type, event = {}) => (handlers[type] || []).forEach(h => h(event))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setup () {
|
|
44
|
+
mockDebounceFns.length = 0
|
|
45
|
+
|
|
46
|
+
const view = makeTarget()
|
|
47
|
+
view.getMinZoom = jest.fn(() => 0)
|
|
48
|
+
view.getMaxZoom = jest.fn(() => 13)
|
|
49
|
+
|
|
50
|
+
const map = makeTarget()
|
|
51
|
+
map.getView = jest.fn(() => view)
|
|
52
|
+
|
|
53
|
+
const source = makeTarget()
|
|
54
|
+
source.changed = jest.fn()
|
|
55
|
+
const eventBus = { emit: jest.fn() }
|
|
56
|
+
|
|
57
|
+
const getZoom = jest.fn(() => 7)
|
|
58
|
+
const getCenter = jest.fn(() => [400000, 300000])
|
|
59
|
+
const getBounds = jest.fn(() => [0, 0, 800, 600])
|
|
60
|
+
const getResolution = jest.fn(() => 56)
|
|
61
|
+
|
|
62
|
+
const handles = attachMapEvents({ map, source, events, eventBus, getZoom, getCenter, getBounds, getResolution })
|
|
63
|
+
|
|
64
|
+
return { map, view, source, eventBus, handles }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('attachMapEvents — load events', () => {
|
|
68
|
+
it('emits MAP_LOADED on first tileloadend', () => {
|
|
69
|
+
const { source, eventBus } = setup()
|
|
70
|
+
source.trigger('tileloadend')
|
|
71
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_LOADED)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('does not re-emit MAP_LOADED on subsequent tileloadend', () => {
|
|
75
|
+
const { source, eventBus } = setup()
|
|
76
|
+
source.trigger('tileloadend')
|
|
77
|
+
eventBus.emit.mockClear()
|
|
78
|
+
source.trigger('tileloadend')
|
|
79
|
+
expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_LOADED)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('emits MAP_FIRST_IDLE on rendercomplete', () => {
|
|
83
|
+
const { map, eventBus } = setup()
|
|
84
|
+
map.trigger('rendercomplete')
|
|
85
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_FIRST_IDLE, expect.any(Object))
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('attachMapEvents — move events', () => {
|
|
90
|
+
it('emits MAP_MOVE_START on first view change', () => {
|
|
91
|
+
const { view, eventBus } = setup()
|
|
92
|
+
view.trigger('change')
|
|
93
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_START)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not re-emit MAP_MOVE_START while already moving', () => {
|
|
97
|
+
const { view, eventBus } = setup()
|
|
98
|
+
view.trigger('change')
|
|
99
|
+
eventBus.emit.mockClear()
|
|
100
|
+
view.trigger('change')
|
|
101
|
+
expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_MOVE_START)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('emits MAP_MOVE_END (with state) when debounced callback fires', () => {
|
|
105
|
+
const { view, eventBus } = setup()
|
|
106
|
+
view.trigger('change')
|
|
107
|
+
mockDebounceFns[0].fn() // emitMoveEnd's inner callback
|
|
108
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.any(Object))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('resets moving flag after MAP_MOVE_END so MAP_MOVE_START can fire again', () => {
|
|
112
|
+
const { view, eventBus } = setup()
|
|
113
|
+
view.trigger('change')
|
|
114
|
+
mockDebounceFns[0].fn() // resets moving = false
|
|
115
|
+
eventBus.emit.mockClear()
|
|
116
|
+
view.trigger('change')
|
|
117
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_START)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('emits MAP_MOVE on postrender when moving', () => {
|
|
121
|
+
const { view, map, eventBus } = setup()
|
|
122
|
+
view.trigger('change') // sets moving = true (debounce wrapper does not reset it)
|
|
123
|
+
map.trigger('postrender')
|
|
124
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE, expect.any(Object))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('does not emit MAP_MOVE on postrender when not moving', () => {
|
|
128
|
+
const { map, eventBus } = setup()
|
|
129
|
+
map.trigger('postrender')
|
|
130
|
+
expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_MOVE, expect.anything())
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('attachMapEvents — click events', () => {
|
|
135
|
+
it('emits MAP_CLICK with point and coords on click with no prior pointerdown', () => {
|
|
136
|
+
const { map, eventBus } = setup()
|
|
137
|
+
map.trigger('click', { pixel: [100, 200], coordinate: [400000, 300000] })
|
|
138
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_CLICK, {
|
|
139
|
+
point: { x: 100, y: 200 },
|
|
140
|
+
coords: [400000, 300000]
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('emits MAP_CLICK when drag distance is within tolerance', () => {
|
|
145
|
+
const { map, eventBus } = setup()
|
|
146
|
+
// Math.hypot(4, 3) = 5 < DRAG_TOLERANCE(6)
|
|
147
|
+
map.trigger('pointerdown', { pixel: [100, 200] })
|
|
148
|
+
map.trigger('click', { pixel: [104, 203], coordinate: [400000, 300000] })
|
|
149
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_CLICK, expect.any(Object))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('suppresses MAP_CLICK when drag distance exceeds tolerance', () => {
|
|
153
|
+
const { map, eventBus } = setup()
|
|
154
|
+
// Math.hypot(7, 0) = 7 > DRAG_TOLERANCE(6)
|
|
155
|
+
map.trigger('pointerdown', { pixel: [100, 200] })
|
|
156
|
+
map.trigger('click', { pixel: [107, 200], coordinate: [400000, 300000] })
|
|
157
|
+
expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_CLICK, expect.anything())
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('attachMapEvents — render and data events', () => {
|
|
162
|
+
it('emits MAP_RENDER on every postrender', () => {
|
|
163
|
+
const { map, eventBus } = setup()
|
|
164
|
+
map.trigger('postrender')
|
|
165
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_RENDER)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('emits MAP_DATA_CHANGE (with state) when debounced tileloadend callback fires', () => {
|
|
169
|
+
const { source, eventBus } = setup()
|
|
170
|
+
source.trigger('tileloadend')
|
|
171
|
+
mockDebounceFns[1].fn() // emitDataChange's inner callback
|
|
172
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_DATA_CHANGE, expect.any(Object))
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('attachMapEvents — remove', () => {
|
|
177
|
+
it('calls cancel on all debouncers', () => {
|
|
178
|
+
const { handles } = setup()
|
|
179
|
+
handles.remove()
|
|
180
|
+
expect(mockDebounceFns[0].wrapper.cancel).toHaveBeenCalled()
|
|
181
|
+
expect(mockDebounceFns[1].wrapper.cancel).toHaveBeenCalled()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('calls un on all registered event targets', () => {
|
|
185
|
+
const { handles, map, view, source } = setup()
|
|
186
|
+
handles.remove()
|
|
187
|
+
expect(view.un).toHaveBeenCalled()
|
|
188
|
+
expect(map.un).toHaveBeenCalled()
|
|
189
|
+
expect(source.un).toHaveBeenCalled()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('prevents state-bearing events from emitting after destroy', () => {
|
|
193
|
+
const { eventBus, handles } = setup()
|
|
194
|
+
handles.remove()
|
|
195
|
+
mockDebounceFns[0].fn() // emitMoveEnd fires after destroy → getMapState returns null
|
|
196
|
+
expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_MOVE_END, expect.anything())
|
|
197
|
+
})
|
|
198
|
+
})
|