@defra/interactive-map 0.0.19-alpha → 0.0.21-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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/esm/im-shell.js +1 -1
  3. package/dist/umd/index.js +1 -1
  4. package/docs/api/button-definition.md +22 -5
  5. package/docs/api/map-style-config.md +2 -11
  6. package/docs/api/marker-config.md +19 -5
  7. package/docs/api.md +75 -46
  8. package/docs/architecture.md +0 -2
  9. package/{assets → docs/assets}/css/docusaurus.css +78 -1
  10. package/docs/assets/images/basic-map.jpg +0 -0
  11. package/docs/assets/images/button-first.jpg +0 -0
  12. package/{assets → docs/assets}/images/hero.png +0 -0
  13. package/docs/assets/images/marker-panel.jpg +0 -0
  14. package/docs/examples/add-marker-with-panel.mdx +112 -48
  15. package/docs/examples/basic-map.mdx +43 -14
  16. package/docs/examples/button-map.mdx +43 -14
  17. package/docs/examples/index.mdx +5 -5
  18. package/docs/getting-started.md +80 -53
  19. package/docs/plugins/datasets.md +11 -11
  20. package/docusaurus.config.cjs +5 -4
  21. package/package.json +7 -3
  22. package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +1 -0
  23. package/plugins/beta/datasets/adapters/maplibre/dist/esm/index.js +1 -0
  24. package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +2 -0
  25. package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt +1 -0
  26. package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +1 -0
  27. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  28. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  29. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +4 -4
  30. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +26 -7
  31. package/plugins/beta/datasets/src/defaults.js +1 -1
  32. package/plugins/beta/datasets/src/manifest.js +1 -1
  33. package/plugins/beta/datasets/src/panels/Layers.jsx +4 -4
  34. package/plugins/beta/datasets/src/utils/mergeSublayer.js +1 -1
  35. package/plugins/beta/draw-ml/dist/css/index.css +19 -2
  36. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  37. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  38. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  39. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  40. package/providers/maplibre/src/maplibreProvider.js +5 -3
  41. package/providers/maplibre/src/maplibreProvider.test.js +44 -2
  42. package/providers/maplibre/src/utils/patternImages.js +16 -10
  43. package/providers/maplibre/src/utils/patternImages.test.js +50 -19
  44. package/rollup.esm.mjs +9 -0
  45. package/src/InteractiveMap/InteractiveMap.js +2 -2
  46. package/src/InteractiveMap/InteractiveMap.test.js +4 -4
  47. package/src/services/eventBus.js +61 -0
  48. package/src/services/eventBus.test.js +70 -0
  49. package/src/utils/patternUtils.js +8 -3
  50. package/src/utils/patternUtils.test.js +17 -0
  51. package/webpack.umd.mjs +1 -0
  52. package/docs/assets/basic-map.jpg +0 -0
  53. package/docs/assets/button-first.jpg +0 -0
  54. package/docs/assets/maker-panel.jpg +0 -0
  55. /package/docs/assets/{screens-blue.jpg → images/screens-blue.jpg} +0 -0
  56. /package/docs/assets/{screens-white.jpg → images/screens-white.jpg} +0 -0
  57. /package/{assets → docs/assets}/images/slot-map.svg +0 -0
package/rollup.esm.mjs CHANGED
@@ -255,6 +255,15 @@ const ALL_BUILDS = [
255
255
  outDir: 'plugins/beta/datasets/dist/esm',
256
256
  manualChunks: (id) => { if (id.includes('/manifest')) return 'im-datasets-plugin' }
257
257
  },
