gridviz 3.0.19 → 3.0.21
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/README.md +128 -128
- package/dist/gridviz.js +7875 -7857
- package/dist/gridviz.min.js +1 -1
- package/package.json +74 -74
- package/src/button/ZoomButtons.js +101 -101
- package/src/core/Dataset.js +154 -154
- package/src/core/Drawable.js +78 -60
- package/src/core/GeoCanvas.js +430 -430
- package/src/core/Layer.js +23 -23
- package/src/core/Legend.js +108 -108
- package/src/core/Map.js +516 -516
- package/src/core/MultiResolutionDataset.js +93 -93
- package/src/core/Style.js +98 -98
- package/src/dataset/CSVGrid.js +112 -112
- package/src/dataset/GeoTIFF.js +122 -122
- package/src/dataset/JSGrid.js +55 -55
- package/src/dataset/TiledGrid.js +302 -302
- package/src/dataset/worker.js +18 -18
- package/src/index.js +77 -77
- package/src/layer/BackgroundLayer.js +162 -162
- package/src/layer/BackgroundLayerImage.js +81 -81
- package/src/layer/BackgroundLayerWMS.js +104 -104
- package/src/layer/GeoJSONLayer.js +214 -214
- package/src/layer/GridLayer.js +148 -148
- package/src/layer/LabelLayer.js +194 -194
- package/src/legend/ColorCategoryLegend.js +109 -109
- package/src/legend/ColorDiscreteLegend.js +100 -100
- package/src/legend/ColorLegend.js +115 -115
- package/src/legend/OrientationLegend.js +89 -89
- package/src/legend/SizeLegend.js +224 -224
- package/src/legend/TernaryLegend.js +302 -302
- package/src/style/CompositionStyle.js +291 -291
- package/src/style/DotDensityStyle.js +133 -133
- package/src/style/ImageStyle.js +100 -100
- package/src/style/IsoFenceStyle.js +217 -217
- package/src/style/JoyPlotStyle.js +127 -127
- package/src/style/LegoStyle.js +110 -110
- package/src/style/MosaicStyle.js +99 -99
- package/src/style/NinjaStarStyle.js +102 -102
- package/src/style/PillarStyle.js +207 -207
- package/src/style/SegmentStyle.js +105 -105
- package/src/style/ShapeColorSizeStyle.js +147 -147
- package/src/style/SideCategoryStyle.js +112 -112
- package/src/style/SideStyle.js +212 -212
- package/src/style/SquareColorCategoryWebGLStyle.js +107 -107
- package/src/style/SquareColorWebGLStyle.js +126 -126
- package/src/style/StrokeStyle.js +135 -135
- package/src/style/TanakaStyle.js +69 -69
- package/src/style/TextStyle.js +110 -110
- package/src/style/TimeSeriesStyle.js +222 -222
- package/src/utils/scale.js +218 -218
- package/src/utils/stretching.js +120 -120
- package/src/utils/ternary.js +208 -208
- package/src/utils/utils.js +58 -58
- package/src/utils/webGLUtils.js +72 -72
package/src/core/Map.js
CHANGED
|
@@ -1,516 +1,516 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
// internal imports
|
|
5
|
-
import { GeoCanvas } from './GeoCanvas.js'
|
|
6
|
-
import { Tooltip } from './Tooltip.js'
|
|
7
|
-
import { ZoomButtons } from '../button/ZoomButtons.js'
|
|
8
|
-
import { FullscreenButton } from '../button/FullscreenButton.js'
|
|
9
|
-
|
|
10
|
-
// external imports
|
|
11
|
-
import { select } from 'd3-selection'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A gridviz application.
|
|
15
|
-
*
|
|
16
|
-
* @module core
|
|
17
|
-
* @author Joseph Davies, Julien Gaffuri
|
|
18
|
-
*/
|
|
19
|
-
export class Map {
|
|
20
|
-
/**
|
|
21
|
-
* @param {HTMLDivElement} container
|
|
22
|
-
* @param {object} opts
|
|
23
|
-
*/
|
|
24
|
-
constructor(container, opts) {
|
|
25
|
-
opts = opts || {}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The layers.
|
|
29
|
-
* @type {Array.<import("./Layer.js").Layer>}
|
|
30
|
-
* */
|
|
31
|
-
this.layers = opts.layers || []
|
|
32
|
-
|
|
33
|
-
//get container element
|
|
34
|
-
this.container = container || document.getElementById('gridviz')
|
|
35
|
-
if (!this.container) {
|
|
36
|
-
console.error('Cannot find gridviz container element.')
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
//https://css-tricks.com/absolute-positioning-inside-relative-positioning/
|
|
41
|
-
this.container.style.position = 'relative' // container element must have relative positioning
|
|
42
|
-
|
|
43
|
-
//set dimensions
|
|
44
|
-
/** @type {number} */
|
|
45
|
-
this.w = opts.w || this.container.offsetWidth
|
|
46
|
-
/** @type {number} */
|
|
47
|
-
this.h = opts.h || this.container.offsetHeight
|
|
48
|
-
|
|
49
|
-
// Create the main canvas (for rendering to screen)
|
|
50
|
-
/** @type {HTMLCanvasElement} */
|
|
51
|
-
this._canvas = opts.canvas || this.initialiseCanvas()
|
|
52
|
-
|
|
53
|
-
/** Initialize GeoCanvas
|
|
54
|
-
* @type {GeoCanvas}
|
|
55
|
-
* @private */
|
|
56
|
-
this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
|
|
57
|
-
|
|
58
|
-
this.geoCanvas.redraw = () => {
|
|
59
|
-
this.redraw()
|
|
60
|
-
}
|
|
61
|
-
this.geoCanvas.cancelCurrentRequests = () => {
|
|
62
|
-
// when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
|
|
63
|
-
for (const layer of this.layers) {
|
|
64
|
-
//multires
|
|
65
|
-
if (layer.dataset?.datasets) {
|
|
66
|
-
for (const dataset of layer.dataset?.datasets) {
|
|
67
|
-
if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
//single res
|
|
71
|
-
if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// legend div
|
|
76
|
-
this.legend = opts.legendContainer
|
|
77
|
-
? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
|
|
78
|
-
: null
|
|
79
|
-
if (!this.legend) this.initialiseLegend()
|
|
80
|
-
|
|
81
|
-
//tooltip
|
|
82
|
-
|
|
83
|
-
// set App container as default parent element for tooltip
|
|
84
|
-
if (!opts.tooltip) opts.tooltip = {}
|
|
85
|
-
if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* @private
|
|
89
|
-
* @type {Tooltip} */
|
|
90
|
-
this.tooltip = new Tooltip(opts.tooltip)
|
|
91
|
-
|
|
92
|
-
// add event listeners to container
|
|
93
|
-
this.mouseOverHandler = (e) => this.focusCell(e)
|
|
94
|
-
this.mouseMoveHandler = (e) => this.focusCell(e)
|
|
95
|
-
this.mouseOutHandler = (e) => this.tooltip.hide()
|
|
96
|
-
this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
|
|
97
|
-
this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
|
|
98
|
-
this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
|
|
99
|
-
|
|
100
|
-
// listen for resize events on the App's container and handle them
|
|
101
|
-
this.defineResizeObserver()
|
|
102
|
-
|
|
103
|
-
// add extra logic to onZoomStartFun
|
|
104
|
-
this.geoCanvas.onZoomStartFun = (e) => {
|
|
105
|
-
if (opts.onZoomStartFun) opts.onZoomStartFun(e)
|
|
106
|
-
this.tooltip.hide()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
//for mouse over
|
|
110
|
-
/**
|
|
111
|
-
* @private
|
|
112
|
-
* @type {HTMLCanvasElement|null} */
|
|
113
|
-
this.canvasSave = null
|
|
114
|
-
|
|
115
|
-
this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
|
|
116
|
-
this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
|
|
117
|
-
|
|
118
|
-
// transparent background (e.g. leaflet) 'red painting' fix
|
|
119
|
-
this.transparentBackground = opts.transparentBackground
|
|
120
|
-
|
|
121
|
-
//set default globalCompositeOperation
|
|
122
|
-
this.defaultGlobalCompositeOperation =
|
|
123
|
-
opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @protected
|
|
128
|
-
* @returns {HTMLCanvasElement}
|
|
129
|
-
*/
|
|
130
|
-
initialiseCanvas() {
|
|
131
|
-
const canvas = document.createElement('canvas')
|
|
132
|
-
canvas.setAttribute('width', '' + this.w)
|
|
133
|
-
canvas.setAttribute('height', '' + this.h)
|
|
134
|
-
this.container.appendChild(canvas)
|
|
135
|
-
return canvas
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
initialiseLegend() {
|
|
139
|
-
this.legend = select(this.container)
|
|
140
|
-
.append('div') // Create a new container
|
|
141
|
-
.attr('id', 'gridviz-legend')
|
|
142
|
-
.style('position', 'absolute')
|
|
143
|
-
.style('width', 'auto')
|
|
144
|
-
.style('height', 'auto')
|
|
145
|
-
.style('background', '#FFFFFF')
|
|
146
|
-
//.style("padding", this.padding)
|
|
147
|
-
.style('border', '0px')
|
|
148
|
-
//.style('border-radius', '5px')
|
|
149
|
-
.style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
|
|
150
|
-
.style('font-family', 'Helvetica, Arial, sans-serif')
|
|
151
|
-
.style('bottom', '15px')
|
|
152
|
-
.style('right', '15px')
|
|
153
|
-
//hide
|
|
154
|
-
//.style("visibility", "hidden")
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Set/get layer stack.
|
|
159
|
-
*
|
|
160
|
-
* @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
|
|
161
|
-
* @returns { this | import("./Layer.js").Layer[] }
|
|
162
|
-
*/
|
|
163
|
-
layers_(layers) {
|
|
164
|
-
if (arguments.length === 0) return this.layers
|
|
165
|
-
if (arguments.length === 1)
|
|
166
|
-
if (Array.isArray(layers)) this.layers = layers
|
|
167
|
-
else this.layers = [layers]
|
|
168
|
-
else this.layers = arguments
|
|
169
|
-
return this
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** @returns {this} */
|
|
173
|
-
redraw() {
|
|
174
|
-
//remove legend elements
|
|
175
|
-
if (this.legend) this.legend.selectAll('*').remove()
|
|
176
|
-
|
|
177
|
-
//clear
|
|
178
|
-
this.geoCanvas.initCanvasTransform()
|
|
179
|
-
this.geoCanvas.clear(this.geoCanvas.backgroundColor)
|
|
180
|
-
|
|
181
|
-
const z = this.geoCanvas.view.z
|
|
182
|
-
this.updateExtentGeo()
|
|
183
|
-
|
|
184
|
-
//go through the layers
|
|
185
|
-
for (const layer of this.layers) {
|
|
186
|
-
//check if layer is visible
|
|
187
|
-
if (layer.visible && !layer.visible(z)) continue
|
|
188
|
-
|
|
189
|
-
//set layer alpha and blend mode
|
|
190
|
-
this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
|
|
191
|
-
if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
|
|
192
|
-
|
|
193
|
-
//set affin transform to draw with geographical coordinates
|
|
194
|
-
this.geoCanvas.setCanvasTransform()
|
|
195
|
-
|
|
196
|
-
//draw layer
|
|
197
|
-
layer.draw(this.geoCanvas, this.legend)
|
|
198
|
-
|
|
199
|
-
//draw layer filter
|
|
200
|
-
if (layer.filterColor) layer.drawFilter(this.geoCanvas)
|
|
201
|
-
|
|
202
|
-
//restore default alpha and blend operation
|
|
203
|
-
this.geoCanvas.offscreenCtx.globalAlpha = 1.0
|
|
204
|
-
this.geoCanvas.offscreenCtx.globalCompositeOperation = this.defaultGlobalCompositeOperation
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// one drawImage call: draw the offscreen canvas to the main canvas
|
|
208
|
-
this.geoCanvas.initCanvasTransform()
|
|
209
|
-
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
210
|
-
|
|
211
|
-
this.canvasSave = null
|
|
212
|
-
|
|
213
|
-
return this
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* @param {number} marginPx
|
|
218
|
-
* @returns {import('./GeoCanvas.js').Envelope}
|
|
219
|
-
* @public
|
|
220
|
-
*/
|
|
221
|
-
updateExtentGeo(marginPx = 20) {
|
|
222
|
-
return this.geoCanvas.updateExtentGeo(marginPx)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** @param {MouseEvent} e */
|
|
226
|
-
focusCell(e) {
|
|
227
|
-
//compute mouse geo position
|
|
228
|
-
const mousePositionGeo = {
|
|
229
|
-
x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
|
|
230
|
-
y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
|
|
231
|
-
}
|
|
232
|
-
/** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
|
|
233
|
-
const focus = this.getCellFocusInfo(mousePositionGeo)
|
|
234
|
-
|
|
235
|
-
// transparent background (e.g. leaflet) 'red painting' fix
|
|
236
|
-
if (this.transparentBackground) {
|
|
237
|
-
if (focus) {
|
|
238
|
-
this.tooltip.html(focus.html)
|
|
239
|
-
this.tooltip.setPosition(e)
|
|
240
|
-
this.tooltip.show()
|
|
241
|
-
} else {
|
|
242
|
-
this.tooltip.hide()
|
|
243
|
-
}
|
|
244
|
-
this.canvasSave = document.createElement('canvas')
|
|
245
|
-
this.canvasSave.setAttribute('width', '' + this.w)
|
|
246
|
-
this.canvasSave.setAttribute('height', '' + this.h)
|
|
247
|
-
this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.canvas, 0, 0)
|
|
248
|
-
this.geoCanvas.initCanvasTransform()
|
|
249
|
-
return
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (focus) {
|
|
253
|
-
this.tooltip.html(focus.html)
|
|
254
|
-
this.tooltip.setPosition(e)
|
|
255
|
-
this.tooltip.show()
|
|
256
|
-
|
|
257
|
-
//show cell position as a rectangle
|
|
258
|
-
if (!this.canvasSave) {
|
|
259
|
-
this.canvasSave = document.createElement('canvas')
|
|
260
|
-
this.canvasSave.setAttribute('width', '' + this.w)
|
|
261
|
-
this.canvasSave.setAttribute('height', '' + this.h)
|
|
262
|
-
this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
263
|
-
} else {
|
|
264
|
-
this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
//draw image saved + draw rectangle
|
|
268
|
-
const rectWPix = this.selectionRectangleWidthPix
|
|
269
|
-
? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
|
|
270
|
-
: 4
|
|
271
|
-
this.geoCanvas.initCanvasTransform()
|
|
272
|
-
const ctx = this.geoCanvas.offscreenCtx
|
|
273
|
-
ctx.strokeStyle = this.selectionRectangleColor
|
|
274
|
-
ctx.lineWidth = rectWPix
|
|
275
|
-
ctx.beginPath()
|
|
276
|
-
|
|
277
|
-
ctx.rect(
|
|
278
|
-
this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
|
|
279
|
-
this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
|
|
280
|
-
focus.resolution / this.geoCanvas.view.z + rectWPix,
|
|
281
|
-
-focus.resolution / this.geoCanvas.view.z - rectWPix
|
|
282
|
-
)
|
|
283
|
-
ctx.stroke()
|
|
284
|
-
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
285
|
-
} else {
|
|
286
|
-
this.tooltip.hide()
|
|
287
|
-
if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Return the cell HTML info at a given geo position.
|
|
293
|
-
* This is usefull for user interactions, to show this info where the user clicks for example.
|
|
294
|
-
*
|
|
295
|
-
* @param {{x:number,y:number}} posGeo
|
|
296
|
-
* @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
|
|
297
|
-
* @protected
|
|
298
|
-
*/
|
|
299
|
-
getCellFocusInfo(posGeo) {
|
|
300
|
-
//go through the layers, starting from top
|
|
301
|
-
const z = this.geoCanvas.view.z
|
|
302
|
-
for (let i = this.layers.length - 1; i >= 0; i--) {
|
|
303
|
-
/** @type {import("./Layer.js").Layer} */
|
|
304
|
-
const layer = this.layers[i]
|
|
305
|
-
if (layer.visible && !layer.visible(z)) continue
|
|
306
|
-
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)
|
|
307
|
-
if (!layer.cellInfoHTML) continue
|
|
308
|
-
if (!layer.getDataset) continue
|
|
309
|
-
const dsc = layer.getDataset(z)
|
|
310
|
-
if (!dsc) continue
|
|
311
|
-
|
|
312
|
-
//get cell at mouse position
|
|
313
|
-
/** @type {import('./Dataset.js').Cell|undefined} */
|
|
314
|
-
const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
|
|
315
|
-
//console.log(cell, dsc.resolution)
|
|
316
|
-
if (!cell) return undefined
|
|
317
|
-
|
|
318
|
-
//rare case for a dataset with mixed resolutions
|
|
319
|
-
if (dsc.mixedResolution) {
|
|
320
|
-
const r = +dsc.mixedResolution(cell)
|
|
321
|
-
const html = layer.cellInfoHTML(cell, r)
|
|
322
|
-
if (!html) return undefined
|
|
323
|
-
return { cell: cell, html: html, resolution: r }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const html = layer.cellInfoHTML(cell, dsc.getResolution())
|
|
327
|
-
if (!html) return undefined
|
|
328
|
-
return { cell: cell, html: html, resolution: dsc.getResolution() }
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* @param {number} x
|
|
334
|
-
* @param {number} y
|
|
335
|
-
* @param {number|undefined} z
|
|
336
|
-
*/
|
|
337
|
-
setView(x, y, z = undefined) {
|
|
338
|
-
this.geoCanvas.view.x = x
|
|
339
|
-
this.geoCanvas.view.y = y
|
|
340
|
-
if (z != undefined) this.geoCanvas.view.z = z
|
|
341
|
-
return this
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** @returns {import('./GeoCanvas.js').View} */
|
|
345
|
-
getView() {
|
|
346
|
-
return this.geoCanvas.view
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** @returns {number} */
|
|
350
|
-
getZoom() {
|
|
351
|
-
return this.geoCanvas.view.z
|
|
352
|
-
}
|
|
353
|
-
/** @param {number} z @returns {this} */
|
|
354
|
-
setZoom(z) {
|
|
355
|
-
this.geoCanvas.view.z = z
|
|
356
|
-
return this
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** @returns {Array.<number|undefined>} */
|
|
360
|
-
getCenterExtent() {
|
|
361
|
-
return this.geoCanvas.getCenterExtent()
|
|
362
|
-
}
|
|
363
|
-
/** @param {Array.<number>} val @returns {this} */
|
|
364
|
-
setCenterExtent(val) {
|
|
365
|
-
this.geoCanvas.setCenterExtent(val)
|
|
366
|
-
return this
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** @returns {Array.<number>} */
|
|
370
|
-
getZoomExtent() {
|
|
371
|
-
return this.geoCanvas.getZoomExtent()
|
|
372
|
-
}
|
|
373
|
-
/** @param {Array.<number>} val @returns {this} */
|
|
374
|
-
setZoomExtent(val) {
|
|
375
|
-
this.geoCanvas.setZoomExtent(val)
|
|
376
|
-
return this
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** @returns {string} */
|
|
380
|
-
getBackgroundColor() {
|
|
381
|
-
return this.geoCanvas.backgroundColor
|
|
382
|
-
}
|
|
383
|
-
/** @param {string} val @returns {this} */
|
|
384
|
-
setBackgroundColor(val) {
|
|
385
|
-
this.geoCanvas.backgroundColor = val
|
|
386
|
-
return this
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Adds a set of zoom buttons to the map
|
|
391
|
-
*
|
|
392
|
-
* @param {object} opts
|
|
393
|
-
* @returns {this}
|
|
394
|
-
*/
|
|
395
|
-
addZoomButtons(opts) {
|
|
396
|
-
// * opts.id
|
|
397
|
-
// * opts.onZoom - custom event handler function
|
|
398
|
-
// * opts.x
|
|
399
|
-
// * opts.y
|
|
400
|
-
// * opts.delta - zoom delta applied on each click
|
|
401
|
-
|
|
402
|
-
this.zoomButtons = new ZoomButtons({
|
|
403
|
-
map: this,
|
|
404
|
-
id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
|
|
405
|
-
class: opts?.class,
|
|
406
|
-
x: opts?.x,
|
|
407
|
-
y: opts?.y,
|
|
408
|
-
onZoom: opts?.onZoom,
|
|
409
|
-
delta: opts?.delta || 0.2,
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
return this
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Adds a fullscreen toggle button to the app
|
|
417
|
-
*
|
|
418
|
-
* @param {object} opts
|
|
419
|
-
* @returns {this}
|
|
420
|
-
*/
|
|
421
|
-
addFullscreenButton(opts) {
|
|
422
|
-
// * opts.map - the gridviz map
|
|
423
|
-
// * opts.id
|
|
424
|
-
// * opts.x
|
|
425
|
-
// * opts.y
|
|
426
|
-
|
|
427
|
-
this.fullscreenButton = new FullscreenButton({
|
|
428
|
-
map: this,
|
|
429
|
-
id: opts?.id || 'gridviz-fullscreen-button',
|
|
430
|
-
class: opts?.class,
|
|
431
|
-
x: opts?.x,
|
|
432
|
-
y: opts?.y,
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
return this
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/** @returns {this} */
|
|
439
|
-
setViewFromURL() {
|
|
440
|
-
this.geoCanvas.setViewFromURL()
|
|
441
|
-
return this
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* @description Add a resize event observer to the Apps container and update the canvas accordingly
|
|
446
|
-
* @memberof App
|
|
447
|
-
*/
|
|
448
|
-
defineResizeObserver() {
|
|
449
|
-
// Track whether the observer is currently processing a resize event
|
|
450
|
-
let resizePending = false
|
|
451
|
-
|
|
452
|
-
const resizeObserver = new ResizeObserver((entries) => {
|
|
453
|
-
if (!Array.isArray(entries) || !entries.length) return
|
|
454
|
-
|
|
455
|
-
let container = this.container
|
|
456
|
-
|
|
457
|
-
// Ensure the container has valid dimensions
|
|
458
|
-
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
459
|
-
if (!resizePending) {
|
|
460
|
-
resizePending = true // Prevent overlapping resize triggers
|
|
461
|
-
|
|
462
|
-
window.requestAnimationFrame(() => {
|
|
463
|
-
resizePending = false // Reset the flag after processing
|
|
464
|
-
|
|
465
|
-
// Check for size changes
|
|
466
|
-
if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
|
|
467
|
-
this.h = container.clientHeight
|
|
468
|
-
this.w = container.clientWidth
|
|
469
|
-
|
|
470
|
-
// Update geoCanvas sizes
|
|
471
|
-
this.geoCanvas.h = this.h
|
|
472
|
-
this.geoCanvas.w = this.w
|
|
473
|
-
this.geoCanvas.canvas.setAttribute('width', String(this.w))
|
|
474
|
-
this.geoCanvas.canvas.setAttribute('height', String(this.h))
|
|
475
|
-
this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
|
|
476
|
-
this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
|
|
477
|
-
|
|
478
|
-
this.redraw()
|
|
479
|
-
|
|
480
|
-
// Optionally reposition UI elements
|
|
481
|
-
// if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
|
|
482
|
-
// if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
|
|
483
|
-
}
|
|
484
|
-
})
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
resizeObserver.observe(this.container)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* @description Destroy the map and it's event listeners
|
|
494
|
-
* This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
|
|
495
|
-
* @memberof App
|
|
496
|
-
*/
|
|
497
|
-
destroy() {
|
|
498
|
-
// clear layers
|
|
499
|
-
this.layers = []
|
|
500
|
-
this.bgLayers = []
|
|
501
|
-
|
|
502
|
-
// remove event listeners from container
|
|
503
|
-
this.container.removeEventListener('mouseover', this.mouseOverHandler)
|
|
504
|
-
this.container.removeEventListener('mousemove', this.mouseMoveHandler)
|
|
505
|
-
this.container.removeEventListener('mouseout', this.mouseOutHandler)
|
|
506
|
-
|
|
507
|
-
// remove canvas
|
|
508
|
-
this.geoCanvas.canvas.remove()
|
|
509
|
-
|
|
510
|
-
// remove legend
|
|
511
|
-
this.legend?.remove()
|
|
512
|
-
|
|
513
|
-
// remove tooltip
|
|
514
|
-
this.tooltip.tooltip?.remove()
|
|
515
|
-
}
|
|
516
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// internal imports
|
|
5
|
+
import { GeoCanvas } from './GeoCanvas.js'
|
|
6
|
+
import { Tooltip } from './Tooltip.js'
|
|
7
|
+
import { ZoomButtons } from '../button/ZoomButtons.js'
|
|
8
|
+
import { FullscreenButton } from '../button/FullscreenButton.js'
|
|
9
|
+
|
|
10
|
+
// external imports
|
|
11
|
+
import { select } from 'd3-selection'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A gridviz application.
|
|
15
|
+
*
|
|
16
|
+
* @module core
|
|
17
|
+
* @author Joseph Davies, Julien Gaffuri
|
|
18
|
+
*/
|
|
19
|
+
export class Map {
|
|
20
|
+
/**
|
|
21
|
+
* @param {HTMLDivElement} container
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
*/
|
|
24
|
+
constructor(container, opts) {
|
|
25
|
+
opts = opts || {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The layers.
|
|
29
|
+
* @type {Array.<import("./Layer.js").Layer>}
|
|
30
|
+
* */
|
|
31
|
+
this.layers = opts.layers || []
|
|
32
|
+
|
|
33
|
+
//get container element
|
|
34
|
+
this.container = container || document.getElementById('gridviz')
|
|
35
|
+
if (!this.container) {
|
|
36
|
+
console.error('Cannot find gridviz container element.')
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//https://css-tricks.com/absolute-positioning-inside-relative-positioning/
|
|
41
|
+
this.container.style.position = 'relative' // container element must have relative positioning
|
|
42
|
+
|
|
43
|
+
//set dimensions
|
|
44
|
+
/** @type {number} */
|
|
45
|
+
this.w = opts.w || this.container.offsetWidth
|
|
46
|
+
/** @type {number} */
|
|
47
|
+
this.h = opts.h || this.container.offsetHeight
|
|
48
|
+
|
|
49
|
+
// Create the main canvas (for rendering to screen)
|
|
50
|
+
/** @type {HTMLCanvasElement} */
|
|
51
|
+
this._canvas = opts.canvas || this.initialiseCanvas()
|
|
52
|
+
|
|
53
|
+
/** Initialize GeoCanvas
|
|
54
|
+
* @type {GeoCanvas}
|
|
55
|
+
* @private */
|
|
56
|
+
this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
|
|
57
|
+
|
|
58
|
+
this.geoCanvas.redraw = () => {
|
|
59
|
+
this.redraw()
|
|
60
|
+
}
|
|
61
|
+
this.geoCanvas.cancelCurrentRequests = () => {
|
|
62
|
+
// when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
|
|
63
|
+
for (const layer of this.layers) {
|
|
64
|
+
//multires
|
|
65
|
+
if (layer.dataset?.datasets) {
|
|
66
|
+
for (const dataset of layer.dataset?.datasets) {
|
|
67
|
+
if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//single res
|
|
71
|
+
if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// legend div
|
|
76
|
+
this.legend = opts.legendContainer
|
|
77
|
+
? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
|
|
78
|
+
: null
|
|
79
|
+
if (!this.legend) this.initialiseLegend()
|
|
80
|
+
|
|
81
|
+
//tooltip
|
|
82
|
+
|
|
83
|
+
// set App container as default parent element for tooltip
|
|
84
|
+
if (!opts.tooltip) opts.tooltip = {}
|
|
85
|
+
if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @private
|
|
89
|
+
* @type {Tooltip} */
|
|
90
|
+
this.tooltip = new Tooltip(opts.tooltip)
|
|
91
|
+
|
|
92
|
+
// add event listeners to container
|
|
93
|
+
this.mouseOverHandler = (e) => this.focusCell(e)
|
|
94
|
+
this.mouseMoveHandler = (e) => this.focusCell(e)
|
|
95
|
+
this.mouseOutHandler = (e) => this.tooltip.hide()
|
|
96
|
+
this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
|
|
97
|
+
this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
|
|
98
|
+
this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
|
|
99
|
+
|
|
100
|
+
// listen for resize events on the App's container and handle them
|
|
101
|
+
this.defineResizeObserver()
|
|
102
|
+
|
|
103
|
+
// add extra logic to onZoomStartFun
|
|
104
|
+
this.geoCanvas.onZoomStartFun = (e) => {
|
|
105
|
+
if (opts.onZoomStartFun) opts.onZoomStartFun(e)
|
|
106
|
+
this.tooltip.hide()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//for mouse over
|
|
110
|
+
/**
|
|
111
|
+
* @private
|
|
112
|
+
* @type {HTMLCanvasElement|null} */
|
|
113
|
+
this.canvasSave = null
|
|
114
|
+
|
|
115
|
+
this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
|
|
116
|
+
this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
|
|
117
|
+
|
|
118
|
+
// transparent background (e.g. leaflet) 'red painting' fix
|
|
119
|
+
this.transparentBackground = opts.transparentBackground
|
|
120
|
+
|
|
121
|
+
//set default globalCompositeOperation
|
|
122
|
+
this.defaultGlobalCompositeOperation =
|
|
123
|
+
opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @protected
|
|
128
|
+
* @returns {HTMLCanvasElement}
|
|
129
|
+
*/
|
|
130
|
+
initialiseCanvas() {
|
|
131
|
+
const canvas = document.createElement('canvas')
|
|
132
|
+
canvas.setAttribute('width', '' + this.w)
|
|
133
|
+
canvas.setAttribute('height', '' + this.h)
|
|
134
|
+
this.container.appendChild(canvas)
|
|
135
|
+
return canvas
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
initialiseLegend() {
|
|
139
|
+
this.legend = select(this.container)
|
|
140
|
+
.append('div') // Create a new container
|
|
141
|
+
.attr('id', 'gridviz-legend')
|
|
142
|
+
.style('position', 'absolute')
|
|
143
|
+
.style('width', 'auto')
|
|
144
|
+
.style('height', 'auto')
|
|
145
|
+
.style('background', '#FFFFFF')
|
|
146
|
+
//.style("padding", this.padding)
|
|
147
|
+
.style('border', '0px')
|
|
148
|
+
//.style('border-radius', '5px')
|
|
149
|
+
.style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
|
|
150
|
+
.style('font-family', 'Helvetica, Arial, sans-serif')
|
|
151
|
+
.style('bottom', '15px')
|
|
152
|
+
.style('right', '15px')
|
|
153
|
+
//hide
|
|
154
|
+
//.style("visibility", "hidden")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Set/get layer stack.
|
|
159
|
+
*
|
|
160
|
+
* @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
|
|
161
|
+
* @returns { this | import("./Layer.js").Layer[] }
|
|
162
|
+
*/
|
|
163
|
+
layers_(layers) {
|
|
164
|
+
if (arguments.length === 0) return this.layers
|
|
165
|
+
if (arguments.length === 1)
|
|
166
|
+
if (Array.isArray(layers)) this.layers = layers
|
|
167
|
+
else this.layers = [layers]
|
|
168
|
+
else this.layers = arguments
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @returns {this} */
|
|
173
|
+
redraw() {
|
|
174
|
+
//remove legend elements
|
|
175
|
+
if (this.legend) this.legend.selectAll('*').remove()
|
|
176
|
+
|
|
177
|
+
//clear
|
|
178
|
+
this.geoCanvas.initCanvasTransform()
|
|
179
|
+
this.geoCanvas.clear(this.geoCanvas.backgroundColor)
|
|
180
|
+
|
|
181
|
+
const z = this.geoCanvas.view.z
|
|
182
|
+
this.updateExtentGeo()
|
|
183
|
+
|
|
184
|
+
//go through the layers
|
|
185
|
+
for (const layer of this.layers) {
|
|
186
|
+
//check if layer is visible
|
|
187
|
+
if (layer.visible && !layer.visible(z)) continue
|
|
188
|
+
|
|
189
|
+
//set layer alpha and blend mode
|
|
190
|
+
this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
|
|
191
|
+
if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
|
|
192
|
+
|
|
193
|
+
//set affin transform to draw with geographical coordinates
|
|
194
|
+
this.geoCanvas.setCanvasTransform()
|
|
195
|
+
|
|
196
|
+
//draw layer
|
|
197
|
+
layer.draw(this.geoCanvas, this.legend)
|
|
198
|
+
|
|
199
|
+
//draw layer filter
|
|
200
|
+
if (layer.filterColor) layer.drawFilter(this.geoCanvas)
|
|
201
|
+
|
|
202
|
+
//restore default alpha and blend operation
|
|
203
|
+
this.geoCanvas.offscreenCtx.globalAlpha = 1.0
|
|
204
|
+
this.geoCanvas.offscreenCtx.globalCompositeOperation = this.defaultGlobalCompositeOperation
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// one drawImage call: draw the offscreen canvas to the main canvas
|
|
208
|
+
this.geoCanvas.initCanvasTransform()
|
|
209
|
+
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
210
|
+
|
|
211
|
+
this.canvasSave = null
|
|
212
|
+
|
|
213
|
+
return this
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param {number} marginPx
|
|
218
|
+
* @returns {import('./GeoCanvas.js').Envelope}
|
|
219
|
+
* @public
|
|
220
|
+
*/
|
|
221
|
+
updateExtentGeo(marginPx = 20) {
|
|
222
|
+
return this.geoCanvas.updateExtentGeo(marginPx)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @param {MouseEvent} e */
|
|
226
|
+
focusCell(e) {
|
|
227
|
+
//compute mouse geo position
|
|
228
|
+
const mousePositionGeo = {
|
|
229
|
+
x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
|
|
230
|
+
y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
|
|
231
|
+
}
|
|
232
|
+
/** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
|
|
233
|
+
const focus = this.getCellFocusInfo(mousePositionGeo)
|
|
234
|
+
|
|
235
|
+
// transparent background (e.g. leaflet) 'red painting' fix
|
|
236
|
+
if (this.transparentBackground) {
|
|
237
|
+
if (focus) {
|
|
238
|
+
this.tooltip.html(focus.html)
|
|
239
|
+
this.tooltip.setPosition(e)
|
|
240
|
+
this.tooltip.show()
|
|
241
|
+
} else {
|
|
242
|
+
this.tooltip.hide()
|
|
243
|
+
}
|
|
244
|
+
this.canvasSave = document.createElement('canvas')
|
|
245
|
+
this.canvasSave.setAttribute('width', '' + this.w)
|
|
246
|
+
this.canvasSave.setAttribute('height', '' + this.h)
|
|
247
|
+
this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.canvas, 0, 0)
|
|
248
|
+
this.geoCanvas.initCanvasTransform()
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (focus) {
|
|
253
|
+
this.tooltip.html(focus.html)
|
|
254
|
+
this.tooltip.setPosition(e)
|
|
255
|
+
this.tooltip.show()
|
|
256
|
+
|
|
257
|
+
//show cell position as a rectangle
|
|
258
|
+
if (!this.canvasSave) {
|
|
259
|
+
this.canvasSave = document.createElement('canvas')
|
|
260
|
+
this.canvasSave.setAttribute('width', '' + this.w)
|
|
261
|
+
this.canvasSave.setAttribute('height', '' + this.h)
|
|
262
|
+
this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
263
|
+
} else {
|
|
264
|
+
this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//draw image saved + draw rectangle
|
|
268
|
+
const rectWPix = this.selectionRectangleWidthPix
|
|
269
|
+
? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
|
|
270
|
+
: 4
|
|
271
|
+
this.geoCanvas.initCanvasTransform()
|
|
272
|
+
const ctx = this.geoCanvas.offscreenCtx
|
|
273
|
+
ctx.strokeStyle = this.selectionRectangleColor
|
|
274
|
+
ctx.lineWidth = rectWPix
|
|
275
|
+
ctx.beginPath()
|
|
276
|
+
|
|
277
|
+
ctx.rect(
|
|
278
|
+
this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
|
|
279
|
+
this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
|
|
280
|
+
focus.resolution / this.geoCanvas.view.z + rectWPix,
|
|
281
|
+
-focus.resolution / this.geoCanvas.view.z - rectWPix
|
|
282
|
+
)
|
|
283
|
+
ctx.stroke()
|
|
284
|
+
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
285
|
+
} else {
|
|
286
|
+
this.tooltip.hide()
|
|
287
|
+
if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Return the cell HTML info at a given geo position.
|
|
293
|
+
* This is usefull for user interactions, to show this info where the user clicks for example.
|
|
294
|
+
*
|
|
295
|
+
* @param {{x:number,y:number}} posGeo
|
|
296
|
+
* @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
|
|
297
|
+
* @protected
|
|
298
|
+
*/
|
|
299
|
+
getCellFocusInfo(posGeo) {
|
|
300
|
+
//go through the layers, starting from top
|
|
301
|
+
const z = this.geoCanvas.view.z
|
|
302
|
+
for (let i = this.layers.length - 1; i >= 0; i--) {
|
|
303
|
+
/** @type {import("./Layer.js").Layer} */
|
|
304
|
+
const layer = this.layers[i]
|
|
305
|
+
if (layer.visible && !layer.visible(z)) continue
|
|
306
|
+
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)
|
|
307
|
+
if (!layer.cellInfoHTML) continue
|
|
308
|
+
if (!layer.getDataset) continue
|
|
309
|
+
const dsc = layer.getDataset(z)
|
|
310
|
+
if (!dsc) continue
|
|
311
|
+
|
|
312
|
+
//get cell at mouse position
|
|
313
|
+
/** @type {import('./Dataset.js').Cell|undefined} */
|
|
314
|
+
const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
|
|
315
|
+
//console.log(cell, dsc.resolution)
|
|
316
|
+
if (!cell) return undefined
|
|
317
|
+
|
|
318
|
+
//rare case for a dataset with mixed resolutions
|
|
319
|
+
if (dsc.mixedResolution) {
|
|
320
|
+
const r = +dsc.mixedResolution(cell)
|
|
321
|
+
const html = layer.cellInfoHTML(cell, r)
|
|
322
|
+
if (!html) return undefined
|
|
323
|
+
return { cell: cell, html: html, resolution: r }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const html = layer.cellInfoHTML(cell, dsc.getResolution())
|
|
327
|
+
if (!html) return undefined
|
|
328
|
+
return { cell: cell, html: html, resolution: dsc.getResolution() }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {number} x
|
|
334
|
+
* @param {number} y
|
|
335
|
+
* @param {number|undefined} z
|
|
336
|
+
*/
|
|
337
|
+
setView(x, y, z = undefined) {
|
|
338
|
+
this.geoCanvas.view.x = x
|
|
339
|
+
this.geoCanvas.view.y = y
|
|
340
|
+
if (z != undefined) this.geoCanvas.view.z = z
|
|
341
|
+
return this
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @returns {import('./GeoCanvas.js').View} */
|
|
345
|
+
getView() {
|
|
346
|
+
return this.geoCanvas.view
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @returns {number} */
|
|
350
|
+
getZoom() {
|
|
351
|
+
return this.geoCanvas.view.z
|
|
352
|
+
}
|
|
353
|
+
/** @param {number} z @returns {this} */
|
|
354
|
+
setZoom(z) {
|
|
355
|
+
this.geoCanvas.view.z = z
|
|
356
|
+
return this
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** @returns {Array.<number|undefined>} */
|
|
360
|
+
getCenterExtent() {
|
|
361
|
+
return this.geoCanvas.getCenterExtent()
|
|
362
|
+
}
|
|
363
|
+
/** @param {Array.<number>} val @returns {this} */
|
|
364
|
+
setCenterExtent(val) {
|
|
365
|
+
this.geoCanvas.setCenterExtent(val)
|
|
366
|
+
return this
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** @returns {Array.<number>} */
|
|
370
|
+
getZoomExtent() {
|
|
371
|
+
return this.geoCanvas.getZoomExtent()
|
|
372
|
+
}
|
|
373
|
+
/** @param {Array.<number>} val @returns {this} */
|
|
374
|
+
setZoomExtent(val) {
|
|
375
|
+
this.geoCanvas.setZoomExtent(val)
|
|
376
|
+
return this
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @returns {string} */
|
|
380
|
+
getBackgroundColor() {
|
|
381
|
+
return this.geoCanvas.backgroundColor
|
|
382
|
+
}
|
|
383
|
+
/** @param {string} val @returns {this} */
|
|
384
|
+
setBackgroundColor(val) {
|
|
385
|
+
this.geoCanvas.backgroundColor = val
|
|
386
|
+
return this
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Adds a set of zoom buttons to the map
|
|
391
|
+
*
|
|
392
|
+
* @param {object} opts
|
|
393
|
+
* @returns {this}
|
|
394
|
+
*/
|
|
395
|
+
addZoomButtons(opts) {
|
|
396
|
+
// * opts.id
|
|
397
|
+
// * opts.onZoom - custom event handler function
|
|
398
|
+
// * opts.x
|
|
399
|
+
// * opts.y
|
|
400
|
+
// * opts.delta - zoom delta applied on each click
|
|
401
|
+
|
|
402
|
+
this.zoomButtons = new ZoomButtons({
|
|
403
|
+
map: this,
|
|
404
|
+
id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
|
|
405
|
+
class: opts?.class,
|
|
406
|
+
x: opts?.x,
|
|
407
|
+
y: opts?.y,
|
|
408
|
+
onZoom: opts?.onZoom,
|
|
409
|
+
delta: opts?.delta || 0.2,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
return this
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Adds a fullscreen toggle button to the app
|
|
417
|
+
*
|
|
418
|
+
* @param {object} opts
|
|
419
|
+
* @returns {this}
|
|
420
|
+
*/
|
|
421
|
+
addFullscreenButton(opts) {
|
|
422
|
+
// * opts.map - the gridviz map
|
|
423
|
+
// * opts.id
|
|
424
|
+
// * opts.x
|
|
425
|
+
// * opts.y
|
|
426
|
+
|
|
427
|
+
this.fullscreenButton = new FullscreenButton({
|
|
428
|
+
map: this,
|
|
429
|
+
id: opts?.id || 'gridviz-fullscreen-button',
|
|
430
|
+
class: opts?.class,
|
|
431
|
+
x: opts?.x,
|
|
432
|
+
y: opts?.y,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
return this
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** @returns {this} */
|
|
439
|
+
setViewFromURL() {
|
|
440
|
+
this.geoCanvas.setViewFromURL()
|
|
441
|
+
return this
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @description Add a resize event observer to the Apps container and update the canvas accordingly
|
|
446
|
+
* @memberof App
|
|
447
|
+
*/
|
|
448
|
+
defineResizeObserver() {
|
|
449
|
+
// Track whether the observer is currently processing a resize event
|
|
450
|
+
let resizePending = false
|
|
451
|
+
|
|
452
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
453
|
+
if (!Array.isArray(entries) || !entries.length) return
|
|
454
|
+
|
|
455
|
+
let container = this.container
|
|
456
|
+
|
|
457
|
+
// Ensure the container has valid dimensions
|
|
458
|
+
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
459
|
+
if (!resizePending) {
|
|
460
|
+
resizePending = true // Prevent overlapping resize triggers
|
|
461
|
+
|
|
462
|
+
window.requestAnimationFrame(() => {
|
|
463
|
+
resizePending = false // Reset the flag after processing
|
|
464
|
+
|
|
465
|
+
// Check for size changes
|
|
466
|
+
if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
|
|
467
|
+
this.h = container.clientHeight
|
|
468
|
+
this.w = container.clientWidth
|
|
469
|
+
|
|
470
|
+
// Update geoCanvas sizes
|
|
471
|
+
this.geoCanvas.h = this.h
|
|
472
|
+
this.geoCanvas.w = this.w
|
|
473
|
+
this.geoCanvas.canvas.setAttribute('width', String(this.w))
|
|
474
|
+
this.geoCanvas.canvas.setAttribute('height', String(this.h))
|
|
475
|
+
this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
|
|
476
|
+
this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
|
|
477
|
+
|
|
478
|
+
this.redraw()
|
|
479
|
+
|
|
480
|
+
// Optionally reposition UI elements
|
|
481
|
+
// if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
|
|
482
|
+
// if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
resizeObserver.observe(this.container)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @description Destroy the map and it's event listeners
|
|
494
|
+
* This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
|
|
495
|
+
* @memberof App
|
|
496
|
+
*/
|
|
497
|
+
destroy() {
|
|
498
|
+
// clear layers
|
|
499
|
+
this.layers = []
|
|
500
|
+
this.bgLayers = []
|
|
501
|
+
|
|
502
|
+
// remove event listeners from container
|
|
503
|
+
this.container.removeEventListener('mouseover', this.mouseOverHandler)
|
|
504
|
+
this.container.removeEventListener('mousemove', this.mouseMoveHandler)
|
|
505
|
+
this.container.removeEventListener('mouseout', this.mouseOutHandler)
|
|
506
|
+
|
|
507
|
+
// remove canvas
|
|
508
|
+
this.geoCanvas.canvas.remove()
|
|
509
|
+
|
|
510
|
+
// remove legend
|
|
511
|
+
this.legend?.remove()
|
|
512
|
+
|
|
513
|
+
// remove tooltip
|
|
514
|
+
this.tooltip.tooltip?.remove()
|
|
515
|
+
}
|
|
516
|
+
}
|