@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.
Files changed (76) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/umd/im-core.js +1 -1
  3. package/docs/api.md +19 -0
  4. package/docs/architecture.md +14 -0
  5. package/docs/getting-started.md +10 -0
  6. package/docs/plugins/datasets.md +9 -3
  7. package/jest.setup.js +8 -0
  8. package/package.json +8 -3
  9. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  10. package/plugins/datasets/dist/esm/im-datasets-ol-adapter.js +1 -0
  11. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  12. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  13. package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js +2 -0
  14. package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js.LICENSE.txt +1 -0
  15. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  16. package/plugins/datasets/dist/umd/index.js +1 -1
  17. package/plugins/datasets/src/adapters/loadLayerAdapter.js +7 -3
  18. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +17 -0
  19. package/plugins/datasets/src/adapters/mapboxStyleLayerAdapter.js +74 -0
  20. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +14 -79
  21. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +4 -0
  22. package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +3 -89
  23. package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.js +111 -0
  24. package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.test.js +198 -0
  25. package/plugins/datasets/src/adapters/openlayers/layerBuilders.js +205 -0
  26. package/plugins/datasets/src/adapters/openlayers/layerBuilders.test.js +233 -0
  27. package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.js +276 -0
  28. package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.test.js +431 -0
  29. package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.js +172 -0
  30. package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.test.js +262 -0
  31. package/plugins/datasets/src/fetch/createDynamicSource.js +12 -8
  32. package/plugins/datasets/src/fetch/createDynamicSource.test.js +61 -52
  33. package/plugins/datasets/src/initialise/DatasetsInit.jsx +13 -1
  34. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +20 -0
  35. package/plugins/datasets/src/initialise/initialiseDatasets.js +1 -1
  36. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +1 -1
  37. package/plugins/datasets/src/registry/mapboxStyleDataset.js +108 -0
  38. package/plugins/datasets/src/registry/mapboxStyleDataset.test.js +240 -0
  39. package/plugins/datasets/src/utils/bbox.js +5 -7
  40. package/plugins/datasets/src/utils/bbox.test.js +3 -10
  41. package/plugins/draw/dist/css/index.css +1 -0
  42. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  43. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  44. package/plugins/draw/src/index.js +2 -0
  45. package/plugins/draw/src/index.test.js +1 -0
  46. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  47. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  48. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  49. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  50. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  51. package/providers/beta/openlayers/src/index.js +16 -0
  52. package/providers/beta/openlayers/src/openlayersProvider.js +160 -7
  53. package/providers/beta/openlayers/src/openlayersProvider.test.js +13 -0
  54. package/providers/beta/openlayers/src/utils/bngProjection.js +9 -0
  55. package/providers/beta/openlayers/src/utils/filterEvaluator.js +32 -0
  56. package/providers/beta/openlayers/src/utils/filterEvaluator.test.js +36 -0
  57. package/providers/beta/openlayers/src/utils/highlightFeatures.js +38 -23
  58. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +153 -8
  59. package/providers/beta/openlayers/src/utils/hoverCursor.js +5 -1
  60. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +40 -0
  61. package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -54
  62. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +143 -1
  63. package/providers/beta/openlayers/src/utils/symbolImages.js +79 -5
  64. package/providers/beta/openlayers/src/utils/symbolImages.test.js +111 -1
  65. package/providers/beta/openlayers/src/utils/vtTileFragments.js +20 -8
  66. package/providers/beta/openlayers/src/utils/vtTileFragments.test.js +162 -0
  67. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  68. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  69. package/providers/maplibre/dist/umd/index.js +1 -1
  70. package/providers/maplibre/src/utils/patternImages.js +4 -44
  71. package/rollup.esm.mjs +2 -0
  72. package/src/services/patternRegistry.js +38 -0
  73. package/src/services/patternRegistry.test.js +61 -0
  74. package/src/types.js +11 -0
  75. package/webpack.dev.mjs +1 -0
  76. /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,501));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})());
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 proj4 from 'proj4'
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 = 'EPSG:27700'
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
- // Register British National Grid with proj4 so OL can use it
32
- 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')
33
- register(proj4)
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
- if (!layer._highlightOriginalStyle) {
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
- layer.setStyle((feature, resolution) => {
119
+ const wrappedStyle = (feature, resolution) => {
117
120
  const base = orig(feature, resolution)
118
- const styleLayerId = feature.get('mapbox-layer')?.id
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 = null
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
- let properties
175
- map.getLayers().forEach(l => {
176
- if (properties || l.get('layerType') !== 'vector' || l.get(HIGHLIGHT_MARKER) || l.get('layerId') !== layerId) {
177
- return
178
- }
179
- const feature = l.getSource()?.getFeatureById(String(featureId))
180
- if (feature) {
181
- properties = feature.getProperties()
182
- }
183
- })
184
- return properties
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) ?? buildHighlightStyles(stylesMap?.[layerId], 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)