@vcmap/ui 5.0.0-rc.14 → 5.0.0-rc.16

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 (132) hide show
  1. package/README.md +33 -31
  2. package/build/build.js +9 -0
  3. package/build/buildHelpers.js +12 -10
  4. package/build/commonViteConfig.js +3 -10
  5. package/config/base.config.json +30 -24
  6. package/config/dev.config.json +13 -1
  7. package/config/www.config.json +104 -17
  8. package/dist/assets/cesium.430460.js +137226 -0
  9. package/dist/assets/cesium.js +1 -1
  10. package/dist/assets/core.5089ba.js +16024 -0
  11. package/dist/assets/core.js +1 -1
  12. package/dist/assets/index.854f8e2b.js +1 -0
  13. package/dist/assets/ol.9be53a.js +44279 -0
  14. package/dist/assets/ol.js +1 -1
  15. package/dist/assets/{ui.15ef6a.css → ui.49010a.css} +1 -1
  16. package/dist/assets/ui.49010a.js +16776 -0
  17. package/dist/assets/ui.js +1 -1
  18. package/dist/assets/vue.247c1c.js +4675 -0
  19. package/dist/assets/vue.js +5 -2
  20. package/dist/assets/{vuetify.202322.css → vuetify.735e58.css} +1 -1
  21. package/dist/assets/vuetify.735e58.js +21019 -0
  22. package/dist/assets/vuetify.js +5 -2
  23. package/dist/index.html +1 -1
  24. package/index.html +77 -0
  25. package/index.js +8 -1
  26. package/package.json +12 -10
  27. package/plugins/@vcmap/create-link/fallbackCreateLink.vue +4 -1
  28. package/plugins/@vcmap/create-link/index.js +4 -1
  29. package/plugins/@vcmap/pluginExample/exampleActions.js +45 -0
  30. package/plugins/@vcmap/pluginExample/index.js +38 -1
  31. package/plugins/@vcmap/pluginExample/pluginExampleComponent.vue +152 -98
  32. package/plugins/@vcmap/project-selector/ContextsListComponent.vue +8 -1
  33. package/plugins/@vcmap/project-selector/ProjectSelectorComponent.vue +27 -1
  34. package/plugins/@vcmap/search-nominatim/LICENSE.md +14 -0
  35. package/plugins/@vcmap/search-nominatim/README.md +2 -0
  36. package/plugins/@vcmap/search-nominatim/config.json +4 -0
  37. package/plugins/@vcmap/search-nominatim/index.js +26 -0
  38. package/plugins/@vcmap/search-nominatim/nominatim.js +170 -0
  39. package/plugins/@vcmap/search-nominatim/package.json +43 -0
  40. package/plugins/@vcmap/theme-changer/ThemeChangerComponent.vue +26 -0
  41. package/plugins/buttonExamples/ButtonExamples.vue +28 -1
  42. package/plugins/categoryTest/Categories.vue +16 -0
  43. package/plugins/categoryTest/Category.vue +30 -4
  44. package/plugins/example/mySuperComponent.vue +12 -1
  45. package/plugins/notifier/index.js +31 -0
  46. package/plugins/notifier/notifierTester.vue +88 -0
  47. package/plugins/package.json +2 -1
  48. package/plugins/simple-graph/SimpleGraphComponent.vue +5 -11
  49. package/plugins/test/allIconsComponent.vue +16 -0
  50. package/plugins/test/editor.vue +3 -0
  51. package/plugins/test/emptyComponent.vue +3 -0
  52. package/plugins/test/index.js +22 -0
  53. package/plugins/test/myCustomHeader.vue +9 -1
  54. package/plugins/test/testList.vue +287 -0
  55. package/plugins/test/vcsContent.vue +3 -0
  56. package/plugins/test/windowManagerExample.vue +3 -0
  57. package/plugins/wizardExample/index.js +41 -0
  58. package/plugins/wizardExample/wizardExample.vue +77 -0
  59. package/src/actions/actionHelper.js +103 -2
  60. package/src/actions/styleSelector.vue +9 -0
  61. package/src/application/VcsApp.vue +95 -17
  62. package/src/application/VcsAttributions.vue +63 -0
  63. package/src/application/VcsAttributionsFooter.vue +87 -0
  64. package/src/application/{Navbar.vue → VcsNavbar.vue} +35 -2
  65. package/src/application/VcsSettings.vue +4 -0
  66. package/src/application/attributionsHelper.js +150 -0
  67. package/src/application/vcsAppWrapper.vue +5 -1
  68. package/src/components/buttons/VcsActionButtonList.vue +8 -1
  69. package/src/components/buttons/VcsButton.vue +7 -1
  70. package/src/components/form-inputs-controls/VcsCheckbox.vue +7 -2
  71. package/src/components/form-inputs-controls/VcsColorPicker.vue +4 -0
  72. package/src/components/form-inputs-controls/VcsFormSection.vue +55 -9
  73. package/src/components/form-inputs-controls/VcsRadio.vue +7 -1
  74. package/src/components/form-inputs-controls/VcsSelect.vue +38 -2
  75. package/src/components/form-inputs-controls/VcsTextArea.vue +2 -0
  76. package/src/components/form-inputs-controls/VcsTextField.vue +16 -4
  77. package/src/components/form-inputs-controls/VcsWizard.vue +133 -0
  78. package/src/components/imageElementInjector.vue +22 -0
  79. package/src/components/lists/VcsActionList.vue +12 -1
  80. package/src/components/lists/VcsList.vue +466 -0
  81. package/src/components/lists/VcsTreeview.vue +7 -3
  82. package/src/components/lists/VcsTreeviewLeaf.vue +23 -51
  83. package/src/components/lists/VcsTreeviewSearchbar.vue +6 -23
  84. package/src/components/notification/VcsTooltip.vue +14 -9
  85. package/src/components/tables/VcsTable.vue +129 -38
  86. package/src/contentTree/LayerTree.vue +1 -1
  87. package/src/contentTree/contentTreeItem.js +13 -13
  88. package/src/contentTree/subContentTreeItem.js +1 -1
  89. package/src/contentTree/vcsObjectContentTreeItem.js +1 -1
  90. package/src/featureInfo/AddressBalloonComponent.vue +17 -1
  91. package/src/featureInfo/BalloonComponent.vue +63 -27
  92. package/src/featureInfo/balloonFeatureInfoView.js +14 -14
  93. package/src/featureInfo/balloonHelper.js +4 -0
  94. package/src/featureInfo/featureInfo.js +23 -2
  95. package/src/featureInfo/featureInfoInteraction.js +1 -1
  96. package/src/i18n/de.js +22 -0
  97. package/src/i18n/en.js +22 -0
  98. package/src/icons/+all.js +4 -0
  99. package/src/icons/WandIcon.vue +63 -0
  100. package/src/legend/legendHelper.js +18 -12
  101. package/src/legend/styleLegendItem.vue +20 -1
  102. package/src/legend/vcsLegend.vue +29 -3
  103. package/src/manager/toolbox/GroupToolboxComponent.vue +13 -1
  104. package/src/manager/toolbox/SelectToolboxComponent.vue +13 -1
  105. package/src/manager/toolbox/ToolboxManager.vue +3 -0
  106. package/src/manager/window/WindowComponent.vue +15 -2
  107. package/src/manager/window/WindowComponentHeader.vue +38 -7
  108. package/src/manager/window/WindowManager.vue +1 -0
  109. package/src/manager/window/windowManager.js +11 -1
  110. package/src/navigation/mapNavigation.vue +15 -36
  111. package/src/navigation/orientationToolsButton.vue +6 -1
  112. package/src/navigation/overviewMap.js +19 -47
  113. package/src/navigation/tiltSlider.vue +3 -0
  114. package/src/navigation/vcsCompass.vue +2 -0
  115. package/src/notifier/notifier.js +121 -0
  116. package/src/notifier/notifierComponent.vue +84 -0
  117. package/src/search/resultItem.vue +89 -0
  118. package/src/search/resultsComponent.vue +98 -0
  119. package/src/search/search.js +326 -0
  120. package/src/search/searchComponent.vue +90 -0
  121. package/src/styles/_typography.scss +3 -0
  122. package/src/styles/utils/_cursor.scss +4 -0
  123. package/src/styles/variables.scss +23 -4
  124. package/src/vcsUiApp.js +35 -1
  125. package/src/vuePlugins/vuetify.js +2 -0
  126. package/dist/assets/cesium.9489f8.js +0 -8699
  127. package/dist/assets/core.aa346a.js +0 -4
  128. package/dist/assets/index.3cd4fffa.js +0 -1
  129. package/dist/assets/ol.39651b.js +0 -439
  130. package/dist/assets/ui.15ef6a.js +0 -71
  131. package/dist/assets/vue.cbe9d8.js +0 -9
  132. package/dist/assets/vuetify.202322.js +0 -148
