@defra/interactive-map 0.0.50-alpha → 0.0.51-apha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/docs/api.md +19 -0
- package/docs/architecture.md +14 -0
- package/docs/getting-started.md +10 -0
- package/docs/plugins/datasets.md +9 -3
- package/jest.setup.js +8 -0
- package/package.json +8 -3
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ol-adapter.js +1 -0
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js +2 -0
- package/plugins/datasets/dist/umd/im-datasets-ol-adapter.js.LICENSE.txt +1 -0
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/loadLayerAdapter.js +7 -3
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +17 -0
- package/plugins/datasets/src/adapters/mapboxStyleLayerAdapter.js +74 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +14 -79
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +4 -0
- package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +3 -89
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.js +111 -0
- package/plugins/datasets/src/adapters/openlayers/canvasPatternStyle.test.js +198 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.js +205 -0
- package/plugins/datasets/src/adapters/openlayers/layerBuilders.test.js +233 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.js +276 -0
- package/plugins/datasets/src/adapters/openlayers/openlayersLayerAdapter.test.js +431 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.js +172 -0
- package/plugins/datasets/src/adapters/openlayers/registry/openLayersDataset.test.js +262 -0
- package/plugins/datasets/src/fetch/createDynamicSource.js +12 -8
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +61 -52
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +13 -1
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +20 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +1 -1
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +1 -1
- package/plugins/datasets/src/registry/mapboxStyleDataset.js +108 -0
- package/plugins/datasets/src/registry/mapboxStyleDataset.test.js +240 -0
- package/plugins/datasets/src/utils/bbox.js +5 -7
- package/plugins/datasets/src/utils/bbox.test.js +3 -10
- package/plugins/draw/dist/css/index.css +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/src/index.js +2 -0
- package/plugins/draw/src/index.test.js +1 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/index.js +16 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +160 -7
- package/providers/beta/openlayers/src/openlayersProvider.test.js +13 -0
- package/providers/beta/openlayers/src/utils/bngProjection.js +9 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.js +32 -0
- package/providers/beta/openlayers/src/utils/filterEvaluator.test.js +36 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +38 -23
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +153 -8
- package/providers/beta/openlayers/src/utils/hoverCursor.js +5 -1
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +40 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -54
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +143 -1
- package/providers/beta/openlayers/src/utils/symbolImages.js +79 -5
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +111 -1
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +20 -8
- package/providers/beta/openlayers/src/utils/vtTileFragments.test.js +162 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/patternImages.js +4 -44
- package/rollup.esm.mjs +2 -0
- package/src/services/patternRegistry.js +38 -0
- package/src/services/patternRegistry.test.js +61 -0
- package/src/types.js +11 -0
- package/webpack.dev.mjs +1 -0
- /package/plugins/datasets/dist/esm/{esriLayerAdapter.js → im-datasets-esri-adapter.js} +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import OpenLayersLayerAdapter from './openlayersLayerAdapter.js'
|
|
2
|
+
import { datasetRegistry } from '../../registry/datasetRegistry.js'
|
|
3
|
+
import { patternRegistry } from '../../../../../src/services/patternRegistry.js'
|
|
4
|
+
import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
|
|
5
|
+
import { logger } from '../../../../../src/services/logger.js'
|
|
6
|
+
import { clearSymbolImageCache } from '../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
|
|
7
|
+
import VectorLayer from 'ol/layer/Vector.js'
|
|
8
|
+
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
9
|
+
import * as layerBuilders from './layerBuilders.js'
|
|
10
|
+
|
|
11
|
+
jest.mock('../../registry/datasetRegistry.js')
|
|
12
|
+
|
|
13
|
+
// The mocked datasetRegistry's beforeEach attaches full demo data alongside this file's own
|
|
14
|
+
// fixtures (see __mocks__/datasetRegistry.js), and demo data includes real fillPattern configs
|
|
15
|
+
// — so pattern registration needs the real patternRegistry singleton, not an empty stub, even
|
|
16
|
+
// though none of this file's own fixtures configure a pattern themselves.
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
clearSymbolImageCache()
|
|
19
|
+
// Covers the full pipeline exercised here: SVG -> Image -> canvas (rasteriseToImageData needs
|
|
20
|
+
// drawImage/getImageData) -> ImageData -> canvas (getOrCreateSymbolImage needs putImageData;
|
|
21
|
+
// canvasPatternStyle's crisp-pattern Fill needs createPattern).
|
|
22
|
+
HTMLCanvasElement.prototype.getContext = jest.fn(() => ({
|
|
23
|
+
drawImage: jest.fn(),
|
|
24
|
+
getImageData: jest.fn((_x, _y, w, h) => ({ width: w, height: h })),
|
|
25
|
+
putImageData: jest.fn(),
|
|
26
|
+
createPattern: jest.fn(() => ({}))
|
|
27
|
+
}))
|
|
28
|
+
HTMLCanvasElement.prototype.toDataURL = jest.fn(() => 'data:image/png;base64,mock')
|
|
29
|
+
globalThis.Image = class {
|
|
30
|
+
constructor (w, h) { this.width = w; this.height = h; this._src = '' }
|
|
31
|
+
get src () { return this._src }
|
|
32
|
+
set src (val) { this._src = val; this.onload?.() }
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const point = (id, coords) => ({ type: 'Feature', id, properties: {}, geometry: { type: 'Point', coordinates: coords } })
|
|
37
|
+
|
|
38
|
+
const makeMap = (pixelRatio = 1) => {
|
|
39
|
+
const layers = new Set()
|
|
40
|
+
return {
|
|
41
|
+
layers,
|
|
42
|
+
addLayer: jest.fn(layer => layers.add(layer)),
|
|
43
|
+
removeLayer: jest.fn(layer => layers.delete(layer)),
|
|
44
|
+
getPixelRatio: jest.fn(() => pixelRatio)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const makeMapProvider = (map) => ({ map })
|
|
49
|
+
|
|
50
|
+
let map, mapProvider, adapter
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
map = makeMap()
|
|
54
|
+
mapProvider = makeMapProvider(map)
|
|
55
|
+
adapter = new OpenLayersLayerAdapter(mapProvider, symbolRegistry, patternRegistry)
|
|
56
|
+
datasetRegistry.attachCreateDataset(adapter.createDataset)
|
|
57
|
+
datasetRegistry.mockExtend({
|
|
58
|
+
'ds-fill': { id: 'ds-fill', style: { fill: '#0000ff' }, geojson: { type: 'FeatureCollection', features: [point(1, [0, 0])] } },
|
|
59
|
+
'ds-bare': { id: 'ds-bare' },
|
|
60
|
+
'ds-tiles': { id: 'ds-tiles', tiles: ['https://example.com/{z}/{x}/{y}'], style: { fill: '#0000ff' } },
|
|
61
|
+
'ds-tiles-bare': { id: 'ds-tiles-bare', tiles: ['https://example.com/{z}/{x}/{y}'] },
|
|
62
|
+
'ds-dynamic': { id: 'ds-dynamic', style: { fill: '#0000ff' }, dynamicGeoJSON: { url: 'https://example.com', idProperty: 'ref' } },
|
|
63
|
+
'ds-parent': { id: 'ds-parent', sublayerIds: ['ds-child-a', 'ds-child-b'], geojson: { type: 'FeatureCollection', features: [point(1, [0, 0])] } },
|
|
64
|
+
'ds-child-a': { id: 'ds-child-a', parentId: 'ds-parent', style: { fill: '#ff0000' }, filter: ['==', ['get', 'cat'], 'a'] },
|
|
65
|
+
'ds-child-b': { id: 'ds-child-b', parentId: 'ds-parent', style: { fill: '#00ff00' }, filter: ['==', ['get', 'cat'], 'b'] },
|
|
66
|
+
'ds-pattern': { id: 'ds-pattern', style: { fillPattern: 'dot' }, geojson: { type: 'FeatureCollection', features: [] } },
|
|
67
|
+
'ds-symbol': { id: 'ds-symbol', style: { symbol: 'pin' }, geojson: { type: 'FeatureCollection', features: [point(1, [0, 0])] } },
|
|
68
|
+
// Two top-level datasets pointing at the same tiles URL share one OL source by sourceId
|
|
69
|
+
// (see layerBuilders.js's createDatasetSource) — mirrors the real demo's
|
|
70
|
+
// existing-fields/hedge-control pair, both reading field_parcels_with_hedges_*.
|
|
71
|
+
'ds-tiles-shared-a': { id: 'ds-tiles-shared-a', tiles: ['https://example.com/shared/{z}/{x}/{y}'], sourceLayer: 'layer-a', style: { fill: '#0000ff' } },
|
|
72
|
+
'ds-tiles-shared-b': { id: 'ds-tiles-shared-b', tiles: ['https://example.com/shared/{z}/{x}/{y}'], sourceLayer: 'layer-b', style: { fill: '#ff0000' } }
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// createDatasetLayer doesn't tag layers with the dataset id, so tests read a specific layer
|
|
77
|
+
// back from the adapter's private map instead of the fake map's Set.
|
|
78
|
+
const getLayer = (id) => adapter._layersById.get(id)
|
|
79
|
+
|
|
80
|
+
describe('init / addDataset', () => {
|
|
81
|
+
it('adds a layer for a dataset with a fill', async () => {
|
|
82
|
+
await adapter.init()
|
|
83
|
+
expect(getLayer('ds-fill')).toBeInstanceOf(VectorLayer)
|
|
84
|
+
expect(map.addLayer).toHaveBeenCalledWith(getLayer('ds-fill'))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('skips a dataset with no fill, stroke, or symbol', async () => {
|
|
88
|
+
await adapter.init()
|
|
89
|
+
expect(getLayer('ds-bare')).toBeUndefined()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('adds a VectorTileLayer for a tiles-backed dataset with a renderable style', async () => {
|
|
93
|
+
await adapter.init()
|
|
94
|
+
expect(getLayer('ds-tiles')).toBeInstanceOf(VectorTileLayer)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('skips a tiles-backed dataset with no fill, stroke, or symbol (nothing to render, not a tiles limitation)', async () => {
|
|
98
|
+
await adapter.init()
|
|
99
|
+
expect(getLayer('ds-tiles-bare')).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('adds one layer per sublayer, sharing one OL source', async () => {
|
|
103
|
+
await adapter.init()
|
|
104
|
+
const layerA = getLayer('ds-child-a')
|
|
105
|
+
const layerB = getLayer('ds-child-b')
|
|
106
|
+
expect(layerA).toBeInstanceOf(VectorLayer)
|
|
107
|
+
expect(layerB).toBeInstanceOf(VectorLayer)
|
|
108
|
+
expect(layerA.getSource()).toBe(layerB.getSource())
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('addDataset adds a single dataset after init', async () => {
|
|
112
|
+
await adapter.addDataset('ds-fill')
|
|
113
|
+
expect(getLayer('ds-fill')).toBeInstanceOf(VectorLayer)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('addDataset is a no-op for an unknown dataset id', async () => {
|
|
117
|
+
await expect(adapter.addDataset('does-not-exist')).resolves.toBeUndefined()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('removeDataset', () => {
|
|
122
|
+
it('removes the layer and source for a simple dataset', async () => {
|
|
123
|
+
await adapter.addDataset('ds-fill')
|
|
124
|
+
const layer = getLayer('ds-fill')
|
|
125
|
+
adapter.removeDataset('ds-fill')
|
|
126
|
+
expect(map.removeLayer).toHaveBeenCalledWith(layer)
|
|
127
|
+
expect(getLayer('ds-fill')).toBeUndefined()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('removes all sublayer layers when removing the parent', async () => {
|
|
131
|
+
await adapter.addDataset('ds-parent')
|
|
132
|
+
adapter.removeDataset('ds-parent')
|
|
133
|
+
expect(getLayer('ds-child-a')).toBeUndefined()
|
|
134
|
+
expect(getLayer('ds-child-b')).toBeUndefined()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('is a no-op for an unknown dataset id', () => {
|
|
138
|
+
expect(() => adapter.removeDataset('does-not-exist')).not.toThrow()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('does not delete the shared source while another dataset still uses it', async () => {
|
|
142
|
+
await adapter.addDataset('ds-tiles-shared-a')
|
|
143
|
+
await adapter.addDataset('ds-tiles-shared-b')
|
|
144
|
+
const sharedSource = getLayer('ds-tiles-shared-b').getSource()
|
|
145
|
+
adapter.removeDataset('ds-tiles-shared-a')
|
|
146
|
+
// If the shared source had been wrongly deleted, re-adding would build a brand new one
|
|
147
|
+
// instead of reusing the source ds-tiles-shared-b's own layer is still attached to.
|
|
148
|
+
await adapter.addDataset('ds-tiles-shared-a')
|
|
149
|
+
expect(getLayer('ds-tiles-shared-a').getSource()).toBe(sharedSource)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('skips a layer id that never got built, rather than calling removeLayer(undefined)', async () => {
|
|
153
|
+
const buildSourceSpy = jest.spyOn(layerBuilders, 'createDatasetSource').mockImplementationOnce(() => { throw new Error('boom') })
|
|
154
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {})
|
|
155
|
+
await adapter.addDataset('ds-fill') // fails to build — never added to the adapter's own layer map
|
|
156
|
+
expect(() => adapter.removeDataset('ds-fill')).not.toThrow()
|
|
157
|
+
expect(map.removeLayer).not.toHaveBeenCalled()
|
|
158
|
+
buildSourceSpy.mockRestore()
|
|
159
|
+
warnSpy.mockRestore()
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('setData', () => {
|
|
164
|
+
it('replaces the features on the dataset source', async () => {
|
|
165
|
+
await adapter.addDataset('ds-dynamic')
|
|
166
|
+
adapter.setData('ds-dynamic', { type: 'FeatureCollection', features: [point('f1', [1, 1]), point('f2', [2, 2])] })
|
|
167
|
+
expect(getLayer('ds-dynamic').getSource().getFeatures()).toHaveLength(2)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('promotes idProperty onto feature ids', async () => {
|
|
171
|
+
await adapter.addDataset('ds-dynamic')
|
|
172
|
+
adapter.setData('ds-dynamic', { type: 'FeatureCollection', features: [{ type: 'Feature', properties: { ref: 'abc' }, geometry: { type: 'Point', coordinates: [1, 1] } }] })
|
|
173
|
+
expect(getLayer('ds-dynamic').getSource().getFeatures()[0].getId()).toBe('abc')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('clears existing features and adds none when the new geojson has no features', async () => {
|
|
177
|
+
await adapter.addDataset('ds-dynamic')
|
|
178
|
+
adapter.setData('ds-dynamic', { type: 'FeatureCollection', features: [point('f1', [1, 1])] })
|
|
179
|
+
expect(getLayer('ds-dynamic').getSource().getFeatures()).toHaveLength(1)
|
|
180
|
+
adapter.setData('ds-dynamic', { type: 'FeatureCollection', features: [] })
|
|
181
|
+
expect(getLayer('ds-dynamic').getSource().getFeatures()).toHaveLength(0)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('is a no-op for a dataset with no tracked source', () => {
|
|
185
|
+
expect(() => adapter.setData('does-not-exist', { type: 'FeatureCollection', features: [] })).not.toThrow()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('opacity', () => {
|
|
190
|
+
it('applyDatasetOpacity sets the layer opacity', async () => {
|
|
191
|
+
await adapter.addDataset('ds-fill')
|
|
192
|
+
datasetRegistry.mockExtend({ 'ds-fill': { id: 'ds-fill', style: { fill: '#0000ff', opacity: 0.4 }, geojson: { type: 'FeatureCollection', features: [] } } })
|
|
193
|
+
adapter.applyDatasetOpacity('ds-fill')
|
|
194
|
+
expect(getLayer('ds-fill').getOpacity()).toBe(0.4)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('applyDatasetOpacity is a no-op for an unknown dataset id', () => {
|
|
198
|
+
expect(() => adapter.applyDatasetOpacity('does-not-exist')).not.toThrow()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('applyGlobalOpacity updates every tracked layer', async () => {
|
|
202
|
+
await adapter.addDataset('ds-parent')
|
|
203
|
+
// A second mockExtend call replaces the whole registry with {pristine demo data, ...extras}
|
|
204
|
+
// rather than layering onto the previous mockExtend — so ds-parent must be included again
|
|
205
|
+
// here, or forEachDataset (which only walks top-level datasets) would skip its children.
|
|
206
|
+
datasetRegistry.mockExtend({
|
|
207
|
+
'ds-parent': { id: 'ds-parent', sublayerIds: ['ds-child-a', 'ds-child-b'] },
|
|
208
|
+
'ds-child-a': { id: 'ds-child-a', parentId: 'ds-parent', style: { fill: '#ff0000', opacity: 0.2 } },
|
|
209
|
+
'ds-child-b': { id: 'ds-child-b', parentId: 'ds-parent', style: { fill: '#00ff00', opacity: 0.6 } }
|
|
210
|
+
})
|
|
211
|
+
adapter.applyGlobalOpacity()
|
|
212
|
+
expect(getLayer('ds-child-a').getOpacity()).toBe(0.2)
|
|
213
|
+
expect(getLayer('ds-child-b').getOpacity()).toBe(0.6)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe('visibility', () => {
|
|
218
|
+
it('applyDatasetVisibility hides the layer when visible is false', async () => {
|
|
219
|
+
await adapter.addDataset('ds-fill')
|
|
220
|
+
datasetRegistry.mockExtend({ 'ds-fill': { id: 'ds-fill', visible: false, style: { fill: '#0000ff' }, geojson: { type: 'FeatureCollection', features: [] } } })
|
|
221
|
+
adapter.applyDatasetVisibility('ds-fill')
|
|
222
|
+
expect(getLayer('ds-fill').getVisible()).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('applyDatasetVisibility is a no-op for an unknown dataset id', () => {
|
|
226
|
+
expect(() => adapter.applyDatasetVisibility('does-not-exist')).not.toThrow()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('applyGlobalVisibility updates every tracked layer', async () => {
|
|
230
|
+
await adapter.addDataset('ds-fill')
|
|
231
|
+
datasetRegistry.mockExtend({
|
|
232
|
+
'ds-fill': { id: 'ds-fill', visible: false, style: { fill: '#0000ff' }, geojson: { type: 'FeatureCollection', features: [] } }
|
|
233
|
+
})
|
|
234
|
+
adapter.applyGlobalVisibility()
|
|
235
|
+
expect(getLayer('ds-fill').getVisible()).toBe(false)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('applyFeatureFilter', () => {
|
|
240
|
+
it('re-derives flatStyle to reflect newly hidden features', async () => {
|
|
241
|
+
await adapter.addDataset('ds-fill')
|
|
242
|
+
datasetRegistry.mockExtend({ 'ds-fill': { id: 'ds-fill', style: { fill: '#0000ff' }, hiddenFeatures: [1], geojson: { type: 'FeatureCollection', features: [] } } })
|
|
243
|
+
adapter.applyFeatureFilter('ds-fill')
|
|
244
|
+
// flatStyle now wraps in a filter rule — confirm the layer's style function reflects it by
|
|
245
|
+
// checking the underlying style was re-set to the dataset's current (filtered) flatStyle.
|
|
246
|
+
const registryDataset = datasetRegistry.getDataset('ds-fill')
|
|
247
|
+
expect(getLayer('ds-fill').getStyle()).toBeDefined()
|
|
248
|
+
expect(registryDataset.filter).not.toBeNull()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('is a no-op for an unknown dataset id', () => {
|
|
252
|
+
expect(() => adapter.applyFeatureFilter('does-not-exist')).not.toThrow()
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('applyStyle', () => {
|
|
257
|
+
it('re-sets style on an existing layer', async () => {
|
|
258
|
+
await adapter.addDataset('ds-fill')
|
|
259
|
+
const layer = getLayer('ds-fill')
|
|
260
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
261
|
+
await adapter.applyStyle('ds-fill')
|
|
262
|
+
expect(setStyleSpy).toHaveBeenCalled()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('adds the layer when a previously bare dataset becomes renderable', async () => {
|
|
266
|
+
await adapter.addDataset('ds-bare')
|
|
267
|
+
expect(getLayer('ds-bare')).toBeUndefined()
|
|
268
|
+
datasetRegistry.mockExtend({
|
|
269
|
+
'ds-bare': { id: 'ds-bare', style: { fill: '#0000ff' }, geojson: { type: 'FeatureCollection', features: [] } }
|
|
270
|
+
})
|
|
271
|
+
await adapter.applyStyle('ds-bare')
|
|
272
|
+
expect(getLayer('ds-bare')).toBeInstanceOf(VectorLayer)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('is a no-op for an unknown dataset id', async () => {
|
|
276
|
+
await expect(adapter.applyStyle('does-not-exist')).resolves.toBeUndefined()
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('robustness — malformed dataset config or a failed source/layer build', () => {
|
|
281
|
+
afterEach(() => {
|
|
282
|
+
jest.restoreAllMocks()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('logs and leaves the existing layer in place when a restyle filter is invalid for OL\'s expression parser', async () => {
|
|
286
|
+
// ol/style/flat's "in" operator requires a real array second argument, unlike MapLibre's own
|
|
287
|
+
// filter dialect (which also accepts a bare scalar) — a hand-authored filter written for
|
|
288
|
+
// MapLibre can hit this on restyle (applyFeatureFilter/applyStyle/onMapStyleChange all funnel
|
|
289
|
+
// through _setLayerStyle), and one bad dataset's filter must not crash every other dataset.
|
|
290
|
+
await adapter.addDataset('ds-fill')
|
|
291
|
+
const layer = getLayer('ds-fill')
|
|
292
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {})
|
|
293
|
+
datasetRegistry.mockExtend({
|
|
294
|
+
'ds-fill': { id: 'ds-fill', style: { fill: '#0000ff' }, filter: ['in', ['get', 'category'], 'a'], geojson: { type: 'FeatureCollection', features: [] } }
|
|
295
|
+
})
|
|
296
|
+
expect(() => adapter.applyFeatureFilter('ds-fill')).not.toThrow()
|
|
297
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('failed to build a style for dataset "ds-fill"'))
|
|
298
|
+
expect(getLayer('ds-fill')).toBe(layer)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('logs and skips a dataset whose source fails to build, without adding a layer for it', async () => {
|
|
302
|
+
jest.spyOn(layerBuilders, 'createDatasetSource').mockImplementationOnce(() => { throw new Error('boom') })
|
|
303
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {})
|
|
304
|
+
await expect(adapter.addDataset('ds-fill')).resolves.toBeUndefined()
|
|
305
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('failed to build a source for dataset "ds-fill"'))
|
|
306
|
+
expect(getLayer('ds-fill')).toBeUndefined()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('does not stop other datasets from loading when one dataset\'s source build fails', async () => {
|
|
310
|
+
jest.spyOn(layerBuilders, 'createDatasetSource').mockImplementationOnce(() => { throw new Error('boom') })
|
|
311
|
+
jest.spyOn(logger, 'warn').mockImplementation(() => {})
|
|
312
|
+
await adapter.init()
|
|
313
|
+
expect(getLayer('ds-symbol')).toBeInstanceOf(VectorLayer)
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('onMapStyleChange', () => {
|
|
318
|
+
it('re-sets style on every tracked layer without removing it', async () => {
|
|
319
|
+
await adapter.addDataset('ds-fill')
|
|
320
|
+
const layer = getLayer('ds-fill')
|
|
321
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
322
|
+
await adapter.onMapStyleChange()
|
|
323
|
+
expect(setStyleSpy).toHaveBeenCalled()
|
|
324
|
+
expect(map.removeLayer).not.toHaveBeenCalled()
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// A pattern dataset always goes through canvasPatternStyle.js's genuinely-crisp style FUNCTION
|
|
329
|
+
// (see layerBuilders.resolveLayerStyle).
|
|
330
|
+
describe('pattern registration', () => {
|
|
331
|
+
it('registers a crisp canvas pattern before init() adds its layer, so its style is already resolved', async () => {
|
|
332
|
+
await adapter.init()
|
|
333
|
+
const styleFn = getLayer('ds-pattern').getStyle()
|
|
334
|
+
expect(typeof styleFn).toBe('function')
|
|
335
|
+
expect(styleFn({}).getFill().getColor()).toBeTruthy()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('registers a crisp canvas pattern before addDataset() adds its layer', async () => {
|
|
339
|
+
await adapter.addDataset('ds-pattern')
|
|
340
|
+
const styleFn = getLayer('ds-pattern').getStyle()
|
|
341
|
+
expect(styleFn({}).getFill().getColor()).toBeTruthy()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('re-registers patterns for a newly-introduced pattern on applyStyle()', async () => {
|
|
345
|
+
await adapter.addDataset('ds-fill')
|
|
346
|
+
expect(getLayer('ds-fill').getStyle()).toEqual({ 'fill-color': '#0000ff' })
|
|
347
|
+
datasetRegistry.mockExtend({
|
|
348
|
+
'ds-fill': { id: 'ds-fill', style: { fillPattern: 'dot' }, geojson: { type: 'FeatureCollection', features: [] } }
|
|
349
|
+
})
|
|
350
|
+
await adapter.applyStyle('ds-fill')
|
|
351
|
+
const styleFn = getLayer('ds-fill').getStyle()
|
|
352
|
+
expect(typeof styleFn).toBe('function')
|
|
353
|
+
expect(styleFn({}).getFill().getColor()).toBeTruthy()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('re-registers patterns on onMapStyleChange() in case fg/bg colours are mapStyle-keyed', async () => {
|
|
357
|
+
await adapter.addDataset('ds-pattern')
|
|
358
|
+
const layer = getLayer('ds-pattern')
|
|
359
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
360
|
+
await adapter.onMapStyleChange()
|
|
361
|
+
expect(setStyleSpy).toHaveBeenCalledWith(expect.any(Function))
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('onMapSizeChange() re-registers and re-applies crisp canvas pattern styles, so they stay sized for the map\'s current pixelRatio', async () => {
|
|
365
|
+
await adapter.addDataset('ds-pattern')
|
|
366
|
+
const layer = getLayer('ds-pattern')
|
|
367
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
368
|
+
await adapter.onMapSizeChange()
|
|
369
|
+
expect(setStyleSpy).toHaveBeenCalledWith(expect.any(Function))
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('onMapSizeChange() does not touch a plain fill/stroke dataset\'s style', async () => {
|
|
373
|
+
await adapter.addDataset('ds-fill')
|
|
374
|
+
const layer = getLayer('ds-fill')
|
|
375
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
376
|
+
await adapter.onMapSizeChange()
|
|
377
|
+
expect(setStyleSpy).not.toHaveBeenCalled()
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('symbol registration', () => {
|
|
382
|
+
it('registers a symbol before init() adds its layer, so flatStyle already resolves it', async () => {
|
|
383
|
+
await adapter.init()
|
|
384
|
+
expect(getLayer('ds-symbol').getStyle()).toMatchObject({ 'icon-src': 'data:image/png;base64,mock' })
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('registers a symbol before addDataset() adds its layer', async () => {
|
|
388
|
+
await adapter.addDataset('ds-symbol')
|
|
389
|
+
expect(getLayer('ds-symbol').getStyle()).toMatchObject({ 'icon-src': 'data:image/png;base64,mock' })
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('re-registers a symbol for a newly-introduced one on applyStyle()', async () => {
|
|
393
|
+
await adapter.addDataset('ds-fill')
|
|
394
|
+
expect(getLayer('ds-fill').getStyle()).toEqual({ 'fill-color': '#0000ff' })
|
|
395
|
+
datasetRegistry.mockExtend({
|
|
396
|
+
'ds-fill': { id: 'ds-fill', style: { symbol: 'pin' }, geojson: { type: 'FeatureCollection', features: [] } }
|
|
397
|
+
})
|
|
398
|
+
await adapter.applyStyle('ds-fill')
|
|
399
|
+
expect(getLayer('ds-fill').getStyle()).toMatchObject({ 'icon-src': 'data:image/png;base64,mock' })
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('re-registers symbols on onMapStyleChange() in case colours are mapStyle-keyed', async () => {
|
|
403
|
+
await adapter.addDataset('ds-symbol')
|
|
404
|
+
const layer = getLayer('ds-symbol')
|
|
405
|
+
const setStyleSpy = jest.spyOn(layer, 'setStyle')
|
|
406
|
+
await adapter.onMapStyleChange()
|
|
407
|
+
expect(setStyleSpy).toHaveBeenCalledWith(expect.objectContaining({ 'icon-src': 'data:image/png;base64,mock' }))
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// symbolMeta.imageId is only ever resolved for the 'normal' variant, but that's still
|
|
411
|
+
// mapStyle-dependent (see registerSymbol's resolve() call) — so a stale, pre-switch id left
|
|
412
|
+
// on the layer would make highlightFeatures.js's reverse-map keep resolving the OLD theme's
|
|
413
|
+
// selected/active image forever, regardless of how many times the map style actually changes.
|
|
414
|
+
it('refreshes symbolMeta on onMapStyleChange(), not just the layer\'s own style', async () => {
|
|
415
|
+
await adapter.addDataset('ds-symbol')
|
|
416
|
+
const layer = getLayer('ds-symbol')
|
|
417
|
+
const before = layer.get('symbolMeta')
|
|
418
|
+
const setSpy = jest.spyOn(layer, 'set')
|
|
419
|
+
await adapter.onMapStyleChange()
|
|
420
|
+
expect(setSpy).toHaveBeenCalledWith('symbolMeta', expect.objectContaining({ imageId: before?.imageId }))
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
describe('destroy', () => {
|
|
425
|
+
it('removes every tracked layer from the map', async () => {
|
|
426
|
+
await adapter.addDataset('ds-fill')
|
|
427
|
+
const layer = getLayer('ds-fill')
|
|
428
|
+
adapter.destroy()
|
|
429
|
+
expect(map.removeLayer).toHaveBeenCalledWith(layer)
|
|
430
|
+
})
|
|
431
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { MapboxStyleDataset } from '../../../registry/mapboxStyleDataset.js'
|
|
2
|
+
import { datasetRegistry } from '../../../registry/datasetRegistry.js'
|
|
3
|
+
import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.js'
|
|
4
|
+
import { getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js'
|
|
5
|
+
import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
|
|
6
|
+
import { getCachedSymbolDataUri, SYMBOL_RASTER_PIXEL_RATIO } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
|
|
7
|
+
|
|
8
|
+
const MAX_TILE_ZOOM = 22
|
|
9
|
+
const DEFAULT_STROKE_WIDTH = 1
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OpenLayers implementation of the shared Dataset registry model.
|
|
13
|
+
*
|
|
14
|
+
* Unlike MapLibre — where fill/stroke/symbol are always separate map layers because a
|
|
15
|
+
* MapLibre layer has exactly one paint type — OL's style is an ol/style/flat spec that can
|
|
16
|
+
* combine fill+stroke (or an icon) for one feature in a single layer. So a dataset/sublayer
|
|
17
|
+
* here maps to at most one OL layer (see leafLayerIds), not up to three.
|
|
18
|
+
*
|
|
19
|
+
* Opacity and visibility are applied at the OL *layer* level (layer.setOpacity/setVisible) by
|
|
20
|
+
* the adapter, not baked into the style — so, unlike MapLibre's getFillSource/getStrokeSource/
|
|
21
|
+
* getSymbolSource paint objects, flatStyle carries no opacity/visibility properties.
|
|
22
|
+
*/
|
|
23
|
+
export class OpenLayersDataset extends MapboxStyleDataset {
|
|
24
|
+
get leafLayerIds () {
|
|
25
|
+
if (this.hasSublayers) {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
return (this.hasFill || this.hasStroke || this.hasSymbol) ? [this.id] : []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get source () {
|
|
32
|
+
// Dynamic sources always start empty and are populated later via setData — the
|
|
33
|
+
// MapLibre-shaped promoteId/generateId keys on dynamicGeoJSON.source don't apply to OL,
|
|
34
|
+
// which has no source-level id-promotion concept (see idStrategy).
|
|
35
|
+
if (this.hasDynamicGeoJSON) {
|
|
36
|
+
return { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }
|
|
37
|
+
}
|
|
38
|
+
if (this.tiles) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'vector',
|
|
41
|
+
tiles: this.tiles,
|
|
42
|
+
minzoom: this.minZoom || 0,
|
|
43
|
+
maxzoom: this.maxZoom || MAX_TILE_ZOOM
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (this.geojson) {
|
|
47
|
+
return { type: 'geojson', data: this.geojson }
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How to resolve each OL Feature's id from a GeoJSON feature, since ol/format/GeoJSON has no
|
|
54
|
+
* MapLibre-style "promoteId" source option — a property can only be promoted to the feature's
|
|
55
|
+
* id at ingestion time, by the layer builder, using this.
|
|
56
|
+
* @returns {string|null} A feature property name to copy onto the OL feature's id, or null to
|
|
57
|
+
* use the GeoJSON feature's own native id (ol/format/GeoJSON's default).
|
|
58
|
+
*/
|
|
59
|
+
get idStrategy () {
|
|
60
|
+
// Mirrors _hiddenFeaturesIdExpression's own dynamicGeoJSON-first precedence: a dynamic
|
|
61
|
+
// dataset's idProperty lives on its dynamicGeoJSON config, not the dataset definition
|
|
62
|
+
// itself (see registry/dynamicGeoJson.js).
|
|
63
|
+
if (this.hasDynamicGeoJSON) {
|
|
64
|
+
return this.dynamicGeoJSON.idProperty || null
|
|
65
|
+
}
|
|
66
|
+
return this.idProperty || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extends the shared Dataset.filter with a sourceLayer check when reading from a vector tile
|
|
71
|
+
* source — several top-level datasets can point at the same tiles URL but each render only one
|
|
72
|
+
* named layer within the tile (e.g. existing-fields/hedge-control both read
|
|
73
|
+
* field_parcels_with_hedges_osgb36, but only field_parcels_osgb36/hedge_control respectively —
|
|
74
|
+
* see layerBuilders.js's createDatasetSource for how they end up sharing one
|
|
75
|
+
* VectorTileSource). MapLibre doesn't need this: its paint layers declare their own native
|
|
76
|
+
* 'source-layer' property, entirely separate from `filter`. OL's vector tile layers have no
|
|
77
|
+
* equivalent per-layer scoping, so it has to be folded into the same filter expression that's
|
|
78
|
+
* evaluated for styling, checking the property ol/format/MVT's default layerName exposes it
|
|
79
|
+
* under ('layer').
|
|
80
|
+
* @returns {Array|undefined}
|
|
81
|
+
*/
|
|
82
|
+
get filter () {
|
|
83
|
+
const baseFilter = super.filter
|
|
84
|
+
if (!this.sourceLayer) {
|
|
85
|
+
return baseFilter
|
|
86
|
+
}
|
|
87
|
+
const sourceLayerFilter = ['==', ['get', 'layer'], this.sourceLayer]
|
|
88
|
+
if (!baseFilter) {
|
|
89
|
+
return sourceLayerFilter
|
|
90
|
+
}
|
|
91
|
+
return baseFilter[0] === 'all' ? [...baseFilter, sourceLayerFilter] : ['all', baseFilter, sourceLayerFilter]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// No filter → a plain FlatStyle applies unconditionally. With a filter (own filter and/or
|
|
95
|
+
// hidden features), wrap in a single-rule array — OL renders nothing for features that
|
|
96
|
+
// don't match, since there's no `else` rule. This is the same filter expression shape
|
|
97
|
+
// MapLibre's Dataset.filter produces (get/in/literal/to-string/all/!), which OL's own
|
|
98
|
+
// expression evaluator (ol/expr) shares operator-for-operator.
|
|
99
|
+
_wrapWithFilter (style) {
|
|
100
|
+
return this.filter ? [{ filter: this.filter, style }] : style
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The resolved base (normal) symbol imageId + anchor for this dataset — layerBuilders.js tags
|
|
105
|
+
* the OL layer with this so highlightFeatures.js can resolve a select/active highlight image
|
|
106
|
+
* for it (via symbolImages.js's getActiveSymbolImageId/getSelectedSymbolImageId) without
|
|
107
|
+
* reaching back into the datasets plugin's own registries from a provider-level file, the same
|
|
108
|
+
* way MapLibre's highlight code reads icon-image/icon-anchor straight off the rendered style
|
|
109
|
+
* layer instead of re-deriving them.
|
|
110
|
+
* @returns {{ imageId: string, anchor: number[] }|null}
|
|
111
|
+
*/
|
|
112
|
+
get symbolMeta () {
|
|
113
|
+
if (!this.hasSymbol) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
const imageId = symbolRegistry.getSymbolImageId(this.style, datasetRegistry.mapStyle, false, SYMBOL_RASTER_PIXEL_RATIO)
|
|
117
|
+
if (!imageId) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
return { imageId, anchor: getSymbolAnchor(this.style, symbolRegistry.getSymbolDef(this.style)) }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// icon-scale genuinely gives resolution-independent crispness for symbols (see
|
|
124
|
+
// symbolImages.js's module doc), so unlike patterns this never needs re-registration on
|
|
125
|
+
// resize. Falls back to an empty style (no icon at all) until the image has been rasterised
|
|
126
|
+
// ahead of time by the adapter (registerSymbol is async; this getter must stay synchronous).
|
|
127
|
+
// Reuses symbolMeta rather than re-resolving getSymbolImageId/getSymbolDef itself — both
|
|
128
|
+
// getters need the same imageId+anchor, and symbolRegistry.resolve()/getSymbolDef() aren't
|
|
129
|
+
// free (colour resolution + hashing), so recomputing them a second time here would double the
|
|
130
|
+
// cost of every layer build/restyle for no benefit.
|
|
131
|
+
_symbolStyle () {
|
|
132
|
+
const style = {}
|
|
133
|
+
const meta = this.symbolMeta
|
|
134
|
+
const dataUri = meta && getCachedSymbolDataUri(meta.imageId)
|
|
135
|
+
if (dataUri) {
|
|
136
|
+
style['icon-src'] = dataUri
|
|
137
|
+
style['icon-anchor'] = meta.anchor
|
|
138
|
+
style['icon-scale'] = 1 / SYMBOL_RASTER_PIXEL_RATIO
|
|
139
|
+
}
|
|
140
|
+
return this._wrapWithFilter(style)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* ol/style/flat FlatStyleLike for this dataset. A symbol dataset gets icon-src alone (see
|
|
145
|
+
* _symbolStyle) — hasFill/hasStroke are false whenever hasSymbol is true, per the base Dataset.
|
|
146
|
+
*
|
|
147
|
+
* A pattern fill is never resolved here — it's built by canvasPatternStyle.js's genuinely
|
|
148
|
+
* crisp CanvasPattern-based style instead (see layerBuilders.js's resolveLayerStyle), which
|
|
149
|
+
* this getter knows nothing about. hasFill is also true for a pattern dataset (see base
|
|
150
|
+
* Dataset.hasFill), so that's explicitly excluded here rather than emitting a meaningless
|
|
151
|
+
* fill-color for it.
|
|
152
|
+
* @returns {Object|Array<Object>}
|
|
153
|
+
*/
|
|
154
|
+
get flatStyle () {
|
|
155
|
+
if (this.hasSymbol) {
|
|
156
|
+
return this._symbolStyle()
|
|
157
|
+
}
|
|
158
|
+
const mapStyleId = datasetRegistry.mapStyle?.id
|
|
159
|
+
const style = {}
|
|
160
|
+
if (this.hasFill && !this.hasPattern) {
|
|
161
|
+
style['fill-color'] = getValueForStyle(this.style.fill, mapStyleId)
|
|
162
|
+
}
|
|
163
|
+
if (this.hasStroke) {
|
|
164
|
+
style['stroke-color'] = getValueForStyle(this.style.stroke, mapStyleId)
|
|
165
|
+
style['stroke-width'] = this.style.strokeWidth || DEFAULT_STROKE_WIDTH
|
|
166
|
+
if (this.style.strokeDashArray) {
|
|
167
|
+
style['stroke-line-dash'] = this.style.strokeDashArray
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return this._wrapWithFilter(style)
|
|
171
|
+
}
|
|
172
|
+
}
|