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

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 (116) hide show
  1. package/README.md +11 -4
  2. package/build/build.js +0 -3
  3. package/build/buildHelpers.js +0 -1
  4. package/build/buildPreview.js +7 -0
  5. package/config/aerowest.config.json +13 -3
  6. package/config/base.config.json +89 -64
  7. package/config/codes.config.json +397 -0
  8. package/config/dev.config.json +169 -0
  9. package/config/graphFeatureInfo.config.json +100 -0
  10. package/config/www.config.json +1232 -0
  11. package/dist/assets/{cesium.eb5667.js → cesium.e67536.js} +0 -0
  12. package/dist/assets/cesium.js +1 -1
  13. package/dist/assets/core.ebf665.js +4 -0
  14. package/dist/assets/core.js +1 -1
  15. package/dist/assets/{index.4ccd4433.js → index.9b213929.js} +1 -1
  16. package/dist/assets/{ol.ef03b1.js → ol.8bbd50.js} +0 -0
  17. package/dist/assets/ol.js +1 -1
  18. package/dist/assets/ui.fdfe0d.css +1 -0
  19. package/dist/assets/ui.fdfe0d.js +68 -0
  20. package/dist/assets/ui.js +1 -1
  21. package/dist/assets/vue.0bb7c6.js +9 -0
  22. package/dist/assets/vue.js +2 -1
  23. package/dist/assets/{vuetify.401a29.css → vuetify.53300f.css} +1 -1
  24. package/dist/assets/{vuetify.401a29.js → vuetify.53300f.js} +71 -71
  25. package/dist/assets/vuetify.js +2 -2
  26. package/dist/index.html +1 -1
  27. package/index.js +36 -5
  28. package/lib/vue.js +1 -0
  29. package/map.config.json +15 -6
  30. package/package.json +6 -7
  31. package/plugins/@vcmap/create-link/fallbackCreateLink.vue +71 -0
  32. package/plugins/@vcmap/create-link/index.js +83 -0
  33. package/plugins/@vcmap/create-link/package.json +6 -0
  34. package/plugins/@vcmap/pluginExample/index.js +1 -1
  35. package/plugins/@vcmap/pluginExample/pluginExampleComponent.vue +20 -3
  36. package/plugins/@vcmap/project-selector/ProjectSelectorComponent.vue +1 -1
  37. package/plugins/@vcmap/project-selector/index.js +1 -1
  38. package/plugins/@vcmap/project-selector/package.json +1 -2
  39. package/plugins/@vcmap/theme-changer/ThemeChangerComponent.vue +1 -1
  40. package/plugins/@vcmap/theme-changer/index.js +1 -1
  41. package/plugins/@vcmap/theme-changer/package.json +1 -2
  42. package/plugins/categoryTest/Categories.vue +89 -1
  43. package/plugins/categoryTest/Category.vue +1 -1
  44. package/plugins/simple-graph/README.md +51 -0
  45. package/plugins/simple-graph/SimpleGraphComponent.vue +70 -0
  46. package/plugins/simple-graph/index.js +17 -0
  47. package/plugins/simple-graph/package.json +11 -0
  48. package/plugins/simple-graph/simpleGraphView.js +76 -0
  49. package/plugins/test/editor.vue +1 -1
  50. package/plugins/test/index.js +63 -2
  51. package/plugins/test/windowManagerExample.vue +1 -1
  52. package/src/actions/stateRefAction.js +2 -2
  53. package/src/actions/styleSelector.vue +1 -1
  54. package/src/application/Navbar.vue +13 -2
  55. package/src/application/VcsApp.vue +201 -92
  56. package/src/application/VcsMap.vue +1 -1
  57. package/src/application/VcsSettings.vue +1 -1
  58. package/src/application/vcsAppWrapper.vue +1 -0
  59. package/src/components/form-inputs-controls/VcsCheckbox.vue +13 -0
  60. package/src/components/form-inputs-controls/VcsColorPicker.vue +1 -1
  61. package/src/components/form-inputs-controls/VcsRadio.vue +123 -0
  62. package/src/components/form-output/VcsFormattedNumber.vue +1 -1
  63. package/src/components/lists/VcsActionList.vue +13 -4
  64. package/src/components/lists/VcsTreeview.vue +4 -4
  65. package/src/components/lists/VcsTreeviewLeaf.vue +9 -2
  66. package/src/components/lists/VcsTreeviewSearchbar.vue +1 -2
  67. package/src/components/tables/VcsTable.vue +245 -0
  68. package/src/contentTree/LayerTree.vue +1 -1
  69. package/src/contentTree/contentTreeCollection.js +4 -4
  70. package/src/contentTree/contentTreeItem.js +9 -9
  71. package/src/contentTree/groupContentTreeItem.js +1 -1
  72. package/src/contentTree/layerContentTreeItem.js +15 -1
  73. package/src/contentTree/layerGroupContentTreeItem.js +21 -1
  74. package/src/contentTree/nodeContentTreeItem.js +1 -1
  75. package/src/featureInfo/AddressBalloonComponent.vue +47 -0
  76. package/src/featureInfo/BalloonComponent.vue +138 -0
  77. package/src/featureInfo/abstractFeatureInfoView.js +313 -0
  78. package/src/featureInfo/addressBalloonFeatureInfoView.js +118 -0
  79. package/src/featureInfo/balloonFeatureInfoView.js +151 -0
  80. package/src/featureInfo/balloonHelper.js +132 -0
  81. package/src/featureInfo/featureInfo.js +455 -0
  82. package/src/featureInfo/featureInfoInteraction.js +42 -0
  83. package/src/featureInfo/iframeFeatureInfoView.js +95 -0
  84. package/src/featureInfo/tableFeatureInfoView.js +106 -0
  85. package/src/i18n/de.js +16 -0
  86. package/src/i18n/en.js +16 -0
  87. package/src/i18n/i18nCollection.js +17 -0
  88. package/src/manager/buttonManager.js +5 -5
  89. package/src/manager/categoryManager/ComponentsManager.vue +30 -0
  90. package/src/manager/categoryManager/categoryManager.js +500 -0
  91. package/src/manager/contextMenu/contextMenuComponent.vue +43 -0
  92. package/src/manager/contextMenu/contextMenuInteraction.js +42 -0
  93. package/src/manager/contextMenu/contextMenuManager.js +197 -0
  94. package/src/manager/navbarManager.js +8 -8
  95. package/src/manager/toolbox/ToolboxManager.vue +2 -2
  96. package/src/manager/toolbox/toolboxManager.js +7 -3
  97. package/src/manager/window/WindowComponent.vue +1 -1
  98. package/src/manager/window/WindowManager.vue +5 -3
  99. package/src/manager/window/windowManager.js +118 -14
  100. package/src/navigation/mapNavigation.vue +3 -5
  101. package/src/navigation/overviewMap.js +28 -5
  102. package/src/navigation/vcsCompass.vue +1 -1
  103. package/src/setup.js +0 -2
  104. package/src/state.js +256 -0
  105. package/src/styles/_theming.scss +0 -5
  106. package/src/uiConfig.js +79 -0
  107. package/src/vcsUiApp.js +210 -20
  108. package/src/vuePlugins/vuetify.js +14 -4
  109. package/config/berlin.config.json +0 -510
  110. package/dist/assets/core.216494.js +0 -4
  111. package/dist/assets/ui.99a1a7.css +0 -1
  112. package/dist/assets/ui.99a1a7.js +0 -70
  113. package/dist/assets/vue-composition-api.c5aca1.js +0 -14
  114. package/dist/assets/vue-composition-api.js +0 -2
  115. package/dist/assets/vue.762edd.js +0 -9
  116. package/lib/vue-composition-api.js +0 -2
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
+ }
@@ -7,16 +7,11 @@
7
7
  background-image: var(--vcs-primary-logo);
