@vcmap/ui 5.0.0-rc.10 → 5.0.0-rc.13

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.
Files changed (165) hide show
  1. package/README.md +12 -5
  2. package/build/build.js +6 -3
  3. package/build/buildHelpers.js +12 -4
  4. package/build/buildPreview.js +7 -0
  5. package/build/getPluginProxies.js +4 -0
  6. package/config/aerowest.config.json +13 -3
  7. package/config/base.config.json +398 -219
  8. package/config/codes.config.json +397 -0
  9. package/config/dev.config.json +375 -1
  10. package/config/graphFeatureInfo.config.json +100 -0
  11. package/config/www.config.json +1232 -0
  12. package/dist/assets/{cesium.eb5667.js → cesium.21663e.js} +0 -0
  13. package/dist/assets/cesium.js +1 -1
  14. package/dist/assets/core.63242d.js +4 -0
  15. package/dist/assets/core.js +1 -1
  16. package/dist/assets/font/OFL.txt +93 -0
  17. package/dist/assets/font/TitilliumWeb-Regular.woff2 +0 -0
  18. package/dist/assets/{index.4ccd4433.js → index.44b91cfe.js} +1 -1
  19. package/dist/assets/{ol.ef03b1.js → ol.88ba9d.js} +0 -0
  20. package/dist/assets/ol.js +1 -1
  21. package/dist/assets/ui.3c2933.css +1 -0
  22. package/dist/assets/ui.3c2933.js +71 -0
  23. package/dist/assets/ui.js +1 -1
  24. package/dist/assets/vue.c897fc.js +9 -0
  25. package/dist/assets/vue.js +2 -1
  26. package/dist/assets/{vuetify.401a29.css → vuetify.147c3a.css} +1 -1
  27. package/dist/assets/{vuetify.401a29.js → vuetify.147c3a.js} +72 -72
  28. package/dist/assets/vuetify.js +2 -2
  29. package/dist/index.html +1 -5
  30. package/index.js +39 -5
  31. package/lib/vue.js +1 -0
  32. package/map.config.json +15 -6
  33. package/package.json +17 -8
  34. package/plugins/@vcmap/create-link/fallbackCreateLink.vue +71 -0
  35. package/plugins/@vcmap/create-link/index.js +83 -0
  36. package/plugins/@vcmap/create-link/package.json +6 -0
  37. package/plugins/@vcmap/pluginExample/index.js +2 -2
  38. package/plugins/@vcmap/pluginExample/pluginExampleComponent.vue +20 -3
  39. package/plugins/@vcmap/project-selector/ProjectSelectorComponent.vue +1 -1
  40. package/plugins/@vcmap/project-selector/index.js +1 -1
  41. package/plugins/@vcmap/project-selector/package.json +1 -2
  42. package/plugins/@vcmap/theme-changer/ThemeChangerComponent.vue +1 -1
  43. package/plugins/@vcmap/theme-changer/index.js +1 -1
  44. package/plugins/@vcmap/theme-changer/package.json +1 -2
  45. package/plugins/categoryTest/Categories.vue +89 -1
  46. package/plugins/categoryTest/Category.vue +1 -1
  47. package/plugins/example/index.js +10 -23
  48. package/plugins/simple-graph/README.md +51 -0
  49. package/plugins/simple-graph/SimpleGraphComponent.vue +70 -0
  50. package/plugins/simple-graph/index.js +17 -0
  51. package/plugins/simple-graph/package.json +11 -0
  52. package/plugins/simple-graph/simpleGraphView.js +76 -0
  53. package/plugins/test/editor.vue +1 -1
  54. package/plugins/test/index.js +76 -9
  55. package/plugins/test/toolbox-data.js +82 -57
  56. package/plugins/test/windowManagerExample.vue +1 -1
  57. package/src/actions/stateRefAction.js +2 -2
  58. package/src/actions/styleSelector.vue +1 -1
  59. package/src/application/Navbar.vue +13 -2
  60. package/src/application/VcsApp.vue +301 -116
  61. package/src/application/VcsMap.vue +1 -1
  62. package/src/application/VcsSettings.vue +1 -1
  63. package/src/application/vcsAppWrapper.vue +1 -0
  64. package/src/assets/font/OFL.txt +93 -0
  65. package/src/assets/font/TitilliumWeb-Regular.woff2 +0 -0
  66. package/src/components/form-inputs-controls/VcsCheckbox.vue +13 -0
  67. package/src/components/form-inputs-controls/VcsColorPicker.vue +1 -1
  68. package/src/components/form-inputs-controls/VcsRadio.vue +123 -0
  69. package/src/components/form-output/VcsFormattedNumber.vue +1 -1
  70. package/src/components/lists/VcsActionList.vue +22 -7
  71. package/src/components/lists/VcsTreeview.vue +4 -4
  72. package/src/components/lists/VcsTreeviewLeaf.vue +10 -3
  73. package/src/components/lists/VcsTreeviewSearchbar.vue +1 -2
  74. package/src/components/tables/VcsTable.vue +245 -0
  75. package/src/contentTree/LayerTree.vue +1 -1
  76. package/src/contentTree/contentTreeCollection.js +4 -4
  77. package/src/contentTree/contentTreeItem.js +9 -9
  78. package/src/contentTree/groupContentTreeItem.js +1 -1
  79. package/src/contentTree/layerContentTreeItem.js +15 -1
  80. package/src/contentTree/layerGroupContentTreeItem.js +21 -1
  81. package/src/contentTree/nodeContentTreeItem.js +1 -1
  82. package/src/featureInfo/AddressBalloonComponent.vue +47 -0
  83. package/src/featureInfo/BalloonComponent.vue +140 -0
  84. package/src/featureInfo/abstractFeatureInfoView.js +313 -0
  85. package/src/featureInfo/addressBalloonFeatureInfoView.js +118 -0
  86. package/src/featureInfo/balloonFeatureInfoView.js +151 -0
  87. package/src/featureInfo/balloonHelper.js +132 -0
  88. package/src/featureInfo/featureInfo.js +457 -0
  89. package/src/featureInfo/featureInfoInteraction.js +42 -0
  90. package/src/featureInfo/iframeFeatureInfoView.js +95 -0
  91. package/src/featureInfo/tableFeatureInfoView.js +106 -0
  92. package/src/i18n/de.js +26 -0
  93. package/src/i18n/en.js +26 -0
  94. package/src/i18n/i18nCollection.js +17 -0
  95. package/src/icons/+all.js +80 -0
  96. package/src/icons/ClippingHorizontalIcon.vue +7 -0
  97. package/src/icons/ClippingIcon.vue +7 -0
  98. package/src/icons/ClippingVerticalIcon.vue +7 -0
  99. package/src/icons/ColorPickerIcon.vue +7 -0
  100. package/src/icons/ComponentsIcon.vue +2 -2
  101. package/src/icons/DimensionsHouseIcon.vue +11 -9
  102. package/src/icons/EditIcon.vue +7 -0
  103. package/src/icons/GlobalTerrainIcon.vue +9 -0
  104. package/src/icons/GroundIcon.vue +18 -0
  105. package/src/icons/HideIcon.vue +12 -0
  106. package/src/icons/LogoutIcon.vue +7 -0
  107. package/src/icons/ObjectAttributeIcon.vue +2 -13
  108. package/src/icons/PedestrianIcon.vue +2 -3
  109. package/src/icons/PenIcon.vue +2 -9
  110. package/src/icons/PoiIcon.vue +5 -2
  111. package/src/icons/PointSelectIcon.vue +4 -2
  112. package/src/icons/QueryIcon.vue +6 -7
  113. package/src/icons/ScreenshotIcon.vue +16 -0
  114. package/src/icons/ShareIcon.vue +4 -16
  115. package/src/icons/SkipNextIcon.vue +3 -1
  116. package/src/icons/TerrainBoxIcon.vue +9 -0
  117. package/src/icons/ToolsIcon.vue +4 -30
  118. package/src/icons/UploadIcon.vue +2 -9
  119. package/src/icons/UserProfileIcon.vue +7 -0
  120. package/src/icons/UserShareIcon.vue +7 -0
  121. package/src/icons/VideoRecorderIcon.vue +5 -9
  122. package/src/icons/ViewpointFlightIcon.vue +11 -0
  123. package/src/icons/ViewpointIcon.vue +11 -0
  124. package/src/icons/Viewshed360Icon.vue +7 -0
  125. package/src/icons/ViewshedConeIcon.vue +7 -0
  126. package/src/icons/ViewshedIcon.vue +7 -0
  127. package/src/icons/WallIcon.vue +4 -9
  128. package/src/legend/legendHelper.js +193 -0
  129. package/src/legend/styleLegendItem.vue +129 -0
  130. package/src/legend/vcsLegend.vue +92 -0
  131. package/src/manager/buttonManager.js +7 -12
  132. package/src/manager/categoryManager/ComponentsManager.vue +30 -0
  133. package/src/manager/categoryManager/categoryManager.js +500 -0
  134. package/src/manager/contextMenu/contextMenuComponent.vue +43 -0
  135. package/src/manager/contextMenu/contextMenuInteraction.js +42 -0
  136. package/src/manager/contextMenu/contextMenuManager.js +197 -0
  137. package/src/manager/navbarManager.js +9 -9
  138. package/src/manager/toolbox/GroupToolboxComponent.vue +118 -0
  139. package/src/manager/toolbox/SelectToolboxComponent.vue +128 -0
  140. package/src/manager/toolbox/ToolboxManager.vue +116 -98
  141. package/src/manager/toolbox/toolboxManager.js +235 -86
  142. package/src/manager/window/WindowComponent.vue +1 -1
  143. package/src/manager/window/WindowManager.vue +5 -3
  144. package/src/manager/window/windowManager.js +118 -14
  145. package/src/navigation/mapNavigation.vue +3 -5
  146. package/src/navigation/overviewMap.js +28 -5
  147. package/src/navigation/vcsCompass.vue +1 -1
  148. package/src/pluginHelper.js +42 -10
  149. package/src/setup.js +0 -2
  150. package/src/state.js +256 -0
  151. package/src/styles/_theming.scss +0 -5
  152. package/src/styles/variables.scss +7 -0
  153. package/src/styles/vcsFont.scss +17 -0
  154. package/src/uiConfig.js +79 -0
  155. package/src/vcsUiApp.js +213 -22
  156. package/src/vuePlugins/vuetify.js +14 -4
  157. package/config/berlin.config.json +0 -510
  158. package/dist/assets/core.216494.js +0 -4
  159. package/dist/assets/ui.99a1a7.css +0 -1
  160. package/dist/assets/ui.99a1a7.js +0 -70
  161. package/dist/assets/vue-composition-api.c5aca1.js +0 -14
  162. package/dist/assets/vue-composition-api.js +0 -2
  163. package/dist/assets/vue.762edd.js +0 -9
  164. package/lib/vue-composition-api.js +0 -2
  165. package/src/manager/toolbox/ToolboxGroupComponent.vue +0 -128
