@defra/interactive-map 0.0.26-alpha → 0.0.27-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.
Files changed (92) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/esm/im-shell.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/dist/umd/index.js +1 -1
  5. package/docs/assets/css/docusaurus.scss +4 -0
  6. package/package.json +1 -1
  7. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  8. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/src/defaults.js +14 -14
  10. package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
  11. package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
  12. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
  13. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
  14. package/plugins/beta/draw-ml/src/reducer.js +4 -1
  15. package/plugins/beta/draw-ml/src/styles.js +42 -39
  16. package/plugins/beta/draw-ol/dist/css/index.css +13 -0
  17. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
  18. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
  19. package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
  20. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
  21. package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
  22. package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
  23. package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
  24. package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
  25. package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
  26. package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
  27. package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
  28. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
  29. package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
  30. package/plugins/beta/draw-ol/src/core/styles.js +98 -0
  31. package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
  32. package/plugins/beta/draw-ol/src/defaults.js +16 -0
  33. package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
  34. package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
  35. package/plugins/beta/draw-ol/src/draw.scss +17 -0
  36. package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
  37. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
  38. package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
  39. package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
  40. package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
  41. package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
  42. package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
  43. package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
  44. package/plugins/beta/draw-ol/src/events.js +114 -0
  45. package/plugins/beta/draw-ol/src/index.js +12 -0
  46. package/plugins/beta/draw-ol/src/manifest.js +121 -0
  47. package/plugins/beta/draw-ol/src/olDraw.js +38 -0
  48. package/plugins/beta/draw-ol/src/reducer.js +41 -0
  49. package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
  50. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
  51. package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
  52. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
  53. package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
  54. package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
  55. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
  56. package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
  57. package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
  58. package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
  59. package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
  60. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  61. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  62. package/plugins/interact/src/defaults.js +1 -0
  63. package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
  64. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
  65. package/plugins/interact/src/reducer.js +7 -0
  66. package/plugins/interact/src/reducer.test.js +16 -0
  67. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  68. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  69. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  70. package/providers/beta/openlayers/src/appEvents.js +9 -13
  71. package/providers/beta/openlayers/src/appEvents.test.js +25 -17
  72. package/providers/beta/openlayers/src/mapEvents.js +32 -16
  73. package/providers/beta/openlayers/src/mapEvents.test.js +43 -10
  74. package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
  75. package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
  76. package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
  77. package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
  78. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
  79. package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
  80. package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
  81. package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
  82. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
  83. package/providers/beta/openlayers/src/utils/tileLayers.js +19 -1
  84. package/providers/beta/openlayers/src/utils/tileLayers.test.js +74 -3
  85. package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
  86. package/rollup.esm.mjs +6 -0
  87. package/src/App/controls/keyboardActions.js +2 -2
  88. package/src/InteractiveMap/InteractiveMap.js +38 -10
  89. package/src/InteractiveMap/InteractiveMap.test.js +282 -356
  90. package/src/InteractiveMap/historyManager.js +7 -0
  91. package/src/InteractiveMap/historyManager.test.js +11 -0
  92. package/webpack.dev.mjs +3 -1
@@ -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,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})());
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,792));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,20 +1,16 @@
1
- import { createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './utils/tileLayers.js'
1
+ import { createTileSource, createVectorTileLayer, createOGCVectorTileLayer, createMapStyleLayer } from './utils/tileLayers.js'
2
2
 
3
- export { createTileSource, createVectorTileLayer, createOGCVectorTileLayer }
3
+ export { createTileSource, createVectorTileLayer, createOGCVectorTileLayer, createMapStyleLayer }
4
4
 
5
- export function attachAppEvents ({ mapProvider, layer, layerType, transformRequest, events, eventBus, map }) {
5
+ export function attachAppEvents ({ mapProvider, transformRequest, events, eventBus, map, onBaseSourceChange }) {
6
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, mapStyle)
12
- map.getLayers().setAt(0, newLayer)
13
- } else {
14
- const { layer: newLayer } = await createVectorTileLayer(mapStyle.url, transformRequest, mapStyle)
15
- map.getLayers().setAt(0, newLayer)
16
- }
7
+ const { layer, source } = await createMapStyleLayer(mapStyle, transformRequest)
8
+ map.getLayers().setAt(0, layer)
9
+ onBaseSourceChange(source)
17
10
  eventBus.emit(events.MAP_STYLE_CHANGE, { mapStyleId: mapStyle.id })
11
+ // MAP_DATA_CHANGE is driven by the original source's tileloadend and won't fire
12
+ // for the new source, so re-apply highlights directly on the new layer.
13
+ mapProvider.reapplyHighlights()
18
14
  }