8
8
  height: 36px;
9
9
  width: 151px;
10
- margin: 0 auto;
11
10
  }
12
11
  .company-logo-mobile {
13
12
  background-image: var(--vcs-mobile-logo);
14
13
  height: 40px;
15
14
  width: 70px;
16
- position: absolute;
17
- top: 1rem;
18
- left: 1rem;
19
- z-index: 1;
20
15
  }
21
16
 
22
17
  // Dev-Only!
@@ -0,0 +1,79 @@
1
+ import { Collection, makeOverrideCollection } from '@vcmap/core';
2
+ import { ref } from 'vue';
3
+
4
+ /**
5
+ * @typedef {Object} UiConfigurationItem
6
+ * @property {string} name
7
+ * @property {*} value
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} UiConfigObject
12
+ * @property {string} [logo] - the company logo to display. this will override any and all css overrides.
13
+ * @property {string} [mobileLogo] - an alternative logo to display in mobile view
14
+ * @property {string} [appTitle] - an optional title to display next to the company logo
15
+ * @property {string} [primaryColor] - an optional primary color to use in all themes
16
+ */
17
+
18
+ /**
19
+ * @class
20
+ * @extends {Collection<UiConfigurationItem>}
21
+ * @implements {import("@vcmap/core").OverrideCollectionInterface<UiConfigurationItem>}
22
+ */
23
+ class UiConfig extends Collection {
24
+ /**
25
+ * @param {function():string} getDynamicContextId
26
+ */
27
+ constructor(getDynamicContextId) {
28
+ super();
29
+
30
+ makeOverrideCollection(this, getDynamicContextId);
31
+ /**
32
+ * This object just acts as a go between for reactivity until we have vue3
33
+ * @todo vue3 cleanup
34
+ * @type {Object<string, *>}
35
+ */
36
+ const configObject = {};
37
+ /**
38
+ * @type {import("vue").Ref<Object<string, *>>}
39
+ * @private
40
+ */
41
+ this._config = ref({});
42
+ /**
43
+ * @type {Array<function():void>}
44
+ * @private
45
+ */
46
+ this._listeners = [
47
+ this.added.addEventListener((item) => {
48
+ if (typeof item?.name === 'string') {
49
+ configObject[item.name] = item.value;
50
+ this._config.value = { ...configObject }; // shallow clone to trip reactivity
51
+ }
52
+ }),
53
+ this.removed.addEventListener((item) => {
54
+ if (typeof item?.name === 'string') {
55
+ delete configObject[item.name];
56
+ this._config.value = { ...configObject }; // shallow clone to trip reactivity
57
+ }
58
+ }),
59
+ ];
60
+ }
61
+
62
+ /**
63
+ * @returns {import("vue").Ref<Object<string, *>|UiConfigObject>}
64
+ */
65
+ get config() {
66
+ return this._config;
67
+ }
68
+
69
+ /**
70
+ * @inheritDoc
71
+ */
72
+ destroy() {
73
+ this._listeners.forEach((cb) => { cb(); });
74
+ this._listeners = [];
75
+ super.destroy();
76
+ }
77
+ }
78
+
79
+ export default UiConfig;
package/src/vcsUiApp.js CHANGED
@@ -5,6 +5,10 @@ import {
5
5
  makeOverrideCollection,
6
6
  destroyCollection,
7
7
  OverrideClassRegistry,
8
+ defaultDynamicContextId,
9
+ ObliqueMap,
10
+ ViewPoint,
11
+ volatileContextId,
8
12
  } from '@vcmap/core';
