@defra/interactive-map 0.0.50-alpha → 0.0.51-apha
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/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/docs/api.md +19 -0
- package/docs/architecture.md +14 -0
- package/docs/getting-started.md +10 -0
- package/docs/plugins/datasets.md +9 -3
- package/jest.setup.js +8 -0
- package/package.json +8 -3
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ol-adapter.js +1 -0
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js +2 -0
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/loadLayerAdapter.js +7 -3
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +17 -0
- package/plugins/datasets/src/adapters/mapboxStyleLayerAdapter.js +74 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +14 -79
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +4 -0
- package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +3 -89
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.js +111 -0
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.test.js +198 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.js +205 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.test.js +233 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.js +276 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.test.js +431 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.js +172 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.test.js +262 -0
- package/plugins/datasets/src/fetch/createDynamicSource.js +12 -8
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +61 -52
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +13 -1
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +20 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +1 -1
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +1 -1
- package/plugins/datasets/src/registry/mapboxStyleDataset.js +108 -0
- package/plugins/datasets/src/registry/mapboxStyleDataset.test.js +240 -0
- package/plugins/datasets/src/utils/bbox.js +5 -7
- package/plugins/datasets/src/utils/bbox.test.js +3 -10
- package/plugins/draw/dist/css/index.css +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/src/index.js +2 -0
- package/plugins/draw/src/index.test.js +1 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/index.js +16 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +160 -7
- package/providers/beta/openlayers/src/openlayersProvider.test.js +13 -0
- package/providers/beta/openlayers/src/utils/bngProjection.js +9 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.js +32 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.test.js +36 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +38 -23
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +153 -8
- package/providers/beta/openlayers/src/utils/hoverCursor.js +5 -1
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +40 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -54
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +143 -1
- package/providers/beta/openlayers/src/utils/symbolImages.js +79 -5
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +111 -1
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +20 -8
- package/providers/beta/openlayers/src/utils/vtTileFragments.test.js +162 -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/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/patternImages.js +4 -44
- package/rollup.esm.mjs +2 -0
- package/src/services/patternRegistry.js +38 -0
- package/src/services/patternRegistry.test.js +61 -0
- package/src/types.js +11 -0
- package/webpack.dev.mjs +1 -0
- /package/plugins/datasets/dist/esm/{esriLayerAdapter.js → im-datasets-esri-adapter.js} +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
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,
|
|
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,382));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})());
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../../src/types.js').MapProviderDescriptor} MapProviderDescriptor
|
|
3
|
+
* @typedef {import('../../../../src/types.js').MapProviderLoadResult} MapProviderLoadResult
|
|
4
|
+
* @typedef {import('../../../../src/types.js').MapProviderConfig} MapProviderConfig
|
|
5
|
+
* @typedef {import('../../../../src/types.js').OpenLayersProviderConfig} OpenLayersProviderConfig
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates an OpenLayers provider descriptor for lazy-loading the map provider.
|
|
10
|
+
*
|
|
11
|
+
* @param {OpenLayersProviderConfig} [config={}] - Optional provider configuration overrides.
|
|
12
|
+
* @returns {MapProviderDescriptor} The map provider descriptor.
|
|
13
|
+
*/
|
|
1
14
|
export default function createOpenLayersProvider ({ zoomAlignment } = {}) {
|
|
2
15
|
return {
|
|
3
16
|
checkDeviceCapabilities: () => ({
|
|
4
17
|
isSupported: !!document.createElement('canvas').getContext,
|
|
5
18
|
error: 'Canvas is not supported in this browser'
|
|
6
19
|
}),
|
|
20
|
+
/** @returns {Promise<MapProviderLoadResult>} */
|
|
7
21
|
load: async () => {
|
|
8
22
|
const MapProvider = (await import(/* webpackChunkName: "im-openlayers-provider" */ './openlayersProvider.js')).default
|
|
9
23
|
|
|
24
|
+
/** @type {MapProviderConfig} */
|
|
10
25
|
const mapProviderConfig = {
|
|
11
26
|
zoomAlignment,
|
|
27
|
+
// Fixed, not user-configurable — the OpenLayers provider is BNG-native only.
|
|
12
28
|
crs: 'EPSG:27700'
|
|
13
29
|
}
|
|
14
30
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../../src/types.js').MapProvider} MapProvider
|
|
3
|
+
* @typedef {import('../../../../src/types.js').MapProviderConfig} MapProviderConfig
|
|
4
|
+
*/
|
|
1
5
|
import { MapProvider } from '../../../mapProvider.js'
|
|
2
6
|
import OlMap from 'ol/Map.js'
|
|
3
7
|
import View from 'ol/View.js'
|
|
4
8
|
import { defaults as defaultInteractions } from 'ol/interaction/defaults.js'
|
|
5
9
|
import { getCenter as getExtentCenter } from 'ol/extent.js'
|
|
6
|
-
import
|
|
7
|
-
import { register } from 'ol/proj/proj4.js'
|
|
10
|
+
import { BNG_CRS } from './utils/bngProjection.js'
|
|
8
11
|
import { supportedShortcuts, DEFAULTS } from './defaults.js'
|
|
9
12
|
import { getViewResolutionConfig, ZOOM_ALIGNMENT } from './utils/zoom.js'
|
|
10
13
|
import { attachMapEvents } from './mapEvents.js'
|
|
@@ -18,7 +21,7 @@ import { applyOpenLayersFixes } from './utils/openLayersFixes.js'
|
|
|
18
21
|
|
|
19
22
|
applyOpenLayersFixes()
|
|
20
23
|
|
|
21
|
-
const CRS =
|
|
24
|
+
const CRS = BNG_CRS
|
|
22
25
|
|
|
23
26
|
const toPaddingArray = (padding) => {
|
|
24
27
|
if (!padding) {
|
|
@@ -28,11 +31,18 @@ const toPaddingArray = (padding) => {
|
|
|
28
31
|
return [top, right, bottom, left]
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* OpenLayers implementation of the MapProvider interface.
|
|
36
|
+
*
|
|
37
|
+
* @implements {MapProvider}
|
|
38
|
+
*/
|
|
35
39
|
export default class OpenLayersProvider extends MapProvider {
|
|
40
|
+
/**
|
|
41
|
+
* @param {Object} options - Constructor options.
|
|
42
|
+
* @param {MapProviderConfig} [options.mapProviderConfig={}] - Provider configuration.
|
|
43
|
+
* @param {Object} options.events - Event name constants.
|
|
44
|
+
* @param {Object} options.eventBus - Event emitter for publishing map events.
|
|
45
|
+
*/
|
|
36
46
|
constructor ({ mapProviderConfig = {}, events, eventBus }) {
|
|
37
47
|
super()
|
|
38
48
|
this.events = events
|
|
@@ -48,6 +58,18 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
48
58
|
return 'OpenLayersProvider'
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
// Unlike MapLibre, initMap() awaits the OL style/sprite fetch before constructing the map, so
|
|
62
|
+
// the base map is already loaded by the time this.map is set.
|
|
63
|
+
isBaseMapReady () {
|
|
64
|
+
return Boolean(this.map)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the map.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} config - Map initialization configuration.
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*/
|
|
51
73
|
async initMap (config) {
|
|
52
74
|
const { container, padding, mapStyle, mapSize, center, zoom, bounds, minZoom, maxZoom, transformRequest, pixelRatio } = config
|
|
53
75
|
this.mapStyleId = mapStyle?.id
|
|
@@ -118,6 +140,7 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
118
140
|
})
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
/** Destroy the map and clean up resources. */
|
|
121
144
|
destroyMap () {
|
|
122
145
|
this.mapEventHandles?.remove()
|
|
123
146
|
this.appEventHandles?.remove()
|
|
@@ -141,6 +164,13 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
141
164
|
// Side-effects
|
|
142
165
|
// ==========================
|
|
143
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Set map view with optional center and zoom.
|
|
169
|
+
*
|
|
170
|
+
* @param {Object} options - View options.
|
|
171
|
+
* @param {[number, number]} [options.center] - Center coordinates [easting, northing].
|
|
172
|
+
* @param {number} [options.zoom] - Zoom level.
|
|
173
|
+
*/
|
|
144
174
|
setView ({ center, zoom }) {
|
|
145
175
|
this.view.animate({
|
|
146
176
|
center: center ?? this.view.getCenter(),
|
|
@@ -149,6 +179,11 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
149
179
|
})
|
|
150
180
|
}
|
|
151
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Zoom in by delta.
|
|
184
|
+
*
|
|
185
|
+
* @param {number} zoomDelta - Amount to zoom in.
|
|
186
|
+
*/
|
|
152
187
|
zoomIn (zoomDelta) {
|
|
153
188
|
this.view.animate({
|
|
154
189
|
zoom: this.view.getZoom() + zoomDelta,
|
|
@@ -156,6 +191,11 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
156
191
|
})
|
|
157
192
|
}
|
|
158
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Zoom out by delta.
|
|
196
|
+
*
|
|
197
|
+
* @param {number} zoomDelta - Amount to zoom out.
|
|
198
|
+
*/
|
|
159
199
|
zoomOut (zoomDelta) {
|
|
160
200
|
this.view.animate({
|
|
161
201
|
zoom: this.view.getZoom() - zoomDelta,
|
|
@@ -163,6 +203,11 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
163
203
|
})
|
|
164
204
|
}
|
|
165
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Pan map by pixel offset [x, y]. Positive x pans right, positive y pans down.
|
|
208
|
+
*
|
|
209
|
+
* @param {[number, number]} offset - Pixel offset [x, y].
|
|
210
|
+
*/
|
|
166
211
|
panBy (offset) {
|
|
167
212
|
const center = this.view.getCenter()
|
|
168
213
|
const resolution = this.view.getResolution()
|
|
@@ -173,11 +218,21 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
173
218
|
})
|
|
174
219
|
}
|
|
175
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Fit map view to the specified bounds or GeoJSON geometry.
|
|
223
|
+
*
|
|
224
|
+
* @param {[number, number, number, number] | object} bounds - Bounds as [west, south, east, north] (BNG easting/northing), or a GeoJSON Feature, FeatureCollection, or geometry.
|
|
225
|
+
*/
|
|
176
226
|
fitToBounds (bounds) {
|
|
177
227
|
const extent = Array.isArray(bounds) ? bounds : getExtentFromGeoJSON(bounds)
|
|
178
228
|
this.view.fit(extent, { duration: DEFAULTS.animationDuration })
|
|
179
229
|
}
|
|
180
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Set map padding as pixel insets from the top, bottom, left and right edges of the map.
|
|
233
|
+
*
|
|
234
|
+
* @param {{ top?: number, bottom?: number, left?: number, right?: number }} padding - Padding in pixels.
|
|
235
|
+
*/
|
|
181
236
|
setPadding (padding) {
|
|
182
237
|
this.view.padding = toPaddingArray(padding)
|
|
183
238
|
}
|
|
@@ -186,6 +241,11 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
186
241
|
// Read-only getters
|
|
187
242
|
// ==========================
|
|
188
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Get current center coordinates [easting, northing].
|
|
246
|
+
*
|
|
247
|
+
* @returns {[number, number]}
|
|
248
|
+
*/
|
|
189
249
|
getCenter () {
|
|
190
250
|
const center = this.view.getCenter()
|
|
191
251
|
const p = DEFAULTS.coordinatePrecision
|
|
@@ -195,19 +255,47 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
195
255
|
]
|
|
196
256
|
}
|
|
197
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Get current zoom level.
|
|
260
|
+
*
|
|
261
|
+
* @returns {number}
|
|
262
|
+
*/
|
|
198
263
|
getZoom () {
|
|
199
264
|
return this.view.getZoom()
|
|
200
265
|
}
|
|
201
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Get current bounds as [west, south, east, north] (BNG easting/northing).
|
|
269
|
+
*
|
|
270
|
+
* @returns {[number, number, number, number]}
|
|
271
|
+
*/
|
|
202
272
|
getBounds () {
|
|
203
273
|
const extent = this.view.calculateExtent(this.map.getSize())
|
|
204
274
|
return extent.map(n => Math.round(n * 100) / 100)
|
|
205
275
|
}
|
|
206
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Query rendered features at a screen pixel position (x from left edge, y from top edge of viewport).
|
|
279
|
+
*
|
|
280
|
+
* @param {{ x: number, y: number }} point - Screen pixel position.
|
|
281
|
+
* @param {Object} [options]
|
|
282
|
+
* @param {number} [options.radius] - Pixel radius to expand the query area. Results sorted closest-first.
|
|
283
|
+
* @returns {any[]}
|
|
284
|
+
*/
|
|
207
285
|
getFeaturesAtPoint (point, options) {
|
|
208
286
|
return queryFeatures(this.map, point, options)
|
|
209
287
|
}
|
|
210
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Get a feature's full geometry, stitched from its vector-tile fragments. A feature that
|
|
291
|
+
* spans multiple tiles is otherwise only visible as whichever single fragment was clicked —
|
|
292
|
+
* this combines every loaded fragment sharing the same id into one Polygon/MultiPolygon.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} layerId
|
|
295
|
+
* @param {string|number} featureId
|
|
296
|
+
* @param {string} [idProperty] - Feature property to match on, if not using the tile's native feature id.
|
|
297
|
+
* @returns {object|null} A GeoJSON Polygon or MultiPolygon geometry, or null if no fragments were found.
|
|
298
|
+
*/
|
|
211
299
|
getFeatureGeometry (layerId, featureId, idProperty) {
|
|
212
300
|
const fragments = collectTileFragments(this.map, layerId, featureId, idProperty)
|
|
213
301
|
if (fragments.length === 0) { return null }
|
|
@@ -216,6 +304,12 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
216
304
|
return { type: 'MultiPolygon', coordinates: coords }
|
|
217
305
|
}
|
|
218
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Set pointer cursor on the map canvas when hovering over any of the given layer IDs.
|
|
309
|
+
* Call with an empty array to remove all hover cursor listeners.
|
|
310
|
+
*
|
|
311
|
+
* @param {string[]} layerIds
|
|
312
|
+
*/
|
|
219
313
|
setHoverCursor (layerIds) {
|
|
220
314
|
if (!this.map) {
|
|
221
315
|
return
|
|
@@ -223,15 +317,38 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
223
317
|
this._onHoverMove = setupHoverCursor(this.map, layerIds, this._onHoverMove)
|
|
224
318
|
}
|
|
225
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Get every currently rendered feature across the given layer IDs (not scoped to a point or
|
|
322
|
+
* area, unlike getFeaturesAtPoint).
|
|
323
|
+
*
|
|
324
|
+
* @param {string[]} layerIds
|
|
325
|
+
* @returns {any[]}
|
|
326
|
+
*/
|
|
226
327
|
getVisibleFeatures (layerIds) {
|
|
227
328
|
return getVisibleFeatures(this.map, layerIds)
|
|
228
329
|
}
|
|
229
330
|
|
|
331
|
+
/**
|
|
332
|
+
* @experimental Update highlighted features on the map. Remembers its arguments (see
|
|
333
|
+
* reapplyHighlights) since, unlike MapLibre, a basemap style change here swaps in a fresh
|
|
334
|
+
* base layer/source that highlights must be explicitly re-applied against.
|
|
335
|
+
*
|
|
336
|
+
* @param {any[]} selectedFeatures - Features to highlight.
|
|
337
|
+
* @param {any} activeFeatures - Features to highlight with the keyboard cursor ring.
|
|
338
|
+
* @param {any} stylesMap - Style configuration for highlighting.
|
|
339
|
+
* @returns {any}
|
|
340
|
+
*/
|
|
230
341
|
updateHighlightedFeatures (selectedFeatures, activeFeatures, stylesMap) {
|
|
231
342
|
this._lastHighlightArgs = { selectedFeatures, activeFeatures, stylesMap }
|
|
232
343
|
return updateHighlightedFeatures(this.map, selectedFeatures, activeFeatures, stylesMap)
|
|
233
344
|
}
|
|
234
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Re-applies the last updateHighlightedFeatures call. Called after a basemap style change
|
|
348
|
+
* (see appEvents.js's handleSetMapStyle) — MAP_DATA_CHANGE won't fire for the new base
|
|
349
|
+
* layer's source, so highlights would otherwise be lost silently until the next selection
|
|
350
|
+
* change.
|
|
351
|
+
*/
|
|
235
352
|
reapplyHighlights () {
|
|
236
353
|
if (!this._lastHighlightArgs) {
|
|
237
354
|
return
|
|
@@ -244,18 +361,41 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
244
361
|
// Spatial helpers
|
|
245
362
|
// ==========================
|
|
246
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Get the dimensions of the visible map area as a formatted string (e.g., '400m by 750m').
|
|
366
|
+
*
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
247
369
|
getAreaDimensions () {
|
|
248
370
|
return getAreaDimensions(getPaddedExtent(this.map))
|
|
249
371
|
}
|
|
250
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Get cardinal direction and distance between two coordinates [easting, northing]. Returns a formatted string (e.g., 'north 400m' or 'south 400m, west 750m').
|
|
375
|
+
*
|
|
376
|
+
* @param {[number, number]} from - Start coordinates [easting, northing].
|
|
377
|
+
* @param {[number, number]} to - End coordinates [easting, northing].
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
251
380
|
getCardinalMove (from, to) {
|
|
252
381
|
return getCardinalMove(from, to)
|
|
253
382
|
}
|
|
254
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Get map resolution in metres per pixel.
|
|
386
|
+
*
|
|
387
|
+
* @returns {number}
|
|
388
|
+
*/
|
|
255
389
|
getResolution () {
|
|
256
390
|
return this.view.getResolution()
|
|
257
391
|
}
|
|
258
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Convert map coordinates [easting, northing] to screen pixel position (x from left edge, y from top edge of viewport).
|
|
395
|
+
*
|
|
396
|
+
* @param {[number, number]} coords - Map coordinates [easting, northing].
|
|
397
|
+
* @returns {{ x: number, y: number }} Screen pixel position.
|
|
398
|
+
*/
|
|
259
399
|
mapToScreen (coords) {
|
|
260
400
|
if (!this.map) {
|
|
261
401
|
return { x: 0, y: 0 }
|
|
@@ -267,10 +407,23 @@ export default class OpenLayersProvider extends MapProvider {
|
|
|
267
407
|
return { x: pixel[0], y: pixel[1] }
|
|
268
408
|
}
|
|
269
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Convert screen pixel position (x from left edge, y from top edge of viewport) to map coordinates [easting, northing].
|
|
412
|
+
*
|
|
413
|
+
* @param {{ x: number, y: number }} point - Screen pixel position.
|
|
414
|
+
* @returns {[number, number]} Map coordinates [easting, northing].
|
|
415
|
+
*/
|
|
270
416
|
screenToMap (point) {
|
|
271
417
|
return this.map.getCoordinateFromPixel([point.x, point.y])
|
|
272
418
|
}
|
|
273
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
|
|
422
|
+
*
|
|
423
|
+
* @param {object} geojson - GeoJSON Feature, FeatureCollection, or geometry.
|
|
424
|
+
* @param {DOMRect} panelRect - Bounding rect of the panel element (viewport coordinates).
|
|
425
|
+
* @returns {boolean}
|
|
426
|
+
*/
|
|
274
427
|
isGeometryObscured (geojson, panelRect) {
|
|
275
428
|
return isGeometryObscured(geojson, panelRect, this.map)
|
|
276
429
|
}
|
|
@@ -127,6 +127,19 @@ describe('OpenLayersProvider', () => {
|
|
|
127
127
|
})
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
+
describe('isBaseMapReady', () => {
|
|
131
|
+
it('returns false before initMap has run', () => {
|
|
132
|
+
const { provider } = makeProvider()
|
|
133
|
+
expect(provider.isBaseMapReady()).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('returns true once initMap has set this.map', async () => {
|
|
137
|
+
const { provider } = makeProvider()
|
|
138
|
+
await provider.initMap(defaultInitConfig)
|
|
139
|
+
expect(provider.isBaseMapReady()).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
130
143
|
describe('initMap', () => {
|
|
131
144
|
it('creates vector tile layer, OL objects, and emits MAP_READY by default', async () => {
|
|
132
145
|
const { provider, eventBus } = makeProvider()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import proj4 from 'proj4'
|
|
2
|
+
import { register } from 'ol/proj/proj4.js'
|
|
3
|
+
|
|
4
|
+
export const BNG_CRS = 'EPSG:27700'
|
|
5
|
+
|
|
6
|
+
// Registers British National Grid with proj4 so OL can use it — shared so callers don't
|
|
7
|
+
// duplicate the proj4.defs() string.
|
|
8
|
+
proj4.defs(BNG_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')
|
|
9
|
+
register(proj4)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { buildExpression, newEvaluationContext } from 'ol/expr/cpu.js'
|
|
2
|
+
import { BooleanType, newParsingContext } from 'ol/expr/expression.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compiles a Mapbox-style filter expression (get/in/literal/to-string/all/! — the same shape
|
|
6
|
+
* Dataset.filter produces, see plugins/datasets/src/registry/dataset.js) into a reusable
|
|
7
|
+
* per-feature predicate. Shared by two consumers with very different call frequencies:
|
|
8
|
+
* canvasPatternStyle.js's style function (evaluated once per feature on every render pass, for
|
|
9
|
+
* every pattern-filtered sublayer sharing a source) and queryFeatures.js's getVisibleFeatures
|
|
10
|
+
* (evaluated on demand, only when reading straight off a shared vector-tile/vector source).
|
|
11
|
+
*
|
|
12
|
+
* The evaluation context is built once here and mutated on every call rather than allocated
|
|
13
|
+
* fresh per feature — confirmed against ol/expr/cpu.js's own compiled evaluators, which only
|
|
14
|
+
* ever read context.properties/featureId/geometryType/resolution/variables synchronously within
|
|
15
|
+
* the call and never retain a reference, so reuse is safe. None of the expressions this
|
|
16
|
+
* evaluates touch resolution/variables, so nothing else needs resetting between features.
|
|
17
|
+
* @param {Array|undefined} filter
|
|
18
|
+
* @returns {(function(import('ol/Feature.js').default): boolean)|null}
|
|
19
|
+
*/
|
|
20
|
+
export const buildFilterEvaluator = (filter) => {
|
|
21
|
+
if (!filter) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
const evaluator = buildExpression(filter, BooleanType, newParsingContext())
|
|
25
|
+
const context = newEvaluationContext()
|
|
26
|
+
return (feature) => {
|
|
27
|
+
context.properties = feature.getProperties()
|
|
28
|
+
context.featureId = feature.getId() ?? null
|
|
29
|
+
context.geometryType = feature.getGeometry()?.getType() ?? ''
|
|
30
|
+
return Boolean(evaluator(context))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Feature from 'ol/Feature.js'
|
|
2
|
+
import Point from 'ol/geom/Point.js'
|
|
3
|
+
import { buildFilterEvaluator } from './filterEvaluator.js'
|
|
4
|
+
|
|
5
|
+
const feature = (props) => new Feature({ geometry: new Point([0, 0]), ...props })
|
|
6
|
+
|
|
7
|
+
describe('buildFilterEvaluator', () => {
|
|
8
|
+
it('returns null for no filter', () => {
|
|
9
|
+
expect(buildFilterEvaluator(undefined)).toBeNull()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('matches a feature whose property satisfies the filter', () => {
|
|
13
|
+
const matches = buildFilterEvaluator(['==', ['get', 'category'], 'a'])
|
|
14
|
+
expect(matches(feature({ category: 'a' }))).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('rejects a feature whose property does not satisfy the filter', () => {
|
|
18
|
+
const matches = buildFilterEvaluator(['==', ['get', 'category'], 'a'])
|
|
19
|
+
expect(matches(feature({ category: 'b' }))).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('evaluates correctly for a feature with no geometry', () => {
|
|
23
|
+
const matches = buildFilterEvaluator(['==', ['get', 'category'], 'a'])
|
|
24
|
+
expect(matches(new Feature({ category: 'a' }))).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('reuses one evaluation context across many features without leaking state between them', () => {
|
|
28
|
+
const matches = buildFilterEvaluator(['==', ['get', 'category'], 'a'])
|
|
29
|
+
const results = [
|
|
30
|
+
matches(feature({ category: 'a' })),
|
|
31
|
+
matches(feature({ category: 'b' })),
|
|
32
|
+
matches(feature({ category: 'a' }))
|
|
33
|
+
]
|
|
34
|
+
expect(results).toEqual([true, false, true])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -7,7 +7,7 @@ import Stroke from 'ol/style/Stroke.js'
|
|
|
7
7
|
import Fill from 'ol/style/Fill.js'
|
|
8
8
|
import Icon from 'ol/style/Icon.js'
|
|
9
9
|
import { collectTileFragments } from './vtTileFragments.js'
|
|
10
|
-
import { getCachedSymbolImage } from './symbolImages.js'
|
|
10
|
+
import { getCachedSymbolImage, getActiveSymbolImageId, getSelectedSymbolImageId, SYMBOL_RASTER_PIXEL_RATIO } from './symbolImages.js'
|
|
11
11
|
import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
|
|
12
12
|
import { getSymbolAnchor } from '../../../../../src/utils/symbolUtils.js'
|
|
13
13
|
|
|
@@ -108,14 +108,19 @@ const wrapVtLayers = (map, selectedKeys, activeKeys, idPropsMap, stylesMap) => {
|
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
// A restyle made directly via layer.setStyle() while a selection stays active (e.g. a
|
|
112
|
+
// theme switch) replaces our wrap with a fresh style function — comparing against our own
|
|
113
|
+
// last-installed wrap detects this and re-captures the new live style instead of going stale.
|
|
114
|
+
if (!layer._highlightOriginalStyle || layer.getStyleFunction() !== layer._highlightWrappedStyle) {
|
|
112
115
|
layer._highlightOriginalStyle = layer.getStyleFunction()
|
|
113
116
|
}
|
|
114
117
|
const orig = layer._highlightOriginalStyle
|
|
115
118
|
|
|
116
|
-
|
|
119
|
+
const wrappedStyle = (feature, resolution) => {
|
|
117
120
|
const base = orig(feature, resolution)
|
|
118
|
-
|
|
121
|
+
// draw-ol's MVT tiles carry a 'mapbox-layer' object; tiles-backed datasets tag the OL
|
|
122
|
+
// layer itself with 'layerId' instead.
|
|
123
|
+
const styleLayerId = feature.get('mapbox-layer')?.id ?? layer.get('layerId')
|
|
119
124
|
if (!styleLayerId) {
|
|
120
125
|
return base
|
|
121
126
|
}
|
|
@@ -136,7 +141,9 @@ const wrapVtLayers = (map, selectedKeys, activeKeys, idPropsMap, stylesMap) => {
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
return [...toStyleArray(base), ...highlightStyles]
|
|
139
|
-
}
|
|
144
|
+
}
|
|
145
|
+
layer._highlightWrappedStyle = wrappedStyle
|
|
146
|
+
layer.setStyle(wrappedStyle)
|
|
140
147
|
// setStyle() calls layer.changed() internally — no source.changed() needed
|
|
141
148
|
// (source.changed() works but causes a visible flicker on selection)
|
|
142
149
|
})
|
|
@@ -149,12 +156,7 @@ const wrapVtLayers = (map, selectedKeys, activeKeys, idPropsMap, stylesMap) => {
|
|
|
149
156
|
// ---------------------------------------------------------------------------
|
|
150
157
|
|
|
151
158
|
const getOrCreateHighlightLayer = (map) => {
|
|
152
|
-
let layer =
|
|
153
|
-
map.getLayers().forEach(l => {
|
|
154
|
-
if (l.get(HIGHLIGHT_MARKER)) {
|
|
155
|
-
layer = l
|
|
156
|
-
}
|
|
157
|
-
})
|
|
159
|
+
let layer = map.getLayers().getArray().find(mapLayer => mapLayer.get(HIGHLIGHT_MARKER))
|
|
158
160
|
if (!layer) {
|
|
159
161
|
layer = new VectorLayer({ source: new VectorSource(), zIndex: HIGHLIGHT_Z + 2 })
|
|
160
162
|
layer.set(HIGHLIGHT_MARKER, true)
|
|
@@ -164,6 +166,11 @@ const getOrCreateHighlightLayer = (map) => {
|
|
|
164
166
|
return layer
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
const findVectorLayer = (map, layerId) =>
|
|
170
|
+
map.getLayers().getArray().find(mapLayer =>
|
|
171
|
+
mapLayer.get('layerType') === 'vector' && !mapLayer.get(HIGHLIGHT_MARKER) && mapLayer.get('layerId') === layerId
|
|
172
|
+
)
|
|
173
|
+
|
|
167
174
|
// interact's selectedFeatures carry a `properties` snapshot taken at selection time, which
|
|
168
175
|
// goes stale for the symbol icon path once a map style change re-resolves the point's image
|
|
169
176
|
// ids — reading straight off the live feature avoids showing the previous theme's variant.
|
|
@@ -171,17 +178,23 @@ const getLiveProperties = (map, layerId, featureId) => {
|
|
|
171
178
|
if (featureId == null) {
|
|
172
179
|
return undefined
|
|
173
180
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
const feature = findVectorLayer(map, layerId)?.getSource()?.getFeatureById(String(featureId))
|
|
182
|
+
return feature?.getProperties()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A dataset symbol point (unlike a drawn one) has no active/selected id of its own — every
|
|
186
|
+
// feature in the layer shares one base imageId, so the variant is resolved from that instead.
|
|
187
|
+
const buildDatasetSymbolHighlightStyle = (map, layerId, isActive) => {
|
|
188
|
+
const symbolMeta = findVectorLayer(map, layerId)?.get('symbolMeta')
|
|
189
|
+
if (!symbolMeta) {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
const targetId = isActive ? getActiveSymbolImageId(symbolMeta.imageId) : getSelectedSymbolImageId(symbolMeta.imageId)
|
|
193
|
+
const canvas = targetId && getCachedSymbolImage(targetId)
|
|
194
|
+
if (!canvas) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
return [new Style({ image: new Icon({ img: canvas, anchor: symbolMeta.anchor, scale: 1 / SYMBOL_RASTER_PIXEL_RATIO }), zIndex: HIGHLIGHT_Z })]
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
const addVectorHighlights = (map, source, features, isActive, stylesMap) => {
|
|
@@ -190,7 +203,9 @@ const addVectorHighlights = (map, source, features, isActive, stylesMap) => {
|
|
|
190
203
|
continue
|
|
191
204
|
}
|
|
192
205
|
const liveProperties = getLiveProperties(map, layerId, featureId)
|
|
193
|
-
const styles = buildSymbolHighlightStyle(liveProperties, isActive) ??
|
|
206
|
+
const styles = buildSymbolHighlightStyle(liveProperties, isActive) ??
|
|
207
|
+
buildDatasetSymbolHighlightStyle(map, layerId, isActive) ??
|
|
208
|
+
buildHighlightStyles(stylesMap?.[layerId], isActive)
|
|
194
209
|
if (styles.length) {
|
|
195
210
|
const olFeature = new Feature({ geometry: geoJsonFormat.readGeometry(geometry) })
|
|
196
211
|
olFeature.setStyle(styles)
|