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/GeoCanvas.js
CHANGED
|
@@ -1,430 +1,430 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
/** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A viewshed.
|
|
8
|
-
* @typedef {{x: number, y: number, z: number}} View */
|
|
9
|
-
|
|
10
|
-
import { select } from 'd3-selection'
|
|
11
|
-
import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
|
|
15
|
-
*
|
|
16
|
-
* @module core
|
|
17
|
-
* @author Joseph Davies, Julien Gaffuri
|
|
18
|
-
*/
|
|
19
|
-
export class GeoCanvas {
|
|
20
|
-
/**
|
|
21
|
-
* @constructor
|
|
22
|
-
* @param {HTMLCanvasElement} canvas
|
|
23
|
-
* @param {number} x The x coordinate of the view
|
|
24
|
-
* @param {number} y The y coordinate of the view
|
|
25
|
-
* @param {number} z The zoom level of the view (pixel size, in ground m)
|
|
26
|
-
* @param {object} opts
|
|
27
|
-
*/
|
|
28
|
-
constructor(canvas, x = 0, y = 0, z = 0, opts = undefined) {
|
|
29
|
-
this.opts = opts || {}
|
|
30
|
-
|
|
31
|
-
/** @type {HTMLCanvasElement} */
|
|
32
|
-
this.canvas = canvas
|
|
33
|
-
|
|
34
|
-
/** @type {number} */
|
|
35
|
-
this.w = this.canvas.offsetWidth
|
|
36
|
-
/** @type {number} */
|
|
37
|
-
this.h = this.canvas.offsetHeight
|
|
38
|
-
|
|
39
|
-
// Adjust canvas width and height based on device pixel ratio
|
|
40
|
-
//const dpr = window.devicePixelRatio || 1 // Get the device pixel ratio
|
|
41
|
-
//this.canvas.width = this.w * dpr // Set canvas width
|
|
42
|
-
//\sthis.canvas.height = this.h * dpr // Set canvas height
|
|
43
|
-
|
|
44
|
-
// Create offscreen canvas for drawing operations
|
|
45
|
-
this.offscreenCanvas = document.createElement('canvas')
|
|
46
|
-
this.offscreenCanvas.width = this.w
|
|
47
|
-
this.offscreenCanvas.height = this.h
|
|
48
|
-
|
|
49
|
-
const ctx = this.canvas.getContext('2d')
|
|
50
|
-
const offscreenCtx = this.offscreenCanvas.getContext('2d')
|
|
51
|
-
if (!ctx) throw 'Impossible to create canvas 2D context'
|
|
52
|
-
if (!offscreenCtx) throw 'Impossible to create canvas 2D context'
|
|
53
|
-
/**@type {CanvasRenderingContext2D} */
|
|
54
|
-
this.ctx = ctx
|
|
55
|
-
this.offscreenCtx = offscreenCtx
|
|
56
|
-
//this.ctx.scale(dpr, dpr) // Scale the context
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* z: pixel size, in m/pix
|
|
60
|
-
* @type {View} */
|
|
61
|
-
this.view = { x: x, y: y, z: z }
|
|
62
|
-
|
|
63
|
-
/** Background color.
|
|
64
|
-
* @type {string} */
|
|
65
|
-
this.backgroundColor = opts.backgroundColor || 'white'
|
|
66
|
-
|
|
67
|
-
/** @type {function(object|undefined):void} */
|
|
68
|
-
this.onZoomStartFun = opts.onZoomStartFun
|
|
69
|
-
|
|
70
|
-
/** @type {function(object|undefined):void} */
|
|
71
|
-
this.onZoomEndFun = opts.onZoomEndFun
|
|
72
|
-
|
|
73
|
-
/** @type {function(object|undefined):void} */
|
|
74
|
-
this.onZoomFun = opts.onZoomFun
|
|
75
|
-
|
|
76
|
-
//current extent
|
|
77
|
-
/** @type {Envelope} */
|
|
78
|
-
this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
|
|
79
|
-
this.updateExtentGeo()
|
|
80
|
-
|
|
81
|
-
//rely on d3 for zoom
|
|
82
|
-
if (!opts.disableZoom) {
|
|
83
|
-
let tP = zoomIdentity
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
let debounceTimeout = null // Add a debounce timeout variable
|
|
86
|
-
const z = d3zoom()
|
|
87
|
-
// to make the zooming a bit faster
|
|
88
|
-
.wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
|
|
89
|
-
.on('zoom', (e) => {
|
|
90
|
-
const t = e.transform
|
|
91
|
-
const zoomFactor = tP.k / t.k
|
|
92
|
-
if (zoomFactor == 1) {
|
|
93
|
-
//pan
|
|
94
|
-
const dx = tP.x - t.x
|
|
95
|
-
const dy = tP.y - t.y
|
|
96
|
-
this.pan(dx * this.view.z, -dy * this.view.z)
|
|
97
|
-
} else {
|
|
98
|
-
handleZoom(e, zoomFactor)
|
|
99
|
-
}
|
|
100
|
-
tP = t
|
|
101
|
-
|
|
102
|
-
if (this.onZoomFun) this.onZoomFun(e)
|
|
103
|
-
})
|
|
104
|
-
.on('start', (e) => {
|
|
105
|
-
// start of zoom event
|
|
106
|
-
// save the current canvas state to keep onscreen during pan/zoom before redrawing
|
|
107
|
-
this.canvasSave.c = document.createElement('canvas')
|
|
108
|
-
this.canvasSave.c.setAttribute('width', '' + this.w)
|
|
109
|
-
this.canvasSave.c.setAttribute('height', '' + this.h)
|
|
110
|
-
this.canvasSave.c.getContext('2d')?.drawImage(this.canvas, 0, 0)
|
|
111
|
-
this.canvasSave.dx = 0
|
|
112
|
-
this.canvasSave.dy = 0
|
|
113
|
-
this.canvasSave.f = 1
|
|
114
|
-
if (this.onZoomStartFun) this.onZoomStartFun(e)
|
|
115
|
-
})
|
|
116
|
-
.on('end', (e) => {
|
|
117
|
-
// end of pan/zoom event
|
|
118
|
-
this.redraw()
|
|
119
|
-
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
120
|
-
|
|
121
|
-
if (this.onZoomEndFun) this.onZoomEndFun(e)
|
|
122
|
-
})
|
|
123
|
-
// @ts-ignore
|
|
124
|
-
z(select(this.canvas))
|
|
125
|
-
|
|
126
|
-
const handleZoom = (event, zoomFactor) => {
|
|
127
|
-
// cancel ongoing data requests
|
|
128
|
-
this.cancelCurrentRequests()
|
|
129
|
-
const se = event.sourceEvent
|
|
130
|
-
|
|
131
|
-
if (se instanceof WheelEvent) {
|
|
132
|
-
//zoom at the mouse position
|
|
133
|
-
this.zoom(
|
|
134
|
-
zoomFactor,
|
|
135
|
-
// @ts-ignore
|
|
136
|
-
this.pixToGeoX(se.offsetX),
|
|
137
|
-
// @ts-ignore
|
|
138
|
-
this.pixToGeoY(se.offsetY)
|
|
139
|
-
)
|
|
140
|
-
} else if (se instanceof TouchEvent) {
|
|
141
|
-
//compute average position of the touches
|
|
142
|
-
let tx = 0,
|
|
143
|
-
ty = 0
|
|
144
|
-
for (let tt of se.targetTouches) {
|
|
145
|
-
tx += tt.clientX
|
|
146
|
-
ty += tt.clientY
|
|
147
|
-
}
|
|
148
|
-
tx /= se.targetTouches.length
|
|
149
|
-
ty /= se.targetTouches.length
|
|
150
|
-
|
|
151
|
-
// Adjust for container's offset
|
|
152
|
-
// tx -= containerRect.left
|
|
153
|
-
// ty -= containerRect.top
|
|
154
|
-
|
|
155
|
-
//zoom at this average position
|
|
156
|
-
this.zoom(zoomFactor, this.pixToGeoX(tx), this.pixToGeoY(ty))
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
//center extent
|
|
162
|
-
/** @type {number|undefined} */
|
|
163
|
-
this.xMin = opts.centerExtent ? opts.centerExtent[0] : undefined
|
|
164
|
-
/** @type {number|undefined} */
|
|
165
|
-
this.yMin = opts.centerExtent ? opts.centerExtent[1] : undefined
|
|
166
|
-
/** @type {number|undefined} */
|
|
167
|
-
this.xMax = opts.centerExtent ? opts.centerExtent[2] : undefined
|
|
168
|
-
/** @type {number|undefined} */
|
|
169
|
-
this.yMax = opts.centerExtent ? opts.centerExtent[3] : undefined
|
|
170
|
-
|
|
171
|
-
/** Zoom extent, to limit zoom in and out
|
|
172
|
-
* @type {Array.<number>} */
|
|
173
|
-
this.zoomExtent = opts.zoomExtent || [0, Infinity]
|
|
174
|
-
|
|
175
|
-
/** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
|
|
176
|
-
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
|
|
177
|
-
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** @returns {View} */
|
|
181
|
-
getView() {
|
|
182
|
-
return this.view
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** @param {Array.<number>} v */
|
|
186
|
-
setCenterExtent(v) {
|
|
187
|
-
this.xMin = v[0]
|
|
188
|
-
this.yMin = v[1]
|
|
189
|
-
this.xMax = v[2]
|
|
190
|
-
this.yMax = v[3]
|
|
191
|
-
}
|
|
192
|
-
/** @returns {Array.<number|undefined>} */
|
|
193
|
-
getCenterExtent() {
|
|
194
|
-
return [this.xMin, this.yMin, this.xMax, this.yMax]
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** @param {Array.<number>} v */
|
|
198
|
-
setZoomExtent(v) {
|
|
199
|
-
this.zoomExtent = v
|
|
200
|
-
}
|
|
201
|
-
/** @returns {Array.<number>} */
|
|
202
|
-
getZoomExtent() {
|
|
203
|
-
return this.zoomExtent
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Initialise canvas transform with identity transformation. */
|
|
207
|
-
initCanvasTransform() {
|
|
208
|
-
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
209
|
-
this.offscreenCtx.setTransform(1, 0, 0, 1, 0, 0)
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
|
|
213
|
-
setCanvasTransform() {
|
|
214
|
-
const k = 1 / this.view.z
|
|
215
|
-
const tx = -this.view.x / this.view.z + this.w * 0.5
|
|
216
|
-
const ty = this.view.y / this.view.z + this.h * 0.5
|
|
217
|
-
this.ctx.setTransform(k, 0, 0, -k, tx, ty)
|
|
218
|
-
this.offscreenCtx.setTransform(k, 0, 0, -k, tx, ty)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
|
|
222
|
-
getWebGLTransform() {
|
|
223
|
-
const kx = 2.0 / (this.w * this.view.z)
|
|
224
|
-
const ky = 2.0 / (this.h * this.view.z)
|
|
225
|
-
return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.view.x, -ky * this.view.y, 1.0]
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** The function specifying how to draw the map. */
|
|
229
|
-
redraw() {
|
|
230
|
-
throw new Error('Method redraw not implemented.')
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
|
|
234
|
-
cancelCurrentRequests() {
|
|
235
|
-
throw new Error('Method cancelCurrentRequests not implemented.')
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Clear. To be used before a redraw for example.
|
|
240
|
-
* @param {string} color
|
|
241
|
-
*/
|
|
242
|
-
clear(color = 'white') {
|
|
243
|
-
if (this.opts.transparentBackground) {
|
|
244
|
-
this.ctx.clearRect(0, 0, this.w, this.h)
|
|
245
|
-
this.offscreenCtx.clearRect(0, 0, this.w, this.h)
|
|
246
|
-
} else {
|
|
247
|
-
if (this.ctx) this.ctx.fillStyle = color
|
|
248
|
-
if (this.offscreenCtx) this.offscreenCtx.fillStyle = color
|
|
249
|
-
this.ctx.fillRect(0, 0, this.w, this.h)
|
|
250
|
-
this.offscreenCtx.fillRect(0, 0, this.w, this.h)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* @param {number} dxGeo
|
|
256
|
-
* @param {number} dyGeo
|
|
257
|
-
*/
|
|
258
|
-
pan(dxGeo = 0, dyGeo = 0) {
|
|
259
|
-
//ensures x/y extent
|
|
260
|
-
if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
|
|
261
|
-
if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
|
|
262
|
-
if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
|
|
263
|
-
if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
|
|
264
|
-
|
|
265
|
-
//pan
|
|
266
|
-
this.view.x += dxGeo
|
|
267
|
-
this.view.y += dyGeo
|
|
268
|
-
this.updateExtentGeo()
|
|
269
|
-
|
|
270
|
-
if (this.canvasSave.c) {
|
|
271
|
-
const scale = 1 / this.view.z
|
|
272
|
-
|
|
273
|
-
// Update saved canvas offset
|
|
274
|
-
this.canvasSave.dx -= dxGeo * scale
|
|
275
|
-
this.canvasSave.dy += dyGeo * scale
|
|
276
|
-
|
|
277
|
-
// clear canvas
|
|
278
|
-
this.clear(this.backgroundColor)
|
|
279
|
-
|
|
280
|
-
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
|
|
281
|
-
//this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
282
|
-
this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
283
|
-
|
|
284
|
-
// Render the offscreen canvas to the visible context
|
|
285
|
-
// this.clear(this.backgroundColor)
|
|
286
|
-
this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
|
|
287
|
-
} else {
|
|
288
|
-
console.log('no canvas save')
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Zoom.
|
|
294
|
-
* @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
|
|
295
|
-
* @param {number} xGeo The x geo position fixed in the screen.
|
|
296
|
-
* @param {number} yGeo The y geo position fixed in the screen.
|
|
297
|
-
*/
|
|
298
|
-
zoom(f = 1, xGeo = this.view.x, yGeo = this.view.y) {
|
|
299
|
-
//TODO force geo extend to remain
|
|
300
|
-
|
|
301
|
-
//trying to zoom in/out beyond limit
|
|
302
|
-
if (this.zoomExtent[0] == this.view.z && f <= 1) return
|
|
303
|
-
if (this.zoomExtent[1] == this.view.z && f >= 1) return
|
|
304
|
-
|
|
305
|
-
//ensure zoom extent preserved
|
|
306
|
-
const newZf = f * this.view.z
|
|
307
|
-
if (newZf < this.zoomExtent[0]) f = this.zoomExtent[0] / this.view.z
|
|
308
|
-
if (newZf > this.zoomExtent[1]) f = this.zoomExtent[1] / this.view.z
|
|
309
|
-
|
|
310
|
-
this.view.z *= f
|
|
311
|
-
|
|
312
|
-
//compute pan
|
|
313
|
-
let dxGeo = (xGeo - this.view.x) * (1 - f)
|
|
314
|
-
let dyGeo = (yGeo - this.view.y) * (1 - f)
|
|
315
|
-
|
|
316
|
-
//ensures x/y extent
|
|
317
|
-
if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
|
|
318
|
-
if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
|
|
319
|
-
if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
|
|
320
|
-
if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
|
|
321
|
-
|
|
322
|
-
//pan
|
|
323
|
-
this.view.x += dxGeo
|
|
324
|
-
this.view.y += dyGeo
|
|
325
|
-
this.updateExtentGeo()
|
|
326
|
-
|
|
327
|
-
// zoom in on the current canvas state
|
|
328
|
-
if (this.canvasSave.c) {
|
|
329
|
-
this.clear(this.backgroundColor)
|
|
330
|
-
this.canvasSave.f /= f
|
|
331
|
-
this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
|
|
332
|
-
this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
|
|
333
|
-
this.clear(this.backgroundColor)
|
|
334
|
-
this.offscreenCtx.drawImage(
|
|
335
|
-
this.canvasSave.c,
|
|
336
|
-
this.canvasSave.dx,
|
|
337
|
-
this.canvasSave.dy,
|
|
338
|
-
this.canvasSave.f * this.canvasSave.c.width,
|
|
339
|
-
this.canvasSave.f * this.canvasSave.c.height
|
|
340
|
-
)
|
|
341
|
-
this.ctx.drawImage(
|
|
342
|
-
this.offscreenCanvas, // Use offscreen canvas as the source
|
|
343
|
-
0,
|
|
344
|
-
0, // Position the offscreen canvas at the top-left corner of the main canvas
|
|
345
|
-
this.canvas.width, // The width of the visible canvas
|
|
346
|
-
this.canvas.height // The height of the visible canvas
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* @param {number} marginPx
|
|
353
|
-
* @returns {Envelope} The envelope of the view, in geo coordinates.
|
|
354
|
-
*/
|
|
355
|
-
updateExtentGeo(marginPx = 20) {
|
|
356
|
-
this.extGeo = {
|
|
357
|
-
xMin: this.pixToGeoX(-marginPx),
|
|
358
|
-
xMax: this.pixToGeoX(this.w + marginPx),
|
|
359
|
-
yMin: this.pixToGeoY(this.h + marginPx),
|
|
360
|
-
yMax: this.pixToGeoY(-marginPx),
|
|
361
|
-
}
|
|
362
|
-
return this.extGeo
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Check if the object has to be drawn
|
|
367
|
-
*
|
|
368
|
-
* @param {{x:number,y:number}} obj
|
|
369
|
-
*/
|
|
370
|
-
toDraw(obj) {
|
|
371
|
-
if (obj.x < this.extGeo.xMin) return false
|
|
372
|
-
if (obj.x > this.extGeo.xMax) return false
|
|
373
|
-
if (obj.y < this.extGeo.yMin) return false
|
|
374
|
-
if (obj.y > this.extGeo.yMax) return false
|
|
375
|
-
return true
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
//conversion functions
|
|
379
|
-
/**
|
|
380
|
-
* @param {number} xGeo Geo x coordinate, in m.
|
|
381
|
-
* @returns {number} Screen x coordinate, in pix.
|
|
382
|
-
*/
|
|
383
|
-
geoToPixX(xGeo) {
|
|
384
|
-
return (xGeo - this.view.x) / this.view.z + this.w * 0.5
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* @param {number} yGeo Geo y coordinate, in m.
|
|
388
|
-
* @returns {number} Screen y coordinate, in pix.
|
|
389
|
-
*/
|
|
390
|
-
geoToPixY(yGeo) {
|
|
391
|
-
return -(yGeo - this.view.y) / this.view.z + this.h * 0.5
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* @param {number} x Screen x coordinate, in pix.
|
|
395
|
-
* @returns {number} Geo x coordinate, in m.
|
|
396
|
-
*/
|
|
397
|
-
pixToGeoX(x) {
|
|
398
|
-
return (x - this.w * 0.5) * this.view.z + this.view.x
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* @param {number} y Screen y coordinate, in pix.
|
|
402
|
-
* @returns {number} Geo y coordinate, in m.
|
|
403
|
-
*/
|
|
404
|
-
pixToGeoY(y) {
|
|
405
|
-
return -(y - this.h * 0.5) * this.view.z + this.view.y
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/** Get x,y,z elements from URL and assign them to the view. */
|
|
409
|
-
setViewFromURL() {
|
|
410
|
-
const x = GeoCanvas.getParameterByName('x'),
|
|
411
|
-
y = GeoCanvas.getParameterByName('y'),
|
|
412
|
-
z = GeoCanvas.getParameterByName('z')
|
|
413
|
-
if (x != null && x != undefined && !isNaN(+x)) this.view.x = +x
|
|
414
|
-
if (y != null && y != undefined && !isNaN(+y)) this.view.y = +y
|
|
415
|
-
if (z != null && z != undefined && !isNaN(+z)) this.view.z = +z
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Get a URL parameter by name.
|
|
420
|
-
*
|
|
421
|
-
* @param {string} name
|
|
422
|
-
* @returns {string | null}
|
|
423
|
-
*/
|
|
424
|
-
static getParameterByName(name) {
|
|
425
|
-
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
|
|
426
|
-
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
|
|
427
|
-
results = regex.exec(location.search)
|
|
428
|
-
return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
|
|
429
|
-
}
|
|
430
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A viewshed.
|
|
8
|
+
* @typedef {{x: number, y: number, z: number}} View */
|
|
9
|
+
|
|
10
|
+
import { select } from 'd3-selection'
|
|
11
|
+
import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
|
|
15
|
+
*
|
|
16
|
+
* @module core
|
|
17
|
+
* @author Joseph Davies, Julien Gaffuri
|
|
18
|
+
*/
|
|
19
|
+
export class GeoCanvas {
|
|
20
|
+
/**
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {HTMLCanvasElement} canvas
|
|
23
|
+
* @param {number} x The x coordinate of the view
|
|
24
|
+
* @param {number} y The y coordinate of the view
|
|
25
|
+
* @param {number} z The zoom level of the view (pixel size, in ground m)
|
|
26
|
+
* @param {object} opts
|
|
27
|
+
*/
|
|
28
|
+
constructor(canvas, x = 0, y = 0, z = 0, opts = undefined) {
|
|
29
|
+
this.opts = opts || {}
|
|
30
|
+
|
|
31
|
+
/** @type {HTMLCanvasElement} */
|
|
32
|
+
this.canvas = canvas
|
|
33
|
+
|
|
34
|
+
/** @type {number} */
|
|
35
|
+
this.w = this.canvas.offsetWidth
|
|
36
|
+
/** @type {number} */
|
|
37
|
+
this.h = this.canvas.offsetHeight
|
|
38
|
+
|
|
39
|
+
// Adjust canvas width and height based on device pixel ratio
|
|
40
|
+
//const dpr = window.devicePixelRatio || 1 // Get the device pixel ratio
|
|
41
|
+
//this.canvas.width = this.w * dpr // Set canvas width
|
|
42
|
+
//\sthis.canvas.height = this.h * dpr // Set canvas height
|
|
43
|
+
|
|
44
|
+
// Create offscreen canvas for drawing operations
|
|
45
|
+
this.offscreenCanvas = document.createElement('canvas')
|
|
46
|
+
this.offscreenCanvas.width = this.w
|
|
47
|
+
this.offscreenCanvas.height = this.h
|
|
48
|
+
|
|
49
|
+
const ctx = this.canvas.getContext('2d')
|
|
50
|
+
const offscreenCtx = this.offscreenCanvas.getContext('2d')
|
|
51
|
+
if (!ctx) throw 'Impossible to create canvas 2D context'
|
|
52
|
+
if (!offscreenCtx) throw 'Impossible to create canvas 2D context'
|
|
53
|
+
/**@type {CanvasRenderingContext2D} */
|
|
54
|
+
this.ctx = ctx
|
|
55
|
+
this.offscreenCtx = offscreenCtx
|
|
56
|
+
//this.ctx.scale(dpr, dpr) // Scale the context
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* z: pixel size, in m/pix
|
|
60
|
+
* @type {View} */
|
|
61
|
+
this.view = { x: x, y: y, z: z }
|
|
62
|
+
|
|
63
|
+
/** Background color.
|
|
64
|
+
* @type {string} */
|
|
65
|
+
this.backgroundColor = opts.backgroundColor || 'white'
|
|
66
|
+
|
|
67
|
+
/** @type {function(object|undefined):void} */
|
|
68
|
+
this.onZoomStartFun = opts.onZoomStartFun
|
|
69
|
+
|
|
70
|
+
/** @type {function(object|undefined):void} */
|
|
71
|
+
this.onZoomEndFun = opts.onZoomEndFun
|
|
72
|
+
|
|
73
|
+
/** @type {function(object|undefined):void} */
|
|
74
|
+
this.onZoomFun = opts.onZoomFun
|
|
75
|
+
|
|
76
|
+
//current extent
|
|
77
|
+
/** @type {Envelope} */
|
|
78
|
+
this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
|
|
79
|
+
this.updateExtentGeo()
|
|
80
|
+
|
|
81
|
+
//rely on d3 for zoom
|
|
82
|
+
if (!opts.disableZoom) {
|
|
83
|
+
let tP = zoomIdentity
|
|
84
|
+
// @ts-ignore
|
|
85
|
+
let debounceTimeout = null // Add a debounce timeout variable
|
|
86
|
+
const z = d3zoom()
|
|
87
|
+
// to make the zooming a bit faster
|
|
88
|
+
.wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
|
|
89
|
+
.on('zoom', (e) => {
|
|
90
|
+
const t = e.transform
|
|
91
|
+
const zoomFactor = tP.k / t.k
|
|
92
|
+
if (zoomFactor == 1) {
|
|
93
|
+
//pan
|
|
94
|
+
const dx = tP.x - t.x
|
|
95
|
+
const dy = tP.y - t.y
|
|
96
|
+
this.pan(dx * this.view.z, -dy * this.view.z)
|
|
97
|
+
} else {
|
|
98
|
+
handleZoom(e, zoomFactor)
|
|
99
|
+
}
|
|
100
|
+
tP = t
|
|
101
|
+
|
|
102
|
+
if (this.onZoomFun) this.onZoomFun(e)
|
|
103
|
+
})
|
|
104
|
+
.on('start', (e) => {
|
|
105
|
+
// start of zoom event
|
|
106
|
+
// save the current canvas state to keep onscreen during pan/zoom before redrawing
|
|
107
|
+
this.canvasSave.c = document.createElement('canvas')
|
|
108
|
+
this.canvasSave.c.setAttribute('width', '' + this.w)
|
|
109
|
+
this.canvasSave.c.setAttribute('height', '' + this.h)
|
|
110
|
+
this.canvasSave.c.getContext('2d')?.drawImage(this.canvas, 0, 0)
|
|
111
|
+
this.canvasSave.dx = 0
|
|
112
|
+
this.canvasSave.dy = 0
|
|
113
|
+
this.canvasSave.f = 1
|
|
114
|
+
if (this.onZoomStartFun) this.onZoomStartFun(e)
|
|
115
|
+
})
|
|
116
|
+
.on('end', (e) => {
|
|
117
|
+
// end of pan/zoom event
|
|
118
|
+
this.redraw()
|
|
119
|
+
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
120
|
+
|
|
121
|
+
if (this.onZoomEndFun) this.onZoomEndFun(e)
|
|
122
|
+
})
|
|
123
|
+
// @ts-ignore
|
|
124
|
+
z(select(this.canvas))
|
|
125
|
+
|
|
126
|
+
const handleZoom = (event, zoomFactor) => {
|
|
127
|
+
// cancel ongoing data requests
|
|
128
|
+
this.cancelCurrentRequests()
|
|
129
|
+
const se = event.sourceEvent
|
|
130
|
+
|
|
131
|
+
if (se instanceof WheelEvent) {
|
|
132
|
+
//zoom at the mouse position
|
|
133
|
+
this.zoom(
|
|
134
|
+
zoomFactor,
|
|
135
|
+
// @ts-ignore
|
|
136
|
+
this.pixToGeoX(se.offsetX),
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
this.pixToGeoY(se.offsetY)
|
|
139
|
+
)
|
|
140
|
+
} else if (se instanceof TouchEvent) {
|
|
141
|
+
//compute average position of the touches
|
|
142
|
+
let tx = 0,
|
|
143
|
+
ty = 0
|
|
144
|
+
for (let tt of se.targetTouches) {
|
|
145
|
+
tx += tt.clientX
|
|
146
|
+
ty += tt.clientY
|
|
147
|
+
}
|
|
148
|
+
tx /= se.targetTouches.length
|
|
149
|
+
ty /= se.targetTouches.length
|
|
150
|
+
|
|
151
|
+
// Adjust for container's offset
|
|
152
|
+
// tx -= containerRect.left
|
|
153
|
+
// ty -= containerRect.top
|
|
154
|
+
|
|
155
|
+
//zoom at this average position
|
|
156
|
+
this.zoom(zoomFactor, this.pixToGeoX(tx), this.pixToGeoY(ty))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//center extent
|
|
162
|
+
/** @type {number|undefined} */
|
|
163
|
+
this.xMin = opts.centerExtent ? opts.centerExtent[0] : undefined
|
|
164
|
+
/** @type {number|undefined} */
|
|
165
|
+
this.yMin = opts.centerExtent ? opts.centerExtent[1] : undefined
|
|
166
|
+
/** @type {number|undefined} */
|
|
167
|
+
this.xMax = opts.centerExtent ? opts.centerExtent[2] : undefined
|
|
168
|
+
/** @type {number|undefined} */
|
|
169
|
+
this.yMax = opts.centerExtent ? opts.centerExtent[3] : undefined
|
|
170
|
+
|
|
171
|
+
/** Zoom extent, to limit zoom in and out
|
|
172
|
+
* @type {Array.<number>} */
|
|
173
|
+
this.zoomExtent = opts.zoomExtent || [0, Infinity]
|
|
174
|
+
|
|
175
|
+
/** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
|
|
176
|
+
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
|
|
177
|
+
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** @returns {View} */
|
|
181
|
+
getView() {
|
|
182
|
+
return this.view
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @param {Array.<number>} v */
|
|
186
|
+
setCenterExtent(v) {
|
|
187
|
+
this.xMin = v[0]
|
|
188
|
+
this.yMin = v[1]
|
|
189
|
+
this.xMax = v[2]
|
|
190
|
+
this.yMax = v[3]
|
|
191
|
+
}
|
|
192
|
+
/** @returns {Array.<number|undefined>} */
|
|
193
|
+
getCenterExtent() {
|
|
194
|
+
return [this.xMin, this.yMin, this.xMax, this.yMax]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @param {Array.<number>} v */
|
|
198
|
+
setZoomExtent(v) {
|
|
199
|
+
this.zoomExtent = v
|
|
200
|
+
}
|
|
201
|
+
/** @returns {Array.<number>} */
|
|
202
|
+
getZoomExtent() {
|
|
203
|
+
return this.zoomExtent
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Initialise canvas transform with identity transformation. */
|
|
207
|
+
initCanvasTransform() {
|
|
208
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
209
|
+
this.offscreenCtx.setTransform(1, 0, 0, 1, 0, 0)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
|
|
213
|
+
setCanvasTransform() {
|
|
214
|
+
const k = 1 / this.view.z
|
|
215
|
+
const tx = -this.view.x / this.view.z + this.w * 0.5
|
|
216
|
+
const ty = this.view.y / this.view.z + this.h * 0.5
|
|
217
|
+
this.ctx.setTransform(k, 0, 0, -k, tx, ty)
|
|
218
|
+
this.offscreenCtx.setTransform(k, 0, 0, -k, tx, ty)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
|
|
222
|
+
getWebGLTransform() {
|
|
223
|
+
const kx = 2.0 / (this.w * this.view.z)
|
|
224
|
+
const ky = 2.0 / (this.h * this.view.z)
|
|
225
|
+
return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.view.x, -ky * this.view.y, 1.0]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The function specifying how to draw the map. */
|
|
229
|
+
redraw() {
|
|
230
|
+
throw new Error('Method redraw not implemented.')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
|
|
234
|
+
cancelCurrentRequests() {
|
|
235
|
+
throw new Error('Method cancelCurrentRequests not implemented.')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Clear. To be used before a redraw for example.
|
|
240
|
+
* @param {string} color
|
|
241
|
+
*/
|
|
242
|
+
clear(color = 'white') {
|
|
243
|
+
if (this.opts.transparentBackground) {
|
|
244
|
+
this.ctx.clearRect(0, 0, this.w, this.h)
|
|
245
|
+
this.offscreenCtx.clearRect(0, 0, this.w, this.h)
|
|
246
|
+
} else {
|
|
247
|
+
if (this.ctx) this.ctx.fillStyle = color
|
|
248
|
+
if (this.offscreenCtx) this.offscreenCtx.fillStyle = color
|
|
249
|
+
this.ctx.fillRect(0, 0, this.w, this.h)
|
|
250
|
+
this.offscreenCtx.fillRect(0, 0, this.w, this.h)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {number} dxGeo
|
|
256
|
+
* @param {number} dyGeo
|
|
257
|
+
*/
|
|
258
|
+
pan(dxGeo = 0, dyGeo = 0) {
|
|
259
|
+
//ensures x/y extent
|
|
260
|
+
if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
|
|
261
|
+
if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
|
|
262
|
+
if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
|
|
263
|
+
if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
|
|
264
|
+
|
|
265
|
+
//pan
|
|
266
|
+
this.view.x += dxGeo
|
|
267
|
+
this.view.y += dyGeo
|
|
268
|
+
this.updateExtentGeo()
|
|
269
|
+
|
|
270
|
+
if (this.canvasSave.c) {
|
|
271
|
+
const scale = 1 / this.view.z
|
|
272
|
+
|
|
273
|
+
// Update saved canvas offset
|
|
274
|
+
this.canvasSave.dx -= dxGeo * scale
|
|
275
|
+
this.canvasSave.dy += dyGeo * scale
|
|
276
|
+
|
|
277
|
+
// clear canvas
|
|
278
|
+
this.clear(this.backgroundColor)
|
|
279
|
+
|
|
280
|
+
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
|
|
281
|
+
//this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
282
|
+
this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
283
|
+
|
|
284
|
+
// Render the offscreen canvas to the visible context
|
|
285
|
+
// this.clear(this.backgroundColor)
|
|
286
|
+
this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
|
|
287
|
+
} else {
|
|
288
|
+
console.log('no canvas save')
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Zoom.
|
|
294
|
+
* @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
|
|
295
|
+
* @param {number} xGeo The x geo position fixed in the screen.
|
|
296
|
+
* @param {number} yGeo The y geo position fixed in the screen.
|
|
297
|
+
*/
|
|
298
|
+
zoom(f = 1, xGeo = this.view.x, yGeo = this.view.y) {
|
|
299
|
+
//TODO force geo extend to remain
|
|
300
|
+
|
|
301
|
+
//trying to zoom in/out beyond limit
|
|
302
|
+
if (this.zoomExtent[0] == this.view.z && f <= 1) return
|
|
303
|
+
if (this.zoomExtent[1] == this.view.z && f >= 1) return
|
|
304
|
+
|
|
305
|
+
//ensure zoom extent preserved
|
|
306
|
+
const newZf = f * this.view.z
|
|
307
|
+
if (newZf < this.zoomExtent[0]) f = this.zoomExtent[0] / this.view.z
|
|
308
|
+
if (newZf > this.zoomExtent[1]) f = this.zoomExtent[1] / this.view.z
|
|
309
|
+
|
|
310
|
+
this.view.z *= f
|
|
311
|
+
|
|
312
|
+
//compute pan
|
|
313
|
+
let dxGeo = (xGeo - this.view.x) * (1 - f)
|
|
314
|
+
let dyGeo = (yGeo - this.view.y) * (1 - f)
|
|
315
|
+
|
|
316
|
+
//ensures x/y extent
|
|
317
|
+
if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
|
|
318
|
+
if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
|
|
319
|
+
if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
|
|
320
|
+
if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
|
|
321
|
+
|
|
322
|
+
//pan
|
|
323
|
+
this.view.x += dxGeo
|
|
324
|
+
this.view.y += dyGeo
|
|
325
|
+
this.updateExtentGeo()
|
|
326
|
+
|
|
327
|
+
// zoom in on the current canvas state
|
|
328
|
+
if (this.canvasSave.c) {
|
|
329
|
+
this.clear(this.backgroundColor)
|
|
330
|
+
this.canvasSave.f /= f
|
|
331
|
+
this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
|
|
332
|
+
this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
|
|
333
|
+
this.clear(this.backgroundColor)
|
|
334
|
+
this.offscreenCtx.drawImage(
|
|
335
|
+
this.canvasSave.c,
|
|
336
|
+
this.canvasSave.dx,
|
|
337
|
+
this.canvasSave.dy,
|
|
338
|
+
this.canvasSave.f * this.canvasSave.c.width,
|
|
339
|
+
this.canvasSave.f * this.canvasSave.c.height
|
|
340
|
+
)
|
|
341
|
+
this.ctx.drawImage(
|
|
342
|
+
this.offscreenCanvas, // Use offscreen canvas as the source
|
|
343
|
+
0,
|
|
344
|
+
0, // Position the offscreen canvas at the top-left corner of the main canvas
|
|
345
|
+
this.canvas.width, // The width of the visible canvas
|
|
346
|
+
this.canvas.height // The height of the visible canvas
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {number} marginPx
|
|
353
|
+
* @returns {Envelope} The envelope of the view, in geo coordinates.
|
|
354
|
+
*/
|
|
355
|
+
updateExtentGeo(marginPx = 20) {
|
|
356
|
+
this.extGeo = {
|
|
357
|
+
xMin: this.pixToGeoX(-marginPx),
|
|
358
|
+
xMax: this.pixToGeoX(this.w + marginPx),
|
|
359
|
+
yMin: this.pixToGeoY(this.h + marginPx),
|
|
360
|
+
yMax: this.pixToGeoY(-marginPx),
|
|
361
|
+
}
|
|
362
|
+
return this.extGeo
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if the object has to be drawn
|
|
367
|
+
*
|
|
368
|
+
* @param {{x:number,y:number}} obj
|
|
369
|
+
*/
|
|
370
|
+
toDraw(obj) {
|
|
371
|
+
if (obj.x < this.extGeo.xMin) return false
|
|
372
|
+
if (obj.x > this.extGeo.xMax) return false
|
|
373
|
+
if (obj.y < this.extGeo.yMin) return false
|
|
374
|
+
if (obj.y > this.extGeo.yMax) return false
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
//conversion functions
|
|
379
|
+
/**
|
|
380
|
+
* @param {number} xGeo Geo x coordinate, in m.
|
|
381
|
+
* @returns {number} Screen x coordinate, in pix.
|
|
382
|
+
*/
|
|
383
|
+
geoToPixX(xGeo) {
|
|
384
|
+
return (xGeo - this.view.x) / this.view.z + this.w * 0.5
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* @param {number} yGeo Geo y coordinate, in m.
|
|
388
|
+
* @returns {number} Screen y coordinate, in pix.
|
|
389
|
+
*/
|
|
390
|
+
geoToPixY(yGeo) {
|
|
391
|
+
return -(yGeo - this.view.y) / this.view.z + this.h * 0.5
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* @param {number} x Screen x coordinate, in pix.
|
|
395
|
+
* @returns {number} Geo x coordinate, in m.
|
|
396
|
+
*/
|
|
397
|
+
pixToGeoX(x) {
|
|
398
|
+
return (x - this.w * 0.5) * this.view.z + this.view.x
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* @param {number} y Screen y coordinate, in pix.
|
|
402
|
+
* @returns {number} Geo y coordinate, in m.
|
|
403
|
+
*/
|
|
404
|
+
pixToGeoY(y) {
|
|
405
|
+
return -(y - this.h * 0.5) * this.view.z + this.view.y
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Get x,y,z elements from URL and assign them to the view. */
|
|
409
|
+
setViewFromURL() {
|
|
410
|
+
const x = GeoCanvas.getParameterByName('x'),
|
|
411
|
+
y = GeoCanvas.getParameterByName('y'),
|
|
412
|
+
z = GeoCanvas.getParameterByName('z')
|
|
413
|
+
if (x != null && x != undefined && !isNaN(+x)) this.view.x = +x
|
|
414
|
+
if (y != null && y != undefined && !isNaN(+y)) this.view.y = +y
|
|
415
|
+
if (z != null && z != undefined && !isNaN(+z)) this.view.z = +z
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get a URL parameter by name.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} name
|
|
422
|
+
* @returns {string | null}
|
|
423
|
+
*/
|
|
424
|
+
static getParameterByName(name) {
|
|
425
|
+
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
|
|
426
|
+
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
|
|
427
|
+
results = regex.exec(location.search)
|
|
428
|
+
return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
|
|
429
|
+
}
|
|
430
|
+
}
|