@defra/interactive-map 0.0.24-alpha → 0.0.26-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.
@@ -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,107));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,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})());
@@ -1,50 +1,39 @@
1
- import XYZ from 'ol/source/XYZ.js'
2
- import TileGrid from 'ol/tilegrid/TileGrid.js'
3
- import TileState from 'ol/TileState.js'
4
- import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from './defaults.js'
1
+ import { createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './utils/tileLayers.js'
5
2
 
6
- const createTileLoadFunction = (transformRequest) => (tile, src) => {
7
- const result = transformRequest(src, 'Tile') || {}
8
- const url = result.url || src
9
- const headers = result.headers || {}
10
- fetch(url, { headers })
11
- .then(r => r.blob())
12
- .then(blob => { tile.getImage().src = URL.createObjectURL(blob) })
13
- .catch(() => tile.setState(TileState.ERROR))
14
- }
15
-
16
- export function createTileSource (url, transformRequest) {
17
- const tileGrid = new TileGrid({
18
- resolutions: TILE_GRID_RESOLUTIONS,
19
- origin: TILE_GRID_ORIGIN,
20
- tileSize: TILE_SIZE
21
- })
3
+ export { createTileSource, createVectorTileLayer, createOGCVectorTileLayer }
22
4
 
23
- const tileUrlFunction = ([z, x, y]) => url
24
- .replace('{z}', z)
25
- .replace('{x}', x)
26
- .replace('{y}', y)
5
+ export function attachAppEvents ({ mapProvider, layer, layerType, transformRequest, events, eventBus, map }) {
6
+ const handleSetMapStyle = async (mapStyle) => {
7
+ if (layerType === 'raster') {
8
+ const source = createTileSource(mapStyle.url, transformRequest)
9
+ layer.setSource(source)
10
+ } else if (mapStyle.type === 'ogc-vt') {
11
+ const { layer: newLayer } = await createOGCVectorTileLayer(mapStyle.url, transformRequest, 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
+ }
17
+ eventBus.emit(events.MAP_STYLE_CHANGE, { mapStyleId: mapStyle.id })
18
+ }
27
19
 
28
- return new XYZ({
29
- projection: 'EPSG:27700',
30
- tileGrid,
31
- tileUrlFunction,
32
- tileLoadFunction: transformRequest ? createTileLoadFunction(transformRequest) : undefined
33
- })
34
- }
20
+ const handleSetPixelRatio = (pixelRatio) => {
21
+ map.setPixelRatio(pixelRatio)
22
+ }
35
23
 
36
- export function attachAppEvents ({ tileLayer, transformRequest, events, eventBus }) {
37
- const handleSetMapStyle = (mapStyle) => {
38
- const source = createTileSource(mapStyle.url, transformRequest)
39
- tileLayer.setSource(source)
40
- eventBus.emit(events.MAP_STYLE_CHANGE, { mapStyleId: mapStyle.id })
24
+ const handleSizeChange = ({ mapSize }) => {
25
+ mapProvider.mapSize = mapSize
41
26
  }
42
27
 
43
28
  eventBus.on(events.MAP_SET_STYLE, handleSetMapStyle)
29
+ eventBus.on(events.MAP_SET_PIXEL_RATIO, handleSetPixelRatio)
30
+ eventBus.on(events.MAP_SIZE_CHANGE, handleSizeChange)
44
31
 
45
32
  return {
46
33
  remove () {
47
34
  eventBus.off(events.MAP_SET_STYLE, handleSetMapStyle)
35
+ eventBus.off(events.MAP_SET_PIXEL_RATIO, handleSetPixelRatio)
36
+ eventBus.off(events.MAP_SIZE_CHANGE, handleSizeChange)
48
37
  }
49
38
  }
50
39
  }
@@ -1,140 +1,118 @@
1
- import XYZ from 'ol/source/XYZ.js'
2
- import TileGrid from 'ol/tilegrid/TileGrid.js'
3
- import { createTileSource, attachAppEvents } from './appEvents.js'
4
- import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from './defaults.js'
1
+ import { attachAppEvents } from './appEvents.js'
5
2
 
6
- const mockTileGridInstance = {}
7
- const mockSourceInstance = {}
8
-
9
- jest.mock('ol/source/XYZ.js', () => ({ __esModule: true, default: jest.fn(() => mockSourceInstance) }))
10
- jest.mock('ol/tilegrid/TileGrid.js', () => ({ __esModule: true, default: jest.fn(() => mockTileGridInstance) }))
11
- jest.mock('ol/TileState.js', () => ({ __esModule: true, default: { ERROR: 'error' } }))
3
+ import { createTileSource, createVectorTileLayer } from './utils/tileLayers.js'
12
4
 
13
- const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0))
5
+ const mockSourceInstance = {}
6
+ const mockVectorTileLayerInstance = {}
14
7
 
15
- describe('createTileSource', () => {
16
- beforeEach(() => jest.clearAllMocks())
8
+ jest.mock('./utils/tileLayers.js', () => ({
9
+ __esModule: true,
10
+ createTileSource: jest.fn(() => mockSourceInstance),
11
+ createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayerInstance, source: {} }))
12
+ }))
17
13
 
18
- it('creates TileGrid with correct OS tile grid config', () => {
19
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', null)
20
- expect(TileGrid).toHaveBeenCalledWith({
21
- resolutions: TILE_GRID_RESOLUTIONS,
22
- origin: TILE_GRID_ORIGIN,
23
- tileSize: TILE_SIZE
24
- })
25
- })
14
+ const events = {
15
+ MAP_SET_STYLE: 'map:setstyle',
16
+ MAP_STYLE_CHANGE: 'map:stylechange',
17
+ MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
18
+ MAP_SIZE_CHANGE: 'map:sizechange'
19
+ }
26
20
 
27
- it('creates XYZ source with EPSG:27700 projection', () => {
28
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', null)
29
- expect(XYZ).toHaveBeenCalledWith(expect.objectContaining({ projection: 'EPSG:27700' }))
30
- })
21
+ function makeMap () {
22
+ return { getLayers: jest.fn(() => ({ setAt: jest.fn() })), setPixelRatio: jest.fn() }
23
+ }
31
24
 
32
- it('tileUrlFunction substitutes z, x, y into url template', () => {
33
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', null)
34
- const { tileUrlFunction } = XYZ.mock.calls[0][0]
35
- expect(tileUrlFunction([7, 3, 5])).toBe('https://tiles.example.com/7/3/5')
36
- })
25
+ function makeProvider () {
26
+ return { mapSize: 'small' }
27
+ }
37
28
 
38
- it('does not set tileLoadFunction when transformRequest is null', () => {
39
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', null)
40
- const { tileLoadFunction } = XYZ.mock.calls[0][0]
41
- expect(tileLoadFunction).toBeUndefined()
42
- })
43
-
44
- it('sets tileLoadFunction when transformRequest is provided', () => {
45
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', jest.fn())
46
- const { tileLoadFunction } = XYZ.mock.calls[0][0]
47
- expect(typeof tileLoadFunction).toBe('function')
48
- })
49
- })
50
-
51
- describe('tileLoadFunction (via transformRequest)', () => {
52
- const url = 'https://tiles.example.com/7/3/5'
53
- const mockImg = { src: null }
54
- const mockTile = { getImage: () => mockImg, setState: jest.fn() }
55
-
56
- beforeEach(() => {
57
- jest.clearAllMocks()
58
- mockImg.src = null
59
- global.URL.createObjectURL = jest.fn(() => 'blob:test')
60
- global.fetch = jest.fn().mockResolvedValue({
61
- blob: () => Promise.resolve(new Blob())
29
+ describe('attachAppEvents', () => {
30
+ describe('raster', () => {
31
+ function makeSetup () {
32
+ jest.clearAllMocks()
33
+ const layer = { setSource: jest.fn() }
34
+ const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
35
+ const mapProvider = makeProvider()
36
+ const handles = attachAppEvents({ mapProvider, layer, layerType: 'raster', transformRequest: null, events, eventBus, map: makeMap() })
37
+ const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
38
+ return { layer, eventBus, handles, handlerFor, mapProvider }
39
+ }
40
+
41
+ it('subscribes to MAP_SET_STYLE on the event bus', () => {
42
+ const { eventBus } = makeSetup()
43
+ expect(eventBus.on).toHaveBeenCalledWith(events.MAP_SET_STYLE, expect.any(Function))
62
44
  })
63
- })
64
-
65
- function getTileLoadFn (transformRequest) {
66
- createTileSource('https://tiles.example.com/{z}/{x}/{y}', transformRequest)
67
- return XYZ.mock.calls[0][0].tileLoadFunction
68
- }
69
45
 
70
- it('calls transformRequest with src and "Tile" resource type', async () => {
71
- const transformRequest = jest.fn(() => null)
72
- const fn = getTileLoadFn(transformRequest)
73
- fn(mockTile, url)
74
- expect(transformRequest).toHaveBeenCalledWith(url, 'Tile')
75
- })
76
-
77
- it('uses url and headers from transformRequest result', async () => {
78
- const transformRequest = jest.fn(() => ({ url: 'https://proxied.example.com/tile', headers: { Authorization: 'Bearer abc' } }))
79
- const fn = getTileLoadFn(transformRequest)
80
- fn(mockTile, url)
81
- await flushPromises()
82
- expect(fetch).toHaveBeenCalledWith('https://proxied.example.com/tile', { headers: { Authorization: 'Bearer abc' } })
83
- })
46
+ it('on MAP_SET_STYLE: creates a tile source and sets it on the layer', async () => {
47
+ const { layer, handlerFor } = makeSetup()
48
+ await handlerFor(events.MAP_SET_STYLE)({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle' })
49
+ expect(createTileSource).toHaveBeenCalledWith('https://new.tiles.com/{z}/{x}/{y}', null)
50
+ expect(layer.setSource).toHaveBeenCalledWith(mockSourceInstance)
51
+ })
84
52
 
85
- it('falls back to original src and empty headers when transformRequest returns null', async () => {
86
- const fn = getTileLoadFn(() => null)
87
- fn(mockTile, url)
88
- await flushPromises()
89
- expect(fetch).toHaveBeenCalledWith(url, { headers: {} })
90
- })
53
+ it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
54
+ const { eventBus, handlerFor } = makeSetup()
55
+ await handlerFor(events.MAP_SET_STYLE)({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle' })
56
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, { mapStyleId: 'newStyle' })
57
+ })
91
58
 
92
- it('sets tile image src via createObjectURL on success', async () => {
93
- const fn = getTileLoadFn(() => null)
94
- fn(mockTile, url)
95
- await flushPromises()
96
- expect(mockImg.src).toBe('blob:test')
59
+ it('remove() unsubscribes all handlers', () => {
60
+ const { eventBus, handles, handlerFor } = makeSetup()
61
+ handles.remove()
62
+ expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SET_STYLE, handlerFor(events.MAP_SET_STYLE))
63
+ expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SET_PIXEL_RATIO, handlerFor(events.MAP_SET_PIXEL_RATIO))
64
+ expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SIZE_CHANGE, handlerFor(events.MAP_SIZE_CHANGE))
65
+ })
97
66
  })
98
67
 
99
- it('sets tile state to ERROR on fetch failure', async () => {
100
- global.fetch = jest.fn().mockRejectedValue(new Error('network error'))
101
- const fn = getTileLoadFn(() => null)
102
- fn(mockTile, url)
103
- await flushPromises()
104
- expect(mockTile.setState).toHaveBeenCalledWith('error')
105
- })
106
- })
68
+ describe('vector', () => {
69
+ function makeSetup () {
70
+ jest.clearAllMocks()
71
+ const setAt = jest.fn()
72
+ const map = { getLayers: jest.fn(() => ({ setAt })), setPixelRatio: jest.fn() }
73
+ const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
74
+ const mapProvider = makeProvider()
75
+ const handles = attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
76
+ const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
77
+ return { map, setAt, eventBus, handles, handlerFor, mapProvider }
78
+ }
79
+
80
+ it('on MAP_SET_STYLE: creates a vector tile layer and places it at index 0', async () => {
81
+ const { setAt, handlerFor } = makeSetup()
82
+ const mapStyle = { url: 'https://example.com/styles', id: 'newVts' }
83
+ await handlerFor(events.MAP_SET_STYLE)(mapStyle)
84
+ expect(createVectorTileLayer).toHaveBeenCalledWith(mapStyle.url, null, mapStyle)
85
+ expect(setAt).toHaveBeenCalledWith(0, mockVectorTileLayerInstance)
86
+ })
107
87
 
108
- describe('attachAppEvents', () => {
109
- const events = {
110
- MAP_SET_STYLE: 'map:setstyle',
111
- MAP_STYLE_CHANGE: 'map:stylechange'
112
- }
113
-
114
- function makeSetup () {
115
- jest.clearAllMocks()
116
- const tileLayer = { setSource: jest.fn() }
117
- const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
118
- const handles = attachAppEvents({ tileLayer, transformRequest: null, events, eventBus })
119
- const styleHandler = eventBus.on.mock.calls[0][1]
120
- return { tileLayer, eventBus, handles, styleHandler }
121
- }
122
-
123
- it('subscribes to MAP_SET_STYLE on the event bus', () => {
124
- const { eventBus } = makeSetup()
125
- expect(eventBus.on).toHaveBeenCalledWith(events.MAP_SET_STYLE, expect.any(Function))
88
+ it('on MAP_SET_STYLE: emits MAP_STYLE_CHANGE with the new style id', async () => {
89
+ const { eventBus, handlerFor } = makeSetup()
90
+ await handlerFor(events.MAP_SET_STYLE)({ url: 'https://example.com/styles', id: 'newVts' })
91
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, { mapStyleId: 'newVts' })
92
+ })
126
93
  })
127
94
 
128
- it('on MAP_SET_STYLE: updates tile layer source and emits MAP_STYLE_CHANGE', () => {
129
- const { tileLayer, eventBus, styleHandler } = makeSetup()
130
- styleHandler({ url: 'https://new.tiles.com/{z}/{x}/{y}', id: 'newStyle' })
131
- expect(tileLayer.setSource).toHaveBeenCalledWith(mockSourceInstance)
132
- expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, { mapStyleId: 'newStyle' })
133
- })
95
+ describe('pixel ratio and map size', () => {
96
+ function makeSetup () {
97
+ jest.clearAllMocks()
98
+ const map = makeMap()
99
+ const eventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
100
+ const mapProvider = makeProvider()
101
+ attachAppEvents({ mapProvider, layer: {}, layerType: 'vector', transformRequest: null, events, eventBus, map })
102
+ const handlerFor = (event) => eventBus.on.mock.calls.find(c => c[0] === event)[1]
103
+ return { map, mapProvider, handlerFor }
104
+ }
105
+
106
+ it('on MAP_SET_PIXEL_RATIO: calls map.setPixelRatio with the new ratio', () => {
107
+ const { map, handlerFor } = makeSetup()
108
+ handlerFor(events.MAP_SET_PIXEL_RATIO)(3)
109
+ expect(map.setPixelRatio).toHaveBeenCalledWith(3)
110
+ })
134
111
 
135
- it('remove() unsubscribes from MAP_SET_STYLE', () => {
136
- const { eventBus, handles, styleHandler } = makeSetup()
137
- handles.remove()
138
- expect(eventBus.off).toHaveBeenCalledWith(events.MAP_SET_STYLE, styleHandler)
112
+ it('on MAP_SIZE_CHANGE: updates mapProvider.mapSize', () => {
113
+ const { mapProvider, handlerFor } = makeSetup()
114
+ handlerFor(events.MAP_SIZE_CHANGE)({ mapSize: 'large' })
115
+ expect(mapProvider.mapSize).toBe('large')
116
+ })
139
117
  })
140
118
  })
@@ -82,8 +82,6 @@ export function attachMapEvents ({
82
82
  const debouncers = []
83
83
 
84
84
  const view = map.getView()
85
- const viewMinZoom = view.getMinZoom()
86
- const viewMaxZoom = view.getMaxZoom()
87
85
 
88
86
  const getMapState = () => {
89
87
  if (destroyed) {
@@ -95,8 +93,8 @@ export function attachMapEvents ({
95
93
  bounds: getBounds(),
96
94
  resolution: getResolution(),
97
95
  zoom,
98
- isAtMaxZoom: zoom + ZOOM_TOLERANCE >= viewMaxZoom,
99
- isAtMinZoom: zoom - ZOOM_TOLERANCE <= viewMinZoom
96
+ isAtMaxZoom: zoom + ZOOM_TOLERANCE >= view.getMaxZoom(),
97
+ isAtMinZoom: zoom - ZOOM_TOLERANCE <= view.getMinZoom()
100
98
  }
101
99
  }
102
100
 
@@ -51,6 +51,7 @@ function setup () {
51
51
  map.getView = jest.fn(() => view)
52
52
 
53
53
  const source = makeTarget()
54
+ source.changed = jest.fn()
54
55
  const eventBus = { emit: jest.fn() }
55
56
 
56
57
  const getZoom = jest.fn(() => 7)
@@ -172,6 +173,32 @@ describe('attachMapEvents — render and data events', () => {
172
173
  })
173
174
  })
174
175
 
176
+ describe('attachMapEvents — dynamic zoom bounds', () => {
177
+ it('isAtMaxZoom is updated in emitted event when view max zoom changes', () => {
178
+ const { view, eventBus } = setup()
179
+ view.trigger('change')
180
+ mockDebounceFns[0].fn()
181
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: false }))
182
+
183
+ eventBus.emit.mockClear()
184
+ view.getMaxZoom.mockReturnValue(7)
185
+ mockDebounceFns[0].fn()
186
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMaxZoom: true }))
187
+ })
188
+
189
+ it('isAtMinZoom is updated in emitted event when view min zoom changes', () => {
190
+ const { view, eventBus } = setup()
191
+ view.trigger('change')
192
+ mockDebounceFns[0].fn()
193
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: false }))
194
+
195
+ eventBus.emit.mockClear()
196
+ view.getMinZoom.mockReturnValue(7)
197
+ mockDebounceFns[0].fn()
198
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, expect.objectContaining({ isAtMinZoom: true }))
199
+ })
200
+ })
201
+
175
202
  describe('attachMapEvents — remove', () => {
176
203
  it('calls cancel on all debouncers', () => {
177
204
  const { handles } = setup()
@@ -7,7 +7,7 @@ import { register } from 'ol/proj/proj4.js'
7
7
  import { supportedShortcuts, DEFAULTS } from './defaults.js'
8
8
  import { getViewResolutionConfig, ZOOM_ALIGNMENT } from './utils/zoom.js'
9
9
  import { attachMapEvents } from './mapEvents.js'
10
- import { attachAppEvents, createTileSource } from './appEvents.js'
10
+ import { attachAppEvents, createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './appEvents.js'
11
11
  import { getAreaDimensions, getCardinalMove, getExtentFromGeoJSON, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
12
12
 
13
13
  const CRS = 'EPSG:27700'
@@ -31,19 +31,30 @@ export default class OpenLayersProvider {
31
31
  this.eventBus = eventBus
32
32
  this.capabilities = {
33
33
  supportedShortcuts,
34
- supportsMapSizes: false
34
+ supportsMapSizes: true
35
35
  }
36
36
  Object.assign(this, mapProviderConfig)
37
37
  }
38
38
 
39
39
  async initMap (config) {
40
- const { container, padding, mapStyle, mapSize, center, zoom, bounds, minZoom, maxZoom, transformRequest } = config
40
+ const { container, padding, mapStyle, mapSize, center, zoom, bounds, minZoom, maxZoom, transformRequest, pixelRatio } = config
41
41
  this.mapStyleId = mapStyle?.id
42
42
  this.mapSize = mapSize
43
43
  const { events, eventBus } = this
44
44
 
45
- const source = createTileSource(mapStyle.url, transformRequest)
46
- const tileLayer = new TileLayer({ source })
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
+ }
47
58
 
48
59
  const viewResolutions = getViewResolutionConfig(this.zoomAlignment ?? ZOOM_ALIGNMENT.UK)
49
60
 
@@ -63,7 +74,8 @@ export default class OpenLayersProvider {
63
74
  layers: [tileLayer],
64
75
  view,
65
76
  controls: [],
66
- interactions: defaultInteractions({ doubleClickZoom: false })
77
+ interactions: defaultInteractions({ doubleClickZoom: false }),
78
+ pixelRatio
67
79
  })
68
80
 
69
81
  if (bounds) {
@@ -84,10 +96,13 @@ export default class OpenLayersProvider {
84
96
  })
85
97
 
86
98
  this.appEventHandles = attachAppEvents({
87
- tileLayer,
99
+ mapProvider: this,
100
+ layer: tileLayer,
101
+ layerType: mapStyle.type ?? 'vector',
88
102
  transformRequest,
89
103
  events,
90
- eventBus
104
+ eventBus,
105
+ map
91
106
  }) || []
92
107
 
93
108
  this.map = map
@@ -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 } from './appEvents.js'
4
+ import { attachAppEvents, createTileSource, createVectorTileLayer } from './appEvents.js'
5
5
  import { getExtentFromGeoJSON, isGeometryObscured } from './utils/spatial.js'
6
6
 
7
7
  const mockAnimate = jest.fn()
@@ -36,6 +36,7 @@ const mockMapInstance = {
36
36
 
37
37
  const mockSource = {}
38
38
  const mockTileLayer = { setSource: jest.fn() }
39
+ const mockVectorTileLayer = {}
39
40
  const mockMapEventHandlesRemove = jest.fn()
40
41
  const mockAppEventHandlesRemove = jest.fn()
41
42
 
@@ -56,6 +57,7 @@ jest.mock('./mapEvents.js', () => ({
56
57
  jest.mock('./appEvents.js', () => ({
57
58
  __esModule: true,
58
59
  createTileSource: jest.fn(() => mockSource),
60
+ createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayer, source: mockSource })),
59
61
  attachAppEvents: jest.fn(() => ({ remove: mockAppEventHandlesRemove }))
60
62
  }))
61
63
  jest.mock('./utils/spatial.js', () => ({
@@ -106,9 +108,9 @@ describe('OpenLayersProvider', () => {
106
108
  expect(provider.eventBus).toBe(eventBus)
107
109
  })
108
110
 
109
- it('sets capabilities with supportsMapSizes false and supportedShortcuts array', () => {
111
+ it('sets capabilities with supportsMapSizes true and supportedShortcuts array', () => {
110
112
  const { provider } = makeProvider()
111
- expect(provider.capabilities.supportsMapSizes).toBe(false)
113
+ expect(provider.capabilities.supportsMapSizes).toBe(true)
112
114
  expect(Array.isArray(provider.capabilities.supportedShortcuts)).toBe(true)
113
115
  })
114
116
 
@@ -120,10 +122,10 @@ describe('OpenLayersProvider', () => {
120
122
  })
121
123
 
122
124
  describe('initMap', () => {
123
- it('creates tile source, OL objects, and emits MAP_READY', async () => {
125
+ it('creates vector tile layer, OL objects, and emits MAP_READY by default', async () => {
124
126
  const { provider, eventBus } = makeProvider()
125
127
  await provider.initMap(defaultInitConfig)
126
- expect(createTileSource).toHaveBeenCalledWith(defaultInitConfig.mapStyle.url, null)
128
+ expect(createVectorTileLayer).toHaveBeenCalledWith(defaultInitConfig.mapStyle.url, null, defaultInitConfig.mapStyle)
127
129
  expect(attachMapEvents).toHaveBeenCalled()
128
130
  expect(attachAppEvents).toHaveBeenCalled()
129
131
  expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({
@@ -148,6 +150,59 @@ describe('OpenLayersProvider', () => {
148
150
  await provider.initMap(defaultInitConfig)
149
151
  expect(mockMapOnce).not.toHaveBeenCalled()
150
152
  })
153
+
154
+ it('calls createVectorTileLayer and skips createTileSource when mapStyle has no type', async () => {
155
+ const { provider } = makeProvider()
156
+ await provider.initMap(defaultInitConfig)
157
+ expect(createVectorTileLayer).toHaveBeenCalled()
158
+ expect(createTileSource).not.toHaveBeenCalled()
159
+ })
160
+
161
+ it('calls createTileSource and skips createVectorTileLayer when mapStyle.type is "raster"', async () => {
162
+ const { provider, eventBus } = makeProvider()
163
+ const rasterConfig = {
164
+ ...defaultInitConfig,
165
+ mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }
166
+ }
167
+ await provider.initMap(rasterConfig)
168
+ expect(createTileSource).toHaveBeenCalledWith('https://tiles.example.com/{z}/{x}/{y}', null)
169
+ expect(createVectorTileLayer).not.toHaveBeenCalled()
170
+ expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({ mapStyleId: 'myStyle' }))
171
+ })
172
+
173
+ it('passes layerType "vector" and map to attachAppEvents by default', async () => {
174
+ const { provider } = makeProvider()
175
+ await provider.initMap(defaultInitConfig)
176
+ 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'
191
+ }))
192
+ })
193
+
194
+ it('passes pixelRatio to OlMap constructor', async () => {
195
+ const OlMap = require('ol/Map.js').default
196
+ const { provider } = makeProvider()
197
+ await provider.initMap({ ...defaultInitConfig, pixelRatio: 2 })
198
+ expect(OlMap).toHaveBeenCalledWith(expect.objectContaining({ pixelRatio: 2 }))
199
+ })
200
+
201
+ it('passes mapProvider to attachAppEvents', async () => {
202
+ const { provider } = makeProvider()
203
+ await provider.initMap(defaultInitConfig)
204
+ expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({ mapProvider: provider }))
205
+ })
151
206
  })
152
207
 
153
208
  describe('destroyMap', () => {
@@ -99,10 +99,19 @@ const getPaddedExtent = (map) => {
99
99
 
100
100
  /**
101
101
  * Returns true if the geometry's screen bounding box overlaps the given panel rectangle.
102
- * GeoJSON coords are expected in WGS84; they are transformed to EPSG:27700 for screen projection.
102
+ * getPixelFromCoordinate returns layout-space pixels (unscaled canvas), so we convert them
103
+ * to screen space using the ratio between the viewport's visual size and the canvas size.
104
+ * This accounts for the CSS scale transform applied on medium/large mapSize.
103
105
  */
104
106
  const isGeometryObscured = (geojson, panelRect, map) => {
105
107
  const containerRect = map.getTargetElement().getBoundingClientRect()
108
+ if (!containerRect.width || !containerRect.height) {
109
+ return false
110
+ }
111
+ const viewportRect = map.getViewport().getBoundingClientRect()
112
+ const scaleX = viewportRect.width / containerRect.width
113
+ const scaleY = viewportRect.height / containerRect.height
114
+
106
115
  const [west, south, east, north] = getBboxFromGeoJSON(geojson)
107
116
 
108
117
  const corners = [[west, south], [west, north], [east, south], [east, north]].map(coord => {
@@ -113,21 +122,16 @@ const isGeometryObscured = (geojson, panelRect, map) => {
113
122
  return false
114
123
  }
115
124
 
116
- const screenMinX = Math.min(...corners.map(c => c[0]))
117
- const screenMaxX = Math.max(...corners.map(c => c[0]))
118
- const screenMinY = Math.min(...corners.map(c => c[1]))
119
- const screenMaxY = Math.max(...corners.map(c => c[1]))
120
-
121
- const panelLeft = panelRect.left - containerRect.left
122
- const panelTop = panelRect.top - containerRect.top
123
- const panelRight = panelRect.right - containerRect.left
124
- const panelBottom = panelRect.bottom - containerRect.top
125
+ const screenMinX = containerRect.left + Math.min(...corners.map(c => c[0])) * scaleX
126
+ const screenMaxX = containerRect.left + Math.max(...corners.map(c => c[0])) * scaleX
127
+ const screenMinY = containerRect.top + Math.min(...corners.map(c => c[1])) * scaleY
128
+ const screenMaxY = containerRect.top + Math.max(...corners.map(c => c[1])) * scaleY
125
129
 
126
130
  return (
127
- screenMinX < panelRight &&
128
- screenMaxX > panelLeft &&
129
- screenMinY < panelBottom &&
130
- screenMaxY > panelTop
131
+ screenMinX < panelRect.right &&
132
+ screenMaxX > panelRect.left &&
133
+ screenMinY < panelRect.bottom &&
134
+ screenMaxY > panelRect.top
131
135
  )
132
136
  }
133
137