@@ -1,18 +1,29 @@
1
1
  <template>
2
2
  <v-container class="fill-height pa-0" absolute fluid>
3
- <Navbar />
3
+ <VcsNavbar />
4
4
  <v-container class="vcs-main pa-0" :class="{ 'vcs-main-xs': $vuetify.breakpoint.xs }" fluid absolute>
5
5
  <template v-if="$vuetify.breakpoint.xs">
6
6
  <img v-if="mobileLogo" :src="mobileLogo" alt="Logo" draggable="false" class="mobile-logo">
7
7
  <div v-else class="company-logo-mobile mobile-logo" />
8
8
  </template>
9
+ <VcsButton
10
+ v-if="!$vuetify.breakpoint.smAndUp && $vuetify.breakpoint.mobile"
11
+ :key="attributionAction.name"
12
+ :tooltip="attributionAction.title"
13
+ :icon="attributionAction.icon"
14
+ :active="attributionAction.active"
15
+ @click.stop="attributionAction.callback($event)"
16
+ small
17
+ class="z-index-1 mobile-attribution-btn"
18
+ />
9
19
  <VcsMap :map-id="mapId" />
10
20
  <MapNavigation />
11
21
  <ToolboxManagerComponent />
12
22
  <WindowManagerComponent />
23
+ <NotifierComponent />
13
24
  </v-container>