19
15
 
20
16
  const handleSetPixelRatio = (pixelRatio) => {
@@ -1,14 +1,13 @@
1
1
  import { attachAppEvents } from './appEvents.js'
2
2
 
3
- import { createTileSource, createVectorTileLayer } from './utils/tileLayers.js'
3
+ import { createMapStyleLayer } from './utils/tileLayers.js'
4
4
 
5
+ const mockLayerInstance = {}
5
6
  const mockSourceInstance = {}
6
- const mockVectorTileLayerInstance = {}
7
7
 
8
8
  jest.mock('./utils/tileLayers.js', () => ({
9
9
  __esModule: true,
10
- createTileSource: jest.fn(() => mockSourceInstance),
11
- createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayerInstance, source: {} }))
10
+ createMapStyleLayer: jest.fn(async () => ({ layer: mockLayerInstance, source: mockSourceInstance }))
12
11
  }))
13
12
 
14
13
  const events = {
@@ -23,19 +22,21 @@ function makeMap () {
23
22
  }
24
23
 
25
24
  function makeProvider () {
26
- return { mapSize: 'small' }
25
+ return { mapSize: 'small', reapplyHighlights: jest.fn() }
27
26
  }
28
27
 
29
28
  describe('attachAppEvents', () => {
30
29
  describe('raster', () => {
31
30
  function makeSetup () {
32
31
  jest.clearAllMocks()
33
- const layer = { setSource: jest.fn() }
32
+ const setAt = jest.fn()
33
+ const map = { getLayers: jest.fn(() => ({ setAt })), setPixelRatio: jest.fn() }
34
+ const onBaseSourceChange = jest.fn()
34
35
  const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
35
36
  const mapProvider = makeProvider()
36
- const handles = attachAppEvents({ mapProvider, layer, layerType: 'raster', transformRequest: null, events, eventBus, map: makeMap() })
37
+ const handles = attachAppEvents({ mapProvider, transformRequest: null, events, eventBus, map, onBaseSourceChange })
37
38
  const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
38
- return { layer, eventBus, handles, handlerFor, mapProvider }
39
+ return { map, setAt, onBaseSourceChange, eventBus, handles, handlerFor, mapProvider }
39
40
  }
40
41
 
41
42
  it('subscribes to MAP_SET_STYLE on the event bus', () => {
@@ -43,11 +44,18 @@ describe('attachAppEvents', () => {
43
44
  expect(eventBus.on).toHaveBeenCalledWith(events.MAP_SET_STYLE, expect.any(Function))
44
45
  })
45
46
 
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)
47
+ it('on MAP_SET_STYLE: creates a map style layer and places it at index 0', async () => {
48
+ const { setAt, handlerFor } = makeSetup()
49
+ const mapStyle = { url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle', type: 'raster' }
50
+ await handlerFor(events.MAP_SET_STYLE)(mapStyle)
51
+ expect(createMapStyleLayer).toHaveBeenCalledWith(mapStyle, null)
52
+ expect(setAt).toHaveBeenCalledWith(0, mockLayerInstance)
53
+ })
54
+
55
+ it('on MAP_SET_STYLE: updates the base source for map events', async () => {
56
+ const { onBaseSourceChange, handlerFor } = makeSetup()
57
+ await handlerFor(events.MAP_SET_STYLE)({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle', type: 'raster' })
58
+ expect(onBaseSourceChange).toHaveBeenCalledWith(mockSourceInstance)
51
59
  })
52
60
 
53
61
  it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
@@ -72,7 +80,7 @@ describe('attachAppEvents', () => {
72
80
  const map = { getLayers: jest.fn(() => ({ setAt })), setPixelRatio: jest.fn() }
73
81
  const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
74
82
  const mapProvider = makeProvider()
75
- const handles = attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
83
+ const handles = attachAppEvents({ mapProvider, transformRequest: null, events, eventBus, map, onBaseSourceChange: jest.fn() })
76
84
  const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
77
85
  return { map, setAt, eventBus, handles, handlerFor, mapProvider }
78
86
  }
@@ -81,8 +89,8 @@ describe('attachAppEvents', () => {
81
89
  const { setAt, handlerFor } = makeSetup()
82
90
  const mapStyle = { url: 'https://example.com/styles', id: 'newVts' }
83
91
  await handlerFor(events.MAP_SET_STYLE)(mapStyle)
84
- expect(createVectorTileLayer).toHaveBeenCalledWith(mapStyle.url, null, mapStyle)
85
- expect(setAt).toHaveBeenCalledWith(0, mockVectorTileLayerInstance)
92
+ expect(createMapStyleLayer).toHaveBeenCalledWith(mapStyle, null)
93
+ expect(setAt).toHaveBeenCalledWith(0, mockLayerInstance)
86
94
  })
87
95
 
88
96
  it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
@@ -98,7 +106,7 @@ describe('attachAppEvents', () => {
98
106
  const map = makeMap()
99
107
  const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
100
108
  const mapProvider = makeProvider()
101
- attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
109
+ attachAppEvents({ mapProvider, transformRequest: null, events, eventBus, map, onBaseSourceChange: jest.fn() })
102
110
  const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
103
111
  return { map, mapProvider, handlerFor }
104
112
  }
@@ -6,17 +6,6 @@ const DEBOUNCE_IDLE_TIME = 500
6
6
  const MOVE_THROTTLE_TIME = 10
7
7
  const DRAG_TOLERANCE = 6
8
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
9
  function attachMoveEvents ({ map, view, emit, eventBus, events, on, debouncers }) {
21
10
  let moving = false
22
11
 
@@ -79,6 +68,7 @@ export function attachMapEvents ({
79
68
  }) {
80
69
  let destroyed = false
81
70
  const listeners = []
71
+ let sourceListeners = []
82
72
  const debouncers = []
83
73
 
84
74
  const view = map.getView()
@@ -110,21 +100,47 @@ export function attachMapEvents ({
110
100
  listeners.push([target, type, handler])
111
101
  }
112
102
 
113
- attachLoadEvents({ source, map, emit, eventBus, events, on })
114
- attachMoveEvents({ map, view: map.getView(), emit, eventBus, events, on, debouncers })
115
- attachClickEvents({ map, eventBus, events, on })
103
+ const onSource = (target, type, handler) => {
104
+ target.on(type, handler)
105
+ sourceListeners.push([target, type, handler])
106
+ }
116
107
 
117
- on(map, 'postrender', () => eventBus.emit(events.MAP_RENDER))
108
+ let loadedEmitted = false
109
+ const emitLoaded = () => {
110
+ if (!loadedEmitted) {
111
+ loadedEmitted = true
112
+ eventBus.emit(events.MAP_LOADED)
113
+ }
114
+ }
118
115
 
119
116
  const emitDataChange = debounce(() => emit(events.MAP_DATA_CHANGE), DEBOUNCE_IDLE_TIME)
120
117
  debouncers.push(emitDataChange)
121
- on(source, 'tileloadend', emitDataChange)
118
+
119
+ const setSource = (newSource) => {
120
+ if (destroyed) {
121
+ return
122
+ }
123
+
124
+ sourceListeners.forEach(([target, type, handler]) => target.un(type, handler))
125
+ sourceListeners = []
126
+ onSource(newSource, 'tileloadend', emitLoaded)
127
+ onSource(newSource, 'tileloadend', emitDataChange)
128
+ }
129
+
130
+ attachMoveEvents({ map, view: map.getView(), emit, eventBus, events, on, debouncers })
131
+ setSource(source)
132
+ map.once('rendercomplete', () => emit(events.MAP_FIRST_IDLE))
133
+ attachClickEvents({ map, eventBus, events, on })
134
+
135
+ on(map, 'postrender', () => eventBus.emit(events.MAP_RENDER))
122
136
 
123
137
  return {
138
+ setSource,
124
139
  remove () {
125
140
  destroyed = true
126
141
  debouncers.forEach(d => d.cancel())
127
142
  listeners.forEach(([target, type, handler]) => target.un(type, handler))
143
+ sourceListeners.forEach(([target, type, handler]) => target.un(type, handler))
128
144
  }
129
145
  }
130
146
  }
@@ -35,7 +35,9 @@ function makeTarget () {
35
35
  return {
36
36
  on: jest.fn((type, handler) => { (handlers[type] = handlers[type] || []).push(handler) }),
37
37
  once: jest.fn((type, handler) => { (handlers[type] = handlers[type] || []).push(handler) }),
38
- un: jest.fn(),
38
+ un: jest.fn((type, handler) => {
39
+ handlers[type] = (handlers[type] || []).filter(h => h !== handler)
40
+ }),
39
41
  trigger: (type, event = {}) => (handlers[type] || []).forEach(h => h(event))
40
42
  }
41
43
  }
@@ -104,14 +106,14 @@ describe('attachMapEvents — move events', () => {
104
106
  it('emits MAP_MOVE_END (with state) when debounced callback fires', () => {
105
107
  const { view, eventBus } = setup()
106
108
  view.trigger('change')
107
- mockDebounceFns[0].fn() // emitMoveEnd's inner callback
109
+ mockDebounceFns[1].fn() // emitMoveEnd's inner callback
108
110
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.any(Object))
109
111
  })
110
112
 
111
113
  it('resets moving flag after MAP_MOVE_END so MAP_MOVE_START can fire again', () => {
112
114
  const { view, eventBus } = setup()
113
115
  view.trigger('change')
114
- mockDebounceFns[0].fn() // resets moving = false
116
+ mockDebounceFns[1].fn() // resets moving = false
115
117
  eventBus.emit.mockClear()
116
118
  view.trigger('change')
117
119
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_START)
@@ -168,33 +170,46 @@ describe('attachMapEvents — render and data events', () => {
168
170
  it('emits MAP_DATA_CHANGE (with state) when debounced tileloadend callback fires', () => {
169
171
  const { source, eventBus } = setup()
170
172
  source.trigger('tileloadend')
171
- mockDebounceFns[1].fn() // emitDataChange's inner callback
173
+ mockDebounceFns[0].fn() // emitDataChange's inner callback
172
174
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_DATA_CHANGE, expect.any(Object))
173
175
  })
176
+
177
+ it('moves source listeners when source changes', () => {
178
+ const { source, handles, eventBus } = setup()
179
+ const newSource = makeTarget()
180
+
181
+ handles.setSource(newSource)
182
+
183
+ source.trigger('tileloadend')
184
+ expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_LOADED)
185
+
186
+ newSource.trigger('tileloadend')
187
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_LOADED)
188
+ })
174
189
  })
175
190
 
176
191
  describe('attachMapEvents — dynamic zoom bounds', () => {
177
192
  it('isAtMaxZoom is updated in emitted event when view max zoom changes', () => {
178
193
  const { view, eventBus } = setup()
179
194
  view.trigger('change')
180
- mockDebounceFns[0].fn()
195
+ mockDebounceFns[1].fn()
181
196
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: false }))
182
197
 
183
198
  eventBus.emit.mockClear()
184
199
  view.getMaxZoom.mockReturnValue(7)
185
- mockDebounceFns[0].fn()
200
+ mockDebounceFns[1].fn()
186
201
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: true }))
187
202
  })
188
203
 
189
204
  it('isAtMinZoom is updated in emitted event when view min zoom changes', () => {
190
205
  const { view, eventBus } = setup()
191
206
  view.trigger('change')
192
- mockDebounceFns[0].fn()
207
+ mockDebounceFns[1].fn()
193
208
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: false }))
194
209
 
195
210
  eventBus.emit.mockClear()
196
211
  view.getMinZoom.mockReturnValue(7)
197
- mockDebounceFns[0].fn()
212
+ mockDebounceFns[1].fn()
198
213
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: true }))
199
214
  })
200
215
  })