9
13
  import { getLogger as getLoggerByName } from '@vcsuite/logger';
10
14
  import {
@@ -20,6 +24,11 @@ import { createContentTreeCollection } from './contentTree/contentTreeCollection
20
24
  import { contentTreeClassRegistry } from './contentTree/contentTreeItem.js';
21
25
  import OverviewMap from './navigation/overviewMap.js';
22
26
  import I18nCollection from './i18n/i18nCollection.js';
27
+ import CategoryManager from './manager/categoryManager/categoryManager.js';
28
+ import ContextMenuManager from './manager/contextMenu/contextMenuManager.js';
29
+ import FeatureInfo from './featureInfo/featureInfo.js';
30
+ import UiConfig from './uiConfig.js';
31
+ import { createEmptyState, getStateFromURL } from './state.js';
23
32
 
24
33
  /**
25
34
  * @typedef {import("@vcmap/core").VcsAppConfig} VcsUiAppConfig
@@ -44,21 +53,25 @@ import I18nCollection from './i18n/i18nCollection.js';
44
53
  /**
45
54
  * @interface VcsPlugin
46
55
  * @template {Object} P
56
+ * @template {Object} S
47
57
  * @property {string} version
48
58
  * @property {string} name
49
- * @property {function(VcsUiApp)} initialize - called on plugin added
59
+ * @property {Object<string, *>} [i18n] - the i18n messages of this plugin
60
+ * @property {function(VcsUiApp, S=)} initialize - called on plugin added. Is passed the VcsUiApp and optionally, the state for the plugin
50
61
  * @property {function(VcsUiApp)} onVcsAppMounted - called on mounted of VcsApp.vue
51
62
  * @property {function():P} [toJSON] - serialization
52
63
  * @property {function():Promise<void>} destroy
64
+ * @property {function(boolean=):S|Promise<S>} [getState] - should return the plugins state or a promise for said state. is passed a "for url" flag. If true, only the state relevant for sharing a URL should be passed and short keys shall be used
53
65
  * @api
54
66
  */
55
67
 
68
+
56
69
  /**
57
70
  * @interface VcsComponentManager
58
71
  * @template {Object} T - the component type
59
72
  * @template {Object} O - component options
60
- * @property {VcsEvent<T>} added
61
- * @property {VcsEvent<T>} removed
73
+ * @property {import("@vcmap/core").VcsEvent<T>} added
74
+ * @property {import("@vcmap/core").VcsEvent<T>} removed
62
75
  * @property {string[]} componentIds - all registered component ids as reactive array
63
76
  * @property {function(string):T} get - get component by id
64
77
  * @property {function(string):boolean} has - has component with id
@@ -84,7 +97,7 @@ class VcsUiApp extends VcsApp {
84
97
  constructor() {
85
98
  super();
86
99
  /**
87
- * @type {OverrideCollection<VcsPlugin>}
100
+ * @type {import("@vcmap/core").OverrideCollection<VcsPlugin>}
88
101
  * @private
89
102
  */
90
103
  this._plugins = makeOverrideCollection(
@@ -93,11 +106,37 @@ class VcsUiApp extends VcsApp {
93
106
  serializePlugin,
94
107
  deserializePlugin,
95
108
  );
96
- this._pluginAddedListener = this._plugins.added.addEventListener((plugin) => {
97
- if (plugin.initialize) {
98
- plugin.initialize(this);
99
- }
100
- });
109
+ /**
110
+ * @type {Array<function():void>}
111
+ * @private
112
+ */
113
+ this._pluginListeners = [
114
+ this._plugins.added.addEventListener((plugin) => {
115
+ this._windowManager.removeOwner(plugin.name);
116
+ this._navbarManager.removeOwner(plugin.name);
117
+ this._toolboxManager.removeOwner(plugin.name);
118
+ this._categoryManager.removeOwner(plugin.name);
119
+ this._contextMenuManager.removeOwner(plugin.name);
120
+ if (plugin.i18n) {
121
+ this.i18n.addPluginMessages(plugin.name, plugin[contextIdSymbol], plugin.i18n);
122
+ }
123
+ if (plugin.initialize) {
124
+ let state;
125
+ if (this._cachedAppState.contextIds.includes(plugin[contextIdSymbol])) {
126
+ state = this._cachedAppState.plugins.find(s => s.name === plugin.name);
127
+ }
128
+ plugin.initialize(this, state?.state);
129
+ }
130
+ }),
131
+ this._plugins.removed.addEventListener(async (plugin) => {
132
+ this._windowManager.removeOwner(plugin.name);
133
+ this._navbarManager.removeOwner(plugin.name);
134
+ this._toolboxManager.removeOwner(plugin.name);
135
+ this._categoryManager.removeOwner(plugin.name);
136
+ this._contextMenuManager.removeOwner(plugin.name);
137
+ this.i18n.removePluginMessages(plugin.name, plugin[contextIdSymbol]);
138
+ }),
139
+ ];
101
140
 
102
141
  /**
103
142
  * @type {OverrideClassRegistry<ContentTreeItem>}
@@ -124,10 +163,20 @@ class VcsUiApp extends VcsApp {
124
163
  */
125
164
  this._windowManager = new WindowManager();
126
165
  /**
127
- * @type {ButtonManager}
166
+ * @type {NavbarManager}
128
167
  * @private
129
168
  */
130
169
  this._navbarManager = new NavbarManager();
170
+ /**
171
+ * @type {UiConfig}
172
+ * @private
173
+ */
174
+ this._uiConfig = new UiConfig(() => this.dynamicContextId);
175
+ /**
176
+ * @type {FeatureInfo}
177
+ * @private
178
+ */
179
+ this._featureInfo = new FeatureInfo(this);
131
180
 
132
181
  /**
133
182
  * @type {OverviewMap}
@@ -140,16 +189,34 @@ class VcsUiApp extends VcsApp {
140
189
  * @private
141
190
  */
142
191
  this._i18n = new I18nCollection(() => this.dynamicContextId);
192
+
193
+ /**
194
+ * @type {CategoryManager}
195
+ * @private
196
+ */
197
+ this._categoryManager = new CategoryManager(this);
198
+
199
+ /**
200
+ * @type {ContextMenuManager}
201
+ * @private
202
+ */
203
+ this._contextMenuManager = new ContextMenuManager(this);
204
+
205
+ /**
206
+ * @type {AppState}
207
+ * @private
208
+ */
209
+ this._cachedAppState = getStateFromURL(new URL(window.location.href));
143
210
  }
144
211
 
145
212
  /**
146
- * @type {OverrideCollection<VcsPlugin>}
213
+ * @type {import("@vcmap/core").OverrideCollection<VcsPlugin>}
147
214
  * @readonly
148
215
  */
149
216
  get plugins() { return this._plugins; }
150
217
 
151
218
  /**
152
- * @type {OverrideCollection<ContentTreeItem>}
219
+ * @type {OverrideContentTreeCollection}
153
220
  * @readonly
154
221
  */
155
222
  get contentTree() { return this._contentTree; }
@@ -173,11 +240,17 @@ class VcsUiApp extends VcsApp {
173
240
  get windowManager() { return this._windowManager; }
174
241
 
175
242
  /**
176
- * @returns {ButtonManager}
243
+ * @returns {NavbarManager}
177
244
  * @readonly
178
245
  */
179
246
  get navbarManager() { return this._navbarManager; }
180
247
 
248
+ /**
249
+ * @returns {FeatureInfo}
250
+ * @readonly
251
+ */
252
+ get featureInfo() { return this._featureInfo; }
253
+
181
254
  /**
182
255
  * @type {OverviewMap}
183
256
  * @readonly
@@ -190,6 +263,77 @@ class VcsUiApp extends VcsApp {
190
263
  */
191
264
  get i18n() { return this._i18n; }
192
265
 
266
+ /**
267
+ * @returns {CategoryManager}
268
+ * @readonly
269
+ */
270
+ get categoryManager() { return this._categoryManager; }
271
+
272
+ /**
273
+ * @type {ContextMenuManager}
274
+ * @readonly
275
+ */
276
+ get contextMenuManager() { return this._contextMenuManager; }
277
+
278
+ /**
279
+ * @type {UiConfig}
280
+ * @readonly
281
+ */
282
+ get uiConfig() { return this._uiConfig; }
283
+
284
+ /**
285
+ * Get the state of the application. When passed the forUrl flag, only a minimal set of states shall be provided for a sharable link to the current state (to ensure
286
+ * the maximum URL length is not exceeded). This includes: layer active state & styling, active map, active viewpoint,
287
+ * currently selected feature info & any state deemed required for a sharable URL by the currently loaded plugins.
288
+ * @param {boolean=} forUrl
289
+ * @returns {Promise<AppState>}
290
+ */
291
+ async getState(forUrl) {
292
+ const state = createEmptyState();
293
+ state.contextIds = this.contexts
294
+ .filter(({ id }) => id !== defaultDynamicContextId)
295
+ .map(({ id }) => id);
296
+
297
+ state.activeMap = this.maps.activeMap.name;
298
+ const viewPoint = await this.maps.activeMap.getViewPoint();
299
+ state.activeViewpoint = viewPoint?.isValid?.() ? viewPoint.toJSON() : undefined;
300
+ state.layers = [...this.layers]
301
+ .filter(l => l.isSupported(this.maps.activeMap) &&
302
+ l[contextIdSymbol] !== defaultDynamicContextId &&
303
+ l[contextIdSymbol] !== volatileContextId &&
304
+ (
305
+ ((l.active || l.loading) && !l.activeOnStartup) ||
306
+ (!l.active && l.activeOnStartup) ||
307
+ ((l.active || l.loading) && l.style !== l.defaultStyle && this.styles.has(l.style))
308
+ ))
309
+ .map((l) => {
310
+ const layerState = {
311
+ name: l.name,
312
+ active: l.active || l.loading,
313
+ };
314
+ if (
315
+ l.style.name !== l.defaultStyle.name &&
316
+ this.styles.has(l.style) &&
317
+ l.style[contextIdSymbol] !== defaultDynamicContextId &&
318
+ l.style[contextIdSymbol] !== volatileContextId
319
+ ) {
320
+ layerState.styleName = l.style.name;
321
+ }
322
+ return layerState;
323
+ });
324
+
325
+ state.plugins = await Promise.all([...this.plugins]
326
+ .filter(p => p[contextIdSymbol] !== defaultDynamicContextId &&
327
+ p[contextIdSymbol] !== volatileContextId &&
328
+ typeof p.getState === 'function')
329
+ .map(async p => ({ name: p.name, state: await p.getState(forUrl) })));
330
+
331
+ if (this.maps.activeMap instanceof ObliqueMap) {
332
+ state.activeObliqueCollection = this.maps.activeMap.collection.name;
333
+ }
334
+ return state;
335
+ }
336
+
193
337
  /**
194
338
  * @param {import("@vcmap/core").Context} context
195
339
  * @returns {Promise<void>}
@@ -212,17 +356,56 @@ class VcsUiApp extends VcsApp {
212
356
 
213
357
  plugins
214
358
  .filter(p => p)
215
- .map(p => this._plugins.override(p))
216
- .filter(p => p.i18n)
217
- .forEach((p) => {
218
- this.i18n.addPluginMessages(p.name, context.id, p.i18n);
219
- });
359
+ .map(p => this._plugins.override(p));
220
360
  }
221
361
  if (Array.isArray(config.i18n)) {
222
362
  await this.i18n.parseItems(config.i18n, context.id);
223
363
  }
224
364
  await super._parseContext(context);
225
365
  await this._contentTree.parseItems(config.contentTree, context.id);
366
+ await this._uiConfig.parseItems(config.uiConfig, context.id);
367
+ await this._featureInfo.collection.parseItems(config.featureInfo, context.id);
368
+ }
369
+
370
+ /**
371
+ * @param {import("@vcmap/core").Context} context
372
+ * @returns {Promise<void>}
373
+ * @protected
374
+ */
375
+ async _setContextState(context) {
376
+ await super._setContextState(context);
377
+ if (this._cachedAppState.contextIds.includes(context.id)) {
378
+ this._cachedAppState.layers.forEach((layerState) => {
379
+ const layer = this.layers.getByKey(layerState.name);
380
+ if (layer) {
381
+ if (layerState.active) {
382
+ layer.activate();
383
+ } else {
384
+ layer.deactivate();
385
+ }
386
+
387
+ if (layerState.styleName && this.styles.hasKey(layerState.styleName) && layer.setStyle) {
388
+ layer.setStyle(this.styles.getByKey(layerState.styleName));
389
+ }
390
+ }
391
+ });
392
+ if (this._cachedAppState.activeMap && this.maps.hasKey(this._cachedAppState.activeMap)) {
393
+ await this.maps.setActiveMap(this._cachedAppState.activeMap);
394
+ }
395
+ if (
396
+ this._cachedAppState.activeObliqueCollection &&
397
+ this.maps.activeMap instanceof ObliqueMap &&
398
+ this.obliqueCollections.hasKey(this._cachedAppState.activeObliqueCollection)
399
+ ) {
400
+ await this.maps.activeMap.setCollection(
401
+ this.obliqueCollections.getByKey(this._cachedAppState.activeObliqueCollection),
402
+ this._cachedAppState.activeViewpoint,
403
+ );
404
+ } else if (this._cachedAppState.activeViewpoint && this.maps.activeMap) {
405
+ await this.maps.activeMap.gotoViewPoint(new ViewPoint(this._cachedAppState.activeViewpoint));
406
+ }
407
+ this._cachedAppState.contextIds.splice(this._cachedAppState.contextIds.indexOf(context.id), 1);
408
+ }
226
409
  }
227
410
 
228
411
  /**
@@ -236,6 +419,8 @@ class VcsUiApp extends VcsApp {
236
419
  this._plugins.removeContext(contextId),
237
420
  this._i18n.removeContext(contextId),
238
421
  this._contentTree.removeContext(contextId),
422
+ this._featureInfo.collection.removeContext(contextId),
423
+ this._uiConfig.removeContext(contextId),
239
424
  ]);
240
425
  }
241
426
 
@@ -245,13 +430,18 @@ class VcsUiApp extends VcsApp {
245
430
  destroy() {
246
431
  this.windowManager.destroy();
247
432
  this.navbarManager.destroy();
433
+ this.toolboxManager.destroy();
434
+ this.categoryManager.destroy();
435
+ this.contextMenuManager.destroy();
248
436
  this._overviewMap.destroy();
249
- // TODO destroy other manager
250
- this._pluginAddedListener();
437
+ this._pluginListeners.forEach((cb) => { cb(); });
438
+ this._pluginListeners = [];
251
439
  destroyCollection(this._plugins);
252
440
  destroyCollection(this._contentTree);
253
441
  destroyCollection(this._i18n);
254
442
  this._contentTreeClassRegistry.destroy();
443
+ this._featureInfo.destroy();
444
+ this._uiConfig.destroy();
255
445
  super.destroy();
256
446
  }
257
447
  }