14
- <v-footer absolute v-if="!$vuetify.breakpoint.xs">
15
- {{ $t('footer.title') }}
25
+ <v-footer absolute v-if="$vuetify.breakpoint.smAndUp" min-height="22px">
26
+ <VcsAttributionsFooter :entries="attributionEntries" :attribution-action="attributionAction" />
16
27
  </v-footer>
17
28
  </v-container>
18
29
  </template>
@@ -26,7 +37,7 @@
26
37
  top: 48px;
27
38
  left: 0;
28
39
  right: 0;
29
- bottom: 33px;
40
+ bottom: 22px;
30
41
  }
31
42
 
32
43
  .vcs-main-xs {
@@ -43,6 +54,12 @@
43
54
  z-index: 1;
44
55
  }
45
56
 
57
+ .mobile-attribution-btn{
58
+ position: fixed;
59
+ right: 2px;
60
+ bottom: 56px;
61
+ }
62
+
46
63
  </style>
47
64
 
48
65
  <script>
@@ -56,12 +73,14 @@
56
73
  watch,
57
74
  } from 'vue';
58
75
  import { getVcsAppById } from '@vcmap/core';
76
+ import { VContainer, VFooter } from 'vuetify/lib';
77
+ import { getLogger } from '@vcsuite/logger';
59
78
  import WindowManagerComponent from '../manager/window/WindowManager.vue';
60
79
  import ToolboxManagerComponent from '../manager/toolbox/ToolboxManager.vue';
61
80
  import { ButtonLocation } from '../manager/navbarManager.js';
62
81
  import { vcsAppSymbol } from '../pluginHelper.js';
63
82
  import VcsMap from './VcsMap.vue';
64
- import Navbar from './Navbar.vue';
83
+ import VcsNavbar from './VcsNavbar.vue';
65
84
  import { createMapButtonAction, createToggleAction } from '../actions/actionHelper.js';
66
85
  import MapNavigation from '../navigation/mapNavigation.vue';
67
86
  import VcsSettings from './VcsSettings.vue';
@@ -70,6 +89,11 @@
70
89
  import { defaultPrimaryColor } from '../vuePlugins/vuetify.js';
71
90
  import VcsLegend from '../legend/vcsLegend.vue';
72
91
  import { getLegendEntries } from '../legend/legendHelper.js';
92
+ import VcsAttributionsFooter from './VcsAttributionsFooter.vue';
93
+ import VcsButton from '../components/buttons/VcsButton.vue';
94
+ import VcsAttributions from './VcsAttributions.vue';
95
+ import { getAttributions } from './attributionsHelper.js';
96
+ import NotifierComponent from '../notifier/notifierComponent.vue';
73
97
 
