@vcmap/viewshed 2.0.1

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.
@@ -0,0 +1,578 @@
1
+ import { VcsEvent, VcsObject } from '@vcmap/core';
2
+ import {
3
+ Camera,
4
+ Cartesian3,
5
+ Math as CesiumMath,
6
+ ShadowMap,
7
+ Color,
8
+ } from '@vcmap-cesium/engine';
9
+ import { check } from '@vcsuite/check';
10
+ import { parseEnumValue } from '@vcsuite/parsers';
11
+ import ViewshedCameraPrimitive from './viewshedPrimitive.js';
12
+
13
+ /**
14
+ * @enum {string}
15
+ * @property {string} CONE A cone Viewshed, that can be used for visibility analysis.
16
+ * @property {string} THREESIXTY A 360 degree Viewshed, that can be used for sensor analysis.
17
+ */
18
+ export const ViewshedTypes = {
19
+ CONE: 'cone',
20
+ THREESIXTY: '360',
21
+ };
22
+
23
+ /**
24
+ * @typedef {Object} ShadowFrustumOptions
25
+ * @property {number} fov The angle of the field of view (FOV), in radians.
26
+ * @property {number} aspectRatio The aspect ratio of the frustum's width to it's height.
27
+ * @property {number} near The distance of the near plane.
28
+ * @property {number} far The distance of the far plane.
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} ColorOptions
33
+ * @property {string=} [visibleColor] CSS color string of the visible parts of the shadow map
34
+ * @property {string=} [shadowColor] CSS color string of the hidden parts of the shadow map
35
+ */
36
+
37
+ /**
38
+ * Creates camera and sets frustum options and orientation.
39
+ * @param {import("@vcmap-cesium/engine").Scene} scene
40
+ * @param {ShadowFrustumOptions} frustumOptions
41
+ * @returns {import("@vcmap-cesium/engine").Camera}
42
+ */
43
+ function createShadowCamera(scene, frustumOptions) {
44
+ const camera = new Camera(scene);
45
+
46
+ const perspectiveFrustum =
47
+ /** @type {import("@vcmap-cesium/engine").PerspectiveFrustum} */ (
48
+ camera.frustum
49
+ );
50
+ perspectiveFrustum.fov = frustumOptions.fov;
51
+ perspectiveFrustum.near = frustumOptions.near;
52
+ perspectiveFrustum.aspectRatio = frustumOptions.aspectRatio;
53
+ perspectiveFrustum.far = frustumOptions.far;
54
+
55
+ return camera;
56
+ }
57
+
58
+ /**
59
+ * @typedef {Object} ViewshedSpecificOptions
60
+ * @property {ViewshedTypes} viewshedType Whether the viewshed has a spot light with limited field of view (cone) or a point light with 360° coverage (360).
61
+ * @property {import("ol/coordinate").Coordinate=} [position] The position of the viewshed. Height offset is added to Z value of position to determine actual height of viewshed.
62
+ * @property {ShadowFrustumOptions} [frustumOptions] The frustum options with far, near, fov and aspect ratio.
63
+ * @property {import("@vcmap-cesium/engine").HeadingPitchRollValues} [orientation] The values for heading and pitch. Roll is ignored.
64
+ * @property {ColorOptions} [colorOptions] The colors of the visible and the hidden areas.
65
+ * @property {boolean} [showPrimitive=false] Whether the viewsheds primitve should be shown.
66
+ * @property {number} [heightOffset=0] Height offset. Is added to Z value of position.
67
+ * @typedef {import("@vcmap/core").VcsObjectOptions & ViewshedSpecificOptions} ViewshedOptions
68
+ */
69
+
70
+ /**
71
+ * Viewshed class consists of a Cesium Shadow map and a primitive. Since there can only be one ShadowMap for each Scene, only one viewshed instance at a time is possible.
72
+ */
73
+ export default class Viewshed extends VcsObject {
74
+ static get className() {
75
+ return 'Viewshed';
76
+ }
77
+
78
+ /**
79
+ * Returns the default viewshed options.
80
+ * @returns {{frustum: ShadowFrustumOptions, shadowColor: string, visibleColor: string, orientation: import("@vcmap-cesium/engine").HeadingPitchRollValues, position: import("ol/coordinate").Coordinate}}
81
+ */
82
+ static getDefaultOptions() {
83
+ return {
84
+ frustum: {
85
+ fov: CesiumMath.PI / 3,
86
+ near: 1.0,
87
+ aspectRatio: 1.0,
88
+ far: 300,
89
+ },
90
+ shadowColor: '#3333331A',
91
+ visibleColor: '#FF990080',
92
+ orientation: {
93
+ heading: 0,
94
+ pitch: 0,
95
+ roll: 0,
96
+ },
97
+ position: [0, 0, 0],
98
+ };
99
+ }
100
+
101
+ static MIN_DISTANCE = 10;
102
+
103
+ /**
104
+ * @param {ViewshedOptions} options The options for the viewshed.
105
+ * @param {import("@vcmap/core").CesiumMap} [cesiumMap] The cesiumMap the viewshed should be applied to. If this parameter is passed, the viewshed is activated and all other viewsheds are deactivated.
106
+ */
107
+ constructor(options, cesiumMap) {
108
+ super(options);
109
+ /**
110
+ * @type {import("@vcmap/core").CesiumMap | null}
111
+ * @private
112
+ */
113
+ this._cesiumMap = null;
114
+ /**
115
+ * @type {import("@vcmap-cesium/engine").Camera | null}
116
+ * @private
117
+ */
118
+ this._shadowCamera = null;
119
+ /**
120
+ * @type {import("@vcmap-cesium/engine").Scene | null}
121
+ * @private
122
+ */
123
+ this._scene = null;
124
+
125
+ /**
126
+ * @type {ShadowFrustumOptions}
127
+ * @private
128
+ */
129
+ this._frustumOptions = {
130
+ fov:
131
+ options.frustumOptions?.fov || Viewshed.getDefaultOptions().frustum.fov,
132
+ far:
133
+ options.frustumOptions?.far || Viewshed.getDefaultOptions().frustum.far,
134
+ near:
135
+ options.frustumOptions?.near ||
136
+ Viewshed.getDefaultOptions().frustum.near,
137
+ aspectRatio:
138
+ options.frustumOptions?.aspectRatio ||
139
+ Viewshed.getDefaultOptions().frustum.aspectRatio,
140
+ };
141
+
142
+ /**
143
+ * @type {import("@vcmap-cesium/engine").HeadingPitchRollValues}
144
+ * @private
145
+ */
146
+ this._headingPitchRollValues = options.orientation || {
147
+ ...Viewshed.getDefaultOptions().orientation,
148
+ };
149
+
150
+ /**
151
+ * The viewsheds position, excluding height offset, in lat/lon degrees.
152
+ * @type {import("ol/coordinate.js").Coordinate}
153
+ * @private
154
+ */
155
+ this._position = options.position || Viewshed.getDefaultOptions().position;
156
+
157
+ /**
158
+ * @type {ViewshedTypes}
159
+ * @private
160
+ */
161
+ this._viewshedType = parseEnumValue(options.viewshedType, ViewshedTypes);
162
+ /**
163
+ * @type {{visibleColor: import("@vcmap-cesium/engine").Color, shadowColor: import("@vcmap-cesium/engine").Color}}
164
+ * @private
165
+ */
166
+ this._colors = {
167
+ visibleColor: Color.fromCssColorString(
168
+ options.colorOptions?.visibleColor ||
169
+ Viewshed.getDefaultOptions().visibleColor,
170
+ ),
171
+ shadowColor: Color.fromCssColorString(
172
+ options.colorOptions?.shadowColor ||
173
+ Viewshed.getDefaultOptions().shadowColor,
174
+ ),
175
+ };
176
+
177
+ /**
178
+ * @type {number}
179
+ * @private
180
+ */
181
+ this._heightOffset = options.heightOffset || 0;
182
+
183
+ /**
184
+ * @type {import("@vcmap-cesium/engine").ShadowMap | null}
185
+ * @private
186
+ */
187
+ this._shadowMap = null;
188
+
189
+ /**
190
+ * @type {ViewshedCameraPrimitive | null}
191
+ * @private
192
+ */
193
+ this._primitive = null;
194
+ /**
195
+ * @type {boolean}
196
+ * @private
197
+ */
198
+ this._showPrimitive = !!options.showPrimitive;
199
+
200
+ /**
201
+ * Makes sure that the viewshed instance is deactivated, if shadow map of another viewshed or plugin is applied to the scene of the CesiumMap.
202
+ * @private
203
+ */
204
+ this._shadowMapChangedListener = null;
205
+
206
+ /**
207
+ * @type {import("@vcmap/core").VcsEvent<number[]>}
208
+ * @private
209
+ */
210
+ this._positionChanged = new VcsEvent();
211
+
212
+ if (cesiumMap) {
213
+ this.activate(cesiumMap);
214
+ } else {
215
+ /**
216
+ * @type {boolean}
217
+ * @private
218
+ */
219
+ this._active = false;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Sets a new position for the shadow map. If no shadow map was created yet, due to missing position, this method triggers the creation of a shadow map.
225
+ * @param {import("ol/coordinate").Coordinate} coords The new positions coordinates in degrees
226
+ */
227
+ set position(coords) {
228
+ check(coords, [Number]);
229
+ this._position = coords;
230
+ if (this._shadowCamera) {
231
+ this._shadowCamera.position = Cartesian3.fromDegrees(
232
+ coords[0],
233
+ coords[1],
234
+ coords[2] + this._heightOffset,
235
+ );
236
+ // makes sure, that roll is always 0, even if user moves around the globe with viewshed in create mode.
237
+ this._shadowCamera.setView({ orientation: this._headingPitchRollValues });
238
+ this._updatePrimitive();
239
+ }
240
+ this._positionChanged.raiseEvent(coords);
241
+ }
242
+
243
+ /**
244
+ * @returns {import("ol/coordinate").Coordinate} The current position of the viewshed source.
245
+ */
246
+ get position() {
247
+ return [...this._position];
248
+ }
249
+
250
+ /**
251
+ * Getter for Event that is triggered each time the position is changed.
252
+ * @returns {import("@vcmap/core").VcsEvent<number[]>} The new position
253
+ */
254
+ get positionChanged() {
255
+ return this._positionChanged;
256
+ }
257
+
258
+ /**
259
+ * Sets the height offset of the viewshed.
260
+ * @param {number} value the offset that is added to the position height.
261
+ */
262
+ set heightOffset(value) {
263
+ this._heightOffset = value;
264
+ if (this._shadowCamera) {
265
+ this._shadowCamera.position = Cartesian3.fromDegrees(
266
+ this.position[0],
267
+ this.position[1],
268
+ this.position[2] + value,
269
+ );
270
+ this._updateShadowMap();
271
+ this._updatePrimitive();
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Getter for height offset.
277
+ * @returns {number}
278
+ */
279
+ get heightOffset() {
280
+ return this._heightOffset;
281
+ }
282
+
283
+ /**
284
+ * Sets the reach of the viewshed.
285
+ * @param {number} value The distance in meters.
286
+ */
287
+ set distance(value) {
288
+ this._frustumOptions.far =
289
+ value > Viewshed.MIN_DISTANCE ? value : Viewshed.MIN_DISTANCE;
290
+ if (this._shadowCamera) {
291
+ this._shadowCamera.frustum.far = this._frustumOptions.far;
292
+ this._updateShadowMap();
293
+ if (this._viewshedType === ViewshedTypes.CONE) {
294
+ this._updatePrimitive();
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Returns the reach of the viewshed.
301
+ * @returns {number} The distance in meters.
302
+ */
303
+ get distance() {
304
+ return this._frustumOptions.far;
305
+ }
306
+
307
+ /**
308
+ * Sets the field of view of a cone viewshed. Does not have a impact on 360 viewshed.
309
+ * @param {number} value The field of view in degrees.
310
+ */
311
+ set fov(value) {
312
+ this._frustumOptions.fov = value * CesiumMath.RADIANS_PER_DEGREE;
313
+ if (this._shadowCamera) {
314
+ /** @type {import("@vcmap-cesium/engine").PerspectiveFrustum} */ (
315
+ this._shadowCamera.frustum
316
+ ).fov = value * CesiumMath.RADIANS_PER_DEGREE;
317
+ this._updateShadowMap();
318
+ if (this._viewshedType === ViewshedTypes.CONE) {
319
+ this._updatePrimitive();
320
+ }
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Returns the field of view.
326
+ * @returns {number} The field of view in degrees.
327
+ */
328
+ get fov() {
329
+ return this._frustumOptions.fov * CesiumMath.DEGREES_PER_RADIAN;
330
+ }
331
+
332
+ /**
333
+ * Sets the heading of a cone viewshed. Does not have impact on 360 viewshed.
334
+ * @param {number} value Heading in degrees.
335
+ */
336
+ set heading(value) {
337
+ this._headingPitchRollValues.heading =
338
+ value * CesiumMath.RADIANS_PER_DEGREE;
339
+ if (this._shadowCamera) {
340
+ this._shadowCamera.setView({
341
+ orientation: {
342
+ heading: value * CesiumMath.RADIANS_PER_DEGREE,
343
+ pitch: this._shadowCamera.pitch,
344
+ roll: 0,
345
+ },
346
+ });
347
+ if (this._viewshedType === ViewshedTypes.CONE) {
348
+ this._updatePrimitive();
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Getter for the heading of a cone viewshed.
355
+ * @returns {number} The heading in degress.
356
+ */
357
+ get heading() {
358
+ return this._headingPitchRollValues.heading * CesiumMath.DEGREES_PER_RADIAN;
359
+ }
360
+
361
+ /**
362
+ * Sets the pitch of a cone viewshed. 0 is horizontal. Does not have impact on 360 viewshed.
363
+ * @param {number} value The pitch in degrees.
364
+ */
365
+ set pitch(value) {
366
+ this._headingPitchRollValues.pitch = value * CesiumMath.RADIANS_PER_DEGREE;
367
+ if (this._shadowCamera) {
368
+ const amount =
369
+ value * CesiumMath.RADIANS_PER_DEGREE - this._shadowCamera.pitch;
370
+ this._shadowCamera.lookUp(amount);
371
+ if (this._viewshedType === ViewshedTypes.CONE) {
372
+ this._updatePrimitive();
373
+ }
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Getter for the pitch of a cone viewshed.
379
+ * @returns {number} The pitch in degrees.
380
+ */
381
+ get pitch() {
382
+ return this._headingPitchRollValues.pitch * CesiumMath.DEGREES_PER_RADIAN;
383
+ }
384
+
385
+ /**
386
+ * Sets whether the primitive of the viewshed should be shown or not.
387
+ * @param {boolean} value
388
+ */
389
+ set showPrimitive(value) {
390
+ this._showPrimitive = value;
391
+ this._updatePrimitive();
392
+ }
393
+
394
+ /**
395
+ * Gets if the primitive of the viewshed is shown or not.
396
+ * @returns {boolean}
397
+ */
398
+ get showPrimitive() {
399
+ return this._showPrimitive;
400
+ }
401
+
402
+ /**
403
+ * Retruns the type of the Viewshed.
404
+ * @returns {ViewshedTypes}
405
+ */
406
+ get type() {
407
+ return this._viewshedType;
408
+ }
409
+
410
+ /**
411
+ * Deactivates the viewshed by removing primitve and removing shadowMap.
412
+ */
413
+ deactivate() {
414
+ if (this._active) {
415
+ this._removePrimitive();
416
+ this._shadowMapChangedListener?.();
417
+ if (this._cesiumMap?.getScene()?.shadowMap === this._shadowMap) {
418
+ this._cesiumMap.setDefaultShadowMap();
419
+ }
420
+ this._shadowMap?.destroy();
421
+ this._shadowCamera = null;
422
+ this._scene = null;
423
+ this._cesiumMap = null;
424
+ this._active = false;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Activates viewshed. Sets the CesiumMap and adds shadowMapChanged listener to CesiumMap. If viewshed is active but
430
+ * @param {import("@vcmap/core").CesiumMap} cesiumMap The cesium map to which the shadow map of the viewshed is applied to.
431
+ */
432
+ activate(cesiumMap) {
433
+ if (!this._active) {
434
+ this._active = true;
435
+ }
436
+ // since active === false means that this._cesiumMap === null,
437
+ // this is always true when activating previously deactivated viewshed,
438
+ // or when chaning the cesiumMap for an already active viewshed
439
+ if (cesiumMap !== this._cesiumMap) {
440
+ this._cesiumMap = cesiumMap;
441
+ const scene = cesiumMap.getScene();
442
+ if (scene) {
443
+ this._shadowCamera = createShadowCamera(scene, this._frustumOptions);
444
+ if (this._position) {
445
+ this.position = this._position; // Applies the cached position to the shadowCamera
446
+ }
447
+ this._scene = scene;
448
+ } else {
449
+ throw new Error('CesiumMap contains no scene');
450
+ }
451
+
452
+ this._updateShadowMap();
453
+ this._updatePrimitive();
454
+ this._shadowMapChangedListener =
455
+ this._cesiumMap.shadowMapChanged.addEventListener(() => {
456
+ this.deactivate();
457
+ });
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Updates the shadow map by creating a new one and applying all the current parameters.
463
+ */
464
+ _updateShadowMap() {
465
+ if (
466
+ !this._active ||
467
+ !this._shadowCamera ||
468
+ !this._cesiumMap ||
469
+ !this._scene
470
+ ) {
471
+ return;
472
+ }
473
+
474
+ // @ts-ignore
475
+ this._shadowMap = new ShadowMap({
476
+ // @ts-ignore
477
+ context: this._scene.context,
478
+ lightCamera: this._shadowCamera,
479
+ enabled: true,
480
+ isPointLight: this._viewshedType === ViewshedTypes.THREESIXTY,
481
+ softShadows: true,
482
+ fromLightSource: true,
483
+ cascadesEnabled: false,
484
+ pointLightRadius: this._shadowCamera.frustum.far,
485
+ maximumDistance: 200,
486
+ size: 2048,
487
+ });
488
+ this._shadowMap.viewshed = this._colors;
489
+ this._scene.shadowMap = this._shadowMap;
490
+ this._cesiumMap.setShadowMap(this._shadowMap);
491
+ }
492
+
493
+ _removePrimitive() {
494
+ if (this._primitive) {
495
+ this._scene?.primitives.remove(this._primitive);
496
+ this._primitive.destroy();
497
+ this._primitive = null;
498
+ }
499
+ }
500
+
501
+ _updatePrimitive() {
502
+ if (this._showPrimitive && this._active && this._scene) {
503
+ this._removePrimitive();
504
+ this._primitive = new ViewshedCameraPrimitive({
505
+ camera: this._shadowCamera,
506
+ allowPicking: false,
507
+ spot: this._viewshedType === ViewshedTypes.CONE,
508
+ });
509
+ this._scene.primitives.add(this._primitive);
510
+ } else {
511
+ this._removePrimitive();
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Sets distance and, in case of cone viewsheds, also heading and pitch. Only works when viewshed is **active**.
517
+ * @param {number[]} target Point to calculate distance, heading and pitch from.
518
+ */
519
+ lookAt(target) {
520
+ if (!this._shadowCamera) {
521
+ return;
522
+ }
523
+
524
+ const direction = Cartesian3.fromDegrees(target[0], target[1], target[2]);
525
+ // accuracy of distance calculation should be enough for this usecase
526
+ this.distance = Cartesian3.distance(this._shadowCamera.position, direction);
527
+
528
+ if (
529
+ this.type === ViewshedTypes.CONE &&
530
+ !direction.equals(this._shadowCamera.position)
531
+ ) {
532
+ const up = new Cartesian3();
533
+
534
+ Cartesian3.subtract(direction, this._shadowCamera.position, direction);
535
+ Cartesian3.normalize(direction, direction);
536
+ Cartesian3.normalize(this._shadowCamera.position, up);
537
+
538
+ this._shadowCamera.setView({
539
+ orientation: {
540
+ direction,
541
+ up,
542
+ },
543
+ });
544
+
545
+ this._headingPitchRollValues.heading = this._shadowCamera.heading;
546
+ this._headingPitchRollValues.pitch = this._shadowCamera.pitch;
547
+ }
548
+
549
+ this._updatePrimitive();
550
+ }
551
+
552
+ /**
553
+ * Returns an object with all the settings of a viewshed instance.
554
+ * @returns {ViewshedOptions}
555
+ */
556
+ toJSON() {
557
+ return {
558
+ ...super.toJSON(),
559
+ viewshedType: this.type,
560
+ position: this.position,
561
+ frustumOptions: { ...this._frustumOptions },
562
+ orientation: { ...this._headingPitchRollValues },
563
+ colorOptions: {
564
+ visibleColor: this._colors.visibleColor.toCssHexString(),
565
+ shadowColor: this._colors.shadowColor.toCssHexString(),
566
+ },
567
+ showPrimitive: this.showPrimitive,
568
+ heightOffset: this.heightOffset,
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Destroys the viewshed.
574
+ */
575
+ destroy() {
576
+ this.deactivate();
577
+ }
578
+ }