@geode/opengeodeweb-front 10.38.0-rc.2 → 10.38.0-rc.3

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.
@@ -284,9 +284,7 @@ function onVerticalResizeStart(event: MouseEvent, index: number): void {
284
284
  >
285
285
  <ModelTree
286
286
  data-testid="modelComponentsObjectTree"
287
- :id="view.modelId || view.id"
288
- :view-id="view.id"
289
- :view-type="view.viewType"
287
+ :id="view.id"
290
288
  @show-menu="emit('show-menu', $event)"
291
289
  />
292
290
  </ViewerObjectTreeBox>
@@ -8,7 +8,6 @@ import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonT
8
8
  import type { DisplayItem } from "@ogw_front/composables/virtual_tree";
9
9
  import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
10
10
  import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
11
- import { consola } from "consola";
12
11
  import { useDataStore } from "@ogw_front/stores/data";
13
12
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
14
13
  import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
@@ -110,31 +109,6 @@ function isModel(item: TreeGroupItem): boolean {
110
109
  );
111
110
  }
112
111
 
113
- const hasCollectionsMap = reactive<Record<string, boolean>>({});
114
-
115
- watch(
116
- () => treeviewStore.items,
117
- async (newItems) => {
118
- const models = newItems
119
- .flatMap((group) => group.children || [])
120
- .filter((item) => isModel(item));
121
- const fetchPromises = models.map(async (model) => {
122
- if (hasCollectionsMap[model.id] === undefined) {
123
- hasCollectionsMap[model.id] = false;
124
- try {
125
- const hasCollections = await dataStore.hasCollectionComponents(model.id);
126
- hasCollectionsMap[model.id] = hasCollections;
127
- } catch (error) {
128
- consola.error("Failed to check collections", error);
129
- }
130
- }
131
- });
132
-
133
- await Promise.all(fetchPromises);
134
- },
135
- { immediate: true, deep: true },
136
- );
137
-
138
112
  function handleHoverEnter({
139
113
  item,
140
114
  immediate = false,
@@ -245,29 +219,11 @@ function expandAll(): void {
245
219
  item.id as string,
246
220
  item.title as string | undefined,
247
221
  item.geode_object_type as string,
248
- 'model_components',
249
222
  )
250
223
  "
251
224
  >
252
225
  <v-icon size="18">mdi-magnify-expand</v-icon>
253
226
  </v-btn>
254
- <v-btn
255
- v-if="isModel(item as unknown as TreeGroupItem) && hasCollectionsMap[item.id as string]"
256
- data-testid="expandModelCollectionsButton"
257
- icon="mdi-format-list-group"
258
- size="medium"
259
- class="ml-2"
260
- variant="text"
261
- v-tooltip="'Model\'s collections'"
262
- @click.stop="
263
- treeviewStore.displayAdditionalTree(
264
- item.id as string,
265
- item.title as string | undefined,
266
- item.geode_object_type as string,
267
- 'model_collections',
268
- )
269
- "
270
- />
271
227
  </template>
272
228
  </template>
273
229
  </CommonTreeView>
@@ -3,24 +3,19 @@ import { sortAndFormatItems, useTreeFilter } from "@ogw_front/composables/tree_f
3
3
  import CommonTreeView from "@ogw_front/components/Viewer/ObjectTree/Base/CommonTreeView.vue";
4
4
  import type { DisplayItem } from "@ogw_front/composables/virtual_tree";
5
5
  import FetchingData from "@ogw_front/components/FetchingData.vue";
6
- import { MESH_COMPONENT_TYPES } from "@ogw_front/utils/default_styles";
7
6
  import type { ModelComponentMenuPayload } from "@ogw_front/utils/treeview";
8
7
  import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
9
8
  import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
10
9
  import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
11
10
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
12
- import { useModelCollections } from "@ogw_front/composables/model_collections";
13
- import { useModelComponents } from "@ogw_front/composables/model_components";
11
+ import { useModelTree } from "@ogw_front/composables/model_tree";
14
12
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
15
13
 
16
14
  interface Props {
17
15
  id: string;
18
- viewId?: string;
19
- viewType?: string;
20
16
  }
21
17
 
22
- const { id, viewId = undefined, viewType = "model_components" } = defineProps<Props>();
23
- const actualViewId = viewId || id;
18
+ const { id } = defineProps<Props>();
24
19
 
25
20
  interface CollectionTreeItem {
26
21
  raw?: CollectionTreeItem;
@@ -41,29 +36,20 @@ const emit = defineEmits<Emits>();
41
36
 
42
37
  const treeviewStore = useTreeviewStore();
43
38
 
44
- const isCollections = computed(() => viewType === "model_collections");
45
-
46
- const composable = isCollections.value ? useModelCollections(id) : useModelComponents(id);
47
39
  const {
48
- items: rawItems,
40
+ isLoading,
49
41
  localCategories,
42
+ cache,
43
+ collectionTypes,
50
44
  selection: visibleComponents,
51
45
  updateVisibility,
52
- } = composable;
53
-
54
- const cache = computed(
55
- () =>
56
- ("componentsCache" in composable ? composable.componentsCache.value : undefined) ??
57
- ("collectionsCache" in composable ? composable.collectionsCache.value : undefined),
58
- );
46
+ } = useModelTree(id);
59
47
 
60
- const currentView = computed(() =>
61
- treeviewStore.opened_views.find((view) => view.id === actualViewId),
62
- );
48
+ const currentView = computed(() => treeviewStore.opened_views.find((view) => view.id === id));
63
49
 
64
50
  const opened = computed<string[]>({
65
51
  get: () => currentView.value?.opened || [],
66
- set: (val) => treeviewStore.setOpened(actualViewId, val),
52
+ set: (val) => treeviewStore.setOpened(id, val),
67
53
  });
68
54
 
69
55
  const {
@@ -84,25 +70,37 @@ function onUpdateSelection(newSelection: string[]): void {
84
70
 
85
71
  const visibleSelection = computed<string[]>(() => applySearchFilter(visibleComponents.value, []));
86
72
 
73
+ function matchesSearch(item: CollectionTreeItem, query: string): boolean {
74
+ return (
75
+ (item.title ?? "").toLowerCase().includes(query) ||
76
+ item.id.toLowerCase().includes(query) ||
77
+ (item.children ?? []).some((child) => matchesSearch(child, query))
78
+ );
79
+ }
80
+
81
+ function searchGroups(query: string): CollectionTreeItem[] {
82
+ const groups = (cache.value ?? {}) as Record<string, CollectionTreeItem[]>;
83
+ const result: CollectionTreeItem[] = [];
84
+ for (const [type, components] of Object.entries(groups)) {
85
+ const matches = components.filter((component) => matchesSearch(component, query));
86
+ if (matches.length > 0) {
87
+ result.push({
88
+ id: type,
89
+ title: `${type}s (${matches.length})`,
90
+ children: sortAndFormatItems(matches, sortType.value) as unknown as CollectionTreeItem[],
91
+ });
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
87
97
  const itemsForTreeView = computed<CollectionTreeItem[]>(() => {
88
- if (search.value && cache.value) {
98
+ if (search.value) {
89
99
  const query = search.value.toLowerCase();
90
- const result: CollectionTreeItem[] = [];
91
- for (const type of Object.keys(cache.value)) {
92
- const matches = (cache.value[type] ?? []).filter(
93
- (component: { title: string; id: string }) =>
94
- component.title.toLowerCase().includes(query) ||
95
- component.id.toLowerCase().includes(query),
96
- );
97
- if (matches.length > 0) {
98
- result.push({
99
- id: type,
100
- title: `${type}s (${matches.length})`,
101
- children: sortAndFormatItems(matches, sortType.value) as unknown as CollectionTreeItem[],
102
- });
103
- }
104
- }
105
- return result;
100
+ return sortAndFormatItems(
101
+ searchGroups(query),
102
+ sortType.value,
103
+ ) as unknown as CollectionTreeItem[];
106
104
  }
107
105
 
108
106
  const result: CollectionTreeItem[] = [];
@@ -127,9 +125,13 @@ function extractComponentIds(node: CollectionTreeItem): string[] {
127
125
  return [node.id];
128
126
  }
129
127
 
128
+ function isCollectionNode(item: CollectionTreeItem): boolean {
129
+ return collectionTypes.value.has(item.category ?? item.id);
130
+ }
131
+
130
132
  function showContextMenu(event: MouseEvent, item: CollectionTreeItem): void {
131
133
  const actualItem = item.raw || item;
132
- if (isCollections.value && !MESH_COMPONENT_TYPES.includes(actualItem.category ?? "")) {
134
+ if (isCollectionNode(actualItem)) {
133
135
  emit("show-menu", {
134
136
  event,
135
137
  itemId: id,
@@ -217,9 +219,10 @@ function getLeafViewerIds(item: CollectionTreeItem): number[] {
217
219
  @expand-all="expandAll"
218
220
  />
219
221
 
220
- <FetchingData v-if="rawItems === undefined" :size="48" :width="4" text="" />
222
+ <FetchingData v-if="isLoading" :size="48" :width="4" text="" />
221
223
 
222
224
  <CommonTreeView
225
+ v-else
223
226
  :selected="visibleSelection"
224
227
  v-model:opened="opened"
225
228
  v-model:active="treeviewStore.activeItems"
@@ -233,7 +236,7 @@ function getLeafViewerIds(item: CollectionTreeItem): number[] {
233
236
  class="transparent-treeview virtual-tree-height"
234
237
  @update:selected="(val) => onUpdateSelection(val as string[])"
235
238
  @click:item="onUpdateSelection([$event.id as string, ...visibleComponents])"
236
- @update:scroll-top="treeviewStore.setScrollTop(actualViewId, $event)"
239
+ @update:scroll-top="treeviewStore.setScrollTop(id, $event)"
237
240
  @hover:enter="({ item }) => handleHoverEnter({ item: item as unknown as CollectionTreeItem })"
238
241
  @hover:leave="handleHoverLeave"
239
242
  @contextmenu="showContextMenu($event.event, $event.item as unknown as CollectionTreeItem)"
@@ -0,0 +1,82 @@
1
+ import type {
2
+ CollectionComponent,
3
+ CollectionComponentGroup,
4
+ } from "@ogw_front/stores/data_helpers/collections";
5
+ import type {
6
+ FormattedComponent,
7
+ FormattedComponentGroup,
8
+ } from "@ogw_front/stores/data_helpers/mesh";
9
+ import { compareSelections } from "@ogw_front/utils/treeview";
10
+ import { useDataStore } from "@ogw_front/stores/data";
11
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
12
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
13
+
14
+ type ModelTreeCategory = FormattedComponentGroup | CollectionComponentGroup;
15
+ type ModelTreeCache = Record<string, (FormattedComponent | CollectionComponent)[]>;
16
+
17
+ function useModelVisibility(modelId: string): {
18
+ selection: typeof selection;
19
+ updateVisibility: typeof updateVisibility;
20
+ } {
21
+ const dataStyleStore = useDataStyleStore();
22
+ const hybridViewerStore = useHybridViewerStore();
23
+ const selection = dataStyleStore.visibleMeshComponents(modelId);
24
+
25
+ async function updateVisibility(current: readonly string[]): Promise<void> {
26
+ const { added, removed } = compareSelections([...current], selection.value);
27
+ if (added.length > 0) {
28
+ await dataStyleStore.setModelComponentsVisibility(modelId, added, true);
29
+ }
30
+ if (removed.length > 0) {
31
+ await dataStyleStore.setModelComponentsVisibility(modelId, removed, false);
32
+ }
33
+ if (added.length > 0 || removed.length > 0) {
34
+ await hybridViewerStore.remoteRender();
35
+ }
36
+ }
37
+
38
+ return { selection, updateVisibility };
39
+ }
40
+
41
+ export function useModelTree(modelId: string): {
42
+ isLoading: ComputedRef<boolean>;
43
+ localCategories: Ref<ModelTreeCategory[]>;
44
+ cache: Ref<ModelTreeCache | undefined>;
45
+ collectionTypes: Ref<Set<string>>;
46
+ } & ReturnType<typeof useModelVisibility> {
47
+ const dataStore = useDataStore();
48
+
49
+ const meshGroups = dataStore.refFormatedMeshComponents(modelId);
50
+ const collectionGroups = dataStore.refFormatedCollectionComponents(modelId);
51
+ const cache = ref<ModelTreeCache | undefined>(undefined);
52
+ const localCategories = ref<ModelTreeCategory[]>([]);
53
+ const collectionTypes = ref<Set<string>>(new Set());
54
+
55
+ // The mesh cache also groups collection components by their type; the collections cache overrides those keys with the real collection -> mesh components hierarchy.
56
+ watch(
57
+ [meshGroups, collectionGroups],
58
+ async ([mesh, collections]) => {
59
+ if (!mesh || !collections) {
60
+ return;
61
+ }
62
+ const [meshByType, collectionsByType] = await Promise.all([
63
+ dataStore.fetchAllMeshComponents(modelId),
64
+ dataStore.fetchAllCollectionComponents(modelId),
65
+ ]);
66
+ cache.value = markRaw({ ...meshByType, ...collectionsByType });
67
+ localCategories.value = [...mesh, ...collections];
68
+ collectionTypes.value = new Set(Object.keys(collectionsByType));
69
+ },
70
+ { immediate: true },
71
+ );
72
+
73
+ const isLoading = computed(() => cache.value === undefined);
74
+
75
+ return {
76
+ isLoading,
77
+ localCategories,
78
+ cache,
79
+ collectionTypes,
80
+ ...useModelVisibility(modelId),
81
+ };
82
+ }
@@ -132,6 +132,7 @@ export const useDataStyleStore = defineStore("dataStyle", () => {
132
132
  ...component_style_promises,
133
133
  ...model_component_type_style_promises,
134
134
  ]);
135
+ await dataStyleState.loadFromDatabase();
135
136
  }
136
137
 
137
138
  async function applyAllStylesFromState(): Promise<void> {
@@ -6,7 +6,6 @@ import { defineStore } from "pinia";
6
6
 
7
7
  import type { Table } from "dexie";
8
8
  import { ref, watch } from "vue";
9
- import { compareSelections } from "@ogw_front/utils/treeview";
10
9
  import { database } from "@ogw_internal/database/database";
11
10
 
12
11
  const PANEL_WIDTH = 300;
@@ -17,8 +16,6 @@ interface OpenedView {
17
16
  title: string;
18
17
  scrollTop: number;
19
18
  opened: string[];
20
- modelId?: string;
21
- viewType?: string;
22
19
  geode_object_type?: string;
23
20
  }
24
21
 
@@ -144,18 +141,6 @@ export const useTreeviewStore = defineStore("treeview", () => {
144
141
  opened_views.value = opened_views.value.filter((view: ReadonlyOpenedView) => view.id !== id);
145
142
  }
146
143
 
147
- watch(selection, (current: readonly string[], previous: readonly string[]) => {
148
- const { removed } = compareSelections(current, previous);
149
- for (const id of removed) {
150
- const index = opened_views.value.findIndex(
151
- (view: ReadonlyOpenedView) => view.type === "component" && view.id === id,
152
- );
153
- if (index !== -1) {
154
- closeView(id);
155
- }
156
- }
157
- });
158
-
159
144
  function toggleView(id: string): void {
160
145
  const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === id);
161
146
  if (index !== -1) {
@@ -229,20 +214,16 @@ export const useTreeviewStore = defineStore("treeview", () => {
229
214
  id: string,
230
215
  title: string | undefined,
231
216
  geodeObjectType: string,
232
- viewType = "model_components",
233
217
  ): void {
234
- const viewId = `${id}_${viewType}`;
235
- const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === viewId);
218
+ const index = opened_views.value.findIndex((view: ReadonlyOpenedView) => view.id === id);
236
219
  if (index !== -1) {
237
- closeView(viewId);
220
+ closeView(id);
238
221
  return;
239
222
  }
240
223
  additionalPanelWidth.value = panelWidth.value;
241
224
  opened_views.value.push({
242
225
  type: "component",
243
- id: viewId,
244
- modelId: id,
245
- viewType,
226
+ id,
246
227
  title: title ?? id,
247
228
  geode_object_type: geodeObjectType,
248
229
  scrollTop: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.38.0-rc.2",
3
+ "version": "10.38.0-rc.3",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -1,91 +0,0 @@
1
- import type {
2
- CollectionComponent,
3
- CollectionComponentGroup,
4
- } from "@ogw_front/stores/data_helpers/collections";
5
- import { compareSelections } from "@ogw_front/utils/treeview";
6
- import { useDataStore } from "@ogw_front/stores/data";
7
- import { useDataStyleStore } from "@ogw_front/stores/data_style";
8
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
-
10
- // The `watch` callback receives the freshly-fetched collection groups purely for reading (matching against and copying into `localCategories`, never mutated in place), so it's typed with its own fully-readonly mirror of `CollectionComponentGroup` rather than that (intentionally mutable, see `existing.title` below) store type directly.
11
- interface ReadonlyCollectionComponentGroup {
12
- readonly id: string;
13
- readonly title: string;
14
- readonly children: readonly CollectionComponent[];
15
- }
16
-
17
- export function useModelCollections(viewId: string): {
18
- items: typeof items;
19
- collectionsCache: typeof collectionsCache;
20
- localCategories: typeof localCategories;
21
- selection: typeof selection;
22
- updateVisibility: typeof updateVisibility;
23
- } {
24
- const dataStore = useDataStore();
25
- const dataStyleStore = useDataStyleStore();
26
- const hybridViewerStore = useHybridViewerStore();
27
-
28
- const items = dataStore.refFormatedCollectionComponents(viewId);
29
- const collectionsCache = ref<Record<string, CollectionComponent[]> | undefined>(undefined);
30
- const localCategories = ref<CollectionComponentGroup[]>([]);
31
-
32
- onMounted(async () => {
33
- const data = await dataStore.fetchAllCollectionComponents(viewId);
34
- collectionsCache.value = markRaw(data);
35
- });
36
-
37
- watch(
38
- items,
39
- async (newItems: readonly ReadonlyCollectionComponentGroup[] | undefined) => {
40
- if (!newItems) {
41
- localCategories.value = [];
42
- return;
43
- }
44
-
45
- const data = await dataStore.fetchAllCollectionComponents(viewId);
46
- collectionsCache.value = markRaw(data);
47
-
48
- localCategories.value = newItems.map((newCategory: ReadonlyCollectionComponentGroup) => {
49
- const existing = localCategories.value.find(
50
- (category: ReadonlyCollectionComponentGroup) => category.id === newCategory.id,
51
- );
52
- if (existing) {
53
- existing.title = newCategory.title || newCategory.id;
54
- return existing;
55
- }
56
- return reactive({
57
- ...newCategory,
58
- title: newCategory.title || newCategory.id,
59
- });
60
- });
61
- },
62
- { immediate: true },
63
- );
64
-
65
- const selection = dataStyleStore.visibleMeshComponents(viewId);
66
-
67
- async function updateVisibility(current: readonly string[]): Promise<void> {
68
- const previous = selection.value;
69
- const { added, removed } = compareSelections([...current], previous);
70
-
71
- if (added.length === 0 && removed.length === 0) {
72
- return;
73
- }
74
-
75
- if (added.length > 0) {
76
- await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
77
- }
78
- if (removed.length > 0) {
79
- await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
80
- }
81
- await hybridViewerStore.remoteRender();
82
- }
83
-
84
- return {
85
- items,
86
- collectionsCache,
87
- localCategories,
88
- selection,
89
- updateVisibility,
90
- };
91
- }
@@ -1,91 +0,0 @@
1
- import type {
2
- FormattedComponent,
3
- FormattedComponentGroup,
4
- } from "@ogw_front/stores/data_helpers/mesh";
5
- import { compareSelections } from "@ogw_front/utils/treeview";
6
- import { useDataStore } from "@ogw_front/stores/data";
7
- import { useDataStyleStore } from "@ogw_front/stores/data_style";
8
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
9
-
10
- // The `watch` callback receives the freshly-fetched component groups purely for reading (matching against and copying into `localCategories`, never mutated in place), so it's typed with its own fully-readonly mirror of `FormattedComponentGroup` rather than that (intentionally mutable, see `existing.title` below) store type directly.
11
- interface ReadonlyFormattedComponentGroup {
12
- readonly id: string;
13
- readonly title: string;
14
- readonly children: readonly FormattedComponent[];
15
- }
16
-
17
- export function useModelComponents(viewId: string): {
18
- items: typeof items;
19
- componentsCache: typeof componentsCache;
20
- localCategories: typeof localCategories;
21
- selection: typeof selection;
22
- updateVisibility: typeof updateVisibility;
23
- } {
24
- const dataStore = useDataStore();
25
- const dataStyleStore = useDataStyleStore();
26
- const hybridViewerStore = useHybridViewerStore();
27
-
28
- const items = dataStore.refFormatedMeshComponents(viewId);
29
- const componentsCache = ref<Record<string, FormattedComponent[]> | undefined>(undefined);
30
- const localCategories = ref<FormattedComponentGroup[]>([]);
31
-
32
- onMounted(async () => {
33
- const data = await dataStore.fetchAllMeshComponents(viewId);
34
- componentsCache.value = markRaw(data);
35
- });
36
-
37
- watch(
38
- items,
39
- async (newItems: readonly ReadonlyFormattedComponentGroup[] | undefined) => {
40
- if (!newItems) {
41
- localCategories.value = [];
42
- return;
43
- }
44
-
45
- const data = await dataStore.fetchAllMeshComponents(viewId);
46
- componentsCache.value = markRaw(data);
47
-
48
- localCategories.value = newItems.map((newCategory: ReadonlyFormattedComponentGroup) => {
49
- const existing = localCategories.value.find(
50
- (category: ReadonlyFormattedComponentGroup) => category.id === newCategory.id,
51
- );
52
- if (existing) {
53
- existing.title = newCategory.title || newCategory.id;
54
- return existing;
55
- }
56
- return reactive({
57
- ...newCategory,
58
- title: newCategory.title || newCategory.id,
59
- });
60
- });
61
- },
62
- { immediate: true },
63
- );
64
-
65
- const selection = dataStyleStore.visibleMeshComponents(viewId);
66
-
67
- async function updateVisibility(current: readonly string[]): Promise<void> {
68
- const previous = selection.value;
69
- const { added, removed } = compareSelections([...current], previous);
70
-
71
- if (added.length === 0 && removed.length === 0) {
72
- return;
73
- }
74
-
75
- if (added.length > 0) {
76
- await dataStyleStore.setModelComponentsVisibility(viewId, added, true);
77
- }
78
- if (removed.length > 0) {
79
- await dataStyleStore.setModelComponentsVisibility(viewId, removed, false);
80
- }
81
- await hybridViewerStore.remoteRender();
82
- }
83
-
84
- return {
85
- items,
86
- componentsCache,
87
- localCategories,
88
- selection,
89
- updateVisibility,
90
- };
91
- }