74
98
  /**
75
99
  * You should call this function in the component providing the vcsUiApp to your
@@ -80,17 +104,23 @@
80
104
  * @returns {function():void}
81
105
  */
82
106
  export function setupPluginMountedListeners(app) {
83
- [...app.plugins].forEach((plugin) => {
107
+ /**
108
+ * wrapped execution of onVcsAppMounted hook
109
+ * @param {VcsPlugin} plugin
110
+ */
111
+ function onVcsAppMounted(plugin) {
84
112
  if (plugin.onVcsAppMounted) {
85
- plugin.onVcsAppMounted(app);
113
+ try {
114
+ plugin.onVcsAppMounted(app);
115
+ } catch (e) {
116
+ getLogger('VcsUiApp').error(`Error in plugin ${plugin.name} onVcsAppMounted hook`, e);
117
+ }
86
118
  }
87
- });
119
+ }
88
120
 
89
- return app.plugins.added.addEventListener((plugin) => {
90
- if (plugin.onVcsAppMounted) {
91
- plugin.onVcsAppMounted(app);
92
- }
93
- });
121
+ [...app.plugins].forEach(onVcsAppMounted);
122
+
123
+ return app.plugins.added.addEventListener(onVcsAppMounted);
94
124
  }
95
125
 
96
126
  /**
@@ -178,10 +208,10 @@
178
208
 
179
209
  /**
180
210
  * adds or removes the legend button, depending on the number of entries
181
- * @param {import("vue").Reactive<{string,LegendEntry}>} newEntries
211
+ * @param {Array<LegendEntry>} newEntries
182
212
  */
