@defra/interactive-map 0.0.25-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 (93) 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/api/map-style-config.md +30 -0
  6. package/docs/assets/css/docusaurus.scss +4 -0
  7. package/package.json +1 -1
  8. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  9. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  10. package/plugins/beta/draw-ml/src/defaults.js +14 -14
  11. package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
  12. package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
  13. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
  14. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
  15. package/plugins/beta/draw-ml/src/reducer.js +4 -1
  16. package/plugins/beta/draw-ml/src/styles.js +42 -39
  17. package/plugins/beta/draw-ol/dist/css/index.css +13 -0
  18. package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
  19. package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
  20. package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
  21. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
  22. package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
  23. package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
  24. package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
  25. package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
  26. package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
  27. package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
  28. package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
  29. package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
  30. package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
  31. package/plugins/beta/draw-ol/src/core/styles.js +98 -0
  32. package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
  33. package/plugins/beta/draw-ol/src/defaults.js +16 -0
  34. package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
  35. package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
  36. package/plugins/beta/draw-ol/src/draw.scss +17 -0
  37. package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
  38. package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
  39. package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
  40. package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
  41. package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
  42. package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
  43. package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
  44. package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
  45. package/plugins/beta/draw-ol/src/events.js +114 -0
  46. package/plugins/beta/draw-ol/src/index.js +12 -0
  47. package/plugins/beta/draw-ol/src/manifest.js +121 -0
  48. package/plugins/beta/draw-ol/src/olDraw.js +38 -0
  49. package/plugins/beta/draw-ol/src/reducer.js +41 -0
  50. package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
  51. package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
  52. package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
  53. package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
  54. package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
  55. package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
  56. package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
  57. package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
  58. package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
  59. package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
  60. package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
  61. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  62. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  63. package/plugins/interact/src/defaults.js +1 -0
  64. package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
  65. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
  66. package/plugins/interact/src/reducer.js +7 -0
  67. package/plugins/interact/src/reducer.test.js +16 -0
  68. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  69. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  70. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  71. package/providers/beta/openlayers/src/appEvents.js +9 -13
  72. package/providers/beta/openlayers/src/appEvents.test.js +27 -18
  73. package/providers/beta/openlayers/src/mapEvents.js +34 -20
  74. package/providers/beta/openlayers/src/mapEvents.test.js +65 -6
  75. package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
  76. package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
  77. package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
  78. package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
  79. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
  80. package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
  81. package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
  82. package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
  83. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
  84. package/providers/beta/openlayers/src/utils/tileLayers.js +23 -5
  85. package/providers/beta/openlayers/src/utils/tileLayers.test.js +81 -5
  86. package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
  87. package/rollup.esm.mjs +6 -0
  88. package/src/App/controls/keyboardActions.js +2 -2
  89. package/src/InteractiveMap/InteractiveMap.js +38 -10
  90. package/src/InteractiveMap/InteractiveMap.test.js +282 -356
  91. package/src/InteractiveMap/historyManager.js +7 -0
  92. package/src/InteractiveMap/historyManager.test.js +11 -0
  93. 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)
12
- map.getLayers().setAt(0, newLayer)
13
- } else {
14
- const { layer: newLayer } = await createVectorTileLayer(mapStyle.url, transformRequest)
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,16 +80,17 @@ 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
  }
79
87
 
80
88
  it('on MAP_SET_STYLE: creates a vector tile layer and places it at index 0', async () => {
81
89
  const { setAt, handlerFor } = makeSetup()
82
- await handlerFor(events.MAP_SET_STYLE)({ url: 'https://example.com/styles', id: 'newVts' })
83
- expect(createVectorTileLayer).toHaveBeenCalledWith('https://example.com/styles', null)
84
- expect(setAt).toHaveBeenCalledWith(0, mockVectorTileLayerInstance)
90
+ const mapStyle = { url: 'https://example.com/styles', id: 'newVts' }
91
+ await handlerFor(events.MAP_SET_STYLE)(mapStyle)
92
+ expect(createMapStyleLayer).toHaveBeenCalledWith(mapStyle, null)
93
+ expect(setAt).toHaveBeenCalledWith(0, mockLayerInstance)
85
94
  })
86
95
 
87
96
  it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
