@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,216 @@
1
+ import { reactive } from 'vue';
2
+ import { Category, CesiumMap, VcsEvent } from '@vcmap/core';
3
+ import {
4
+ createListExportAction,
5
+ createListImportAction,
6
+ downloadText,
7
+ } from '@vcmap/ui';
8
+ import { name } from '../package.json';
9
+ import Viewshed from './viewshed.js';
10
+
11
+ /**
12
+ * @typedef {Object} ViewshedCategoryHelper
13
+ * @property {import("@vcmap/core").VcsEvent} renamed
14
+ * @property {import("@vcmap/core").VcsEvent} visibilityChanged Event that is raised when the visibility of a viewshed is changed.
15
+ * @property {function(string | null):void} setSelection Sets a single viewshed item as selected by providing its name.
16
+ * @property {function():void} clearSelection Clears all selected items.
17
+ * @property {function(string, boolean):void} setVisibility Sets the visibility of a viewshed item by providing its name and a boolean value.
18
+ * @property {function(import("./viewshed.js").default):void} add Adds a viewshed to the categories collection. Also assigns new title to the viewshed.
19
+ * @property {function(string):void} remove Removes viewshed from category collection by providing a name.
20
+ * @property {import("@vcmap/ui").CollectionComponent} collectionComponent The collection component of the category.
21
+ * @property {function():void} destroy Destroys category helper
22
+ */
23
+
24
+ class ViewshedCategory extends Category {
25
+ static get className() {
26
+ return 'ViewshedCategory';
27
+ }
28
+
29
+ async _deserializeItem(item) {
30
+ const cesiumMap =
31
+ /** @type {import("@vcmap/core").CesiumMap | undefined} */ (
32
+ this._app?.maps.getByType(CesiumMap.className)[0]
33
+ );
34
+ if (cesiumMap) {
35
+ return new Viewshed(item, cesiumMap);
36
+ } else {
37
+ throw new Error('No CesiumMap available');
38
+ }
39
+ }
40
+ }
41
+
42
+ export default ViewshedCategory;
43
+
44
+ /**
45
+ *
46
+ * @param {import("./viewshed.js").ViewshedTypes} viewshedType
47
+ * @param {Array<import("./viewshed.js").default>} persistedViewsheds
48
+ * @returns {string} The title for the viewshed.
49
+ */
50
+ export function getTitleForViewshed(viewshedType, persistedViewsheds) {
51
+ let viewshedTitle;
52
+ let count = 0;
53
+
54
+ const sameTypeViewshedsNames = new Set(
55
+ persistedViewsheds
56
+ .filter((viewshed) => viewshed.type === viewshedType)
57
+ .map((viewshed) => viewshed.properties.title),
58
+ );
59
+
60
+ do {
61
+ count += 1;
62
+ if (!sameTypeViewshedsNames.has(`${viewshedType}-${count}`)) {
63
+ viewshedTitle = `${viewshedType}-${count}`;
64
+ }
65
+ } while (!viewshedTitle);
66
+
67
+ return viewshedTitle;
68
+ }
69
+
70
+ /**
71
+ *
72
+ * @param {import("@vcmap/ui").VcsUiApp} app
73
+ * @returns {Promise<ViewshedCategoryHelper>}
74
+ */
75
+ export async function createCategory(app) {
76
+ const renamed = new VcsEvent();
77
+ const visibilityChanged = new VcsEvent();
78
+
79
+ const { collectionComponent, category } =
80
+ await app.categoryManager.requestCategory(
81
+ {
82
+ type: ViewshedCategory.className,
83
+ name: 'Viewsheds',
84
+ title: 'Viewsheds',
85
+ },
86
+ name,
87
+ {
88
+ selectable: true,
89
+ renamable: true,
90
+ removable: true,
91
+ },
92
+ );
93
+
94
+ const itemMappingFunction = (item, c, listItem) => {
95
+ listItem.title = item.properties.title;
96
+
97
+ listItem.titleChanged = (title) => {
98
+ item.properties.title = title;
99
+ listItem.title = title;
100
+ renamed.raiseEvent({ item, title });
101
+ };
102
+
103
+ listItem.actions.push(
104
+ reactive({
105
+ name: 'visibilityAction',
106
+ icon: '$vcsCheckbox',
107
+ callback() {
108
+ visibilityChanged.raiseEvent(item);
109
+ },
110
+ }),
111
+ );
112
+ };
113
+
114
+ app.categoryManager.addMappingFunction(
115
+ () => true,
116
+ itemMappingFunction,
117
+ name,
118
+ [collectionComponent.id],
119
+ );
120
+
121
+ const { action: exportAction, destroy: destroyExportAction } =
122
+ createListExportAction(
123
+ collectionComponent.selection,
124
+ () => {
125
+ const viewsheds = collectionComponent.selection.value.map((item) =>
126
+ collectionComponent.collection.getByKey(item.name),
127
+ );
128
+ downloadText(JSON.stringify(viewsheds), 'viewsheds.json');
129
+ },
130
+ name,
131
+ );
132
+
133
+ const { action: importAction, destroy: destroyImportAction } =
134
+ createListImportAction(
135
+ async (files) => {
136
+ const promises = files.map((file) => {
137
+ const reader = new FileReader();
138
+ return new Promise((resolve, reject) => {
139
+ reader.onload = () => {
140
+ try {
141
+ const viewshedOptions = JSON.parse(reader.result);
142
+ viewshedOptions.forEach((options) => {
143
+ const viewshed = new Viewshed(options);
144
+ category.collection.add(viewshed);
145
+ });
146
+ resolve();
147
+ } catch (e) {
148
+ reject(e);
149
+ }
150
+ };
151
+ reader.readAsText(file);
152
+ });
153
+ });
154
+ await Promise.all(promises);
155
+ },
156
+ app.windowManager,
157
+ name,
158
+ 'category-manager',
159
+ );
160
+
161
+ collectionComponent.addActions([exportAction, importAction]);
162
+
163
+ return {
164
+ renamed,
165
+ visibilityChanged,
166
+ setSelection(itemName) {
167
+ if (itemName) {
168
+ collectionComponent.selection.value =
169
+ collectionComponent.items.value.filter((i) => itemName === i.name);
170
+ }
171
+ },
172
+ clearSelection() {
173
+ if (collectionComponent.selection.value.length) {
174
+ collectionComponent.selection.value = [];
175
+ }
176
+ },
177
+ setVisibility(itemName, visible) {
178
+ const listItem = collectionComponent.items.value.find(
179
+ (i) => itemName === i.name,
180
+ );
181
+ if (listItem) {
182
+ const visibilityAction = listItem.actions.find(
183
+ (action) => action.name === 'visibilityAction',
184
+ );
185
+ if (visibilityAction) {
186
+ visibilityAction.icon = visible
187
+ ? '$vcsCheckboxChecked'
188
+ : '$vcsCheckbox';
189
+ }
190
+ }
191
+ },
192
+ add(viewshed) {
193
+ viewshed.properties.title = getTitleForViewshed(
194
+ viewshed.type,
195
+ /** @type {import("./viewshed.js").default[]} */ ([
196
+ ...category.collection,
197
+ ]),
198
+ );
199
+ category.collection.add(viewshed);
200
+ },
201
+ remove(itemName) {
202
+ const item = category.collection.getByKey(itemName);
203
+ if (item) {
204
+ category.collection.remove(item);
205
+ }
206
+ },
207
+ collectionComponent,
208
+ destroy() {
209
+ app.categoryManager.removeOwner(name);
210
+ renamed.destroy();
211
+ visibilityChanged.destroy();
212
+ destroyExportAction();
213
+ destroyImportAction();
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ AbstractInteraction,
3
+ EventType,
4
+ Projection,
5
+ VcsEvent,
6
+ } from '@vcmap/core';
7
+
8
+ class ViewshedInteraction extends AbstractInteraction {
9
+ /**
10
+ *
11
+ * @param {import("./viewshed").default} viewshed
12
+ */
13
+ constructor(viewshed) {
14
+ super(EventType.CLICKMOVE);
15
+ this._viewshed = viewshed;
16
+ this._positioned = new VcsEvent();
17
+ this._finished = new VcsEvent();
18
+ this._position = false;
19
+
20
+ this.setActive();
21
+ }
22
+
23
+ get finished() {
24
+ return this._finished;
25
+ }
26
+
27
+ get positioned() {
28
+ return this._positioned;
29
+ }
30
+
31
+ async pipe(event) {
32
+ if (event.position) {
33
+ if (!this._position) {
34
+ this._viewshed.position = Projection.mercatorToWgs84(event.position);
35
+ if (event.type & EventType.CLICK) {
36
+ this._position = true;
37
+ this._positioned.raiseEvent(null);
38
+ }
39
+ } else {
40
+ this._viewshed.lookAt(Projection.mercatorToWgs84(event.position));
41
+ if (event.type & EventType.CLICK) {
42
+ this.setActive(false);
43
+ this._finished.raiseEvent(null);
44
+ }
45
+ }
46
+ }
47
+ return event;
48
+ }
49
+
50
+ destroy() {
51
+ super.destroy();
52
+ this._finished.destroy();
53
+ this._positioned.destroy();
54
+ }
55
+ }
56
+
57
+ export default ViewshedInteraction;
@@ -0,0 +1,416 @@
1
+ import { nextTick, ref, shallowRef } from 'vue';
2
+ import {
3
+ CesiumMap,
4
+ EventType,
5
+ Projection,
6
+ SessionType,
7
+ VectorLayer,
8
+ markVolatile,
9
+ maxZIndex,
10
+ startEditFeaturesSession,
11
+ startEditGeometrySession,
12
+ wgs84Projection,
13
+ } from '@vcmap/core';
14
+ import { HeightReference } from '@vcmap-cesium/engine';
15
+ import { Feature } from 'ol';
16
+ import { Point } from 'ol/geom';
17
+ import { Style } from 'ol/style.js';
18
+ import { unByKey } from 'ol/Observable.js';
19
+ import Viewshed from './viewshed.js';
20
+ import ViewshedInteraction from './viewshedInteraction.js';
21
+
22
+ /**
23
+ * @typedef {Object} ViewshedManager
24
+ * @property {import("vue").ShallowRef<import("./viewshed.js").default | null>} currentViewshed The current viewshed, that is displayed in the map.
25
+ * @property {import("vue").Ref<null | boolean>} currentIsPersisted Whether current viewshed is persisted or not.
26
+ * @property {import("vue").Ref<import("@vcmap/core").EditFeaturesSession | import("@vcmap/core").EditGeometrySession | null>} currentEditSession The current edit session, when in MOVE mode. Read only.
27
+ * @property {function(import("./viewshed.js").ViewshedTypes): void} createViewshed Creates a new viewshed and stops a running create process.
28
+ * @property {function(import("./viewshed.js").default):void} viewViewshed Changes mode to VIEW for passed Viewshed.
29
+ * @property {function(import("./viewshed.js").default):void} editViewshed Changes mode to EDIT for passed Viewshed.
30
+ * @property {function(boolean):void} moveCurrentViewshed Changes mode to MOVE. If heightMode is ABSOLUTE a translate EditFeatureSession is started, if RELATIVE a EditGeometrySession is started.
31
+ * @property {function():void} setupMultiSelect Changes mode to MULTI_SELECT.
32
+ * @property {function():void} persistCurrent Adds current viewshed to the category collection.
33
+ * @property {import("vue").Ref<ViewshedPluginModes | null>} mode Viewshed mode. Should only be used to watch and get the mode, not to set the mode.
34
+ * @property {import("vue").Ref<HeightModes>} heightMode The height mode the viewshed plugin is currently in. Use changeHeightMode to change height mode when there is a currentViewshed.
35
+ * @property {function():void} changeHeightMode Sets heightMode and calculates the Z value according to the input heightMode.
36
+ * @property {function():void} placeCurrentFeaturesOnTerrain Places viewshed on terrain. Only available when in 'move' mode and height mode 'absolute'.
37
+ * @property {function(boolean=):void} stop Stops the creation and removes current viewshed.
38
+ * @property {function():void} destroy Destroys the viewshed manager.
39
+ */
40
+
41
+ /**
42
+ * @enum {string}
43
+ * @property {string} ABSOLUTE Absolute
44
+ * @property {string} RELATIVE Relative to ground
45
+ */
46
+ export const HeightModes = {
47
+ ABSOLUTE: 'absolute',
48
+ RELATIVE: 'relative',
49
+ };
50
+
51
+ /**
52
+ * @enum {string}
53
+ * @property {string} CREATE Window with instructions is open, map interaction for setting viewshed is active
54
+ * @property {string} VIEW Viewshed is visible in map, window is closed
55
+ * @property {string} EDIT Viewshed is visible in map, window is open
56
+ * @property {string} MOVE Viewshed is visible in map, window is open, move interaction is active
57
+ */
58
+ export const ViewshedPluginModes = {
59
+ CREATE: 'create',
60
+ VIEW: 'view',
61
+ EDIT: 'edit',
62
+ MOVE: 'move',
63
+ MULTI_SELECT: 'multiSelect',
64
+ };
65
+
66
+ /** The default height offset of a viewshed. */
67
+ export const defaultHeightOffset = 1.8;
68
+
69
+ /**
70
+ * Creates layer with feature at a specified position.
71
+ * @param {number[]} position Position for feature.
72
+ * @returns {{layer: import("@vcmap/core").VectorLayer, feature: import("ol").Feature, destroy: function():void}} A layer and the added feature at the passed position.
73
+ */
74
+ function createLayerWithFeature(position) {
75
+ const layer = new VectorLayer({
76
+ projection: wgs84Projection.toJSON(),
77
+ zIndex: maxZIndex - 1,
78
+ });
79
+ markVolatile(layer);
80
+ layer.activate();
81
+
82
+ const feature = new Feature(new Point(position));
83
+ // hide feature
84
+ feature.setStyle(new Style({}));
85
+ layer.addFeatures([feature]);
86
+ layer.vectorProperties.altitudeMode = HeightReference.NONE;
87
+
88
+ return {
89
+ layer,
90
+ feature,
91
+ destroy() {
92
+ feature.dispose();
93
+ layer.destroy();
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Sets the feature interaction eventType for the interaction itself as well as for the position picking.
100
+ * In case of heightMode ABSOLUTE the eventType is CLICKMOVE, and therefore sets the viewshed on top of terrain AND buildings.
101
+ * In case of heightMode RELATIVE the eventType is NONE which means viewshed is only set on top of terrain.
102
+ * Run featureInteraction.setActive() to reset eventType, pickPosition and pullPickedPosition.
103
+ * @param {import("@vcmap/core").FeatureAtPixelInteraction} featureInteraction The featureInteraction of the maps eventHandler
104
+ * @param {HeightModes} heightMode The current height mode of the viewshed plugin
105
+ */
106
+ function updateFeatureInteraction(featureInteraction, heightMode) {
107
+ const eventType =
108
+ heightMode === HeightModes.ABSOLUTE ? EventType.CLICKMOVE : EventType.NONE;
109
+ featureInteraction.setActive(eventType);
110
+ featureInteraction.pickPosition = eventType;
111
+ featureInteraction.pullPickedPosition = 1.8;
112
+ }
113
+
114
+ /**
115
+ *
116
+ * @param {import("@vcmap/ui").VcsUiApp} app The VcsUiApp instance
117
+ * @param {import("./index.js").ViewshedPluginOptions} config
118
+ * @param {import("./viewshedCategory.js").ViewshedCategoryHelper} categoryHelper
119
+ * @returns {ViewshedManager} The viewshed manager, which is responsible for managing the creation and editing of viewsheds.
120
+ */
121
+ export default function createViewshedManager(app, config, categoryHelper) {
122
+ /** @type {import("vue").ShallowRef<import("./viewshed.js").default | null>} */
123
+ const currentViewshed = shallowRef(null);
124
+ /** @type {import("vue").Ref<boolean | null>} */
125
+ const currentIsPersisted = ref(null);
126
+ /** @type {import("vue").Ref<ViewshedPluginModes | null>} */
127
+ const mode = ref(null);
128
+ const heightMode = ref(HeightModes.ABSOLUTE);
129
+ let removeInteraction = () => {};
130
+ /** @type {import("vue").Ref<import("@vcmap/core").EditFeaturesSession | import("@vcmap/core").EditGeometrySession | null>} */
131
+ const currentEditSession = shallowRef(null);
132
+
133
+ function setCurrentViewshed(viewshed) {
134
+ if (currentIsPersisted.value && currentViewshed.value) {
135
+ currentViewshed.value.deactivate();
136
+ categoryHelper.setVisibility(currentViewshed.value.name, false);
137
+ } else {
138
+ currentViewshed.value?.destroy();
139
+ }
140
+ currentViewshed.value = viewshed;
141
+ currentViewshed.value?.activate(app.maps.activeMap);
142
+ }
143
+
144
+ /**
145
+ * Stops the viewshed operation.
146
+ * @param {boolean} [clear=true] - Indicates whether to clear the selection.
147
+ */
148
+ function stop(clear = true) {
149
+ removeInteraction();
150
+ setCurrentViewshed(null);
151
+ if (clear) {
152
+ categoryHelper.clearSelection();
153
+ }
154
+ currentIsPersisted.value = null;
155
+ mode.value = null;
156
+ }
157
+
158
+ async function createViewshed(viewshedType) {
159
+ stop();
160
+ await nextTick(); // so the viewshedWindow is closed with mode === null and not CREATE
161
+
162
+ const { eventHandler } = app.maps;
163
+ const { featureInteraction } = eventHandler;
164
+
165
+ // create new viewshed instance
166
+ currentViewshed.value = new Viewshed(
167
+ {
168
+ viewshedType,
169
+ colorOptions: {
170
+ visibleColor: config.visibleColor,
171
+ shadowColor: config.shadowColor,
172
+ },
173
+ heightOffset:
174
+ heightMode.value === HeightModes.ABSOLUTE ? 0 : defaultHeightOffset,
175
+ },
176
+ /** @type {import("@vcmap/core").CesiumMap} */ (app.maps.activeMap),
177
+ );
178
+
179
+ // setup viewshed create interaction
180
+ const interaction = new ViewshedInteraction(currentViewshed.value);
181
+
182
+ interaction.finished.addEventListener(() => {
183
+ removeInteraction();
184
+ if (currentViewshed.value) {
185
+ currentViewshed.value.showPrimitive = true;
186
+ mode.value = ViewshedPluginModes.EDIT;
187
+ } else {
188
+ stop();
189
+ }
190
+ });
191
+
192
+ // add viewshed interaction as exclusive interaction
193
+ const removeExclusiveInteraction = eventHandler.addExclusiveInteraction(
194
+ interaction,
195
+ () => {
196
+ interaction.destroy();
197
+ },
198
+ );
199
+ updateFeatureInteraction(featureInteraction, heightMode.value);
200
+ interaction.positioned.addEventListener(() => {
201
+ updateFeatureInteraction(featureInteraction, HeightModes.ABSOLUTE); // always set second click (lookAt) absolute
202
+ });
203
+
204
+ removeInteraction = () => {
205
+ removeExclusiveInteraction();
206
+ interaction.destroy();
207
+ featureInteraction.setActive(); // resets featureInteractions eventType, pickPosition and pullPickedPosition
208
+
209
+ removeInteraction = () => {};
210
+ };
211
+
212
+ mode.value = ViewshedPluginModes.CREATE;
213
+ }
214
+
215
+ function moveCurrentViewshed(activate) {
216
+ if (currentViewshed.value && activate) {
217
+ removeInteraction();
218
+
219
+ const {
220
+ layer,
221
+ feature,
222
+ destroy: destroyLayerWithFeature,
223
+ } = createLayerWithFeature(currentViewshed.value.position);
224
+ app.layers.add(layer);
225
+
226
+ if (heightMode.value === HeightModes.ABSOLUTE) {
227
+ currentEditSession.value = startEditFeaturesSession(app, layer);
228
+ currentEditSession.value.setFeatures([feature]);
229
+ } else {
230
+ currentEditSession.value = startEditGeometrySession(app, layer);
231
+ currentEditSession.value.setFeature(feature);
232
+ }
233
+
234
+ currentEditSession.value.stopped.addEventListener(() => {
235
+ removeInteraction();
236
+ });
237
+
238
+ const geometryListenerKey = feature.getGeometry()?.on('change', () => {
239
+ if (currentViewshed.value) {
240
+ currentViewshed.value.position = Projection.mercatorToWgs84(
241
+ /** @type {import("ol/geom").Point} */ (
242
+ feature.getGeometry()
243
+ ).getCoordinates(),
244
+ );
245
+ } else {
246
+ stop();
247
+ }
248
+ });
249
+
250
+ mode.value = ViewshedPluginModes.MOVE;
251
+
252
+ removeInteraction = () => {
253
+ removeInteraction = () => {}; // needs to be before currentEditSession.value.stop(), otherwise recursion
254
+
255
+ if (geometryListenerKey) {
256
+ unByKey(geometryListenerKey);
257
+ }
258
+ currentEditSession.value?.stop();
259
+ currentEditSession.value = null;
260
+ app.layers.remove(layer);
261
+ destroyLayerWithFeature();
262
+ mode.value = ViewshedPluginModes.EDIT;
263
+ };
264
+ } else {
265
+ removeInteraction();
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Changes the mode and the current viewshed. Only works with viewshedMode EDIT and VIEW.
271
+ * @param {ViewshedPluginModes} viewshedMode
272
+ * @param {import("./viewshed.js").default} viewshed
273
+ */
274
+ function changeMode(viewshedMode, viewshed) {
275
+ removeInteraction();
276
+ if (currentViewshed.value !== viewshed) {
277
+ setCurrentViewshed(viewshed);
278
+ heightMode.value = viewshed.heightOffset
279
+ ? HeightModes.RELATIVE
280
+ : HeightModes.ABSOLUTE;
281
+ }
282
+ currentIsPersisted.value = !!viewshed.properties.title;
283
+ mode.value = viewshedMode;
284
+ if (currentIsPersisted.value) {
285
+ categoryHelper.setVisibility(viewshed.name, true);
286
+ }
287
+ }
288
+
289
+ const categoryListener = [
290
+ categoryHelper.collectionComponent.collection.removed.addEventListener(
291
+ (item) => {
292
+ categoryHelper.remove(item.name);
293
+ if (currentViewshed.value?.name === item.name) {
294
+ stop();
295
+ }
296
+ },
297
+ ),
298
+ categoryHelper.renamed.addEventListener(({ item, title }) => {
299
+ const viewshedWindow = app.windowManager.get(
300
+ `${categoryHelper.collectionComponent.id}-editor`,
301
+ );
302
+ if (viewshedWindow && currentViewshed.value?.name === item.name) {
303
+ viewshedWindow.state.headerTitle = title;
304
+ }
305
+ }),
306
+ categoryHelper.visibilityChanged.addEventListener((item) => {
307
+ const isCurrentlyVisible = currentViewshed.value?.name === item.name;
308
+ if (isCurrentlyVisible) {
309
+ stop();
310
+ } else {
311
+ changeMode(ViewshedPluginModes.VIEW, item);
312
+ categoryHelper.clearSelection();
313
+ }
314
+ categoryHelper.setVisibility(item.name, !isCurrentlyVisible);
315
+ }),
316
+ ];
317
+
318
+ function changeHeightMode(newHeightMode) {
319
+ if (newHeightMode === heightMode.value || !currentViewshed.value) {
320
+ return;
321
+ }
322
+
323
+ heightMode.value = newHeightMode;
324
+
325
+ if (mode.value === ViewshedPluginModes.MOVE) {
326
+ removeInteraction();
327
+ }
328
+
329
+ if (mode.value === ViewshedPluginModes.CREATE) {
330
+ updateFeatureInteraction(
331
+ app.maps.eventHandler.featureInteraction,
332
+ newHeightMode,
333
+ );
334
+ currentViewshed.value.heightOffset =
335
+ newHeightMode === HeightModes.ABSOLUTE ? 0 : defaultHeightOffset;
336
+ } else if (mode.value === ViewshedPluginModes.EDIT) {
337
+ const { position: currentPosition, heightOffset: currentHeightOffset } =
338
+ currentViewshed.value;
339
+ if (newHeightMode === HeightModes.RELATIVE) {
340
+ /** @type {import("@vcmap/core").CesiumMap} */ (app.maps.activeMap)
341
+ .getHeightFromTerrain([Projection.wgs84ToMercator(currentPosition)])
342
+ .then((value) => {
343
+ const newPosition = Projection.mercatorToWgs84(value[0]);
344
+
345
+ if (currentViewshed.value) {
346
+ currentViewshed.value.heightOffset =
347
+ currentPosition[2] - newPosition[2];
348
+ currentViewshed.value.position = newPosition;
349
+ }
350
+ });
351
+ } else {
352
+ currentViewshed.value.position = [
353
+ currentPosition[0],
354
+ currentPosition[1],
355
+ currentPosition[2] + currentHeightOffset,
356
+ ];
357
+ currentViewshed.value.heightOffset = 0;
358
+ }
359
+ }
360
+ }
361
+
362
+ return {
363
+ currentViewshed,
364
+ currentIsPersisted,
365
+ currentEditSession,
366
+ createViewshed,
367
+ viewViewshed(viewshed) {
368
+ changeMode(ViewshedPluginModes.VIEW, viewshed);
369
+ },
370
+ editViewshed(viewshed) {
371
+ changeMode(ViewshedPluginModes.EDIT, viewshed);
372
+ if (currentIsPersisted.value) {
373
+ // makes sure the editor window is open, if it is not triggered by a new selection but e.g. the tristate button
374
+ categoryHelper.setSelection(viewshed.name);
375
+ categoryHelper.collectionComponent.openEditorWindow(viewshed);
376
+ }
377
+ },
378
+ persistCurrent() {
379
+ if (currentViewshed.value) {
380
+ categoryHelper.add(currentViewshed.value);
381
+ currentIsPersisted.value = true;
382
+ categoryHelper.setSelection(currentViewshed.value.name);
383
+ categoryHelper.setVisibility(currentViewshed.value.name, true);
384
+ }
385
+ },
386
+ moveCurrentViewshed,
387
+ setupMultiSelect() {
388
+ removeInteraction();
389
+ mode.value = ViewshedPluginModes.MULTI_SELECT;
390
+ setCurrentViewshed(null);
391
+ },
392
+ mode,
393
+ heightMode,
394
+ changeHeightMode,
395
+ async placeCurrentFeaturesOnTerrain() {
396
+ if (
397
+ currentViewshed.value &&
398
+ currentEditSession.value?.type === SessionType.EDIT_FEATURES &&
399
+ app.maps.activeMap instanceof CesiumMap
400
+ ) {
401
+ const terrainHeight = await app.maps.activeMap.getHeightFromTerrain([
402
+ Projection.wgs84ToMercator(currentViewshed.value?.position),
403
+ ]);
404
+
405
+ const diff = terrainHeight[0][2] - currentViewshed.value.position[2];
406
+
407
+ currentEditSession.value.translate(0, 0, diff);
408
+ }
409
+ },
410
+ stop,
411
+ destroy() {
412
+ stop();
413
+ categoryListener.forEach((l) => l());
414
+ },
415
+ };
416
+ }