258
+ {
259
+ entryPath: './plugins/beta/datasets/src/adapters/maplibre/index.js',
260
+ outDir: 'plugins/beta/datasets/adapters/maplibre/dist/esm',
261
+ manualChunks: (id) => {
262
+ if (id.includes('maplibreLayerAdapter')) {
263
+ return 'im-datasets-ml-adapter'
264
+ }
265
+ }
266
+ },
258
267
  {
259
268
  entryPath: './plugins/beta/map-styles/src/index.js',
260
269
  outDir: 'plugins/beta/map-styles/dist/esm',
@@ -437,7 +437,7 @@ export default class InteractiveMap {
437
437
  * @param {[number, number, number, number] | object} target - Bounds as [west, south, east, north] or [minX, minY, maxX, maxY] depending on the crs, or a GeoJSON Feature, FeatureCollection, or geometry.
438
438
  */
439
439
  fitToBounds (target) {
440
- this.eventBus.emit(events.MAP_FIT_TO_BOUNDS, target)
440
+ this.eventBus.emitWhenReady(events.MAP_FIT_TO_BOUNDS, target)
441
441
  }
442
442
 
443
443
  /**
@@ -446,7 +446,7 @@ export default class InteractiveMap {
446
446
  * @param {{ center?: [number, number], zoom?: number }} opts - View options.
447
447
  */
448
448
  setView (opts) {
449
- this.eventBus.emit(events.MAP_SET_VIEW, opts)
449
+ this.eventBus.emitWhenReady(events.MAP_SET_VIEW, opts)
450
450
  }
451
451
 
452
452
  /**
@@ -39,8 +39,8 @@ jest.mock('../utils/detectInterfaceType.js', () => ({
39
39
  }))
40
40
  jest.mock('../services/reverseGeocode.js', () => ({ createReverseGeocode: jest.fn() }))
41
41
  jest.mock('../services/eventBus.js', () => ({
42
- createEventBus: jest.fn(() => ({ on: jest.fn(), off: jest.fn(), emit: jest.fn(), destroy: jest.fn() })),
43
- default: { on: jest.fn(), off: jest.fn(), emit: jest.fn(), destroy: jest.fn() }
42
+ createEventBus: jest.fn(() => ({ on: jest.fn(), off: jest.fn(), emit: jest.fn(), emitWhenReady: jest.fn(), destroy: jest.fn() })),
43
+ default: { on: jest.fn(), off: jest.fn(), emit: jest.fn(), emitWhenReady: jest.fn(), destroy: jest.fn() }
44
44
  }))
45
45
  jest.mock('../App/initialiseApp.js', () => ({ initialiseApp: jest.fn() }))
46
46
 
@@ -635,12 +635,12 @@ describe('InteractiveMap Public API Methods', () => {
635
635
  it('fitToBounds emits MAP_FIT_TO_BOUNDS with bbox', () => {
636
636
  const bbox = [-0.489, 51.28, 0.236, 51.686]
637
637
  map.fitToBounds(bbox)
638
- expect(map.eventBus.emit).toHaveBeenCalledWith('map:fittobounds', bbox)
638
+ expect(map.eventBus.emitWhenReady).toHaveBeenCalledWith('map:fittobounds', bbox)
639
639
  })
640
640
 
641
641
  it('setView emits MAP_SET_VIEW with opts', () => {
642
642
  const opts = { center: [-0.1276, 51.5074], zoom: 12 }
643
643
  map.setView(opts)
644
- expect(map.eventBus.emit).toHaveBeenCalledWith('map:setview', opts)
644
+ expect(map.eventBus.emitWhenReady).toHaveBeenCalledWith('map:setview', opts)
645
645
  })
646
646
  })
@@ -1,16 +1,43 @@
1
1
  class EventBus {
2
2
  constructor () {
3
3
  this.events = {}
4
+ this._queued = {}
4
5
  }
5
6
 
7
+ /**
8
+ * Register a handler for an event. If a value was queued via `emitWhenReady`
9
+ * before any listeners existed, the handler is called immediately with that
10
+ * value and the queue is cleared.
11
+ *
12
+ * @param {string} eventName
13
+ * @param {Function} handler
14
+ * @returns {this}
15
+ */
6
16
  on (eventName, handler) {
7
17
  if (!this.events[eventName]) {
8
18
  this.events[eventName] = []
9
19
  }
10
20
  this.events[eventName].push(handler)
21
+
22
+ if (this._queued[eventName] !== undefined) {
23
+ try {
24
+ handler(...this._queued[eventName])
25
+ } catch (error) {
26
+ console.error(`Error in event handler for '${eventName}':`, error)
27
+ }
28
+ delete this._queued[eventName]
29
+ }
30
+
11
31
  return this
12
32
  }
13
33
 
34
+ /**
35
+ * Remove a handler for an event. Omit `handler` to remove all handlers.
36
+ *
37
+ * @param {string} eventName
38
+ * @param {Function} [handler]
39
+ * @returns {this}
40
+ */
14
41
  off (eventName, handler) {
15
42
  if (!this.events[eventName]) {
16
43
  return this
@@ -23,6 +50,13 @@ class EventBus {
23
50
  return this
24
51
  }
25
52
 
53
+ /**
54
+ * Emit an event, calling all registered handlers synchronously.
55
+ *
56
+ * @param {string} eventName
57
+ * @param {...*} args
58
+ * @returns {this}
59
+ */
26
60
  emit (eventName, ...args) {
27
61
  if (!this.events[eventName]) {
28
62
  return this
@@ -38,13 +72,40 @@ class EventBus {
38
72
  return this
39
73
  }
40
74
 
75
+ /**
76
+ * Like `emit`, but queues the args if no listeners are registered yet.
77
+ * The first listener to subscribe will receive the queued value immediately,
78
+ * after which the queue is cleared. Subsequent `emitWhenReady` calls for the
79
+ * same event before a listener arrives replace the queued value.
80
+ *
81
+ * Use this for events that may be emitted before the listener side is ready
82
+ * (e.g. called from `map:ready` before React effects have run).
83
+ *
84
+ * @param {string} eventName
85
+ * @param {...*} args
86
+ * @returns {this}
87
+ */
88
+ emitWhenReady (eventName, ...args) {
89
+ if (!this.events[eventName] || this.events[eventName].length === 0) {
90
+ this._queued[eventName] = args
91
+ return this
92
+ }
93
+ return this.emit(eventName, ...args)
94
+ }
95
+
96
+ /**
97
+ * Remove all handlers and clear any queued events.
98
+ */
41
99
  destroy () {
42
100
  this.events = {}
101
+ this._queued = {}
43
102
  }
44
103
  }
45
104
 
46
105
  /**
47
106
  * Factory for map-local EventBus
107
+ *
108
+ * @returns {EventBus}
48
109
  */
49
110
  export function createEventBus () {
50
111
  return new EventBus()
@@ -94,6 +94,76 @@ describe('EventBus singleton', () => {
94
94
  })
95
95
  })
96
96
 
97
+ describe('emitWhenReady', () => {
98
+ beforeEach(() => {
99
+ eventBus.destroy()
100
+ })
101
+
102
+ it('fires immediately when listeners are already registered', () => {
103
+ const handler = jest.fn()
104
+ eventBus.on('ready', handler)
105
+
106
+ eventBus.emitWhenReady('ready', 'value')
107
+
108
+ expect(handler).toHaveBeenCalledWith('value')
109
+ })
110
+
111
+ it('queues the call and replays to the first subscriber when no listeners exist', () => {
112
+ const handler = jest.fn()
113
+
114
+ eventBus.emitWhenReady('ready', 'value')
115
+ expect(handler).not.toHaveBeenCalled()
116
+
117
+ eventBus.on('ready', handler)
118
+ expect(handler).toHaveBeenCalledWith('value')
119
+ })
120
+
121
+ it('clears the queue after replaying so subsequent subscribers do not receive it', () => {
122
+ const first = jest.fn()
123
+ const second = jest.fn()
124
+
125
+ eventBus.emitWhenReady('ready', 'value')
126
+ eventBus.on('ready', first)
127
+ eventBus.on('ready', second)
128
+
129
+ expect(first).toHaveBeenCalledWith('value')
130
+ expect(second).not.toHaveBeenCalled()
131
+ })
132
+
133
+ it('replaces a queued value if emitWhenReady is called again before a subscriber arrives', () => {
134
+ const handler = jest.fn()
135
+
136
+ eventBus.emitWhenReady('ready', 'first')
137
+ eventBus.emitWhenReady('ready', 'second')
138
+ eventBus.on('ready', handler)
139
+
140
+ expect(handler).toHaveBeenCalledTimes(1)
141
+ expect(handler).toHaveBeenCalledWith('second')
142
+ })
143
+
144
+ it('catches and logs errors thrown by a handler during queued replay', () => {
145
+ const error = new Error('boom')
146
+ const handler = jest.fn(() => { throw error })
147
+ jest.spyOn(console, 'error').mockImplementation(() => {})
148
+
149
+ eventBus.emitWhenReady('ready', 'value')
150
+ eventBus.on('ready', handler)
151
+
152
+ expect(console.error).toHaveBeenCalledWith("Error in event handler for 'ready':", error)
153
+ console.error.mockRestore()
154
+ })
155
+
156
+ it('clears queued events on destroy', () => {
157
+ const handler = jest.fn()
158
+
159
+ eventBus.emitWhenReady('ready', 'value')
160
+ eventBus.destroy()
161
+ eventBus.on('ready', handler)
162
+
163
+ expect(handler).not.toHaveBeenCalled()
164
+ })
165
+ })
166
+
97
167
  describe('createEventBus factory', () => {
98
168
  /**
99
169
  * Test to ensure coverage for the factory function (Line 50).
@@ -52,21 +52,26 @@ export const getPatternInnerContent = (dataset, patternRegistry) => {
52
52
  }
53
53
 
54
54
  /**
55
- * Returns a deterministic image ID for a pattern + resolved colour combination.
55
+ * Returns a deterministic image ID for a pattern + resolved colour + pixel ratio combination.
56
56
  *
57
57
  * @param {Object} dataset
58
58
  * @param {string} mapStyleId
59
59
  * @param {Object} patternRegistry
60
+ * @param {number} [pixelRatio=1]
60
61
  * @returns {string|null}
61
62
  */
62
- export const getPatternImageId = (dataset, mapStyleId, patternRegistry) => {
63
+ // Minimum oversampling keeps 16×16 physical pixels as the floor so patterns remain crisp.
64
+ export const PATTERN_MIN_PIXEL_RATIO = 2
65
+
66
+ export const getPatternImageId = (dataset, mapStyleId, patternRegistry, pixelRatio = 1) => {
63
67
  const innerContent = getPatternInnerContent(dataset, patternRegistry)
64
68
  if (!innerContent) {
65
69
  return null
66
70
  }
67
71
  const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
68
72
  const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
69
- return `pattern-${hashString(innerContent + fg + bg)}`
73
+ const effectiveRatio = Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio)
74
+ return `pattern-${hashString(innerContent + fg + bg)}-${effectiveRatio}x`
70
75
  }
71
76
 
72
77
  /**
@@ -103,6 +103,23 @@ describe('getPatternImageId', () => {
103
103
  expect(idA).not.toBe(idB)
104
104
  })
105
105
 
106
+ test('floors effective ratio at PATTERN_MIN_PIXEL_RATIO so low-DPI ids match 2x', () => {
107
+ const dataset = { fillPattern: 'dot' }
108
+ const id1x = getPatternImageId(dataset, 'style-a', mockRegistry, 1)
109
+ const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
110
+ expect(id1x).toBe(id2x)
111
+ expect(id1x).toMatch(/-2x$/)
112
+ })
113
+
114
+ test('produces different ids for pixelRatios above the floor', () => {
115
+ const dataset = { fillPattern: 'dot' }
116
+ const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
117
+ const id3x = getPatternImageId(dataset, 'style-a', mockRegistry, 3)
118
+ expect(id2x).not.toBe(id3x)
119
+ expect(id2x).toMatch(/-2x$/)
120
+ expect(id3x).toMatch(/-3x$/)
121
+ })
122
+
106
123
  test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
107
124
  const id = getPatternImageId({ fillPattern: 'dot' }, 'style-a', mockRegistry)
108
125
  const idExplicit = getPatternImageId(
package/webpack.umd.mjs CHANGED
@@ -140,6 +140,7 @@ const ALL_BUILDS = [
140
140
  { entryPath: './plugins/search/src/index.js', libraryPath: 'searchPlugin', outDir: 'plugins/search/dist/umd' },
141
141
  { entryPath: './plugins/interact/src/index.js', libraryPath: 'interactPlugin', outDir: 'plugins/interact/dist/umd' },
142
142
  { entryPath: './plugins/beta/datasets/src/index.js', libraryPath: 'datasetsPlugin', outDir: 'plugins/beta/datasets/dist/umd' },
143
+ { entryPath: './plugins/beta/datasets/src/adapters/maplibre/index.js', libraryPath: 'datasetsMaplibreAdapter', outDir: 'plugins/beta/datasets/adapters/maplibre/dist/umd' },
143
144
  { entryPath: './plugins/beta/map-styles/src/index.js', libraryPath: 'mapStylesPlugin', outDir: 'plugins/beta/map-styles/dist/umd' },
144
145
  { entryPath: './plugins/beta/draw-ml/src/index.js', libraryPath: 'drawMLPlugin', outDir: 'plugins/beta/draw-ml/dist/umd' },
145
146
  { entryPath: './plugins/beta/frame/src/index.js', libraryPath: 'framePlugin', outDir: 'plugins/beta/frame/dist/umd' }
Binary file
Binary file
Binary file
File without changes