gridviz 3.0.33 → 3.0.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gridviz",
3
- "version": "3.0.33",
3
+ "version": "3.0.35",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
package/src/core/Map.js CHANGED
@@ -90,6 +90,12 @@ export class Map {
90
90
  * @type {Tooltip} */
91
91
  this.tooltip = new Tooltip(opts.tooltip)
92
92
 
93
+ /**
94
+ * Set to true so that the tooltip appears for the first layer starting from top where a cell is found.
95
+ * Otherwise, it shows a tooltip only for the first layer from the top with a cellInfoHTML property, even if there is no cell at the mouse position for this layer.
96
+ * @type {boolean} */
97
+ this.digForTooltip = opts.digForTooltip || false
98
+
93
99
  // add event listeners to container
94
100
  this.mouseOverHandler = (e) => this.focusCell(e)
95
101
  this.mouseMoveHandler = (e) => this.focusCell(e)
@@ -330,15 +336,20 @@ export class Map {
330
336
  * @protected
331
337
  */
332
338
  getCellFocusInfo(posGeo) {
333
- //go through the layers, starting from top
339
+
340
+ // go through the layers, starting from top one
334
341
  const z = this.geoCanvas.view.z
335
342
  for (let i = this.layers.length - 1; i >= 0; i--) {
343
+
336
344
  /** @type {import("./Layer.js").Layer} */
337
345
  const layer = this.layers[i]
346
+
338
347
  if (layer.visible && !layer.visible(z)) continue
339
- if (layer.cellInfoHTML === 'none') continue // this is necessary in order to not show tooltips for layers 'on top' (e.g. population circles on top of squares)
348
+ if (layer.cellInfoHTML === 'none') continue
340
349
  if (!layer.cellInfoHTML) continue
341
350
  if (!layer.getDataset) continue
351
+
352
+ // get dataset
342
353
  const dsc = layer.getDataset(z)
343
354
  if (!dsc) continue
344
355
 
@@ -346,7 +357,12 @@ export class Map {
346
357
  /** @type {import('./Dataset.js').Cell|undefined} */
347
358
  const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
348
359
 
349
- if (!cell) return undefined
360
+ // no cell found at theis position for this layer
361
+ if (!cell)
362
+ // dig to the next layer below
363
+ if (this.digForTooltip) continue
364
+ // no tooltip to show
365
+ else return undefined
350
366
 
351
367
  //rare case for a dataset with mixed resolutions
352
368
  if (dsc.mixedResolution) {
@@ -30,6 +30,7 @@ export class GeoJSONLayer extends Layer {
30
30
  this.preprocess = opts.preprocess
31
31
 
32
32
  //for points
33
+
33
34
  /**
34
35
  * @private
35
36
  * @type {function(object,number):string} */
@@ -39,36 +40,32 @@ export class GeoJSONLayer extends Layer {
39
40
  * @private
40
41
  * @type {function(object,number):number} */
41
42
  this.size = opts.size || ((f, z) => 10)
42
- /**
43
- * @private
44
- * @type {function(object,number):string} */
45
- this.strokeStyle = opts.strokeStyle || ((f, z) => 'red')
46
- /**
47
- * @private
48
- * @type {function(object,number):string} */
49
- this.fillStyle = opts.fillStyle || ((f, z) => 'black')
50
- /**
51
- * In pixel
52
- * @private
53
- * @type {function(object,number):number} */
54
- this.lineWidth = opts.lineWidth || ((f, z) => 2)
55
43
 
56
- //for lines
44
+ // for lines and polygon outlines
57
45
 
58
46
  /**
59
47
  * @private
60
48
  * @type {function(object,number):string} */
61
- this.color = opts.color || ((f, z) => 'gray')
49
+ this.strokeStyle = opts.strokeStyle || opts.color || ((f, z) => 'gray')
62
50
  /**
63
51
  * In pixel
64
52
  * @private
65
53
  * @type {function(object,number):number} */
66
- this.width = opts.width || ((f, z) => 2)
54
+ this.lineWidth = opts.lineWidth || opts.width || ((f, z) => 2)
67
55
  /**
68
56
  * @private
69
57
  * @type {function(object,number):Array.<number>|undefined} */
70
58
  this.lineDash = opts.lineDash || ((f, z) => undefined)
71
59
 
60
+
61
+ // for polygon filling and point symbol filling
62
+ /**
63
+ * @private
64
+ * @type {function(object,number):string} */
65
+ this.fillStyle = opts.fillStyle || ((f, z) => 'black')
66
+
67
+
68
+
72
69
  /**
73
70
  * @private
74
71
  * @type {Array.<object> | undefined} */
@@ -96,81 +93,136 @@ export class GeoJSONLayer extends Layer {
96
93
  const z = geoCanvas.view.z
97
94
  const ctx = geoCanvas.offscreenCtx
98
95
 
99
- for (const f of this.fs) {
100
- const gt = f.geometry.type
101
-
102
- if (gt == 'Point' || gt == 'MultiPoint') {
103
-
104
- //get style parameters for the point feature
105
- const shape = this.shape(f, z)
106
- if (!shape || shape == 'none') continue
107
- const size = this.size(f, z) * z
108
- if (!size) continue
109
- const strokeStyle = this.strokeStyle(f, z)
110
- const fillStyle = this.fillStyle(f, z)
111
- const lineWidth = this.lineWidth(f, z) * z
112
-
113
- //set canvas drawing parameters
114
- if (strokeStyle) ctx.strokeStyle = strokeStyle
115
- if (fillStyle) ctx.fillStyle = fillStyle
116
- if (lineWidth) ctx.lineWidth = lineWidth
117
-
118
- let cs = f.geometry.coordinates
119
- if (gt == 'Point') cs = [cs]
120
-
121
- if (shape == 'circle') {
122
- //draw circle - fill and stroke
123
- for (const c of cs) {
124
- ctx.beginPath()
125
- ctx.arc(c[0], c[1], size / 2, 0, 2 * Math.PI, false)
126
- if (fillStyle) ctx.fill()
127
- if (strokeStyle && lineWidth) ctx.stroke()
128
- }
129
- } else if (shape == 'square') {
130
- //draw square - fill and stroke
131
- for (const c of cs) {
132
- ctx.beginPath()
133
- ctx.rect(c[0] - size / 2, c[1] - size / 2, size, size)
134
- if (fillStyle) ctx.fill()
135
- if (strokeStyle && lineWidth) ctx.stroke()
136
- }
137
- } else {
138
- console.error('Unexpected shape for point geojson: ' + shape)
96
+ // draw each feature
97
+ for (const f of this.fs)
98
+ this.drawGeometry(f, f.geometry, f.geometry.type, z, ctx)
99
+
100
+ }
101
+
102
+ /**
103
+ * @private
104
+ * Draw a geometry on the canvas, according to its type and the layer style.
105
+ * @param {object} f The feature to draw
106
+ * @param {object} geom The geometry of the feature to draw
107
+ * @param {string} gt The geometry type (e.g. 'Point', 'LineString', etc.)
108
+ * @param {number} z The current zoom level of the map
109
+ * @param {CanvasRenderingContext2D} ctx The canvas context to draw on
110
+ * @returns {void}
111
+ */
112
+ drawGeometry(f, geom, gt, z, ctx) {
113
+ if (gt == 'Point' || gt == 'MultiPoint') {
114
+
115
+ //get style parameters for the point feature
116
+ const shape = this.shape(f, z)
117
+ if (!shape || shape == 'none') return
118
+ const size = this.size(f, z) * z
119
+ if (!size) return
120
+
121
+ const strokeStyle = this.strokeStyle(f, z)
122
+ const fillStyle = this.fillStyle(f, z)
123
+ const lineWidth = this.lineWidth(f, z) * z
124
+
125
+ //set canvas drawing parameters
126
+ ctx.save()
127
+ if (strokeStyle) ctx.strokeStyle = strokeStyle
128
+ if (fillStyle) ctx.fillStyle = fillStyle
129
+ if (lineWidth) ctx.lineWidth = lineWidth
130
+
131
+ let cs = geom.coordinates
132
+ if (gt == 'Point') cs = [cs]
133
+
134
+ if (shape == 'circle') {
135
+ //draw circle - fill and stroke
136
+ for (const c of cs) {
137
+ ctx.beginPath()
138
+ ctx.arc(c[0], c[1], size / 2, 0, 2 * Math.PI, false)
139
+ if (fillStyle) ctx.fill()
140
+ if (strokeStyle && lineWidth) ctx.stroke()
141
+ }
142
+ } else if (shape == 'square') {
143
+ //draw square - fill and stroke
144
+ for (const c of cs) {
145
+ ctx.beginPath()
146
+ ctx.rect(c[0] - size / 2, c[1] - size / 2, size, size)
147
+ if (fillStyle) ctx.fill()
148
+ if (strokeStyle && lineWidth) ctx.stroke()
139
149
  }
140
- } else if (gt == 'LineString' || gt == 'MultiLineString') {
150
+ } else {
151
+ console.error('Unexpected shape for point GeoJSON: ' + shape)
152
+ }
153
+ ctx.restore()
154
+
155
+ } else if (gt == 'LineString' || gt == 'MultiLineString') {
141
156
 
142
- //set color
143
- const col = this.color(f, z)
144
- if (!col || col == 'none') continue
145
- ctx.strokeStyle = col
157
+ const col = this.strokeStyle(f, z)
158
+ if (!col || col === 'none') return
146
159
 
147
- //set linewidth
148
- const wP = this.width(f, z)
149
- if (!wP || wP < 0) continue
150
- ctx.lineWidth = wP * z
160
+ const wP = this.lineWidth(f, z)
161
+ if (!wP || wP < 0) return
151
162
 
152
- //set line dash
153
- const ldP = this.lineDash(f, z)
154
- if (ldP) ctx.setLineDash(ldP)
163
+ ctx.save()
164
+ ctx.strokeStyle = col
165
+ ctx.lineWidth = wP * z
166
+ ctx.setLineDash(this.lineDash(f, z) || [])
167
+
168
+ //
169
+ const css = gt === 'LineString' ? [geom.coordinates] : geom.coordinates
170
+
171
+ for (const cs of css) {
172
+ if (cs.length < 2) continue
173
+ ctx.beginPath()
174
+ ctx.moveTo(cs[0][0], cs[0][1])
175
+ for (let i = 1; i < cs.length; i++) ctx.lineTo(cs[i][0], cs[i][1])
176
+ ctx.stroke()
177
+ }
155
178
 
156
- let css = f.geometry.coordinates
157
- if (gt == 'LineString') css = [css]
179
+ ctx.restore()
158
180
 
159
- //draw lines
160
- for (const cs of css) {
181
+ } else if (gt == 'Polygon' || gt == 'MultiPolygon') {
182
+ ctx.save()
183
+
184
+ // A Polygon is [ outerRing, ...holes ]. A MultiPolygon is [ [ outerRing, ...holes ], ... ]
185
+ const polygons = gt === 'Polygon' ? [geom.coordinates] : geom.coordinates
186
+
187
+ ctx.beginPath()
188
+
189
+ for (const rings of polygons) {
190
+ for (const cs of rings) {
161
191
  if (cs.length < 2) continue
162
- ctx.beginPath()
163
192
  ctx.moveTo(cs[0][0], cs[0][1])
164
193
  for (let i = 1; i < cs.length; i++) ctx.lineTo(cs[i][0], cs[i][1])
194
+ ctx.closePath()
195
+ }
196
+ }
197
+
198
+ // Fill
199
+ const fillStyle = this.fillStyle(f, z)
200
+ if (fillStyle && fillStyle !== 'none') {
201
+ ctx.fillStyle = fillStyle
202
+ ctx.fill('evenodd') // even-odd punches holes correctly regardless of ring winding order
203
+ }
204
+
205
+ // Stroke (optional, only if style provides a width)
206
+ const wP = this.lineWidth(f, z)
207
+ if (wP && wP > 0) {
208
+ const strokeStyle = this.strokeStyle?.(f, z) ?? fillStyle
209
+ if (strokeStyle && strokeStyle !== 'none') {
210
+ ctx.strokeStyle = strokeStyle
211
+ ctx.lineWidth = wP * z
212
+ ctx.setLineDash(this.lineDash?.(f, z) || [])
165
213
  ctx.stroke()
166
214
  }
167
- } else {
168
- console.log('Unsupported geometry type in GeoJSONLayer: ' + gt)
169
215
  }
170
- }
216
+ ctx.restore()
217
+
218
+ } else if (gt == 'GeometryCollection') {
219
+ //recursive call for each geometry in the collection
220
+ for (const g of geom.geometries)
221
+ this.drawGeometry(f, g, g.type, z, ctx)
171
222
 
172
- //...
173
- ctx.setLineDash([])
223
+ } else {
224
+ console.log('Unexpected geometry type in GeoJSONLayer: ' + gt)
225
+ }
174
226
  }
175
227
 
176
228
  /**
@@ -180,7 +232,7 @@ export class GeoJSONLayer extends Layer {
180
232
  */
181
233
  async load(callback) {
182
234
  if (!this.url) {
183
- console.log('Failed loading boundaries: No URL specified. ' + this.url)
235
+ console.log('Failed loading GeoJSON data: No URL specified. ' + this.url)
184
236
  this.loadingStatus = 'failed'
185
237
  this.labels = []
186
238
  return
@@ -216,7 +268,7 @@ export class GeoJSONLayer extends Layer {
216
268
  //redraw
217
269
  if (callback) callback()
218
270
  } catch (error) {
219
- console.log('Failed loading boundaries from ' + this.url)
271
+ console.log('Failed loading GeoJSON from ' + this.url)
220
272
  this.fs = []
221
273
  this.loadingStatus = 'failed'
222
274
  }