gridviz 3.0.41 → 3.1.0

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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  // internal
7
7
  import { Dataset } from '../core/Dataset.js'
8
+ import { isVisibleAt } from '../core/Drawable.js'
8
9
  //import { monitor, monitorDuration } from '../utils/Utils.js'
9
10
 
10
11
  // external
@@ -96,7 +97,7 @@ export class TiledGrid extends Dataset {
96
97
  if (!this.map || !this.map.layers) return false
97
98
  const z = this.map.getZoom()
98
99
  for (const lay of this.map.layers) {
99
- if (lay.visible && !lay.visible(z)) continue
100
+ if (!isVisibleAt(lay, z)) continue
100
101
  if (!lay.getDataset) continue
101
102
  if (lay.getDataset(z) == this) return true
102
103
  }
@@ -152,8 +153,6 @@ export class TiledGrid extends Dataset {
152
153
  const yMin = Math.max(tb.yMin, gbYMin)
153
154
  const yMax = Math.min(tb.yMax, gbYMax)
154
155
 
155
- const totalTiles = (xMax - xMin + 1) * (yMax - yMin + 1)
156
- let processedTiles = 0
157
156
  const tilePromises = []
158
157
 
159
158
  // Iterate over tiles within bounds
@@ -161,11 +160,13 @@ export class TiledGrid extends Dataset {
161
160
  for (let yT = yMin; yT <= yMax; yT++) {
162
161
  if (!this.cache[xT]) this.cache[xT] = {}
163
162
 
164
- // Skip already loaded tiles or retry failed ones
165
- if (this.cache[xT][yT] && this.cache[xT][yT] !== 'failed') {
163
+ // Skip the tiles loaded, under way, or that failed recently
164
+ const kept = this.cache[xT][yT]
165
+ const retry =
166
+ kept && kept.failed && !kept.missing && now() - kept.failed >= TiledGrid.retryFailedAfterMs
167
+ if (kept && !retry) {
166
168
  // in the view: not to be dropped
167
- if (typeof this.cache[xT][yT] === 'object') this.cache[xT][yT].lastUsed = now()
168
- ++processedTiles
169
+ if (kept.cells) kept.lastUsed = now()
169
170
  continue
170
171
  }
171
172
 
@@ -173,19 +174,40 @@ export class TiledGrid extends Dataset {
173
174
  this.cache[xT][yT] = 'loading'
174
175
 
175
176
  tilePromises.push(
176
- this.loadTile(xT, yT, signal)
177
- .then((tile) => {
177
+ this.loadTile(xT, yT, signal).then(
178
+ (tile) => {
178
179
  this.cache[xT][yT] = tile
179
180
  this._keep(tile)
180
181
 
181
182
  // Check if this is the last tile
182
- const isLastTile = ++processedTiles === totalTiles
183
+ // (tiles under way for another request count: the one that ends last is the one that draws)
184
+ const isLastTile = !this._anyLoading(xMin, xMax, yMin, yMax)
183
185
  this.checkAndRedraw(tile, isLastTile)
184
- })
185
- .catch(() => {
186
- this.cache[xT][yT] = 'failed'
187
- ++processedTiles
188
- })
186
+ },
187
+ (error) => {
188
+ if (error && error.name === 'AbortError') {
189
+ // cancelled, not failed: the next request loads it
190
+ delete this.cache[xT][yT]
191
+ return
192
+ }
193
+ // Most of the time a tile that fails is one the server doesn't have (the tiles of a grid are
194
+ // often sparse). It is remembered as failed, and not asked for again at every redraw.
195
+ // A 404 is final ('missing'). Any other failure is tried again after retryFailedAfterMs: a
196
+ // browser doesn't give the status of a response without CORS headers, so a 404 from such a
197
+ // server can't be told from a network error.
198
+ this.cache[xT][yT] = {
199
+ failed: now(),
200
+ missing: /^(404|410)\b/.test(error && error.message),
201
+ }
202
+ // it changes nothing on the map, except when it was waited for to draw
203
+ if (
204
+ this.onlyDrawWhenAllTilesReady &&
205
+ !this._anyLoading(xMin, xMax, yMin, yMax) &&
206
+ this.isShown()
207
+ )
208
+ this.map.redraw()
209
+ }
210
+ )
189
211
  )
190
212
  }
191
213
  }
@@ -194,6 +216,25 @@ export class TiledGrid extends Dataset {
194
216
  return this
195
217
  }
196
218
 
219
+ /**
220
+ * Tells if some tiles of a tiling envelope are still being loaded, by this request or any other one.
221
+ *
222
+ * @param {number} xMin
223
+ * @param {number} xMax
224
+ * @param {number} yMin
225
+ * @param {number} yMax
226
+ * @returns {boolean}
227
+ * @private
228
+ */
229
+ _anyLoading(xMin, xMax, yMin, yMax) {
230
+ for (let xT = xMin; xT <= xMax; xT++) {
231
+ const column = this.cache[xT]
232
+ if (!column) continue
233
+ for (let yT = yMin; yT <= yMax; yT++) if (column[yT] === 'loading') return true
234
+ }
235
+ return false
236
+ }
237
+
197
238
  /**
198
239
  * Load a tile.
199
240
  *
@@ -234,19 +275,20 @@ export class TiledGrid extends Dataset {
234
275
  // 1. the dataset belongs to a layer which is visible at the current zoom level
235
276
  if (!this.isShown()) return
236
277
 
278
+ // Waiting for all the tiles: drawn when the last one arrives, wherever it is (the tiles asked for are those of the
279
+ // view and of a margin around it, so the last one to arrive may be outside the view)
280
+ if (this.onlyDrawWhenAllTilesReady) {
281
+ if (isLastTile) this.map.redraw()
282
+ return
283
+ }
284
+
237
285
  // Check if tile intersects the current view
238
286
  const env = this.map.updateExtentGeo()
239
287
  const { xMin, xMax, yMin, yMax } = tile.extGeo
240
288
  if (env.xMax <= xMin || env.xMin >= xMax || env.yMax <= yMin || env.yMin >= yMax) return
241
289
 
242
290
  // Trigger redraw
243
- if (this.onlyDrawWhenAllTilesReady) {
244
- if (isLastTile) {
245
- this.map.redraw()
246
- }
247
- } else {
248
- this.map.redraw()
249
- }
291
+ this.map.redraw()
250
292
  }
251
293
 
252
294
  /**
@@ -256,7 +298,7 @@ export class TiledGrid extends Dataset {
256
298
  * @private
257
299
  */
258
300
  _keep(tile) {
259
- // a failed tile is the string 'failed', not a tile
301
+ // a failed tile is not a tile
260
302
  if (!tile || typeof tile !== 'object' || !tile.cells) return
261
303
  tile.lastUsed = now()
262
304
  tile.owner = this
@@ -316,7 +358,7 @@ export class TiledGrid extends Dataset {
316
358
  //get tile
317
359
  /** @type {object} */
318
360
  const tile = this.cache[xT][yT]
319
- if (!tile || typeof tile === 'string') continue
361
+ if (!tile || !tile.cells) continue
320
362
  tile.lastUsed = now()
321
363
 
322
364
  //get cells
@@ -342,6 +384,14 @@ export class TiledGrid extends Dataset {
342
384
  */
343
385
  TiledGrid.maxCachedCells = 1500000
344
386
 
387
+ /**
388
+ * How long, in ms, a tile that failed to load is remembered, before it is requested again. It doesn't apply to a tile the
389
+ * server answered 404 or 410 for: that one isn't requested again. It can be changed at any time.
390
+ *
391
+ * @type {number}
392
+ */
393
+ TiledGrid.retryFailedAfterMs = 30000
394
+
345
395
  function getGridTile(cells, xT, yT, gridInfo) {
346
396
  const tile = {}
347
397
 
@@ -1,194 +1,195 @@
1
- //@ts-check
2
- 'use strict'
3
-
4
- import { Layer } from '../core/Layer.js'
5
-
6
- /**
7
- * A layer, which specifies a dataset to be shown with specified styles.
8
- *
9
- * @module layer
10
- * @author Joseph Davies, Julien Gaffuri
11
- */
12
- export class GridLayer extends Layer {
13
- /**
14
- * @param {import("../core/Dataset").Dataset|import("../core/MultiResolutionDataset").MultiResolutionDataset} dataset The dataset to show.
15
- * @param {Array.<import("../core/Style").Style>} styles The styles, ordered in drawing order.
16
- * @param {{visible?:function(number):boolean,alpha?:function(number):number,blendOperation?:function(number):GlobalCompositeOperation,minPixelsPerCell?:number,cellInfoHTML?:'none'|function(import("../core/Dataset").Cell):string}} opts
17
- */
18
- constructor(dataset, styles, opts = {}) {
19
- super(opts)
20
- opts = opts || {}
21
-
22
- /** @type {import("../core/Dataset").Dataset|import("../core/MultiResolutionDataset").MultiResolutionDataset} */
23
- this.dataset = dataset
24
-
25
- /** @type {Array.<import("../core/Style").Style>} */
26
- this.styles = styles
27
-
28
- /**
29
- * This parameter is used when the dataset is a MultiResolutionDataset.
30
- * It defines the minimum number of pixels a grid cell should have to select the dataset to display based on its resolution.
31
- * A low value, means that the map will be more detailled (smaller cells).
32
- * A high value, means that the map will be less detailled (larger cells).
33
- * This value should be higher than 1, otherwise it means a grid cell is smaller than the screen resolution.
34
- * For more complex cell representations that require some more map space, this value should be higher.
35
- * @type {number} */
36
- this.minPixelsPerCell = opts.minPixelsPerCell || 3
37
-
38
- /**
39
- * The function returning cell information as HTML.
40
- * This is typically used for tooltip information.
41
- * Set to 'none' to disable cell info.
42
- * @type {'none'|function(import("../core/Dataset").Cell, number):string} */
43
- this.cellInfoHTML = opts.cellInfoHTML || GridLayer.defaultCellInfoHTML
44
- }
45
-
46
- /** */
47
- draw(geoCanvas, legend) {
48
- //get zoom level
49
- const z = geoCanvas.view.z
50
- const ctx = geoCanvas.offscreenCtx
51
-
52
- //get layer dataset component
53
- /** @type {import('../core/Dataset.js').Dataset|undefined} */
54
- const dsc = this.getDataset(z)
55
- if (!dsc) return
56
-
57
- //launch data download, if necessary
58
- dsc.getData(geoCanvas.extGeo)
59
-
60
- //update dataset view cache
61
- dsc.updateViewCache(geoCanvas.extGeo)
62
-
63
-
64
- //set layer alpha and blend mode
65
- if (this.alpha || this.blendOperation) {
66
- ctx.save()
67
- if (this.alpha) ctx.globalAlpha = this.alpha(z)
68
- if (this.blendOperation) ctx.globalCompositeOperation = this.blendOperation(z)
69
- }
70
-
71
- // tell the legends where they are drawn, so that they can fit it (width:'fill')
72
- if (legend) for (const s of this.styles) GridLayer.setLegendContainer(legend.node(), s.legends, s['styles'])
73
-
74
- //draw cells, style by style
75
- for (const s of this.styles) {
76
- //check if style is visible
77
- if (s.visible && !s.visible(z)) continue
78
-
79
- // set style alpha and blend mode
80
- //TODO: multiply style alpha by layer alpha ?
81
- if (s.alpha || s.blendOperation) {
82
- ctx.save()
83
- if (s.alpha) ctx.globalAlpha = s.alpha(z)
84
- if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
85
- }
86
-
87
- //set affin transform to draw with geographical coordinates
88
- geoCanvas.setCanvasTransform()
89
-
90
- //draw with style
91
- s.draw(dsc.getViewCache(), geoCanvas, dsc.getResolution())
92
-
93
- //draw style filter
94
- if (s.filterColor) s.drawFilter(geoCanvas)
95
-
96
- //restore default alpha and blend operation - before style change
97
- if (s.alpha || s.blendOperation) ctx.restore()
98
- }
99
-
100
- //restore default alpha and blend operation - before layer change
101
- if (this.alpha || this.blendOperation) ctx.restore()
102
-
103
- //add legend element
104
- if (legend) {
105
- for (const s of this.styles) {
106
- //check if style is visible
107
- if (s.visible && !s.visible(z)) continue
108
- GridLayer.addLegends(legend, s.legends, s, geoCanvas)
109
-
110
- //case for styles of styles, like kernel smoothing
111
- //TODO do better
112
- if (s['styles']) {
113
- for (const s2 of s['styles']) {
114
- if (s2.visible && !s2.visible(z)) continue
115
- GridLayer.addLegends(legend, s2.legends, s2, geoCanvas)
116
- }
117
- }
118
- }
119
- }
120
- }
121
-
122
- /** @private */
123
- static setLegendContainer(container, legends, subStyles) {
124
- // legends can be nested in arrays, like in Style.updateLegendsRecursive
125
- const set = (lg) => {
126
- if (Array.isArray(lg)) lg.forEach(set)
127
- else if (lg) lg.container = container
128
- }
129
- set(legends)
130
- for (const s2 of subStyles || []) GridLayer.setLegendContainer(container, s2.legends, undefined)
131
- }
132
-
133
- /** @private */
134
- static addLegends(legendComp, lg, style, geoCanvas) {
135
- if (Array.isArray(lg)) for (const lg_ of lg) this.addLegends(legendComp, lg_, style, geoCanvas)
136
- else {
137
- // so that hovering a swatch can highlight the cells of the style on the map
138
- lg.style_ = style
139
- lg.redraw = () => (geoCanvas.redraw ? geoCanvas.redraw() : undefined)
140
- // The legends go in the order they are drawn, after the one placed before them. A hovered legend is
141
- // still in the container (see Map.redrawNow) and is left where it is: moving it would lose its mouseleave,
142
- // and appending the others after it would put it above the legends that come before it.
143
- const container = legendComp.node()
144
- const el = lg.div.node()
145
- const previous = container._gridvizLegendCursor
146
- if (previous) {
147
- if (el.previousSibling !== previous) previous.after(el)
148
- } else if (container.firstChild !== el) container.prepend(el)
149
- container._gridvizLegendCursor = el
150
- }
151
- }
152
-
153
- /**
154
- * Return the relevant dataset component for a specified zoom.
155
- *
156
- * @param {number} z
157
- * @returns {import("../core/Dataset").Dataset|undefined}
158
- * */
159
- getDataset(z) {
160
- return this.dataset.getDataset(z, this.minPixelsPerCell)
161
- }
162
-
163
- /**
164
- * Set/get style stack.
165
- *
166
- * @param {undefined|import("../core/Style").Style|Array.<import("../core/Style").Style>} styles
167
- * @returns { this | Array.<import("../core/Style").Style> }
168
- */
169
- styles_(styles) {
170
- if (arguments.length === 0) return this.styles
171
- if (arguments.length === 1)
172
- if (Array.isArray(styles)) this.styles = styles
173
- else this.styles = [styles]
174
- else this.styles = arguments
175
- return this
176
- }
177
-
178
- /**
179
- * The default function returning cell information as HTML.
180
- * This is typically used for tooltip information.
181
- *
182
- * @param {import("../core/Dataset").Cell} cell
183
- * @returns {string}
184
- */
185
- static defaultCellInfoHTML(cell) {
186
- const buf = []
187
- for (const key of Object.keys(cell)) {
188
- if (key === 'x') continue
189
- if (key === 'y') continue
190
- buf.push('<b>', key, '</b>', ' : ', cell[key], '<br>')
191
- }
192
- return buf.join('')
193
- }
194
- }
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { Layer } from '../core/Layer.js'
5
+ import { isVisibleAt } from '../core/Drawable.js'
6
+
7
+ /**
8
+ * A layer, which specifies a dataset to be shown with specified styles.
9
+ *
10
+ * @module layer
11
+ * @author Joseph Davies, Julien Gaffuri
12
+ */
13
+ export class GridLayer extends Layer {
14
+ /**
15
+ * @param {import("../core/Dataset").Dataset|import("../core/MultiResolutionDataset").MultiResolutionDataset} dataset The dataset to show.
16
+ * @param {Array.<import("../core/Style").Style>} styles The styles, ordered in drawing order.
17
+ * @param {{visible?:function(number):boolean,alpha?:function(number):number,blendOperation?:function(number):GlobalCompositeOperation,minPixelsPerCell?:number,cellInfoHTML?:'none'|function(import("../core/Dataset").Cell):string}} opts
18
+ */
19
+ constructor(dataset, styles, opts = {}) {
20
+ super(opts)
21
+ opts = opts || {}
22
+
23
+ /** @type {import("../core/Dataset").Dataset|import("../core/MultiResolutionDataset").MultiResolutionDataset} */
24
+ this.dataset = dataset
25
+
26
+ /** @type {Array.<import("../core/Style").Style>} */
27
+ this.styles = styles
28
+
29
+ /**
30
+ * This parameter is used when the dataset is a MultiResolutionDataset.
31
+ * It defines the minimum number of pixels a grid cell should have to select the dataset to display based on its resolution.
32
+ * A low value, means that the map will be more detailled (smaller cells).
33
+ * A high value, means that the map will be less detailled (larger cells).
34
+ * This value should be higher than 1, otherwise it means a grid cell is smaller than the screen resolution.
35
+ * For more complex cell representations that require some more map space, this value should be higher.
36
+ * @type {number} */
37
+ this.minPixelsPerCell = opts.minPixelsPerCell || 3
38
+
39
+ /**
40
+ * The function returning cell information as HTML.
41
+ * This is typically used for tooltip information.
42
+ * Set to 'none' to disable cell info.
43
+ * @type {'none'|function(import("../core/Dataset").Cell, number):string} */
44
+ this.cellInfoHTML = opts.cellInfoHTML || GridLayer.defaultCellInfoHTML
45
+ }
46
+
47
+ /** */
48
+ draw(geoCanvas, legend) {
49
+ //get zoom level
50
+ const z = geoCanvas.view.z
51
+ const ctx = geoCanvas.offscreenCtx
52
+
53
+ //get layer dataset component
54
+ /** @type {import('../core/Dataset.js').Dataset|undefined} */
55
+ const dsc = this.getDataset(z)
56
+ if (!dsc) return
57
+
58
+ //launch data download, if necessary
59
+ dsc.getData(geoCanvas.extGeo)
60
+
61
+ //update dataset view cache
62
+ dsc.updateViewCache(geoCanvas.extGeo)
63
+
64
+
65
+ //set layer alpha and blend mode
66
+ if (this.alpha || this.blendOperation) {
67
+ ctx.save()
68
+ if (this.alpha) ctx.globalAlpha = this.alpha(z)
69
+ if (this.blendOperation) ctx.globalCompositeOperation = this.blendOperation(z)
70
+ }
71
+
72
+ // tell the legends where they are drawn, so that they can fit it (width:'fill')
73
+ if (legend) for (const s of this.styles) GridLayer.setLegendContainer(legend.node(), s.legends, s['styles'])
74
+
75
+ //draw cells, style by style
76
+ for (const s of this.styles) {
77
+ //check if style is visible
78
+ if (!isVisibleAt(s, z)) continue
79
+
80
+ // set style alpha and blend mode
81
+ //TODO: multiply style alpha by layer alpha ?
82
+ if (s.alpha || s.blendOperation) {
83
+ ctx.save()
84
+ if (s.alpha) ctx.globalAlpha = s.alpha(z)
85
+ if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
86
+ }
87
+
88
+ //set affin transform to draw with geographical coordinates
89
+ geoCanvas.setCanvasTransform()
90
+
91
+ //draw with style
92
+ s.draw(dsc.getViewCache(), geoCanvas, dsc.getResolution())
93
+
94
+ //draw style filter
95
+ if (s.filterColor) s.drawFilter(geoCanvas)
96
+
97
+ //restore default alpha and blend operation - before style change
98
+ if (s.alpha || s.blendOperation) ctx.restore()
99
+ }
100
+
101
+ //restore default alpha and blend operation - before layer change
102
+ if (this.alpha || this.blendOperation) ctx.restore()
103
+
104
+ //add legend element
105
+ if (legend) {
106
+ for (const s of this.styles) {
107
+ //check if style is visible
108
+ if (!isVisibleAt(s, z)) continue
109
+ GridLayer.addLegends(legend, s.legends, s, geoCanvas)
110
+
111
+ //case for styles of styles, like kernel smoothing
112
+ //TODO do better
113
+ if (s['styles']) {
114
+ for (const s2 of s['styles']) {
115
+ if (!isVisibleAt(s2, z)) continue
116
+ GridLayer.addLegends(legend, s2.legends, s2, geoCanvas)
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ /** @private */
124
+ static setLegendContainer(container, legends, subStyles) {
125
+ // legends can be nested in arrays, like in Style.updateLegendsRecursive
126
+ const set = (lg) => {
127
+ if (Array.isArray(lg)) lg.forEach(set)
128
+ else if (lg) lg.container = container
129
+ }
130
+ set(legends)
131
+ for (const s2 of subStyles || []) GridLayer.setLegendContainer(container, s2.legends, undefined)
132
+ }
133
+
134
+ /** @private */
135
+ static addLegends(legendComp, lg, style, geoCanvas) {
136
+ if (Array.isArray(lg)) for (const lg_ of lg) this.addLegends(legendComp, lg_, style, geoCanvas)
137
+ else {
138
+ // so that hovering a swatch can highlight the cells of the style on the map
139
+ lg.style_ = style
140
+ lg.redraw = () => (geoCanvas.redraw ? geoCanvas.redraw() : undefined)
141
+ // The legends go in the order they are drawn, after the one placed before them. A hovered legend is
142
+ // still in the container (see Map.redrawNow) and is left where it is: moving it would lose its mouseleave,
143
+ // and appending the others after it would put it above the legends that come before it.
144
+ const container = legendComp.node()
145
+ const el = lg.div.node()
146
+ const previous = container._gridvizLegendCursor
147
+ if (previous) {
148
+ if (el.previousSibling !== previous) previous.after(el)
149
+ } else if (container.firstChild !== el) container.prepend(el)
150
+ container._gridvizLegendCursor = el
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Return the relevant dataset component for a specified zoom.
156
+ *
157
+ * @param {number} z
158
+ * @returns {import("../core/Dataset").Dataset|undefined}
159
+ * */
160
+ getDataset(z) {
161
+ return this.dataset.getDataset(z, this.minPixelsPerCell)
162
+ }
163
+
164
+ /**
165
+ * Set/get style stack.
166
+ *
167
+ * @param {undefined|import("../core/Style").Style|Array.<import("../core/Style").Style>} styles
168
+ * @returns { this | Array.<import("../core/Style").Style> }
169
+ */
170
+ styles_(styles) {
171
+ if (arguments.length === 0) return this.styles
172
+ if (arguments.length === 1)
173
+ if (Array.isArray(styles)) this.styles = styles
174
+ else this.styles = [styles]
175
+ else this.styles = arguments
176
+ return this
177
+ }
178
+
179
+ /**
180
+ * The default function returning cell information as HTML.
181
+ * This is typically used for tooltip information.
182
+ *
183
+ * @param {import("../core/Dataset").Cell} cell
184
+ * @returns {string}
185
+ */
186
+ static defaultCellInfoHTML(cell) {
187
+ const buf = []
188
+ for (const key of Object.keys(cell)) {
189
+ if (key === 'x') continue
190
+ if (key === 'y') continue
191
+ buf.push('<b>', key, '</b>', ' : ', cell[key], '<br>')
192
+ }
193
+ return buf.join('')
194
+ }
195
+ }