183
213
  const handleLegendButton = (newEntries) => {
184
- if (Object.keys(newEntries).length > 0) {
214
+ if (newEntries.length > 0) {
185
215
  if (!app.navbarManager.has('legend')) {
186
216
  app.navbarManager.add(
187
217
  {
@@ -197,7 +227,7 @@
197
227
  app.windowManager.remove('legend');
198
228
  }
199
229
  };
200
- handleLegendButton(entries);
230
+ handleLegendButton(entries.value);
201
231
 
202
232
  const stopWatching = watch(
203
233
  entries,
@@ -329,6 +359,45 @@
329
359
  };
330
360
  }
331
361
 
362
+ /**
363
+ * This helper gets attributions of all active maps, layers and oblique collections and returns an array of entries.
364
+ * It also returns a attributionAction to toggle the attributions window and a destroy function.
365
+ * @param {VcsUiApp} app
366
+ * @returns {{attributionEntries: import("vue").Ref<Array<AttributionEntry>>, attributionAction: VcsAction, destroyAttributions: function():void}}
367
+ */
368
+ export function setupAttributions(app) {
369
+ const { entries, destroy } = getAttributions(app);
370
+
371
+ const { action: attributionAction, destroy: attributionDestroy } = createToggleAction(
372
+ {
373
+ name: 'attributionToggle',
374
+ icon: 'mdi-chevron-double-right',
375
+ title: 'footer.attributions.tooltip',
376
+ },
377
+ {
378
+ id: 'attribution',
379
+ component: VcsAttributions,
380
+ state: {
381
+ headerTitle: 'footer.attributions.title',
382
+ headerIcon: 'mdi-copyright',
383
+ },
384
+ slot: WindowSlot.DYNAMIC_RIGHT,
385
+ props: { entries },
386
+ },
387
+ app.windowManager,
388
+ vcsAppSymbol,
389
+ );
390
+
391
+ return {
392
+ attributionEntries: entries,
393
+ attributionAction,
394
+ destroyAttributions: () => {
395
+ destroy();
396
+ attributionDestroy();
397
+ },
398
+ };
399
+ }
400
+
332
401
  /**
333
402
  * The base component to setup the entire application. To embed the VcsApp, use this component.
334
403
  * @vue-prop {string} appId - the id of the app to inject. this will setup listeners on the app to call vcsAppMounted on plugins
@@ -336,11 +405,16 @@
336
405
  */
337
406
  export default {
338
407
  components: {
408
+ VcsButton,
409
+ VcsAttributionsFooter,
339
410
  MapNavigation,
340
- Navbar,
411
+ VcsNavbar,
341
412
  VcsMap,
342
413
  WindowManagerComponent,
343
414
  ToolboxManagerComponent,
415
+ VContainer,
416
+ VFooter,
417
+ NotifierComponent,
344
418
  },
345
419
  props: {
346
420
  appId: {
@@ -360,6 +434,7 @@
360
434
  const settingsDestroy = setupSettingsWindow(app);
361
435
  const destroyComponentsWindow = setupComponentsWindow(app);
362
436
  const destroyThemingListener = setupUiConfigTheming(app, getCurrentInstance().proxy.$vuetify);
437
+ const { attributionEntries, attributionAction, destroyAttributions } = setupAttributions(app);
363
438
 
364
439
  let pluginMountedListener;
365
440
  onMounted(() => {
@@ -376,11 +451,14 @@
376
451
  settingsDestroy();
377
452
  destroyComponentsWindow();
378
453
  destroyThemingListener();
454
+ destroyAttributions();
379
455
  });
380
456
 
381
457
  return {
382
458
  mapId,
383
459
  mobileLogo: computed(() => app.uiConfig.config.value.mobileLogo ?? app.uiConfig.config.value.logo),
460
+ attributionEntries,
461
+ attributionAction,
384
462
  };
385
463
  },
386
464
  };
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <v-list>
3
+ <v-list-item v-for="({key, title, attributions}) in entries" :key="key">
4
+ <v-list-item-content>
5
+ <v-list-item-title>{{ $t(title) }}</v-list-item-title>
6
+ <v-list-item-subtitle
7
+ v-for="attribution in attributions"
8
+ :key="attribution.provider"
9
+ :title="`${$t(attribution.provider)} ${attribution.year}`"
10
+ >
11
+ <a
12
+ :href="attribution.url"
13
+ target="_blank"
14
+ class="text--secondary"
15
+ >
16
+ {{ $t(attribution.provider) }} {{ attribution.year }}
17
+ </a>
18
+ </v-list-item-subtitle>
19
+ </v-list-item-content>
20
+ </v-list-item>
21
+ </v-list>
22
+ </template>
23
+
24
+ <style lang="scss" scoped>
25
+
26
+ ::v-deep {
27
+ a:before {
28
+ content: '\00a9\00a0'
29
+ }
30
+ }
31
+
32
+ </style>
33
+
34
+ <script >
35
+ import {
36
+ VList,
37
+ VListItem,
38
+ VListItemContent,
39
+ VListItemTitle,
40
+ VListItemSubtitle,
41
+ } from 'vuetify/lib';
42
+
43
+ /**
44
+ * Lists attributions of maps, layers and oblique collections
45
+ * @vue-prop {import("vue").Ref<Array<AttributionEntry>} entries - array with one entry per active VcsObject
46
+ */
47
+ export default {
48
+ name: 'VcsAttributions',
49
+ components: {
50
+ VList,
51
+ VListItem,
52
+ VListItemContent,
53
+ VListItemTitle,
54
+ VListItemSubtitle,
55
+ },
56
+ props: {
57
+ entries: {
58
+ type: Object,
59
+ default: () => {},
60
+ },
61
+ },
62
+ };
63
+ </script>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <span class="attribution-wrap">
3
+ <span v-for="attribution in mergedAttributions" class="attribution-span" :key="attribution.provider">
4
+ <a
5
+ :href="attribution.url"
6
+ target="_blank"
7
+ class="text--secondary"
8
+ >{{ $t(attribution.provider) }} <span>{{ attribution.years }}</span></a>
9
+ </span>
10
+ <VcsButton
11
+ class="d-flex text--secondary"
12
+ small
13
+ :key="attributionAction.name"
14
+ :tooltip="attributionAction.title"
15
+ :icon="attributionAction.icon"
16
+ :active="attributionAction.active"
17
+ @click.stop="attributionAction.callback($event)"
18
+ />
19
+ </span>
20
+ </template>
21
+
22
+ <style lang="scss" scoped>
23
+ .attribution-wrap{
24
+ white-space: nowrap;
25
+ margin-right: 40px;
26
+ overflow: hidden;
27
+ text-overflow: ellipsis;
28
+
29
+ .vcs-button-wrap{
30
+ position: absolute;
31
+ right: 4px;
32
+ bottom: 3px;
33
+ }
34
+ }
35
+ a:before {
36
+ content: '\00a9\00a0';
37
+ }
38
+ .attribution-span {
39
+ font-size: smaller;
40
+ &:before {
41
+ content: '\00a0\007c\00a0';
42
+ }
43
+ &:first-child::before{
44
+ content: '';
45
+ }
46
+ span{
47
+ font-size: inherit;
48
+ }
49
+ }
50
+
51
+ </style>
52
+
53
+ <script >
54
+
55
+ import { computed } from 'vue';
56
+ import { mergeAttributions } from './attributionsHelper.js';
57
+ import VcsButton from '../components/buttons/VcsButton.vue';
58
+
59
+ /**
60
+ * Lists attributions of maps, layers and oblique collections within the footer
61
+ * @vue-prop {import("vue").Ref<Array<AttributionEntry>} entries - array with one entry per active VcsObject
62
+ * @vue-prop {VcsAction} attributionAction - action to open attribution window
63
+ * @vue-computed {Array<{provider: string, years: string, url: URL}>} mergedAttributions - array with one entry per provider
64
+ */
65
+ export default {
66
+ name: 'VcsAttributionsFooter',
67
+ components: {
68
+ VcsButton,
69
+ },
70
+ props: {
71
+ entries: {
72
+ type: Array,
73
+ required: true,
74
+ },
75
+ attributionAction: {
76
+ type: Object,
77
+ required: true,
78
+ },
79
+ },
80
+ setup(props) {
81
+ const mergedAttributions = computed(() => mergeAttributions(props.entries));
82
+ return {
83
+ mergedAttributions,
84
+ };
85
+ },
86
+ };
87
+ </script>
@@ -86,6 +86,17 @@
86
86
  :show-icon="true"
87
87
  />
88
88
  </v-menu>
89
+ <VcsButton
90
+ class="d-flex"
91
+ v-if="searchAction"
92
+ large
93
+ :key="searchAction.name"
94
+ :tooltip="searchAction.title"
95
+ :icon="searchAction.icon"
96
+ :active="searchAction.active"
97
+ @click.stop="searchAction.callback($event)"
98
+ v-bind="{...$attrs}"
99
+ />
89
100
  <v-menu
90
101
  offset-y
91
102
  v-if="menuActions.length > 0"
@@ -124,15 +135,30 @@
124
135
  </style>
125
136
 
126
137
  <script>
127
- import { inject, ref, computed } from 'vue';
138
+ import { inject, ref, computed, onUnmounted } from 'vue';
139
+ import {
140
+ VCol, VContainer, VDivider, VMenu, VRow, VToolbar, VToolbarItems,
141
+ } from 'vuetify/lib';
128
142
  import { ButtonLocation, getActionsByLocation } from '../manager/navbarManager.js';
129
143
  import VcsActionButtonList from '../components/buttons/VcsActionButtonList.vue';
130
144
  import VcsActionList from '../components/lists/VcsActionList.vue';
131
145
  import VcsButton from '../components/buttons/VcsButton.vue';
146
+ import { createSearchButtonAction } from '../actions/actionHelper.js';
132
147
 
133
148
  export default {
134
149
  name: 'VcsNavbar',
135
- components: { VcsActionButtonList, VcsActionList, VcsButton },
150
+ components: {
151
+ VcsActionButtonList,
152
+ VcsActionList,
153
+ VcsButton,
154
+ VToolbar,
155
+ VContainer,
156
+ VRow,
157
+ VCol,
158
+ VToolbarItems,
159
+ VDivider,
160
+ VMenu,
161
+ },
136
162
  setup() {
137
163
  const app = inject('vcsApp');
138
164
 
@@ -142,12 +168,19 @@
142
168
  () => getActionsByLocation(buttonComponents.value, location, [...app.plugins].map(p => p.name)),
143
169
  );
144
170
 
171
+ const { searchAction, destroy: destroySearchAction } = createSearchButtonAction(app);
172
+
173
+ onUnmounted(() => {
174
+ destroySearchAction();
175
+ });
176
+
145
177
  return {
146
178
  mapActions: getActions(ButtonLocation.MAP),
147
179
  contentActions: getActions(ButtonLocation.CONTENT),
148
180
  toolActions: getActions(ButtonLocation.TOOL),
149
181
  projectActions: getActions(ButtonLocation.PROJECT),
150
182
  shareActions: getActions(ButtonLocation.SHARE),
183
+ searchAction,
151
184
  menuActions: getActions(ButtonLocation.MENU),
152
185
  config: app.uiConfig.config,
153
186
  };
@@ -34,6 +34,7 @@
34
34
  import {
35
35
  ref, inject, onUnmounted, getCurrentInstance, computed,
36
36
  } from 'vue';
37
+ import { VCol, VContainer, VRow } from 'vuetify/lib';
37
38
  import VcsLabel from '../components/form-inputs-controls/VcsLabel.vue';
38
39
  import VcsSelect from '../components/form-inputs-controls/VcsSelect.vue';
39
40
 
@@ -42,6 +43,9 @@
42
43
  components: {
43
44
  VcsSelect,
44
45
  VcsLabel,
46
+ VContainer,
47
+ VRow,
48
+ VCol,
45
49
  },
46
50
  setup() {
47
51
  const app = inject('vcsApp');
@@ -0,0 +1,150 @@
1
+ import { ref } from 'vue';
2
+ import { ObliqueMap } from '@vcmap/core';
3
+
4
+ /**
5
+ * @typedef {Object} Attribution.Options
6
+ * @property {string} provider - name of the data provider
7
+ * @property {number} [year] - year of dataset
8
+ * @property {URL} url - link to data provider
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} AttributionEntry
13
+ * @property {string} key - name of the VcsObject the attribution applies to
14
+ * @property {string} title - title of the VcsObject the attribution applies to
15
+ * @property {Attribution.Options|Array<Attribution.Options>} attributions - attributions of a map, layer or oblique collection
16
+ */
17
+
18
+ /**
19
+ * merges attribution entries of same providers
20
+ * @param {Array<AttributionEntry>} entries
21
+ * @returns {Array<{provider: string, years: string, url: URL}>}
22
+ */
23
+ export function mergeAttributions(entries) {
24
+ const providers = {};
25
+ entries.forEach(({ attributions }) => {
26
+ attributions.forEach(({ provider, year, url }) => {
27
+ const providerObject = providers[provider];
28
+ if (providerObject) {
29
+ if (year) {
30
+ const index = providerObject.years.indexOf(year);
31
+ if (url && index === -1) {
32
+ if (providerObject.years.every(y => Number(y) < Number(year))) {
33
+ providerObject.url = url;
34
+ }
35
+ if (year) {
36
+ const set = new Set([...providerObject.years, Number(year)]);
37
+ providerObject.years = [...set].sort((a, b) => a - b);
38
+ }
39
+ }
40
+ }
41
+ } else {
42
+ providers[provider] = {
43
+ years: year ? [Number(year)] : [],
44
+ url,
45
+ };
46
+ }
47
+ });
48
+ });
49
+ return Object.keys(providers).map(provider => ({
50
+ provider,
51
+ years: providers[provider].years.join(', '),
52
+ url: providers[provider].url,
53
+ }));
54
+ }
55
+
56
+ /**
57
+ * Gets attributions of all active maps, layers and oblique collections and returns an array of entries.
58
+ * Each entry is defined by a key derived from the object's className and name, and it's associated attributions.
59
+ * Listens to state changes of maps, layers and oblique collections and synchronizes the entries array correspondingly.
60
+ * Returns a destroy function to clear listeners.
61
+ * @param {import("ui").VcsUiApp} app
62
+ * @returns {{entries: import("vue").Ref<Array<AttributionEntry>>, destroy: function():void}}
63
+ */
64
+ export function getAttributions(app) {
65
+ /**
66
+ * @type {import("vue").Ref<Array<AttributionEntry>>}
67
+ */
68
+ const entries = ref([]);
69
+ /**
70
+ * @type {function():void}
71
+ */
72
+ let obliqueListener = () => {};
73
+
74
+ /**
75
+ * Adds an entry for an object using a combination of the object's className and name as key.
76
+ * @param {import("@vcmap/core").VcsMap|import("@vcmap/core").Layer|import("@vcmap/core").ObliqueCollection} object
77
+ */
78
+ function addAttributions(object) {
79
+ const { attributions } = object.properties;
80
+ if (!attributions) { return; }
81
+ const key = `${object.className}_${object.name}`;
82
+ const idx = entries.value.findIndex(e => e.key === key);
83
+ if (idx < 0) {
84
+ entries.value.push({
85
+ key,
86
+ title: object.properties?.title ?? `${object.className}: ${object.name}`,
87
+ attributions: Array.isArray(attributions) ? attributions : [attributions],
88
+ });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {import("@vcmap/core").VcsMap|import("@vcmap/core").Layer|import("@vcmap/core").ObliqueCollection} object
94
+ */
95
+ function removeAttributions(object) {
96
+ const idx = entries.value.findIndex(e => e.key === `${object.className}_${object.name}`);
97
+ if (idx >= 0) {
98
+ entries.value.splice(idx, 1);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * adds or removes a AttributionEntry on layer state changes
104
+ * @param {import("@vcmap/core").VcsMap|import("@vcmap/core").Layer|import("@vcmap/core").ObliqueCollection} object
105
+ */
106
+ function syncAttributions(object) {
107
+ if (object?.properties?.attributions === undefined) { return; }
108
+ if (object.active || object.loaded) {
109
+ addAttributions(object);
110
+ } else {
111
+ removeAttributions(object);
112
+ }
113
+ }
114
+
115
+ /**
116
+ *
117
+ * @param {import("@vcmap/core").VcsMap} map
118
+ */
119
+ function initAttributions(map) {
120
+ if (!map) { return; }
121
+ obliqueListener();
122
+ entries.value.splice(0);
123
+ syncAttributions(map);
124
+ [...map.layerCollection].forEach((layer) => {
125
+ if (layer.isSupported(map)) {
126
+ syncAttributions(layer);
127
+ }
128
+ });
129
+ if (map instanceof ObliqueMap) {
130
+ syncAttributions(map.collection);
131
+ obliqueListener = map.collectionChanged.addEventListener(syncAttributions);
132
+ }
133
+ }
134
+
135
+ const listeners = [
136
+ app.maps.mapActivated.addEventListener(initAttributions),
137
+ app.layers.stateChanged.addEventListener(syncAttributions),
138
+ app.layers.removed.addEventListener(removeAttributions),
139
+ app.maps.removed.addEventListener(removeAttributions),
140
+ ];
141
+
142
+ initAttributions(app.maps.activeMap);
143
+
144
+ const destroy = () => {
145
+ listeners.forEach(cb => cb());
146
+ obliqueListener();
147
+ };
148
+
149
+ return { entries, destroy };
150
+ }
@@ -5,6 +5,7 @@
5
5
  </template>
6
6
 
7
7
  <script>
8
+ import { VApp } from 'vuetify/lib';
8
9
  import VcsApp from './VcsApp.vue';
9
10
 
10
11
  /**
@@ -21,7 +22,10 @@
21
22
  required: true,
22
23
  },
23
24
  },
24
- components: { VcsApp },
25
+ components: {
26
+ VcsApp,
27
+ VApp,
28
+ },
25
29
  };
26
30
  </script>
27
31
 
@@ -38,6 +38,7 @@
38
38
  </style>
39
39
  <script>
40
40
 
41
+ import { VIcon, VMenu, VSpacer } from 'vuetify/lib';
41
42
  import VcsButton from './VcsButton.vue';
42
43
  import VcsActionList, { validateActions } from '../lists/VcsActionList.vue';
43
44
 
@@ -54,7 +55,13 @@
54
55
  */
55
56
  export default {
56
57
  name: 'VcsActionButtonList',
57
- components: { VcsActionList, VcsButton },
58
+ components: {
59
+ VcsActionList,
60
+ VcsButton,
61
+ VMenu,
62
+ VIcon,
63
+ VSpacer,
64
+ },
58
65
  props: {
59
66
  actions: {
60
67
  type: Array,
@@ -94,6 +94,7 @@
94
94
  </style>
95
95
 
96
96
  <script>
97
+ import { VBtn, VIcon } from 'vuetify/lib';
97
98
  import VcsBadge from '../notification/VcsBadge.vue';
98
99
  import VcsTooltip from '../notification/VcsTooltip.vue';
99
100
 
@@ -118,7 +119,12 @@
118
119
  */
119
120
  export default {
120
121
  name: 'VcsButton',
121
- components: { VcsTooltip, VcsBadge },
122
+ components: {
123
+ VcsTooltip,
124
+ VcsBadge,
125
+ VBtn,
126
+ VIcon,
127
+ },
122
128
  inheritAttrs: false,
123
129
  props: {
124
130
  active: {