@@ -97,7 +106,7 @@ describe('attachAppEvents', () => {
97
106
  const map = makeMap()
98
107
  const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
99
108
  const mapProvider = makeProvider()
100
- attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
109
+ attachAppEvents({ mapProvider, transformRequest: null, events, eventBus, map, onBaseSourceChange: jest.fn() })
101
110
  const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
102
111
  return { map, mapProvider, handlerFor }
103
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,11 +68,10 @@ 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()
85
- const viewMinZoom = view.getMinZoom()
86
- const viewMaxZoom = view.getMaxZoom()
87
75
 
88
76
  const getMapState = () => {
89
77
  if (destroyed) {
@@ -95,8 +83,8 @@ export function attachMapEvents ({
95
83
  bounds: getBounds(),
96
84
  resolution: getResolution(),
97
85
  zoom,
98
- isAtMaxZoom: zoom + ZOOM_TOLERANCE >= viewMaxZoom,
99
- isAtMinZoom: zoom - ZOOM_TOLERANCE <= viewMinZoom
86
+ isAtMaxZoom: zoom + ZOOM_TOLERANCE >= view.getMaxZoom(),
87
+ isAtMinZoom: zoom - ZOOM_TOLERANCE <= view.getMinZoom()
100
88
  }
101
89
  }
102
90
 
@@ -112,21 +100,47 @@ export function attachMapEvents ({
112
100
  listeners.push([target, type, handler])
113
101
  }
114
102
 
115
- attachLoadEvents({ source, map, emit, eventBus, events, on })
116
- attachMoveEvents({ map, view: map.getView(), emit, eventBus, events, on, debouncers })
117
- attachClickEvents({ map, eventBus, events, on })
103
+ const onSource = (target, type, handler) => {
104
+ target.on(type, handler)
105
+ sourceListeners.push([target, type, handler])
106
+ }
118
107
 
119
- 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
+ }
120
115
 
121
116
  const emitDataChange = debounce(() => emit(events.MAP_DATA_CHANGE), DEBOUNCE_IDLE_TIME)
122
117
  debouncers.push(emitDataChange)
123
- 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))
124
136
 
125
137
  return {
138
+ setSource,
126
139
  remove () {
127
140
  destroyed = true
128
141
  debouncers.forEach(d => d.cancel())
129
142
  listeners.forEach(([target, type, handler]) => target.un(type, handler))
143
+ sourceListeners.forEach(([target, type, handler]) => target.un(type, handler))
130
144
  }
131
145
  }
132
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,17 +170,56 @@ 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
+ })
189
+ })
190
+
191
+ describe('attachMapEvents — dynamic zoom bounds', () => {
192
+ it('isAtMaxZoom is updated in emitted event when view max zoom changes', () => {
193
+ const { view, eventBus } = setup()
194
+ view.trigger('change')
195
+ mockDebounceFns[1].fn()
196
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: false }))
197
+
198
+ eventBus.emit.mockClear()
199
+ view.getMaxZoom.mockReturnValue(7)
200
+ mockDebounceFns[1].fn()
201
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: true }))
202
+ })
203
+
204
+ it('isAtMinZoom is updated in emitted event when view min zoom changes', () => {
205
+ const { view, eventBus } = setup()
206
+ view.trigger('change')
207
+ mockDebounceFns[1].fn()
208
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: false }))
209
+
210
+ eventBus.emit.mockClear()
211
+ view.getMinZoom.mockReturnValue(7)
212
+ mockDebounceFns[1].fn()
213
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: true }))
214
+ })
174
215
  })
175
216
 
176
217
  describe('attachMapEvents — remove', () => {
177
218
  it('calls cancel on all debouncers', () => {
178
219
  const { handles } = setup()
179
220
  handles.remove()
180
- expect(mockDebounceFns[0].wrapper.cancel).toHaveBeenCalled()
181
221
  expect(mockDebounceFns[1].wrapper.cancel).toHaveBeenCalled()
222
+ expect(mockDebounceFns[0].wrapper.cancel).toHaveBeenCalled()
182
223
  })
183
224
 
184
225
  it('calls un on all registered event targets', () => {
@@ -189,10 +230,28 @@ describe('attachMapEvents — remove', () => {
189
230
  expect(source.un).toHaveBeenCalled()
190
231
  })
191
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
+
192
251
  it('prevents state-bearing events from emitting after destroy', () => {
193
252
  const { eventBus, handles } = setup()
194
253
  handles.remove()
195
- mockDebounceFns[0].fn() // emitMoveEnd fires after destroy → getMapState returns null
254
+ mockDebounceFns[1].fn() // emitMoveEnd fires after destroy → getMapState returns null
196
255
  expect(eventBus.emit).not.toHaveBeenCalledWith(events.MAP_MOVE_END, expect.anything())
197
256
  })
198
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)
51
- tileLayer = vectorTile.layer
52
- source = vectorTile.source
53
- } else {
54
- const vectorTile = await createVectorTileLayer(mapStyle.url, transformRequest)
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)
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
  })