@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.
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@vcmap/viewshed",
3
+ "version": "2.0.1",
4
+ "description": "Tool to simulate a \"viewer\" and their field of vision from a certain standpoint in the map. It allows the user to see which areas and buildings can or can't be seen from a specific standpoint, regarding different viewing distances, viewers' sizes and orientations.Th",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "prepublishOnly": "vcmplugin build",
9
+ "build": "vcmplugin build",
10
+ "bundle": "vcmplugin bundle",
11
+ "start": "vcmplugin serve",
12
+ "preview": "vcmplugin preview",
13
+ "buildStagingApp": "vcmplugin buildStagingApp",
14
+ "lint:js": "eslint . --ext .vue,.js,.cjs,.mjs,.ts,.cts,.mts",
15
+ "lint:prettier": "prettier --check .",
16
+ "lint": "npm run lint:js && npm run lint:prettier",
17
+ "format": "prettier --write --list-different . && npm run lint:js -- --fix",
18
+ "test": "vitest",
19
+ "coverage": "vitest run --coverage"
20
+ },
21
+ "author": "Tobias Krauth <tkrauth@vc.systems>",
22
+ "license": "MIT",
23
+ "keywords": [
24
+ "vcmap",
25
+ "plugin"
26
+ ],
27
+ "files": [
28
+ "src/",
29
+ "dist/",
30
+ "plugin-assets/",
31
+ "LICENSE.md",
32
+ "README.md",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "exports": {
36
+ ".": "./src/index.js",
37
+ "./dist": "./dist/index.js"
38
+ },
39
+ "eslintIgnore": [
40
+ "node_modules",
41
+ "dist",
42
+ "plugin-assets"
43
+ ],
44
+ "eslintConfig": {
45
+ "root": true,
46
+ "extends": "@vcsuite/eslint-config/vue"
47
+ },
48
+ "prettier": "@vcsuite/eslint-config/prettier.js",
49
+ "peerDependencies": {
50
+ "@vcmap-cesium/engine": "^4.0.3",
51
+ "@vcmap/core": "^5.1.0-rc.2",
52
+ "@vcmap/ui": "^5.1.0-rc.1",
53
+ "ol": "^7.5.2",
54
+ "vue": "~2.7.3",
55
+ "vuetify": "~2.6.7"
56
+ },
57
+ "overrides": {
58
+ "@vcmap/core": "^5.1.0-rc.2",
59
+ "@vcmap/ui": "^5.1.0-rc.1"
60
+ },
61
+ "devDependencies": {
62
+ "@vcmap/plugin-cli": "^2.1.16",
63
+ "@vcsuite/eslint-config": "^3.0.5",
64
+ "@vitest/coverage-v8": "^0.34.6",
65
+ "jest-canvas-mock": "^2.5.2",
66
+ "jsdom": "^22.1.0",
67
+ "resize-observer-polyfill": "^1.5.1",
68
+ "vitest": "^0.34.6"
69
+ },
70
+ "dependencies": {
71
+ "@vcsuite/check": "^1.1.2",
72
+ "@vcsuite/parsers": "^1.0.3"
73
+ },
74
+ "mapVersion": "^5.1"
75
+ }
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <AbstractConfigEditor @submit="apply" v-bind="{ ...$attrs, ...$props }">
3
+ <VcsFormSection heading="viewshed.editor.general" v-if="localConfig">
4
+ <v-container class="px-1 py-0">
5
+ <v-row no-gutters>
6
+ <v-col>
7
+ <VcsLabel html-for="viewshed-editor-tools">{{
8
+ $t('viewshed.editor.tools')
9
+ }}</VcsLabel>
10
+ </v-col>
11
+ <v-col>
12
+ <VcsSelect
13
+ id="viewshed-tools"
14
+ :items="toolItems"
15
+ v-model="localConfig.tools"
16
+ :multiple="true"
17
+ />
18
+ </v-col>
19
+ </v-row>
20
+ <v-row no-gutters>
21
+ <v-col>
22
+ <VcsLabel html-for="viewshed-editor-visible-color">{{
23
+ $t('viewshed.editor.visibleColor')
24
+ }}</VcsLabel>
25
+ </v-col>
26
+ <v-col>
27
+ <VColorPicker v-model="localConfig.visibleColor" mode="rgba" />
28
+ </v-col>
29
+ </v-row>
30
+ <v-row no-gutters>
31
+ <v-col>
32
+ <VcsLabel html-for="viewshed-editor-shadow-color">{{
33
+ $t('viewshed.editor.shadowColor')
34
+ }}</VcsLabel>
35
+ </v-col>
36
+ <v-col>
37
+ <VColorPicker v-model="localConfig.shadowColor" mode="rgba" />
38
+ </v-col>
39
+ </v-row>
40
+ </v-container>
41
+ </VcsFormSection>
42
+ </AbstractConfigEditor>
43
+ </template>
44
+
45
+ <script>
46
+ import { ref } from 'vue';
47
+ import { VCol, VColorPicker, VContainer, VRow } from 'vuetify/lib';
48
+ import {
49
+ AbstractConfigEditor,
50
+ VcsFormSection,
51
+ VcsLabel,
52
+ VcsSelect,
53
+ } from '@vcmap/ui';
54
+ import Viewshed, { ViewshedTypes } from './viewshed.js';
55
+
56
+ /**
57
+ * @typedef {Object} ViewshedConfig
58
+ * @property {string} visibleColor The color of the viewsheds visible parts.
59
+ * @property {string} shadowColor The color of the viewsheds hidden parts.
60
+ * @property {Array<ViewshedTypes>} tools The tools that are available in the toolbox.
61
+ */
62
+
63
+ /**
64
+ * Returns the default viewshed options.
65
+ * @returns {ViewshedConfig}
66
+ */
67
+ export function getDefaultOptions() {
68
+ return {
69
+ visibleColor: Viewshed.getDefaultOptions().visibleColor,
70
+ shadowColor: Viewshed.getDefaultOptions().shadowColor,
71
+ tools: [...Object.values(ViewshedTypes)],
72
+ };
73
+ }
74
+
75
+ export default {
76
+ name: 'ViewshedConfigEditor',
77
+ title: 'Viewshed Editor',
78
+ components: {
79
+ AbstractConfigEditor,
80
+ VcsFormSection,
81
+ VContainer,
82
+ VRow,
83
+ VCol,
84
+ VcsLabel,
85
+ VcsSelect,
86
+ VColorPicker,
87
+ },
88
+ props: {
89
+ getConfig: {
90
+ type: Function,
91
+ required: true,
92
+ },
93
+ setConfig: {
94
+ type: Function,
95
+ required: true,
96
+ },
97
+ },
98
+ setup(props) {
99
+ /** @type {import("vue").Ref<import("./index.js").ViewshedPluginOptions>} */
100
+ const localConfig = ref({});
101
+ /** @type {import("vue").Ref<import("./index.js").ViewshedPluginOptions>} */
102
+ props.getConfig().then((config) => {
103
+ localConfig.value = Object.assign(getDefaultOptions(), config);
104
+ });
105
+
106
+ async function apply() {
107
+ await props.setConfig(localConfig.value);
108
+ }
109
+
110
+ return {
111
+ localConfig,
112
+ ViewshedTypes,
113
+ apply,
114
+ toolItems: Object.values(ViewshedTypes).map((type) => ({
115
+ value: type,
116
+ text: `viewshed.${type}`,
117
+ })),
118
+ };
119
+ },
120
+ };
121
+ </script>
@@ -0,0 +1,477 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="viewshedMode === ViewshedPluginModes.CREATE" class="px-2 py-1">
4
+ {{ $t('viewshed.createDescription') }}
5
+ </div>
6
+ <VcsFormSection
7
+ v-else-if="
8
+ viewshedMode === ViewshedPluginModes.EDIT ||
9
+ viewshedMode === ViewshedPluginModes.MOVE
10
+ "
11
+ :header-actions="headerActions"
12
+ heading="viewshed.viewpoint"
13
+ :action-button-list-overflow-count="3"
14
+ >
15
+ <v-container class="px-1 py-0">
16
+ <v-row no-gutters>
17
+ <span class="px-1 py-0">{{ $t('viewshed.position') }}</span>
18
+ </v-row>
19
+ <v-row
20
+ v-if="
21
+ viewshedMode === ViewshedPluginModes.MOVE &&
22
+ heightMode === HeightModes.ABSOLUTE
23
+ "
24
+ no-gutters
25
+ >
26
+ <VcsFeatureTransforms
27
+ :transformation-mode="TransformationMode.TRANSLATE"
28
+ :feature-properties="{ altitudeMode: 'absolute' }"
29
+ :allow-z-input="true"
30
+ />
31
+ </v-row>
32
+ <v-row v-else no-gutters>
33
+ <v-col
34
+ v-for="(value, key, index) in position"
35
+ :key="index"
36
+ class="pb-1"
37
+ >
38
+ <VcsTextField
39
+ :value="value"
40
+ @input="(v) => setPosition(v, key, index)"
41
+ type="number"
42
+ :prefix="key"
43
+ :show-spin-buttons="true"
44
+ :step="key === 'Z' ? 1 : 0.0001"
45
+ :unit="key === 'Z' ? 'm' : '°'"
46
+ :decimals="key === 'Z' ? 2 : 8"
47
+ :disabled="
48
+ viewshedMode === ViewshedPluginModes.MOVE &&
49
+ !(key === 'Z' && heightMode === HeightModes.RELATIVE)
50
+ "
51
+ />
52
+ </v-col>
53
+ </v-row>
54
+ </v-container>
55
+ </VcsFormSection>
56
+ <v-divider />
57
+ <v-container class="px-1 py-0">
58
+ <v-row no-gutters>
59
+ <v-col>
60
+ <VcsLabel>
61
+ {{ $t('viewshed.heightMode') }}
62
+ </VcsLabel>
63
+ </v-col>
64
+ <v-col>
65
+ <VcsSelect
66
+ :items="
67
+ Object.values(HeightModes).map((item) => ({
68
+ value: item,
69
+ text: `viewshed.${item}`,
70
+ }))
71
+ "
72
+ :value="heightMode"
73
+ @input="changeHeightMode"
74
+ ></VcsSelect>
75
+ </v-col>
76
+ </v-row>
77
+ </v-container>
78
+ <v-container
79
+ class="px-1 pt-0 pb-2"
80
+ v-if="
81
+ viewshedMode === ViewshedPluginModes.EDIT ||
82
+ viewshedMode === ViewshedPluginModes.MOVE
83
+ "
84
+ >
85
+ <v-row no-gutters v-for="(value, key) in parameters" :key="key">
86
+ <v-col
87
+ v-if="
88
+ key === 'distance' ||
89
+ (viewshedType === ViewshedTypes.CONE && key !== 'showPrimitive')
90
+ "
91
+ >
92
+ <v-row no-gutters class="pr-1">
93
+ <v-col>
94
+ <VcsLabel :html-for="key">
95
+ {{ $t(`viewshed.${key}`) }}
96
+ </VcsLabel>
97
+ </v-col>
98
+ <v-col class="d-flex justify-end align-center">
99
+ <span v-if="key === 'distance'">{{ `${value} m` }}</span>
100
+ <span v-else>{{ `${value} °` }}</span>
101
+ </v-col>
102
+ </v-row>
103
+ <v-row no-gutters>
104
+ <v-col>
105
+ <VcsSlider
106
+ :id="key"
107
+ :value="value"
108
+ @input="(v) => setParameter(key, v)"
109
+ type="number"
110
+ step="1"
111
+ :min="parameterRanges[key][0]"
112
+ :max="parameterRanges[key][1]"
113
+ height="15"
114
+ />
115
+ </v-col>
116
+ </v-row>
117
+ </v-col>
118
+ </v-row>
119
+ </v-container>
120
+ <v-divider />
121
+ <div
122
+ v-if="
123
+ viewshedMode === ViewshedPluginModes.EDIT ||
124
+ viewshedMode === ViewshedPluginModes.MOVE
125
+ "
126
+ class="d-flex w-full justify-space-between px-2 pt-2 pb-1"
127
+ >
128
+ <VcsFormButton
129
+ @click="addToMyWorkspace()"
130
+ tooltip="viewshed.addToMyWorkspace"
131
+ icon="$vcsComponentsPlus"
132
+ :disabled="currentIsPersisted || undefined"
133
+ />
134
+ <VcsFormButton @click="createNewViewshed()" variant="filled">
135
+ {{ $t('viewshed.new') }}
136
+ </VcsFormButton>
137
+ </div>
138
+ <div
139
+ v-else-if="viewshedMode === ViewshedPluginModes.CREATE"
140
+ class="d-flex w-full justify-end px-2 pt-2 pb-1"
141
+ >
142
+ <VcsFormButton @click="cancel()" variant="filled">
143
+ {{ $t('viewshed.cancel') }}
144
+ </VcsFormButton>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <script>
150
+ import {
151
+ computed,
152
+ inject,
153
+ onMounted,
154
+ onUnmounted,
155
+ reactive,
156
+ ref,
157
+ shallowRef,
158
+ watch,
159
+ watchEffect,
160
+ } from 'vue';
161
+ import { VCol, VContainer, VDivider, VRow } from 'vuetify/lib';
162
+ import { TransformationMode, Viewpoint } from '@vcmap/core';
163
+ import {
164
+ VcsFormButton,
165
+ VcsFormSection,
166
+ VcsLabel,
167
+ VcsSelect,
168
+ VcsSlider,
169
+ VcsTextField,
170
+ VcsFeatureTransforms,
171
+ } from '@vcmap/ui';
172
+ import { ViewshedPluginModes, HeightModes } from './viewshedManager.js';
173
+ import Viewshed, { ViewshedTypes } from './viewshed.js';
174
+
175
+ const parameterRanges = {
176
+ distance: [Viewshed.MIN_DISTANCE, 2000],
177
+ fov: [25, 85],
178
+ heading: [0, 360],
179
+ pitch: [-90, 90],
180
+ };
181
+
182
+ export default {
183
+ components: {
184
+ VcsTextField,
185
+ VContainer,
186
+ VRow,
187
+ VCol,
188
+ VcsSlider,
189
+ VcsLabel,
190
+ VcsFormSection,
191
+ VDivider,
192
+ VcsSelect,
193
+ VcsFormButton,
194
+ VcsFeatureTransforms,
195
+ },
196
+ name: 'ViewshedWindow',
197
+ props: {
198
+ getViewshed: {
199
+ type: Function,
200
+ required: false,
201
+ default: undefined,
202
+ },
203
+ selection: {
204
+ type: Object,
205
+ required: false,
206
+ default: undefined,
207
+ },
208
+ },
209
+ setup(props) {
210
+ const viewshedManager =
211
+ /** @type {import("./viewshedManager.js").ViewshedManager} */ (
212
+ inject('manager')
213
+ );
214
+ const app = /** @type {import("@vcmap/ui").VcsUiApp} */ (
215
+ inject('vcsApp')
216
+ );
217
+ const {
218
+ currentViewshed,
219
+ mode: viewshedMode,
220
+ currentIsPersisted,
221
+ } = viewshedManager;
222
+
223
+ const selection = ref(props.selection);
224
+
225
+ let removePositionChangedListener = () => {};
226
+
227
+ /**
228
+ * Position of the viewshed as coordinates array. Z value is either absolute height (heightMode === HeightModes.ABSOLUTE) or relative height (heightMode === HeightModes.RELATIVE);
229
+ * @type {{X: number | undefined, Y: number | undefined, Z: number | undefined}}
230
+ */
231
+ const position = reactive({
232
+ X: undefined,
233
+ Y: undefined,
234
+ Z: undefined,
235
+ });
236
+ /** Cached view point when jumping to view point of viewshed. This allows to jump back to prev position when clicking 'jump to viewpoint' again, without changing position. */
237
+ const cachedViewpoint = shallowRef();
238
+ /** If currently in `jumpToViewpoint` mode: sets cached viewpoint on null, deletes camera changed listener and reactivates viewshed. Otherwise empty function. */
239
+ let deleteCachedViewpoint = () => {};
240
+
241
+ /**
242
+ * @type {{showPrimitive: boolean, distance: number | undefined, fov: number | undefined, heading: number | undefined, pitch: number | undefined}}
243
+ */
244
+ const parameters = reactive({
245
+ showPrimitive: false,
246
+ distance: undefined,
247
+ fov: undefined,
248
+ heading: undefined,
249
+ pitch: undefined,
250
+ });
251
+
252
+ const viewshedType = ref();
253
+
254
+ function updatePosition() {
255
+ if (currentViewshed.value) {
256
+ position.X = currentViewshed.value.position[0];
257
+ position.Y = currentViewshed.value.position[1];
258
+ position.Z =
259
+ viewshedManager.heightMode.value === HeightModes.ABSOLUTE
260
+ ? currentViewshed.value.position[2]
261
+ : currentViewshed.value.heightOffset;
262
+ }
263
+ }
264
+
265
+ function updateWindow() {
266
+ updatePosition();
267
+
268
+ if (currentViewshed.value) {
269
+ parameters.distance = currentViewshed.value.distance;
270
+ parameters.fov = currentViewshed.value.fov;
271
+ parameters.heading = currentViewshed.value.heading;
272
+ parameters.pitch = currentViewshed.value.pitch;
273
+ parameters.showPrimitive = currentViewshed.value.showPrimitive;
274
+
275
+ viewshedType.value = currentViewshed.value.type;
276
+ }
277
+ }
278
+
279
+ watchEffect(() => {
280
+ if (
281
+ currentViewshed.value &&
282
+ viewshedMode.value !== ViewshedPluginModes.CREATE
283
+ ) {
284
+ updateWindow();
285
+ }
286
+ });
287
+
288
+ watch(
289
+ currentViewshed,
290
+ () => {
291
+ if (currentViewshed.value) {
292
+ removePositionChangedListener();
293
+ removePositionChangedListener =
294
+ currentViewshed.value.positionChanged.addEventListener(() => {
295
+ updatePosition();
296
+ deleteCachedViewpoint();
297
+ });
298
+ }
299
+ },
300
+ { immediate: true },
301
+ );
302
+
303
+ /**
304
+ * Deactivates viewshed and sets up a camera changes listener, which is destroyed when `deleteCachedViewpoint()` is called.
305
+ */
306
+ async function setUpCameraChangedListener() {
307
+ currentViewshed.value?.deactivate();
308
+ const camera = /** @type {import("@vcmap/core").CesiumMap} */ (
309
+ app.maps.activeMap
310
+ ).getScene()?.camera;
311
+ const listener = camera?.changed.addEventListener(() => {
312
+ deleteCachedViewpoint();
313
+ });
314
+ deleteCachedViewpoint = () => {
315
+ listener?.();
316
+ cachedViewpoint.value = null;
317
+ currentViewshed.value?.activate(app.maps.activeMap);
318
+ deleteCachedViewpoint = () => {};
319
+ };
320
+ }
321
+
322
+ onMounted(() => {
323
+ const viewshed = props.getViewshed?.();
324
+ if (viewshed) {
325
+ // only the case for collection component editor
326
+ viewshedManager.editViewshed(viewshed);
327
+ }
328
+ });
329
+
330
+ onUnmounted(() => {
331
+ removePositionChangedListener();
332
+ deleteCachedViewpoint();
333
+
334
+ if (!currentIsPersisted.value) {
335
+ // in case of temp editor
336
+ if (
337
+ (viewshedMode.value === ViewshedPluginModes.EDIT ||
338
+ viewshedMode.value === ViewshedPluginModes.MOVE) &&
339
+ currentViewshed.value
340
+ ) {
341
+ viewshedManager.viewViewshed(currentViewshed.value);
342
+ } else if (viewshedMode.value === ViewshedPluginModes.CREATE) {
343
+ viewshedManager.stop();
344
+ }
345
+ }
346
+ if (selection.value) {
347
+ // in case of collection component editor
348
+ if (selection.value.length > 1) {
349
+ viewshedManager.setupMultiSelect();
350
+ } else if (selection.value.length === 1 && currentViewshed.value) {
351
+ viewshedManager.viewViewshed(currentViewshed.value);
352
+ } else if (
353
+ // when a persited viewshed is deselected, stops plugin
354
+ !selection.value.length &&
355
+ viewshedMode.value === ViewshedPluginModes.EDIT
356
+ ) {
357
+ viewshedManager.stop(false);
358
+ }
359
+ }
360
+ });
361
+
362
+ updateWindow();
363
+
364
+ function setParameter(key, value) {
365
+ deleteCachedViewpoint();
366
+ if (currentViewshed.value) {
367
+ parameters[key] = value;
368
+ currentViewshed.value[key] = value;
369
+ }
370
+ }
371
+
372
+ const headerActions = computed(() => {
373
+ const actions = [];
374
+ if (viewshedType.value === ViewshedTypes.CONE) {
375
+ actions.push({
376
+ name: 'jumpToViewpoint',
377
+ title: cachedViewpoint.value
378
+ ? 'viewshed.returnToViewpoint'
379
+ : 'viewshed.jumpToViewpoint',
380
+ icon: 'mdi-human-male',
381
+ active: !!cachedViewpoint.value,
382
+ async callback() {
383
+ viewshedManager.moveCurrentViewshed(false);
384
+ if (currentViewshed.value && !cachedViewpoint.value) {
385
+ cachedViewpoint.value =
386
+ await app.maps.activeMap?.getViewpoint();
387
+ await app.maps.activeMap?.gotoViewpoint(
388
+ new Viewpoint({
389
+ groundPosition: [
390
+ currentViewshed.value.position[0],
391
+ currentViewshed.value.position[1],
392
+ currentViewshed.value.position[2] +
393
+ currentViewshed.value.heightOffset,
394
+ ],
395
+ heading: currentViewshed.value.heading,
396
+ pitch: currentViewshed.value.pitch,
397
+ distance: 0,
398
+ animate: true, // if not animate, async does not work and the cameraChanged listener is triggered immediately. alternatively set timeout
399
+ }),
400
+ );
401
+ setUpCameraChangedListener();
402
+ } else if (cachedViewpoint.value) {
403
+ app.maps.activeMap?.gotoViewpoint(cachedViewpoint.value);
404
+ deleteCachedViewpoint();
405
+ }
406
+ },
407
+ });
408
+ }
409
+ actions.push({
410
+ name: 'moveViewshed',
411
+ title: 'viewshed.move',
412
+ icon:
413
+ viewshedManager.heightMode.value === HeightModes.RELATIVE
414
+ ? '$vcsEditVertices'
415
+ : 'mdi-axis-arrow',
416
+ active: viewshedMode.value === ViewshedPluginModes.MOVE,
417
+ callback() {
418
+ viewshedManager.moveCurrentViewshed(
419
+ viewshedMode.value !== ViewshedPluginModes.MOVE,
420
+ );
421
+ },
422
+ });
423
+ actions.unshift({
424
+ name: 'showPrimitive',
425
+ title: 'viewshed.showPrimitive',
426
+ icon: 'mdi-eye',
427
+ active: parameters.showPrimitive,
428
+ callback() {
429
+ this.active = !this.active;
430
+ setParameter('showPrimitive', this.active);
431
+ },
432
+ });
433
+ return actions;
434
+ });
435
+
436
+ return {
437
+ position,
438
+ setPosition(value, key, index) {
439
+ deleteCachedViewpoint();
440
+ if (!currentViewshed.value) {
441
+ return;
442
+ }
443
+
444
+ if (
445
+ index === 2 &&
446
+ viewshedManager.heightMode.value === HeightModes.RELATIVE
447
+ ) {
448
+ currentViewshed.value.heightOffset = Number(value);
449
+ } else {
450
+ const newPosition = [...currentViewshed.value.position];
451
+ newPosition[index] = Number(value);
452
+ currentViewshed.value.position = newPosition;
453
+ }
454
+ position[key] = Number(value);
455
+ },
456
+ parameters,
457
+ setParameter,
458
+ parameterRanges,
459
+ ViewshedPluginModes,
460
+ viewshedMode,
461
+ currentIsPersisted,
462
+ viewshedType,
463
+ ViewshedTypes,
464
+ HeightModes,
465
+ heightMode: viewshedManager.heightMode,
466
+ TransformationMode,
467
+ changeHeightMode: viewshedManager.changeHeightMode,
468
+ async createNewViewshed() {
469
+ await viewshedManager.createViewshed(viewshedType.value);
470
+ },
471
+ addToMyWorkspace: () => viewshedManager.persistCurrent(),
472
+ cancel: () => viewshedManager.stop(),
473
+ headerActions,
474
+ };
475
+ },
476
+ };
477
+ </script>