@@ -203,8 +218,8 @@ describe('attachMapEvents — remove', () => {
203
218
  it('calls cancel on all debouncers', () => {
204
219
  const { handles } = setup()
205
220
  handles.remove()
206
- expect(mockDebounceFns[0].wrapper.cancel).toHaveBeenCalled()
207
221
  expect(mockDebounceFns[1].wrapper.cancel).toHaveBeenCalled()
222
+ expect(mockDebounceFns[0].wrapper.cancel).toHaveBeenCalled()
208
223
  })
209
224
 
210
225
  it('calls un on all registered event targets', () => {
@@ -215,10 +230,28 @@ describe('attachMapEvents — remove', () => {
215
230
  expect(source.un).toHaveBeenCalled()
216
231
  })
217
232
 
233
+ it('unbinds source listeners when source changes', () => {
234
+ const { source, handles } = setup()
235
+ handles.setSource(makeTarget())
236
+ expect(source.un).toHaveBeenCalledWith('tileloadend', expect.any(Function))
237
+ })
238
+
239
+ it('does not bind a new source after remove', () => {
240
+ const { eventBus, handles } = setup()
241
+ const newSource = makeTarget()
242
+
243
+ handles.remove()
244
+ handles.setSource(newSource)
245
+ newSource.trigger('tileloadend')
246
+
247
+ expect(newSource.on).not.toHaveBeenCalled()
248
+ expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_LOADED)
249
+ })
250
+
218
251
  it('prevents state-bearing events from emitting after destroy', () => {
219
252
  const { eventBus, handles } = setup()
220
253
  handles.remove()
221
- mockDebounceFns[0].fn() // emitMoveEnd fires after destroy → getMapState returns null
254
+ mockDebounceFns[1].fn() // emitMoveEnd fires after destroy → getMapState returns null
222
255
  expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_MOVE_END, expect.anything())
223
256
  })
224
257
  })
