@deck.gl-community/infovis-layers 9.1.0-beta.8
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/LICENSE +19 -0
- package/README.md +5 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +21 -0
- package/src/layers/time-axis-layer.ts +102 -0
- package/src/layers/time-delta-layer.ts +170 -0
- package/src/layers/vertical-grid-layer.ts +69 -0
- package/src/synchronized-views/synchronized-views.ts +113 -0
- package/src/utils/format-utils.ts +80 -0
- package/src/utils/tick-utils.ts +41 -0
- package/src/views/layer-filter.ts +64 -0
- package/src/views/orthographic-utils.ts +70 -0
- package/src/views/view-state-utils.ts +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2022 - 2023 vis.gl contributors
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
19
|
+
THE SOFTWARE.
|
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB,+BAA+B;AAC/B,oCAAoC;AAEpC,MAAM"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deck.gl-community/infovis-layers",
|
|
3
|
+
"version": "9.1.0-beta.8",
|
|
4
|
+
"description": "Infovis layers (non-geospatial) for deck.gl",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"layers",
|
|
8
|
+
"visualization",
|
|
9
|
+
"gpu",
|
|
10
|
+
"deck.gl"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"require": "./dist/index.cjs",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test-watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@deck.gl/core": "^9.1.12",
|
|
34
|
+
"@deck.gl/extensions": "^9.1.0",
|
|
35
|
+
"@deck.gl/layers": "^9.1.0",
|
|
36
|
+
"@deck.gl/mesh-layers": "^9.1.0",
|
|
37
|
+
"@deck.gl/react": "^9.1.0",
|
|
38
|
+
"@loaders.gl/core": "^4.2.0",
|
|
39
|
+
"@loaders.gl/loader-utils": "^4.2.0",
|
|
40
|
+
"@loaders.gl/schema": "^4.2.0",
|
|
41
|
+
"@luma.gl/constants": "^9.1.9",
|
|
42
|
+
"@luma.gl/core": "^9.1.9",
|
|
43
|
+
"@luma.gl/engine": "^9.1.9",
|
|
44
|
+
"@math.gl/core": "^4.0.0",
|
|
45
|
+
"a5-js": "^0.1.4",
|
|
46
|
+
"h3-js": "^4.2.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@deck.gl/test-utils": "^9.1.12",
|
|
50
|
+
"@luma.gl/webgpu": "^9.1.9"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export {TimeAxisLayer, type TimeAxisLayerProps} from './layers/time-axis-layer';
|
|
6
|
+
export {VerticalGridLayer, type VerticalGridLayerProps} from './layers/vertical-grid-layer';
|
|
7
|
+
export {TimeDeltaLayer, type TimeDeltaLayerProps} from './layers/time-delta-layer';
|
|
8
|
+
|
|
9
|
+
export {fitBoundsOrthographic} from './views/orthographic-utils';
|
|
10
|
+
export {formatTimeMs, formatTimeRangeMs} from './utils/format-utils';
|
|
11
|
+
export {validateViewState, mergeViewStates, type OptionalViewState} from './views/view-state-utils';
|
|
12
|
+
export {makeLayerFilter} from './views/layer-filter';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
HEADER_VIEW_HEIGHT,
|
|
16
|
+
LEGEND_VIEW_WIDTH,
|
|
17
|
+
SYNCHRONIZED_VIEWS,
|
|
18
|
+
SYNCHRONIZED_VIEW_STATE_CONSTRAINTS,
|
|
19
|
+
getSynchronizedViewStates,
|
|
20
|
+
fitSynchronizedViewStatesToBounds
|
|
21
|
+
} from './synchronized-views/synchronized-views';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, type CompositeLayerProps, type UpdateParameters} from '@deck.gl/core';
|
|
6
|
+
import {LineLayer, TextLayer} from '@deck.gl/layers';
|
|
7
|
+
|
|
8
|
+
import {formatTimeMs} from '../utils/format-utils';
|
|
9
|
+
import {getPrettyTicks, getZoomedRange} from '../utils/tick-utils';
|
|
10
|
+
|
|
11
|
+
export type TimeAxisLayerProps = CompositeLayerProps & {
|
|
12
|
+
unit: 'timestamp' | 'milliseconds';
|
|
13
|
+
/** Start time in milliseconds since epoch */
|
|
14
|
+
startTimeMs: number;
|
|
15
|
+
/** End time in milliseconds since epoch */
|
|
16
|
+
endTimeMs: number;
|
|
17
|
+
/** Optional: Number of tick marks (default: 5) */
|
|
18
|
+
tickCount?: number;
|
|
19
|
+
/** Optional: Y-coordinate for the axis line (default: 0) */
|
|
20
|
+
y?: number;
|
|
21
|
+
/** Optional: RGBA color for axis and ticks (default: [0, 0, 0, 255]) */
|
|
22
|
+
color?: [number, number, number, number];
|
|
23
|
+
/** Optional: Bounds for the axis line (default: viewport bounds) */
|
|
24
|
+
bounds?: [number, number, number, number];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class TimeAxisLayer extends CompositeLayer<TimeAxisLayerProps> {
|
|
28
|
+
static override layerName = 'TimeAxisLayer';
|
|
29
|
+
static override defaultProps: Required<Omit<TimeAxisLayerProps, keyof CompositeLayerProps>> = {
|
|
30
|
+
startTimeMs: 0,
|
|
31
|
+
endTimeMs: 100,
|
|
32
|
+
tickCount: 5,
|
|
33
|
+
y: 0,
|
|
34
|
+
color: [0, 0, 0, 255],
|
|
35
|
+
unit: 'timestamp',
|
|
36
|
+
bounds: undefined!
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Called whenever props/data/viewports change
|
|
40
|
+
override shouldUpdateState(params: UpdateParameters<TimeAxisLayer>): boolean {
|
|
41
|
+
return params.changeFlags.viewportChanged || super.shouldUpdateState(params);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override renderLayers() {
|
|
45
|
+
const {startTimeMs, endTimeMs, tickCount = 10, y = 0, color = [0, 0, 0, 255]} = this.props;
|
|
46
|
+
|
|
47
|
+
let bounds: [number, number, number, number];
|
|
48
|
+
try {
|
|
49
|
+
bounds = this.context.viewport.getBounds();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log('Error getting bounds from viewport:', error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const [startTimeZoomed, endTimeZoomed] = getZoomedRange(startTimeMs, endTimeMs, bounds);
|
|
56
|
+
// Generate tick positions and labels
|
|
57
|
+
const ticks = getPrettyTicks(startTimeZoomed, endTimeZoomed, tickCount);
|
|
58
|
+
|
|
59
|
+
const tickLines = ticks.map((x) => ({
|
|
60
|
+
sourcePosition: [x, y - 5],
|
|
61
|
+
targetPosition: [x, y + 5]
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const tickLabels = ticks.map((x) => ({
|
|
65
|
+
position: [x, y - 10],
|
|
66
|
+
text:
|
|
67
|
+
this.props.unit === 'timestamp' ? new Date(x).toLocaleTimeString() : formatTimeMs(x, false)
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
// Axis line
|
|
72
|
+
new LineLayer({
|
|
73
|
+
id: 'axis-line',
|
|
74
|
+
data: [{sourcePosition: [startTimeZoomed, y], targetPosition: [endTimeZoomed, y]}],
|
|
75
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
76
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
77
|
+
getColor: color,
|
|
78
|
+
getWidth: 2
|
|
79
|
+
}),
|
|
80
|
+
// Tick marks
|
|
81
|
+
new LineLayer({
|
|
82
|
+
id: 'tick-marks',
|
|
83
|
+
data: tickLines,
|
|
84
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
85
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
86
|
+
getColor: color,
|
|
87
|
+
getWidth: 1
|
|
88
|
+
}),
|
|
89
|
+
// Tick labels
|
|
90
|
+
new TextLayer({
|
|
91
|
+
id: 'tick-labels',
|
|
92
|
+
data: tickLabels,
|
|
93
|
+
getPosition: (d) => d.position,
|
|
94
|
+
getText: (d) => d.text,
|
|
95
|
+
getSize: 12,
|
|
96
|
+
getColor: color,
|
|
97
|
+
getTextAnchor: 'middle',
|
|
98
|
+
getAlignmentBaseline: 'top'
|
|
99
|
+
})
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, type LayerProps} from '@deck.gl/core';
|
|
6
|
+
import {LineLayer, TextLayer} from '@deck.gl/layers';
|
|
7
|
+
|
|
8
|
+
// import {PathStyleExtension} from '@deck.gl/extensions';
|
|
9
|
+
|
|
10
|
+
import {formatTimeMs} from '../utils/format-utils';
|
|
11
|
+
|
|
12
|
+
export type TimeDeltaLayerProps = LayerProps & {
|
|
13
|
+
unit: 'timestamp' | 'milliseconds';
|
|
14
|
+
minTimeMs: number;
|
|
15
|
+
maxTimeMs: number;
|
|
16
|
+
/** Start time in milliseconds since epoch */
|
|
17
|
+
startTimeMs: number;
|
|
18
|
+
/** End time in milliseconds since epoch */
|
|
19
|
+
endTimeMs: number;
|
|
20
|
+
/** Optional: Y-coordinate for the axis line (default: 0) */
|
|
21
|
+
y?: number;
|
|
22
|
+
header: boolean;
|
|
23
|
+
|
|
24
|
+
/** Optional: RGBA color for axis and ticks (default: [0, 0, 0, 255]) */
|
|
25
|
+
color?: [number, number, number, number];
|
|
26
|
+
/** Minimum Y-coordinate for grid lines. @todo (ib) Remve and calculate from viewport? */
|
|
27
|
+
yMin?: number;
|
|
28
|
+
/** Maximum Y-coordinate for grid lines. @todo (ib) Remve and calculate from viewport? */
|
|
29
|
+
yMax?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class TimeDeltaLayer extends CompositeLayer<TimeDeltaLayerProps> {
|
|
33
|
+
static override layerName = 'TimeDeltaLayer';
|
|
34
|
+
static override defaultProps: Required<Omit<TimeDeltaLayerProps, keyof LayerProps>> = {
|
|
35
|
+
header: false,
|
|
36
|
+
minTimeMs: 0,
|
|
37
|
+
maxTimeMs: 100,
|
|
38
|
+
startTimeMs: 0,
|
|
39
|
+
endTimeMs: 100,
|
|
40
|
+
y: 0,
|
|
41
|
+
color: [0, 0, 0, 255],
|
|
42
|
+
unit: 'timestamp',
|
|
43
|
+
yMin: -1e6, // Should cover full viewport height in most cases
|
|
44
|
+
yMax: 1e6 // Should cover full viewport height in most cases
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
override renderLayers() {
|
|
48
|
+
const {startTimeMs, endTimeMs, color = [0, 0, 0, 255], yMin, yMax} = this.props;
|
|
49
|
+
|
|
50
|
+
const timeDeltaPosition = [(startTimeMs + endTimeMs) / 2, 10];
|
|
51
|
+
const timeDeltaMs = Math.abs(endTimeMs - startTimeMs);
|
|
52
|
+
const timeDeltaLabel = formatTimeMs(timeDeltaMs, false);
|
|
53
|
+
|
|
54
|
+
if (!this.props.header) {
|
|
55
|
+
const timeLines = [
|
|
56
|
+
{
|
|
57
|
+
sourcePosition: [startTimeMs, yMin],
|
|
58
|
+
targetPosition: [startTimeMs, yMax]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
sourcePosition: [endTimeMs, yMin],
|
|
62
|
+
targetPosition: [endTimeMs, yMax]
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
return [
|
|
66
|
+
// Interval end lines
|
|
67
|
+
new LineLayer({
|
|
68
|
+
id: 'time-delta-side-bars',
|
|
69
|
+
data: timeLines,
|
|
70
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
71
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
72
|
+
getColor: color,
|
|
73
|
+
getWidth: 4,
|
|
74
|
+
widthUnits: 'pixels'
|
|
75
|
+
})
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// // Tick marks
|
|
80
|
+
// new LineLayer({
|
|
81
|
+
// id: 'time-delta-marks',
|
|
82
|
+
// data: tickLines,
|
|
83
|
+
// getSourcePosition: (d) => d.sourcePosition,
|
|
84
|
+
// getTargetPosition: (d) => d.targetPosition,
|
|
85
|
+
// getColor: color,
|
|
86
|
+
// getWidth: 1,
|
|
87
|
+
// }),
|
|
88
|
+
|
|
89
|
+
// TODO - triggers an update of monorepo root files
|
|
90
|
+
// new PathLayer({
|
|
91
|
+
// id: 'dotted-path',
|
|
92
|
+
// data: route,
|
|
93
|
+
// getPath: d => d.path,
|
|
94
|
+
// getWidth: 4,
|
|
95
|
+
// getColor: [255, 0, 0],
|
|
96
|
+
|
|
97
|
+
// // Enable rounded caps on each dash
|
|
98
|
+
// rounded: true,
|
|
99
|
+
|
|
100
|
+
// // Add the dash extension
|
|
101
|
+
// extensions: [
|
|
102
|
+
// new PathStyleExtension({
|
|
103
|
+
// dash: true,
|
|
104
|
+
// // highPrecisionDash: true, // uncomment for finer control at low zoom
|
|
105
|
+
// capRounded: true // draw dash ends as semicircles
|
|
106
|
+
// })
|
|
107
|
+
// ],
|
|
108
|
+
|
|
109
|
+
// // [dashLength, gapLength] in pixels
|
|
110
|
+
// // small dash + equal or larger gap = dotted effect
|
|
111
|
+
// getDashArray: () => [2, 6]
|
|
112
|
+
// })
|
|
113
|
+
// ]
|
|
114
|
+
const HEADER_Y = 12;
|
|
115
|
+
const timeLines = [
|
|
116
|
+
{
|
|
117
|
+
sourcePosition: [startTimeMs, -100],
|
|
118
|
+
targetPosition: [startTimeMs, HEADER_Y]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
sourcePosition: [endTimeMs, -100],
|
|
122
|
+
targetPosition: [endTimeMs, HEADER_Y]
|
|
123
|
+
}
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
return [
|
|
127
|
+
// Interval end lines
|
|
128
|
+
new LineLayer({
|
|
129
|
+
id: 'header-time-delta-side-bars',
|
|
130
|
+
data: timeLines,
|
|
131
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
132
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
133
|
+
getColor: color,
|
|
134
|
+
getWidth: 4,
|
|
135
|
+
widthUnits: 'pixels'
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
// Interval center
|
|
139
|
+
new LineLayer({
|
|
140
|
+
id: 'header-time-delta-dotted-line',
|
|
141
|
+
data: [
|
|
142
|
+
{
|
|
143
|
+
sourcePosition: [startTimeMs, HEADER_Y - 7],
|
|
144
|
+
targetPosition: [endTimeMs, HEADER_Y - 7]
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
148
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
149
|
+
getColor: color,
|
|
150
|
+
getWidth: 1,
|
|
151
|
+
widthUnits: 'pixels'
|
|
152
|
+
}),
|
|
153
|
+
|
|
154
|
+
// Label
|
|
155
|
+
new TextLayer({
|
|
156
|
+
id: 'header-time-delta-label',
|
|
157
|
+
data: [{position: timeDeltaPosition, text: timeDeltaLabel}],
|
|
158
|
+
getPosition: (d) => d.position,
|
|
159
|
+
getText: (d) => d.text,
|
|
160
|
+
getSize: 12,
|
|
161
|
+
getColor: color,
|
|
162
|
+
getTextAnchor: 'middle',
|
|
163
|
+
getAlignmentBaseline: 'top',
|
|
164
|
+
background: true,
|
|
165
|
+
getBackgroundColor: [255, 255, 255, 255], // Solid green background
|
|
166
|
+
backgroundPadding: [4, 2] // Horizontal and vertical padding
|
|
167
|
+
})
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, type CompositeLayerProps, type UpdateParameters} from '@deck.gl/core';
|
|
6
|
+
import {LineLayer} from '@deck.gl/layers';
|
|
7
|
+
|
|
8
|
+
import {getPrettyTicks, getZoomedRange} from '../utils/tick-utils';
|
|
9
|
+
|
|
10
|
+
export type VerticalGridLayerProps = CompositeLayerProps & {
|
|
11
|
+
/** Start time in milliseconds since epoch */
|
|
12
|
+
xMin: number;
|
|
13
|
+
/** End time in milliseconds since epoch */
|
|
14
|
+
xMax: number;
|
|
15
|
+
/** Optional: Number of tick marks (default: 5) */
|
|
16
|
+
tickCount?: number;
|
|
17
|
+
/** Minimum Y-coordinate for grid lines */
|
|
18
|
+
yMin?: number;
|
|
19
|
+
/** Maximum Y-coordinate for grid lines */
|
|
20
|
+
yMax?: number;
|
|
21
|
+
/** Optional: Width of the grid lines (default: 1) */
|
|
22
|
+
width?: number;
|
|
23
|
+
/** Optional: RGBA color for grid lines (default: [200, 200, 200, 255]) */
|
|
24
|
+
color?: [number, number, number, number];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class VerticalGridLayer extends CompositeLayer<VerticalGridLayerProps> {
|
|
28
|
+
static override layerName = 'VerticalGridLayer';
|
|
29
|
+
static override defaultProps = {
|
|
30
|
+
yMin: -1e6,
|
|
31
|
+
yMax: 1e6,
|
|
32
|
+
tickCount: 5,
|
|
33
|
+
width: 1,
|
|
34
|
+
color: [200, 200, 200, 255]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
override shouldUpdateState(params: UpdateParameters<VerticalGridLayer>): boolean {
|
|
38
|
+
return params.changeFlags.viewportChanged || super.shouldUpdateState(params);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override renderLayers() {
|
|
42
|
+
const {xMin, xMax, tickCount = 5, yMin, yMax, color} = this.props;
|
|
43
|
+
|
|
44
|
+
// Access the current viewport
|
|
45
|
+
const viewport = this.context.viewport;
|
|
46
|
+
const bounds = viewport.getBounds(); // Returns [minX, minY, maxX, maxY]
|
|
47
|
+
|
|
48
|
+
// Calculate the visible time range based on the viewport bounds
|
|
49
|
+
const [startTimeZoomed, endTimeZoomed] = getZoomedRange(xMin, xMax, bounds);
|
|
50
|
+
|
|
51
|
+
// Generate tick positions
|
|
52
|
+
const tickPositions = getPrettyTicks(startTimeZoomed, endTimeZoomed, tickCount);
|
|
53
|
+
|
|
54
|
+
// Create vertical grid lines at each tick position
|
|
55
|
+
const gridLines = tickPositions.map((x) => ({
|
|
56
|
+
sourcePosition: [x, yMin],
|
|
57
|
+
targetPosition: [x, yMax]
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return new LineLayer({
|
|
61
|
+
id: `${this.props.id}-lines`,
|
|
62
|
+
data: gridLines,
|
|
63
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
64
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
65
|
+
getColor: color,
|
|
66
|
+
getWidth: 1
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {OrthographicController, OrthographicView} from '@deck.gl/core';
|
|
6
|
+
|
|
7
|
+
import {fitBoundsOrthographic} from '../views/orthographic-utils';
|
|
8
|
+
import {mergeViewStates} from '../views/view-state-utils';
|
|
9
|
+
|
|
10
|
+
import type {OptionalViewState} from '../views/view-state-utils';
|
|
11
|
+
import type {OrthographicViewState} from '@deck.gl/core';
|
|
12
|
+
|
|
13
|
+
export const HEADER_VIEW_HEIGHT = 50;
|
|
14
|
+
export const LEGEND_VIEW_WIDTH = 150;
|
|
15
|
+
|
|
16
|
+
export const SYNCHRONIZED_VIEWS = [
|
|
17
|
+
new OrthographicView({
|
|
18
|
+
id: 'main',
|
|
19
|
+
flipY: false,
|
|
20
|
+
clear: true, // [1, 1, 1, 1],
|
|
21
|
+
x: LEGEND_VIEW_WIDTH,
|
|
22
|
+
width: `calc(100% - ${LEGEND_VIEW_WIDTH}px`,
|
|
23
|
+
y: HEADER_VIEW_HEIGHT,
|
|
24
|
+
height: `calc(100% - ${HEADER_VIEW_HEIGHT}px)`,
|
|
25
|
+
controller: {
|
|
26
|
+
type: OrthographicController,
|
|
27
|
+
// @ts-expect-error Specific to OrthographicController
|
|
28
|
+
zoomAxis: 'X',
|
|
29
|
+
inertia: false,
|
|
30
|
+
scrollZoom: true
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
new OrthographicView({
|
|
34
|
+
id: 'header',
|
|
35
|
+
flipY: false,
|
|
36
|
+
clear: true, // [1, 1, 1, 1],
|
|
37
|
+
x: LEGEND_VIEW_WIDTH,
|
|
38
|
+
width: `calc(100% - ${LEGEND_VIEW_WIDTH}px)`,
|
|
39
|
+
height: 50,
|
|
40
|
+
controller: false
|
|
41
|
+
}),
|
|
42
|
+
new OrthographicView({
|
|
43
|
+
id: 'legend',
|
|
44
|
+
flipY: false,
|
|
45
|
+
clear: true, // [1, 1, 1, 1],
|
|
46
|
+
x: 0,
|
|
47
|
+
width: LEGEND_VIEW_WIDTH,
|
|
48
|
+
y: HEADER_VIEW_HEIGHT,
|
|
49
|
+
height: `calc(100% - ${HEADER_VIEW_HEIGHT}px)`,
|
|
50
|
+
controller: false
|
|
51
|
+
})
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const SYNCHRONIZED_VIEW_STATE_CONSTRAINTS = {
|
|
55
|
+
header: {target: [undefined, 20], zoom: [undefined, 0]} satisfies OptionalViewState,
|
|
56
|
+
legend: {target: [-30, undefined], zoom: [1, undefined]} satisfies OptionalViewState
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function getSynchronizedViewStates(viewState: OrthographicViewState) {
|
|
60
|
+
return {
|
|
61
|
+
header: mergeViewStates(viewState, SYNCHRONIZED_VIEW_STATE_CONSTRAINTS.header),
|
|
62
|
+
legend: mergeViewStates(viewState, SYNCHRONIZED_VIEW_STATE_CONSTRAINTS.legend),
|
|
63
|
+
main: viewState
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fitSynchronizedViewStatesToBounds(props: {
|
|
68
|
+
viewState: {
|
|
69
|
+
header: OrthographicViewState;
|
|
70
|
+
legend: OrthographicViewState;
|
|
71
|
+
main: OrthographicViewState;
|
|
72
|
+
};
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
bounds: [[xMin: number, yMin: number], [xMax: number, yMax: number]];
|
|
76
|
+
/** App should set to true on first call */
|
|
77
|
+
initialize: boolean;
|
|
78
|
+
headerHeight?: number;
|
|
79
|
+
legendWidth?: number;
|
|
80
|
+
}): {
|
|
81
|
+
header: OrthographicViewState;
|
|
82
|
+
legend: OrthographicViewState;
|
|
83
|
+
main: OrthographicViewState;
|
|
84
|
+
} {
|
|
85
|
+
const {viewState, initialize} = props;
|
|
86
|
+
const {headerHeight = HEADER_VIEW_HEIGHT, legendWidth = LEGEND_VIEW_WIDTH} = props;
|
|
87
|
+
|
|
88
|
+
// Handle cases where the window size is too small for the header and/or legend
|
|
89
|
+
const width = Math.max(props.width - legendWidth, 1);
|
|
90
|
+
const height = Math.max(props.height - headerHeight, 1);
|
|
91
|
+
|
|
92
|
+
const {target, zoom} = fitBoundsOrthographic(width, height, props.bounds, 'per-axis');
|
|
93
|
+
target[1] = target[1] - 1.5; // ADD SOME TIME SPACE
|
|
94
|
+
|
|
95
|
+
let mainViewState: OrthographicViewState;
|
|
96
|
+
// Avoid messing with y axis if we have already fitted the view state
|
|
97
|
+
if (initialize) {
|
|
98
|
+
mainViewState = {...viewState.main, target, zoom};
|
|
99
|
+
} else {
|
|
100
|
+
mainViewState = {
|
|
101
|
+
...viewState.main,
|
|
102
|
+
target: [viewState.main.target[0], target[1]],
|
|
103
|
+
zoom: [(viewState.main.zoom as [number, number])[0], zoom[1]]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const newViewState = {
|
|
107
|
+
...viewState,
|
|
108
|
+
main: mainViewState,
|
|
109
|
+
header: mergeViewStates(mainViewState, SYNCHRONIZED_VIEW_STATE_CONSTRAINTS.header),
|
|
110
|
+
legend: mergeViewStates(mainViewState, SYNCHRONIZED_VIEW_STATE_CONSTRAINTS.legend)
|
|
111
|
+
};
|
|
112
|
+
return newViewState;
|
|
113
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a time in microseconds to a human-readable string
|
|
7
|
+
* @param us Time in microseconds
|
|
8
|
+
*/
|
|
9
|
+
export function formatTimeMs(timeMs: number, space: boolean = true): string {
|
|
10
|
+
const sep = space ? ' ' : '';
|
|
11
|
+
const us = timeMs * 1000;
|
|
12
|
+
if (us === 0) {
|
|
13
|
+
return '0s';
|
|
14
|
+
}
|
|
15
|
+
if (Math.abs(us) < 1000) {
|
|
16
|
+
return `${floatToStr(us)}${sep}µs`;
|
|
17
|
+
}
|
|
18
|
+
const ms = us / 1000;
|
|
19
|
+
if (Math.abs(ms) < 1000) {
|
|
20
|
+
return `${floatToStr(ms)}${sep} ms`;
|
|
21
|
+
}
|
|
22
|
+
const s = ms / 1000;
|
|
23
|
+
if (Math.abs(s) < 60) {
|
|
24
|
+
return `${floatToStr(s)}${sep} s`;
|
|
25
|
+
}
|
|
26
|
+
const m = s / 60;
|
|
27
|
+
if (Math.abs(m) < 60) {
|
|
28
|
+
return `${floatToStr(m)}${sep} min`;
|
|
29
|
+
}
|
|
30
|
+
const h = m / 60;
|
|
31
|
+
if (Math.abs(h) < 24) {
|
|
32
|
+
return `${floatToStr(h)}${sep} hrs`;
|
|
33
|
+
}
|
|
34
|
+
const d = h / 24;
|
|
35
|
+
return `${floatToStr(d)}${sep} days`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatTimeRangeMs(startMs: number, endMs: number): string {
|
|
39
|
+
return `${formatTimeMs(startMs)} - ${formatTimeMs(endMs)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a float to a string
|
|
44
|
+
*/
|
|
45
|
+
function floatToStr(f: number, roundDigits: number = 5): string {
|
|
46
|
+
if (Number.isInteger(f)) {
|
|
47
|
+
return f.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 1; i < roundDigits - 1; i++) {
|
|
51
|
+
const rounded = parseFloat(f.toPrecision(i));
|
|
52
|
+
if (rounded === f) {
|
|
53
|
+
return rounded.toPrecision(i);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return f.toPrecision(roundDigits);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// export function formatTimesUs(ticks: number[]): string {
|
|
61
|
+
// // Try from 0 up to a reasonable max (e.g. 20)
|
|
62
|
+
// for (let d = 0; d <= 20; d++) {
|
|
63
|
+
// const seen = new Set<string>();
|
|
64
|
+
// let allDistinct = true;
|
|
65
|
+
// for (const t of ticks) {
|
|
66
|
+
// // Format each tick with d decimals
|
|
67
|
+
// const str = t.toFixed(d);
|
|
68
|
+
// if (seen.has(str)) {
|
|
69
|
+
// allDistinct = false;
|
|
70
|
+
// break;
|
|
71
|
+
// }
|
|
72
|
+
// seen.add(str);
|
|
73
|
+
// }
|
|
74
|
+
// if (allDistinct) {
|
|
75
|
+
// return d;
|
|
76
|
+
// }
|
|
77
|
+
// }
|
|
78
|
+
// // Fallback if somehow not distinct even at 20 decimals
|
|
79
|
+
// return 20;
|
|
80
|
+
// }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export function getZoomedRange(
|
|
6
|
+
startTime: number,
|
|
7
|
+
endTime: number,
|
|
8
|
+
bounds: [number, number, number, number]
|
|
9
|
+
) {
|
|
10
|
+
const [startTimeZoomed, , endTimeZoomed] = bounds;
|
|
11
|
+
// console.log(`startTimeZoomed: ${startTimeZoomed}, endTimeZoomed: ${endTimeZoomed}, tickInterval: ${tickInterval} tickCountZoomed: ${tickCountZoomed}`);
|
|
12
|
+
return [Math.max(startTime, startTimeZoomed), Math.min(endTime, endTimeZoomed)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get nicely rounded tick close to the natural spacing
|
|
17
|
+
* @param startTime
|
|
18
|
+
* @param endTime
|
|
19
|
+
* @param tickCount
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export function getPrettyTicks(startTime: number, endTime: number, tickCount: number = 5) {
|
|
23
|
+
const range = endTime - startTime;
|
|
24
|
+
const roughStep = range / (tickCount - 1);
|
|
25
|
+
const exponent = Math.floor(Math.log10(roughStep));
|
|
26
|
+
const base = Math.pow(10, exponent);
|
|
27
|
+
const multiples = [1, 2, 5, 10];
|
|
28
|
+
|
|
29
|
+
// Find the smallest multiple that is greater than or equal to roughStep
|
|
30
|
+
const niceStep = multiples.find((m) => base * m >= roughStep) * base;
|
|
31
|
+
|
|
32
|
+
const niceStart = Math.ceil(startTime / niceStep) * niceStep;
|
|
33
|
+
const niceEnd = Math.floor(endTime / niceStep) * niceStep;
|
|
34
|
+
|
|
35
|
+
const ticks = [];
|
|
36
|
+
for (let t = niceStart; t <= niceEnd; t += niceStep) {
|
|
37
|
+
ticks.push(t);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ticks;
|
|
41
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {type FilterContext} from '@deck.gl/core';
|
|
6
|
+
|
|
7
|
+
export type DeclarativeLayerFilter = Record<
|
|
8
|
+
string,
|
|
9
|
+
| {
|
|
10
|
+
include: string[];
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
exclude: string[];
|
|
14
|
+
}
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
type RegexpLayerFilter = Record<
|
|
18
|
+
string,
|
|
19
|
+
| {
|
|
20
|
+
include: RegExp[];
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
exclude: RegExp[];
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export function makeLayerFilter(
|
|
28
|
+
filters: DeclarativeLayerFilter
|
|
29
|
+
): (context: FilterContext) => boolean {
|
|
30
|
+
// Pre-compile the regexps for performance
|
|
31
|
+
const regexpFilters: RegexpLayerFilter = {};
|
|
32
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
33
|
+
if ('include' in value) {
|
|
34
|
+
regexpFilters[key] = {
|
|
35
|
+
include: value.include.map((v) => new RegExp(v))
|
|
36
|
+
};
|
|
37
|
+
} else {
|
|
38
|
+
regexpFilters[key] = {
|
|
39
|
+
exclude: value.exclude.map((v) => new RegExp(v))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Return a function that checks if a layer matches the filter
|
|
45
|
+
return ({viewport, layer}: FilterContext) => {
|
|
46
|
+
let visible = true;
|
|
47
|
+
const viewFilters = regexpFilters[viewport.id] || ({} as Record<string, RegExp[]>);
|
|
48
|
+
// Check if the layer matches the filters for this viewport
|
|
49
|
+
if ('include' in viewFilters) {
|
|
50
|
+
if (!viewFilters.include.some((regexp) => regexp.test(layer.id))) {
|
|
51
|
+
visible = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if ('exclude' in viewFilters) {
|
|
55
|
+
if (viewFilters.exclude.some((regexp) => regexp.test(layer.id))) {
|
|
56
|
+
visible = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// if (!visible) {
|
|
60
|
+
// console.log(`Viewport ${viewport.id}: filtering out layer ${layer.id}`);
|
|
61
|
+
// }
|
|
62
|
+
return visible;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
type Bounds = [[number, number], [number, number]];
|
|
6
|
+
|
|
7
|
+
export function fitBoundsOrthographic(
|
|
8
|
+
width: number,
|
|
9
|
+
height: number,
|
|
10
|
+
bounds: Readonly<Bounds>,
|
|
11
|
+
zoomMode: 'single'
|
|
12
|
+
): {target: [number, number]; zoom: number};
|
|
13
|
+
|
|
14
|
+
export function fitBoundsOrthographic(
|
|
15
|
+
width: number,
|
|
16
|
+
height: number,
|
|
17
|
+
bounds: Readonly<Bounds>,
|
|
18
|
+
zoomMode: 'per-axis'
|
|
19
|
+
): {target: [number, number]; zoom: [number, number]};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compute center & zoom for an OrthographicViewport so that `bounds` fills the viewport.
|
|
23
|
+
* @param width viewport width in px
|
|
24
|
+
* @param height viewport height in px
|
|
25
|
+
* @param bounds [[minX,minY],[maxX,maxY]] in the same world units you’re rendering
|
|
26
|
+
* @returns { target: [number, number], zoom: number }
|
|
27
|
+
* target: center of the viewport in world units
|
|
28
|
+
* zoom: deck.gl orthographic zoom level (log2(scale))
|
|
29
|
+
* (deck.gl orthographic zoom is the log2 of the scale factor)
|
|
30
|
+
*/
|
|
31
|
+
export function fitBoundsOrthographic(
|
|
32
|
+
width: number,
|
|
33
|
+
height: number,
|
|
34
|
+
bounds: Readonly<Bounds>,
|
|
35
|
+
zoomMode: 'single' | 'per-axis' = 'per-axis'
|
|
36
|
+
): {target: [number, number]; zoom: number | [number, number]} {
|
|
37
|
+
const [[minX, minY], [maxX, maxY]] = bounds;
|
|
38
|
+
|
|
39
|
+
// center of the box
|
|
40
|
+
const centerX = (minX + maxX) / 2;
|
|
41
|
+
const centerY = (minY + maxY) / 2;
|
|
42
|
+
|
|
43
|
+
// size of the box
|
|
44
|
+
const boxW = maxX - minX;
|
|
45
|
+
const boxH = maxY - minY;
|
|
46
|
+
|
|
47
|
+
// scale (world units → screen pixels)
|
|
48
|
+
const scaleX = width / boxW;
|
|
49
|
+
const scaleY = height / boxH;
|
|
50
|
+
|
|
51
|
+
// pick the smaller scale so the whole box fits
|
|
52
|
+
const scale = Math.min(scaleX, scaleY);
|
|
53
|
+
|
|
54
|
+
// deck.gl orthographic zoom is log2(scale)
|
|
55
|
+
const zoom = Math.log2(scale);
|
|
56
|
+
|
|
57
|
+
// 3) axis‐specific zooms (deck.gl’s orthographic zoom = log2(scale))
|
|
58
|
+
const zoomX = Math.log2(scaleX);
|
|
59
|
+
const zoomY = Math.log2(scaleY);
|
|
60
|
+
|
|
61
|
+
if (Number.isNaN(zoom) || Number.isNaN(zoomX) || Number.isNaN(zoom)) {
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn('Invalid zoom values:', {zoom, zoomX, zoomY});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
target: [centerX, centerY],
|
|
68
|
+
zoom: zoomMode === 'single' ? zoom : [zoomX, zoomY]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {type OrthographicViewState} from '@deck.gl/core';
|
|
6
|
+
|
|
7
|
+
export type OptionalizedViewState<T> = {
|
|
8
|
+
[K in keyof T]: T[K] extends [number, number] | undefined
|
|
9
|
+
? [number | undefined, number | undefined] | undefined
|
|
10
|
+
: T[K] extends [number, number, number] | undefined
|
|
11
|
+
? [number | undefined, number | undefined, number | undefined] | undefined
|
|
12
|
+
: T[K] extends [number, number] | number | undefined
|
|
13
|
+
? [number | undefined, number | undefined] | undefined
|
|
14
|
+
: T[K] extends [number, number, number] | [number, number] | undefined
|
|
15
|
+
?
|
|
16
|
+
| [number | undefined, number | undefined, number | undefined]
|
|
17
|
+
| [number | undefined, number | undefined]
|
|
18
|
+
| undefined
|
|
19
|
+
: T[K];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type OptionalViewState = OptionalizedViewState<OrthographicViewState>;
|
|
23
|
+
|
|
24
|
+
export function mergeViewStates(
|
|
25
|
+
viewState1: OrthographicViewState,
|
|
26
|
+
viewState2: OptionalViewState
|
|
27
|
+
): OrthographicViewState {
|
|
28
|
+
const target1 = viewState1.target ?? [0, 0];
|
|
29
|
+
const zoom1 = viewState1.zoom ?? [1, 1];
|
|
30
|
+
const mergedViewState = {
|
|
31
|
+
...viewState1,
|
|
32
|
+
...viewState2,
|
|
33
|
+
target: [viewState2.target?.[0] ?? target1[0], viewState2.target?.[1] ?? target1[1]],
|
|
34
|
+
zoom: [
|
|
35
|
+
// ts-expect-error view state typing is awfully optional
|
|
36
|
+
viewState2.zoom?.[0] ?? zoom1[0],
|
|
37
|
+
// ts-expect-error view state typing is awfully optional
|
|
38
|
+
viewState2.zoom?.[1] ?? zoom1[1]
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
// @ts-expect-error view state typing is awfully optional
|
|
42
|
+
validateViewState(mergedViewState);
|
|
43
|
+
// @ts-expect-error view state typing is awfully optional
|
|
44
|
+
return mergedViewState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line complexity
|
|
48
|
+
export function validateViewState(
|
|
49
|
+
viewState: OrthographicViewState
|
|
50
|
+
): viewState is OrthographicViewState {
|
|
51
|
+
const isTargetValid =
|
|
52
|
+
Array.isArray(viewState.target) &&
|
|
53
|
+
viewState.target[0] !== undefined &&
|
|
54
|
+
!Number.isNaN(viewState.target[0]) &&
|
|
55
|
+
viewState.target[1] !== undefined &&
|
|
56
|
+
!Number.isNaN(viewState.target[1]);
|
|
57
|
+
const isZoomValid =
|
|
58
|
+
Array.isArray(viewState.zoom) &&
|
|
59
|
+
viewState.zoom[0] !== undefined &&
|
|
60
|
+
!Number.isNaN(viewState.zoom[0]) &&
|
|
61
|
+
viewState.zoom[1] !== undefined &&
|
|
62
|
+
!Number.isNaN(viewState.zoom[1]);
|
|
63
|
+
|
|
64
|
+
if (!isTargetValid || !isZoomValid) {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.warn('Invalid viewState:', viewState);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return isTargetValid && isZoomValid;
|
|
70
|
+
}
|