@@ -41,7 +41,7 @@
41
41
  <script>
42
42
  import {
43
43
  onMounted, onUnmounted, computed, ref, nextTick,
44
- } from '@vue/composition-api';
44
+ } from 'vue';
45
45
  import { fromEvent } from 'rxjs';
46
46
  import { switchMap, take, map, tap } from 'rxjs/operators';
47
47
  import { WindowSlot } from './windowManager.js';
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div :class="$vuetify.breakpoint.xs ? 'win-container-mobile' : 'unset'">
2
+ <div
3
+ :class="$vuetify.breakpoint.xs ? 'win-container-mobile' : 'unset'"
4
+ >
3
5
  <WindowComponent
4
6
  v-for="(id, zIndex) in componentIds"
5
7
  :key="id"
@@ -46,7 +48,7 @@
46
48
  </style>
47
49
 
48
50
  <script>
49
- import { inject } from '@vue/composition-api';
51
+ import { inject, ref } from 'vue';
50
52
 
51
53
  import WindowComponent from './WindowComponent.vue';
52
54
  import WindowComponentHeader from './WindowComponentHeader.vue';
@@ -109,7 +111,7 @@
109
111
  };
110
112
 
111
113
  return {
112
- componentIds,
114
+ componentIds: ref(componentIds),
113
115
  getComponent: id => windowManager.get(id).component,
114
116
  getHeaderComponent: id => windowManager.get(id).headerComponent || WindowComponentHeader,
115
117
  getStyles,
@@ -1,4 +1,4 @@
1
- import { reactive, ref } from '@vue/composition-api';
1
+ import { reactive, ref } from 'vue';
2
2
  import { VcsEvent } from '@vcmap/core';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
  import { parseEnumValue } from '@vcsuite/parsers';
@@ -72,8 +72,8 @@ export const WindowPositions = {
72
72
  /**
73
73
  * @typedef WindowComponentOptions
74
74
  * @property {string} [id] Optional ID, If not provided an uuid will be generated.
75
- * @property {VueComponent} component Main Component which is shown below the header.
76
- * @property {VueComponent} [headerComponent] Replaces the Header Component.
75
+ * @property {import("vue").Component} component Main Component which is shown below the header.
76
+ * @property {import("vue").Component} [headerComponent] Replaces the Header Component.
77
77
  * @property {WindowPositionOptions} [position] Will be ignored if WindowSlot !== DETACHED, can be given otherwise or default will be used
78
78
  * @property {WindowState} [state]
79
79
  * @property {WindowSlot} [slot] If WindowSlot is not detached the position will be ignored
@@ -94,8 +94,8 @@ export const WindowPositions = {
94
94
  /**
95
95
  * @typedef WindowComponent
96
96
  * @property {string} id
97
- * @property {VueComponent} component
98
- * @property {VueComponent} [headerComponent]
97
+ * @property {import("vue").Component} component
98
+ * @property {import("vue").Component} [headerComponent]
99
99
  * @property {WindowPosition} position
100
100
  * @property {WindowState} state
101
101
  * @property {Ref<UnwrapRef<WindowSlot>>} slot
@@ -150,20 +150,124 @@ export function windowPositionFromOptions(windowPositionOptions, windowPosition
150
150
  return Object.assign(windowPosition, result);
151
151
  }
152
152
 
153
+ /**
154
+ * @enum {number}
155
+ * @property {number} TOP_LEFT
156
+ * @property {number} TOP_RIGHT
157
+ * @property {number} BOTTOM_LEFT
158
+ * @property {number} BOTTOM_RIGHT
159
+ */
160
+ export const WindowAlignment = {
161
+ TOP_LEFT: 1,
162
+ TOP_RIGHT: 2,
163
+ BOTTOM_LEFT: 3,
164
+ BOTTOM_RIGHT: 4,
165
+ };
166
+
167
+ /**
168
+ * @returns {HTMLElement|null}
169
+ */
170
+ function getActiveMapElement() {
171
+ const mapElements = document.getElementsByClassName('mapElement');
172
+ for (let i = 0; i < mapElements.length; i++) {
173
+ const element = mapElements.item(i);
174
+ if (element.style.display !== 'none') {
175
+ return element;
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
153
181
  /**
154
182
  * WindowPositionOptions from client position relative to a HTMLElement
155
- * @param {number} x
156
- * @param {number} y
157
- * @param {string|HTMLElement} [element='mapElement']
183
+ * @param {number} x - client pixel position
184
+ * @param {number} y - client pixel position
185
+ * @param {HTMLElement} [element='mapElement'] - the element. if none is provided, the currently active mapElement will be taken
186
+ * @param {WindowAlignment} [alignment=WindowAlignment.TOP_LEFT]
158
187
  * @returns {WindowPositionOptions}
159
188
  */
160
- export function getWindowPositionOptions(x, y, element = 'mapElement') {
161
- let mapElement = element;
162
- if (typeof mapElement === 'string') {
163
- mapElement = document.getElementsByClassName(element).item(0);
189
+ export function getWindowPositionOptions(x, y, element, alignment = WindowAlignment.TOP_LEFT) {
190
+ const mapElement = element ?? getActiveMapElement();
191
+ if (!mapElement) {
192
+ return { left: x, top: y };
164
193
  }
194
+
195
+ const { left, top, width, height } = mapElement.getBoundingClientRect();
196
+ if (alignment === WindowAlignment.TOP_LEFT) {
197
+ return { left: x - left, top: y - top };
198
+ } else if (alignment === WindowAlignment.TOP_RIGHT) {
199
+ return { right: (left + width) - x, top: y - top };
200
+ } else if (alignment === WindowAlignment.BOTTOM_LEFT) {
201
+ return { left: x - left, bottom: (height + top) - y };
202
+ }
203
+ return { right: (left + width) - x, bottom: (height + top) - y };
204
+ }
205
+
206
+ /**
207
+ * Get window position options based on a pixel in the map
208
+ * @param {import("@vcmap/cesium").Cartesian2} windowPosition - the window position, as retrieved from an InteractionEvent
209
+ * @param {WindowAlignment} [alignment]
210
+ * @returns {WindowPositionOptions}
211
+ */
212
+ export function getWindowPositionOptionsFromMapEvent(windowPosition, alignment) {
213
+ const mapElement = getActiveMapElement();
214
+ if (!mapElement) {
215
+ return { left: windowPosition.x, top: windowPosition.y };
216
+ }
217
+
218
+ const { left, top } = mapElement.getBoundingClientRect();
219
+ return getWindowPositionOptions(windowPosition.x + left, windowPosition.y + top, mapElement, alignment);
220
+ }
221
+
222
+
223
+ /**
224
+ * Fits a window aligned top left so it fits into the parent. this will change the alignment to be bottom or right depending
225
+ * on if the window would not fit into the parent.
226
+ * @param {number} x - client pixel position
227
+ * @param {number} y - client pixel position
228
+ * @param {number} width - window width to fit
229
+ * @param {number} height - window height to fit
230
+ * @param {HTMLElement} [element='mapElement'] - the element. if none is provided, the currently active mapElement will be taken
231
+ * @returns {WindowPositionOptions}
232
+ */
233
+ export function getFittedWindowPositionOptions(x, y, width, height, element) {
234
+ const mapElement = element ?? getActiveMapElement();
235
+ if (!mapElement) {
236
+ return { left: x, top: y };
237
+ }
238
+
239
+ const { width: parentWidth, height: parentHeight } = mapElement.getBoundingClientRect();
240
+ const bottom = y + height > parentHeight;
241
+ const right = x + width > parentWidth;
242
+ let alignment = WindowAlignment.TOP_LEFT;
243
+ if (bottom) {
244
+ if (right) {
245
+ alignment = WindowAlignment.BOTTOM_RIGHT;
246
+ } else {
247
+ alignment = WindowAlignment.BOTTOM_LEFT;
248
+ }
249
+ } else if (right) {
250
+ alignment = WindowAlignment.TOP_RIGHT;
251
+ }
252
+ return getWindowPositionOptions(x, y, mapElement, alignment);
253
+ }
254
+
255
+ /**
256
+ * Fits a window aligned top left so it fits into currently active map. this will change the alignment to be bottom or right depending
257
+ * on if the window would not fit into active map element.
258
+ * @param {import("@vcmap/cesium").Cartesian2} windowPosition - the window position, as retrieved from an InteractionEvent
259
+ * @param {number} width
260
+ * @param {number} height
261
+ * @returns {WindowPositionOptions}
262
+ */
263
+ export function getFittedWindowPositionOptionsFromMapEvent(windowPosition, width, height) {
264
+ const mapElement = getActiveMapElement();
265
+ if (!mapElement) {
266
+ return { left: windowPosition.x, top: windowPosition.y };
267
+ }
268
+
165
269
  const { left, top } = mapElement.getBoundingClientRect();
166
- return { left: x - left, top: y - top };
270
+ return getFittedWindowPositionOptions(windowPosition.x + left, windowPosition.y + top, width, height, mapElement);
167
271
  }
168
272
 
169
273
  /**
@@ -185,7 +289,7 @@ export class WindowManager {
185
289
  * reactive ordered array of ids,
186
290
  * @type {Array<string>}
187
291
  */
188
- this.componentIds = reactive([]);
292
+ this.componentIds = [];
189
293
 
190
294
  /**
191
295
  * @type {Map<string, WindowComponent>}
@@ -36,7 +36,7 @@
36
36
  </template>
37
37
 
38
38
  <script>
39
- import { computed, inject, ref, onUnmounted } from '@vue/composition-api';
39
+ import { computed, inject, ref, reactive, onUnmounted } from 'vue';
40
40
  import { ObliqueMap, CesiumMap, OpenlayersMap } from '@vcmap/core';
41
41
  import { unByKey } from 'ol/Observable.js';
42
42
  import { createOverviewMapAction } from '../actions/actionHelper.js';
@@ -119,7 +119,7 @@
119
119
  VcsCompass,
120
120
  },
121
121
  setup() {
122
- /** @type {VcsApp} */
122
+ /** @type {VcsUiApp} */
123
123
  const app = inject('vcsApp');
124
124
  const viewMode = ref(OrientationToolsViewMode.TWO_D);
125
125
  const headingRef = ref(0);
@@ -179,9 +179,7 @@
179
179
  isOblique: computed(() => viewMode.value === OrientationToolsViewMode.OBLIQUE),
180
180
  zoomIn() { zoom(app.maps.activeMap); }, // debounce?
181
181
  zoomOut() { zoom(app.maps.activeMap, true); },
182
- left: () => {},
183
- right: () => {},
184
- overviewAction: ref(action),
182
+ overviewAction: reactive(action),
185
183
  };
186
184
  },
187
185
  };
@@ -20,7 +20,7 @@ import { unByKey } from 'ol/Observable.js';
20
20
  import VectorSource from 'ol/source/Vector.js';
21
21
  import { WindowSlot } from '../manager/window/windowManager.js';
22
22
  import OverviewMapClickedInteraction from './overviewMapClickedInteraction.js';
23
- import { vuetify } from '../vuePlugins/vuetify.js';
23
+ import { defaultPrimaryColor } from '../vuePlugins/vuetify.js';
24
24
  import { vcsAppSymbol } from '../pluginHelper.js';
25
25
  import VcsMap from '../application/VcsMap.vue';
26
26
 
@@ -99,8 +99,8 @@ class OverviewMap {
99
99
  */
100
100
  this._obliqueSelectedImageLayer = null;
101
101
 
102
- const { primary, accent } = vuetify.userPreset.theme.themes.light;
103
- const fillColor = Color.fromCssColorString(accent);
102
+ const primary = app.uiConfig.config.value.primaryColor ?? defaultPrimaryColor;
103
+ const fillColor = Color.fromCssColorString('#EDEDED');
104
104
 
105
105
  /**
106
106
  * @type {VectorStyleItem}
@@ -199,7 +199,7 @@ class OverviewMap {
199
199
  * @type {Array<function():void>}
200
200
  * @private
201
201
  */
202
- this._layerCollectionListener = [
202
+ this._collectionListeners = [
203
203
  this._app.maps.layerCollection.added.addEventListener((layer) => {
204
204
  if (layer.properties.showInOverviewMap) {
205
205
  const clone = deserializeLayer(this._app, layer.toJSON());
@@ -219,6 +219,16 @@ class OverviewMap {
219
219
  this._map.layerCollection.remove(clone);
220
220
  }
221
221
  }),
222
+ this._app.uiConfig.added.addEventListener((item) => {
223
+ if (item?.name === 'primaryColor') {
224
+ this._setObliqueColor(item.value);
225
+ }
226
+ }),
227
+ this._app.uiConfig.removed.addEventListener((item) => {
228
+ if (item?.name === 'primaryColor') {
229
+ this._setObliqueColor(defaultPrimaryColor);
230
+ }
231
+ }),
222
232
  ];
223
233
  }
224
234
 
@@ -254,6 +264,18 @@ class OverviewMap {
254
264
  return this._mapClicked;
255
265
  }
256
266
 
267
+ /**
268
+ * @param {string} color
269
+ * @private
270
+ */
271
+ _setObliqueColor(color) {
272
+ this.obliqueUnselectedStyle?.stroke?.setColor(color);
273
+ this.obliqueSelectedStyle?.stroke?.setColor(color);
274
+ this._obliqueTileLayer?.forceRedraw?.();
275
+ this._obliqueImageLayer?.forceRedraw?.();
276
+ this._obliqueSelectedImageLayer?.forceRedraw?.();
277
+ }
278
+
257
279
  /**
258
280
  * @private
259
281
  */
@@ -574,7 +596,8 @@ class OverviewMap {
574
596
 
575
597
  destroy() {
576
598
  this._clearListeners();
577
- this._layerCollectionListener.forEach(cb => cb());
599
+ this._collectionListeners.forEach(cb => cb());
600
+ this._collectionListeners = [];
578
601
  if (this._mapPointerListener) {
579
602
  this._mapPointerListener();
580
603
  this._mapPointerListener = null;
@@ -28,7 +28,7 @@
28
28
 
29
29
 
30
30
  <script>
31
- import { computed, onUnmounted, ref } from '@vue/composition-api';
31
+ import { computed, onUnmounted, ref } from 'vue';
32
32
 
33
33
  import { fromEvent, merge, of, Subject } from 'rxjs';
34
34
  import { takeUntil, tap } from 'rxjs/operators';
@@ -19,6 +19,39 @@ export const vcsAppSymbol = Symbol('vcsApp');
19
19
  */
20
20
  export const pluginFactorySymbol = Symbol('pluginFactory');
21
21
 
22
+ /**
23
+ * A symbol added to each plugin which describes the base URL from which the plugin was loaded (without the filename)
24
+ * @type {symbol}
25
+ */
26
+ export const pluginBaseUrlSymbol = Symbol('pluginBaseUrl');
27
+
28
+ /**
29
+ * A helper function to create an absolute URL from a relative plugin asset URL. For example, when
30
+ * shipping your plugin with a "plugin-asset/icon.png", you can always retrieve said icon with getPluginAssetUrl(app, name, 'pluing-assets/icon.png')
31
+ * Returns null, if the plugin does not exist.
32
+ * @param {VcsUiApp} app
33
+ * @param {string} pluginName
34
+ * @param {string} asset
35
+ * @returns {string|null}
36
+ */
37
+ export function getPluginAssetUrl(app, pluginName, asset) {
38
+ check(pluginName, String);
39
+ check(asset, String);
40
+
41
+ const plugin = app.plugins.getByKey(pluginName);
42
+ if (plugin && plugin[pluginBaseUrlSymbol]) {
43
+ const baseUrl = new URL(plugin[pluginBaseUrlSymbol]);
44
+ const assetUrl = new URL(asset.replace(/^\//, '/'), baseUrl);
45
+ baseUrl.searchParams.forEach((value, key) => {
46
+ if (!assetUrl.searchParams.has(key)) {
47
+ assetUrl.searchParams.set(key, value);
48
+ }
49
+ });
50
+ return assetUrl.toString();
51
+ }
52
+ return null;
53
+ }
54
+
22
55
  /**
23
56
  * validates the name according to package name pattern
24
57
  * @param {string} name
@@ -32,18 +65,14 @@ export function isValidPackageName(name) {
32
65
  }
33
66
 
34
67
  /**
35
- * @param {VcsUiApp} app
36
68
  * @param {string} name
37
69
  * @param {PluginConfig} config
38
- * @param {string} [registry='https://plugins.virtualcitymap.de/']
39
70
  * @returns {Promise<VcsPlugin|null>}
40
71
  */
41
- export async function loadPlugin(app, name, config, registry = 'https://plugins.virtualcitymap.de/') {
72
+ export async function loadPlugin(name, config) {
42
73
  let module = config.entry;
43
74
 
44
- if (!module) {
45
- module = `${registry.replace(/\/?$/, '')}/${name}/${config.version || '*'}/index.js`;
46
- } else if (!/^(https?:\/\/|\/)/.test(module)) {
75
+ if (!/^(https?:\/\/|\/)/.test(module)) {
47
76
  module = `${window.location.origin}${window.location.pathname.replace(/\/?$/, '/')}${module}`;
48
77
  } else if (module === '_dev') {
49
78
  module = `/${name}.js`;
@@ -65,7 +94,9 @@ export async function loadPlugin(app, name, config, registry = 'https://plugins.
65
94
  getLogger().error(`plugin ${name} does not provide a default exported function`);
66
95
  return null;
67
96
  }
68
- const pluginInstance = await plugin.default(config);
97
+ const baseUrl = new URL(module);
98
+ baseUrl.pathname = baseUrl.pathname.replace(/\/[^/]+$/, '/');
99
+ const pluginInstance = await plugin.default(config, baseUrl.toString());
69
100
 
70
101
  if (!pluginInstance.name) {
71
102
  getLogger().error(`plugin ${name} does not expose a name`);
@@ -75,6 +106,7 @@ export async function loadPlugin(app, name, config, registry = 'https://plugins.
75
106
  return null;
76
107
  }
77
108
  pluginInstance[pluginFactorySymbol] = plugin.default;
109
+ pluginInstance[pluginBaseUrlSymbol] = baseUrl.toString();
78
110
  return pluginInstance;
79
111
  } catch (err) {
80
112
  getLogger().error(`failed to load plugin ${name}`);
@@ -89,9 +121,8 @@ export async function loadPlugin(app, name, config, registry = 'https://plugins.
89
121
  */
90
122
  export function serializePlugin(plugin) {
91
123
  const serializedPlugin = plugin.toJSON ? plugin.toJSON() : {};
92
- if (plugin[pluginFactorySymbol]) {
93
- serializedPlugin[pluginFactorySymbol] = plugin[pluginFactorySymbol];
94
- }
124
+ serializedPlugin[pluginFactorySymbol] = plugin[pluginFactorySymbol];
125
+ serializedPlugin[pluginBaseUrlSymbol] = plugin[pluginBaseUrlSymbol];
95
126
  return serializedPlugin;
96
127
  }
97
128
 
@@ -102,5 +133,6 @@ export function serializePlugin(plugin) {
102
133
  export async function deserializePlugin(serializedPlugin) {
103
134
  const reincarnation = await serializedPlugin[pluginFactorySymbol](serializedPlugin);
104
135
  reincarnation[pluginFactorySymbol] = serializedPlugin[pluginFactorySymbol];
136
+ reincarnation[pluginBaseUrlSymbol] = serializedPlugin[pluginBaseUrlSymbol];
105
137
  return reincarnation;
106
138
  }
package/src/setup.js CHANGED
@@ -1,11 +1,9 @@
1
1
  import Vue from 'vue';
2
- import VueCompositionAPI from '@vue/composition-api';
3
2
 
4
3
  // eslint-disable-next-line no-unused-vars
5
4
  import * as core from '@vcmap/core';
6
5
  // pull in entire core for vcsClassRegistry
7
6
 
8
7
  Vue.config.productionTip = false;
9
- Vue.use(VueCompositionAPI);
10
8
 
11
9
  window.CESIUM_BASE_URL = '/node_modules/@vcmap/cesium/Source/';
package/src/state.js ADDED
@@ -0,0 +1,256 @@
1
+ import { check } from '@vcsuite/check';
2
+ import { getLogger } from '@vcsuite/logger';
3
+ import { ViewPoint } from '@vcmap/core';
4
+
5
+ /**
6
+ * @typedef {Object} LayerState
7
+ * @property {string} name
8
+ * @property {boolean} active
9
+ * @property {string} [styleName]
10
+ */
11
+
12
+ /**
13
+ * The URL state of a layer is an array. The first entry is the layer name,
14
+ * the second its active state encoded in an integer (1 active, 0 inactive),
15
+ * the third and optional entry, is an optional styleName to set on the layer
16
+ * @typedef {[string,number,string|0]} UrlLayerState
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PluginState
21
+ * @property {string} name
22
+ * @property {*} state
23
+ */
24
+
25
+ /**
26
+ * The URL state of a plugin is an array. The first entry is the plugin name, the second entry is
27
+ * an encoded object, which is the plugins state.
28
+ * @typedef {[string, *]} UrlPluginState
29
+ */
30
+
31
+ /**
32
+ * The URL state of a viewpoint is an array, the first entry is the camera position (or 0)
33
+ * the second is the ground position (or 0), the third is the distance, the last three are
34
+ * heading, pitch, roll in that order
35
+ * @typedef {[Array<number>|0,Array<number>|0,number,number,number,number]} UrlViewpointState
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} AppState
40
+ * @property {import("@vcmap/core").ViewPointOptions} [activeViewpoint]
41
+ * @property {string} [activeMap]
42
+ * @property {Array<string>} contextIds
43
+ * @property {Array<LayerState>} layers
44
+ * @property {Array<PluginState>} plugins
45
+ * @property {string} [activeObliqueCollection]
46
+ */
47
+
48
+ /**
49
+ * The URL state of the app is an array. To null parameters, pass in 0 instead.
50
+ * The first entry is the viewpoint state, the second the active map name
51
+ * The third is an array of contexts to apply the state to
52
+ * the fourth is an array of layer states
53
+ * the fifth is an array of plugin states
54
+ * the sixth is the currently active oblique collection or 0 if not applicable
55
+ * @typedef {[UrlViewpointState,string,Array<string>,Array<UrlLayerState>,Array<UrlPluginState>,(string|0)]} UrlAppState
56
+ */
57
+
58
+ /**
59
+ * @type {number}
60
+ */
61
+ const MAX_URL_LENGTH = 2048;
62
+
63
+ /**
64
+ * @returns {AppState}
65
+ */
66
+ export function createEmptyState() {
67
+ return {
68
+ contextIds: [],
69
+ layers: [],
70
+ plugins: [],
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @param {UrlViewpointState} state
76
+ * @returns {import("@vcmap/core").ViewPointOptions|null}
77
+ */
78
+ function parseUrlViewpointState(state) {
79
+ const vp = new ViewPoint({
80
+ cameraPosition: state[0] ?? undefined,
81
+ groundPosition: state[1] ?? undefined,
82
+ distance: state[2] > 0 ? state[2] : undefined,
83
+ heading: state[3],
84
+ pitch: state[4],
85
+ roll: state[5],
86
+ });
87
+
88
+ return vp.isValid() ? vp.toJSON() : null;
89
+ }
90
+
91
+ /**
92
+ * @param {UrlLayerState} state
93
+ * @returns {LayerState}
94
+ */
95
+ function parseUrlLayerState(state) {
96
+ const layerState = {
97
+ name: state[0],
98
+ active: !!state[1],
99
+ };
100
+ if (state[2] !== 0) {
101
+ layerState.styleName = state[2];
102
+ }
103
+ return layerState;
104
+ }
105
+
106
+ /**
107
+ * @param {LayerState} state
108
+ * @returns {UrlLayerState}
109
+ */
110
+ function writeUrlLayerState(state) {
111
+ return [state.name, state.active ? 1 : 0, state.styleName ?? 0];
112
+ }
113
+
114
+ /**
115
+ * @param {UrlPluginState} state
116
+ * @returns {PluginState}
117
+ */
118
+ function parseUrlPluginState(state) {
119
+ return {
120
+ name: state[0],
121
+ state: state[1],
122
+ };
123
+ }
124
+
125
+ /**
126
+ * @param {PluginState} state
127
+ * @returns {UrlPluginState}
128
+ */
129
+ function writeUrlPluginState(state) {
130
+ return [state.name, state.state];
131
+ }
132
+
133
+ /**
134
+ * @param {UrlAppState} urlState
135
+ * @returns {AppState}
136
+ */
137
+ function parseUrlAppState(urlState) {
138
+ const state = createEmptyState();
139
+ if (Array.isArray(urlState[0])) {
140
+ state.activeViewpoint = parseUrlViewpointState(urlState[0]);
141
+ }
142
+ if (typeof urlState[1] === 'string') {
143
+ state.activeMap = urlState[1];
144
+ }
145
+ if (Array.isArray(urlState[2])) {
146
+ state.contextIds = urlState[2].slice();
147
+ }
148
+ if (Array.isArray(urlState[3])) {
149
+ urlState[3].forEach((urlLayerState) => {
150
+ if (Array.isArray(urlLayerState)) {
151
+ state.layers.push(parseUrlLayerState(urlLayerState));
152
+ }
153
+ });
154
+ }
155
+ if (Array.isArray(urlState[4])) {
156
+ urlState[4].forEach((urlPluginState) => {
157
+ if (Array.isArray(urlPluginState)) {
158
+ state.plugins.push(parseUrlPluginState(urlPluginState));
159
+ }
160
+ });
161
+ }
162
+ if (typeof urlState[5] === 'string') {
163
+ state.activeObliqueCollection = urlState[5];
164
+ }
165
+ return state;
166
+ }
167
+
168
+ /**
169
+ * @param {AppState} state
170
+ * @param {number} maxLength
171
+ * @returns {UrlAppState}
172
+ */
173
+ function writeUrlAppState(state, maxLength) {
174
+ /**
175
+ * @type {UrlAppState}
176
+ */
177
+ const urlState = new Array(6).fill(0);
178
+ if (state.activeViewpoint) {
179
+ urlState[0] = [
180
+ state.activeViewpoint.cameraPosition?.slice() ?? 0,
181
+ state.activeViewpoint.groundPosition?.slice() ?? 0,
182
+ state.activeViewpoint.distance ?? 0,
183
+ state.activeViewpoint.heading,
184
+ state.activeViewpoint.pitch,
185
+ state.activeViewpoint.roll,
186
+ ];
187
+ }
188
+
189
+ if (state.activeMap) {
190
+ urlState[1] = state.activeMap;
191
+ }
192
+
193
+ urlState[2] = state.contextIds.slice();
194
+ urlState[3] = [];
195
+ urlState[4] = [];
196
+
197
+ if (state.activeObliqueCollection) {
198
+ urlState[5] = state.activeObliqueCollection;
199
+ }
200
+
201
+ state.layers.forEach((layerState) => {
202
+ const layerUrlState = writeUrlLayerState(layerState);
203
+ if ((JSON.stringify(urlState).length + JSON.stringify(layerUrlState).length) < maxLength) {
204
+ urlState[3].push(layerUrlState);
205
+ }
206
+ });
207
+
208
+ state.plugins.forEach((pluginState) => {
209
+ const urlPluginState = writeUrlPluginState(pluginState);
210
+ if ((JSON.stringify(urlState).length + JSON.stringify(urlPluginState).length) < maxLength) {
211
+ urlState[4].push(urlPluginState);
212
+ }
213
+ });
214
+
215
+ if (urlState[3].length !== state.layers.length || urlState[4].length !== state.plugins.length) {
216
+ getLogger('StateManagement').warning('State too large for URL: Not all layers and plugins are represented');
217
+ }
218
+
219
+ return urlState;
220
+ }
221
+
222
+ /**
223
+ * @param {(URL)=} url
224
+ * @returns {AppState}
225
+ */
226
+ export function getStateFromURL(url) {
227
+ check(url, URL);
228
+
229
+ if (url.searchParams.has('state')) {
230
+ try {
231
+ return parseUrlAppState(JSON.parse(url.searchParams.get('state')));
232
+ } catch (e) {
233
+ getLogger('StateManager').error('failed to parse the state URL parameter');
234
+ }
235
+ }
236
+ return createEmptyState();
237
+ }
238
+
239
+ /**
240
+ * @param {AppState} state
241
+ * @param {URL} url - sets the query parameter "state" on this URL
242
+ */
243
+ export function setStateToUrl(state, url) {
244
+ check(state, {
245
+ activeMap: [String, undefined],
246
+ activeViewpoint: [Object, undefined],
247
+ activeObliqueCollection: [String, undefined],
248
+ layers: Array,
249
+ plugins: Array,
250
+ contextIds: [String],
251
+ });
252
+ check(url, URL);
253
+
254
+ const maxLength = MAX_URL_LENGTH - url.toString().length;
255
+ url.searchParams.set('state', JSON.stringify(writeUrlAppState(state, maxLength)));
256
+ }