@@ -1,18 +1,23 @@
1
1
  import OlMap from 'ol/Map.js'
2
2
  import View from 'ol/View.js'
3
- import TileLayer from 'ol/layer/Tile.js'
4
3
  import { defaults as defaultInteractions } from 'ol/interaction/defaults.js'
5
4
  import proj4 from 'proj4'
6
5
  import { register } from 'ol/proj/proj4.js'
7
6
  import { supportedShortcuts, DEFAULTS } from './defaults.js'
8
7
  import { getViewResolutionConfig, ZOOM_ALIGNMENT } from './utils/zoom.js'
9
8
  import { attachMapEvents } from './mapEvents.js'
10
- import { attachAppEvents, createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './appEvents.js'
9
+ import { attachAppEvents, createMapStyleLayer } from './appEvents.js'
11
10
  import { getAreaDimensions, getCardinalMove, getExtentFromGeoJSON, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
11
+ import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
12
+ import { queryFeatures } from './utils/queryFeatures.js'
13
+ import { collectTileFragments } from './utils/vtTileFragments.js'
14
+ import { setupHoverCursor } from './utils/hoverCursor.js'
15
+ import { applyOpenLayersFixes } from './utils/openLayersFixes.js'
16
+
17
+ applyOpenLayersFixes()
12
18
 
13
19
  const CRS = 'EPSG:27700'
14
20
 
15
- // OL view padding is [top, right, bottom, left]; app passes { top, right, bottom, left }
16
21
  const toPaddingArray = (padding) => {
17
22
  if (!padding) {
18
23
  return undefined
@@ -42,19 +47,7 @@ export default class OpenLayersProvider {
42
47
  this.mapSize = mapSize
43
48
  const { events, eventBus } = this
44
49
 
45
- let tileLayer, source
46
- if (mapStyle.type === 'raster') {
47
- source = createTileSource(mapStyle.url, transformRequest)
48
- tileLayer = new TileLayer({ source })
49
- } else if (mapStyle.type === 'ogc-vt') {
50
- const vectorTile = await createOGCVectorTileLayer(mapStyle.url, transformRequest, mapStyle)
51
- tileLayer = vectorTile.layer
52
- source = vectorTile.source
53
- } else {
54
- const vectorTile = await createVectorTileLayer(mapStyle.url, transformRequest, mapStyle)
55
- tileLayer = vectorTile.layer
56
- source = vectorTile.source
57
- }
50
+ const { layer: tileLayer, source } = await createMapStyleLayer(mapStyle, transformRequest)
58
51
 
59
52
  const viewResolutions = getViewResolutionConfig(this.zoomAlignment ?? ZOOM_ALIGNMENT.UK)
60
53
 
@@ -97,18 +90,15 @@ export default class OpenLayersProvider {
97
90
 
98
91
  this.appEventHandles = attachAppEvents({
99
92
  mapProvider: this,
100
- layer: tileLayer,
101
- layerType: mapStyle.type ?? 'vector',
102
93
  transformRequest,
103
94
  events,
104
95
  eventBus,
105
- map
96
+ map,
97
+ onBaseSourceChange: this.mapEventHandles.setSource
106
98
  }) || []
107
99
 
108
100
  this.map = map
109
101
  this.view = view
110
- this.tileLayer = tileLayer
111
- this.source = source
112
102
 
113
103
  // MAP_READY is synchronous — OL map is immediately interactive after construction
114
104
  eventBus.emit(events.MAP_READY, {
@@ -127,13 +117,15 @@ export default class OpenLayersProvider {
127
117
  this.appEventHandles = null
128
118
 
129
119
  if (this.map) {
120
+ if (this._onHoverMove) {
121
+ this.map.un('pointermove', this._onHoverMove)
122
+ this._onHoverMove = null
123
+ }
130
124
  this.map.setTarget(null)
131
125
  this.map = null
132
126
  }
133
127
 
134
128
  this.view = null
135
- this.tileLayer = null
136
- this.source = null
137
129
  }
138
130
 
139
131
  // ==========================
@@ -203,15 +195,42 @@ export default class OpenLayersProvider {
203
195
  return extent.map(n => Math.round(n * 100) / 100)
204
196
  }
205
197
 
206
- getFeaturesAtPoint (_point, _options) {
207
- // Raster tiles have no queryable features
208
- return []
198
+ getFeaturesAtPoint (point, options) {
199
+ return queryFeatures(this.map, point, options)
200
+ }
201
+
202
+ getFeatureGeometry (layerId, featureId, idProperty) {
203
+ const fragments = collectTileFragments(this.map, layerId, featureId, idProperty)
204
+ if (fragments.length === 0) { return null }
205
+ if (fragments.length === 1) { return fragments[0] }
206
+ const coords = fragments.flatMap(g => g.type === 'MultiPolygon' ? g.coordinates : [g.coordinates])
207
+ return { type: 'MultiPolygon', coordinates: coords }
208
+ }
209
+
210
+ setHoverCursor (layerIds) {
211
+ if (!this.map) {
212
+ return
213
+ }
214
+ this._onHoverMove = setupHoverCursor(this.map, layerIds, this._onHoverMove)
209
215
  }
210
216
 
211
217
  getVisibleFeatures (_layerIds) {
212
218
  return []
213
219
  }
214
220
 
221
+ updateHighlightedFeatures (selectedFeatures, activeFeatures, stylesMap) {
222
+ this._lastHighlightArgs = { selectedFeatures, activeFeatures, stylesMap }
223
+ return updateHighlightedFeatures(this.map, selectedFeatures, activeFeatures, stylesMap)
224
+ }
225
+
226
+ reapplyHighlights () {
227
+ if (!this._lastHighlightArgs) {
228
+ return
229
+ }
230
+ const { selectedFeatures, activeFeatures, stylesMap } = this._lastHighlightArgs
231
+ updateHighlightedFeatures(this.map, selectedFeatures, activeFeatures, stylesMap)
232
+ }
233
+
215
234
  // ==========================
216
235
  // Spatial helpers
217
236
  // ==========================
@@ -1,7 +1,7 @@
1
1
  // Mock-prefixed variables are allowed in jest.mock factories by babel-plugin-jest-hoist
2
2
  import OpenLayersProvider from './openlayersProvider.js'
3
3
  import { attachMapEvents } from './mapEvents.js'
4
- import { attachAppEvents, createTileSource, createVectorTileLayer } from './appEvents.js'
4
+ import { attachAppEvents, createMapStyleLayer } from './appEvents.js'
5
5
  import { getExtentFromGeoJSON, isGeometryObscured } from './utils/spatial.js'
6
6
 
7
7
  const mockAnimate = jest.fn()
@@ -35,14 +35,13 @@ const mockMapInstance = {
35
35
  }
36
36
 
37
37
  const mockSource = {}
38
- const mockTileLayer = { setSource: jest.fn() }
39
38
  const mockVectorTileLayer = {}
40
39
  const mockMapEventHandlesRemove = jest.fn()
40
+ const mockSetSource = jest.fn()
41
41
  const mockAppEventHandlesRemove = jest.fn()
42
42
 
43
43
  jest.mock('ol/Map.js', () => ({ __esModule: true, default: jest.fn(() => mockMapInstance) }))
44
44
  jest.mock('ol/View.js', () => ({ __esModule: true, default: jest.fn(() => mockViewInstance) }))
45
- jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayer) }))
46
45
  jest.mock('ol/interaction/defaults.js', () => ({ __esModule: true, defaults: jest.fn(() => []) }))
47
46
  jest.mock('proj4', () => {
48
47
  const fn = jest.fn()
@@ -52,12 +51,11 @@ jest.mock('proj4', () => {
52
51
  jest.mock('ol/proj/proj4.js', () => ({ __esModule: true, register: jest.fn() }))
53
52
  jest.mock('./mapEvents.js', () => ({
54
53
  __esModule: true,
55
- attachMapEvents: jest.fn(() => ({ remove: mockMapEventHandlesRemove }))
54
+ attachMapEvents: jest.fn(() => ({ remove: mockMapEventHandlesRemove, setSource: mockSetSource }))
56
55
  }))
57
56
  jest.mock('./appEvents.js', () => ({
58
57
  __esModule: true,
59
- createTileSource: jest.fn(() => mockSource),
60
- createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayer, source: mockSource })),
58
+ createMapStyleLayer: jest.fn(async () => ({ layer: mockVectorTileLayer, source: mockSource })),
61
59
  attachAppEvents: jest.fn(() => ({ remove: mockAppEventHandlesRemove }))
62
60
  }))
63
61
  jest.mock('./utils/spatial.js', () => ({
@@ -125,7 +123,7 @@ describe('OpenLayersProvider', () => {
125
123
  it('creates vector tile layer, OL objects, and emits MAP_READY by default', async () => {
126
124
  const { provider, eventBus } = makeProvider()
127
125
  await provider.initMap(defaultInitConfig)
128
- expect(createVectorTileLayer).toHaveBeenCalledWith(defaultInitConfig.mapStyle.url, null, defaultInitConfig.mapStyle)
126
+ expect(createMapStyleLayer).toHaveBeenCalledWith(defaultInitConfig.mapStyle, null)
129
127
  expect(attachMapEvents).toHaveBeenCalled()
130
128
  expect(attachAppEvents).toHaveBeenCalled()
131
129
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({
@@ -151,43 +149,29 @@ describe('OpenLayersProvider', () => {
151
149
  expect(mockMapOnce).not.toHaveBeenCalled()
152
150
  })
153
151
 
154
- it('calls createVectorTileLayer and skips createTileSource when mapStyle has no type', async () => {
152
+ it('creates the initial layer from the map style', async () => {
155
153
  const { provider } = makeProvider()
156
154
  await provider.initMap(defaultInitConfig)
157
- expect(createVectorTileLayer).toHaveBeenCalled()
158
- expect(createTileSource).not.toHaveBeenCalled()
155
+ expect(createMapStyleLayer).toHaveBeenCalledWith(defaultInitConfig.mapStyle, null)
159
156
  })
160
157
 
161
- it('calls createTileSource and skips createVectorTileLayer when mapStyle.type is "raster"', async () => {
158
+ it('creates the initial layer from raster map styles', async () => {
162
159
  const { provider, eventBus } = makeProvider()
163
160
  const rasterConfig = {
164
161
  ...defaultInitConfig,
165
162
  mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }
166
163
  }
167
164
  await provider.initMap(rasterConfig)
168
- expect(createTileSource).toHaveBeenCalledWith('https://tiles.example.com/{z}/{x}/{y}', null)
169
- expect(createVectorTileLayer).not.toHaveBeenCalled()
165
+ expect(createMapStyleLayer).toHaveBeenCalledWith(rasterConfig.mapStyle, null)
170
166
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({ mapStyleId: 'myStyle' }))
171
167
  })
172
168
 
173
- it('passes layerType "vector" and map to attachAppEvents by default', async () => {
169
+ it('passes map and source change handler to attachAppEvents', async () => {
174
170
  const { provider } = makeProvider()
175
171
  await provider.initMap(defaultInitConfig)
176
172
  expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({
177
- layerType: 'vector',
178
- map: mockMapInstance
179
- }))
180
- })
181
-
182
- it('passes layerType "raster" to attachAppEvents when mapStyle.type is "raster"', async () => {
183
- const { provider } = makeProvider()
184
- const rasterConfig = {
185
- ...defaultInitConfig,
186
- mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }
187
- }
188
- await provider.initMap(rasterConfig)
189
- expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({
190
- layerType: 'raster'
173
+ map: mockMapInstance,
174
+ onBaseSourceChange: mockSetSource
191
175
  }))
192
176
  })
193
177
 
@@ -218,8 +202,6 @@ describe('OpenLayersProvider', () => {
218
202
  expect(mockMapSetTarget).toHaveBeenCalledWith(null)
219
203
  expect(provider.map).toBeNull()
220
204
  expect(provider.view).toBeNull()
221
- expect(provider.tileLayer).toBeNull()
222
- expect(provider.source).toBeNull()
223
205
  })
224
206
 
225
207
  it('does not throw when called before initMap', () => {
@@ -324,10 +306,6 @@ describe('OpenLayersProvider', () => {
324
306
  expect(provider.getBounds()).toEqual([1.13, 2.46, 3.99, 4])
325
307
  })
326
308
 
327
- it('getFeaturesAtPoint returns empty array', () => {
328
- expect(provider.getFeaturesAtPoint()).toEqual([])
329
- })
330
-
331
309
  it('getVisibleFeatures returns empty array', () => {
332
310
  expect(provider.getVisibleFeatures()).toEqual([])
333
311
  })