@geode/opengeodeweb-front 10.35.0-rc.2 → 10.35.0-rc.4

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.
@@ -2,6 +2,7 @@
2
2
  import ColormapQuickPicker from "@ogw_front/components/Viewer/Options/ColormapQuickPicker.vue";
3
3
  import HybridViewerTooltip from "@ogw_front/components/HybridViewerTooltip.vue";
4
4
  import ViewToolbar from "@ogw_front/components/ViewToolbar.vue";
5
+ import ZoomBoxOverlay from "@ogw_front/components/ZoomBoxOverlay.vue";
5
6
 
6
7
  import { useDataStore } from "@ogw_front/stores/data";
7
8
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
@@ -102,6 +103,7 @@ async function handleClick(event: PointerEvent): Promise<void> {
102
103
  :y="quickColormap.y"
103
104
  :data-id="quickColormap.data_id"
104
105
  />
106
+ <ZoomBoxOverlay v-if="hybridViewerStore.is_zoom_box_active" />
105
107
  <ViewToolbar />
106
108
  <slot name="ui"></slot>
107
109
  <HybridViewerTooltip :container-width="elementWidth" :container-height="elementHeight" />
@@ -3,6 +3,7 @@ import Loading from "@ogw_front/components/Loading.vue";
3
3
  import Recaptcha from "@ogw_front/components/Recaptcha.vue";
4
4
  import { Status } from "@ogw_front/utils/status";
5
5
  import { appMode } from "@ogw_shared/app_mode";
6
+ import { getCloudUrlParam } from "@ogw_front/utils/cloud";
6
7
  import { useInfraStore } from "@ogw_front/stores/infra";
7
8
 
8
9
  interface Props {
@@ -15,8 +16,10 @@ interface Props {
15
16
  const { appName, email = undefined, isUserAuthenticated = false, logo = "" } = defineProps<Props>();
16
17
 
17
18
  const infraStore = useInfraStore();
18
- if (infraStore.app_mode !== appMode.CLOUD) {
19
- infraStore.create_backend();
19
+ // In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
20
+ // Points at an already running service: that one is connected straight away, without authentication.
21
+ if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
22
+ infraStore.create_backend(email);
20
23
  }
21
24
 
22
25
  function cloudCreateBackend(): Promise<void> {
@@ -9,7 +9,6 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
9
9
 
10
10
  import ColormapQuickPicker from "@ogw_front/components/Viewer/Options/ColormapQuickPicker.vue";
11
11
  import ViewToolbar from "@ogw_front/components/ViewToolbar.vue";
12
- import viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
13
12
  import { newInstance as vtkRemoteView } from "@kitware/vtk.js/Rendering/Misc/RemoteView";
14
13
 
15
14
  interface Props {
@@ -32,9 +31,6 @@ async function get_x_y(event: PointerEvent): Promise<void> {
32
31
  const { offsetX, offsetY, clientX, clientY } = event;
33
32
  if (viewerStore.picking_mode === true) {
34
33
  viewerStore.set_picked_point(offsetX, offsetY);
35
- const schema = viewer_schemas.opengeodeweb_viewer.viewer.get_point_position;
36
- const params = { x: offsetX, y: offsetY };
37
- viewerStore.request({ schema, params });
38
34
  } else {
39
35
  await pickColormap(offsetX, offsetY, clientX, clientY);
40
36
  }
@@ -0,0 +1,291 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ MESH_ELEMENT_KINDS,
4
+ MODEL_COMPONENT_KINDS,
5
+ getAttributeRange,
6
+ } from "@ogw_front/utils/attributes";
7
+ import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
8
+ import ToolPanel from "@ogw_front/components/ToolPanel.vue";
9
+ import ViewerOptionsAttributeRangeSelector from "@ogw_front/components/Viewer/Options/AttributeRangeSelector.vue";
10
+ import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
11
+ import { useBackStore } from "@ogw_front/stores/back";
12
+ import { useDataStore } from "@ogw_front/stores/data";
13
+ import { useDebounceFn } from "@vueuse/core";
14
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
15
+
16
+ const DEBOUNCE_DELAY = 100;
17
+
18
+ interface Props {
19
+ escapeFunction?: () => void;
20
+ }
21
+
22
+ const { escapeFunction = undefined } = defineProps<Props>();
23
+
24
+ interface AttributeSource {
25
+ title: string;
26
+ schema: JsonRpcSchema;
27
+ params: Record<string, unknown>;
28
+ location: "point" | "cell";
29
+ }
30
+
31
+ interface AttributeInfo {
32
+ attribute_name: string;
33
+ nb_items: number;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ const show = defineModel<boolean>("show", { default: false });
38
+ const backStore = useBackStore();
39
+ const dataStore = useDataStore();
40
+ const hybridViewerStore = useHybridViewerStore();
41
+ const selectedDatasetId = ref<string>();
42
+ const sources = ref<AttributeSource[]>([]);
43
+ const selectedSourceIndex = ref<number>();
44
+ const attributes = ref<AttributeInfo[]>([]);
45
+ const attributeName = ref<string>();
46
+ const attributeItem = ref<number>(0);
47
+ const minimum = ref<number>();
48
+ const maximum = ref<number>();
49
+
50
+ const allItems = dataStore.refAllItems();
51
+ const availableDatasets = computed<{ title: string; value: string }[]>(() =>
52
+ allItems.value.map((item) => ({
53
+ title: item.name || item.id,
54
+ value: item.id,
55
+ })),
56
+ );
57
+ const availableSources = computed<{ title: string; value: number }[]>(() =>
58
+ sources.value.map((source, index) => ({ title: source.title, value: index })),
59
+ );
60
+ const currentAttribute = computed<AttributeInfo | undefined>(() =>
61
+ attributes.value.find((attribute) => attribute.attribute_name === attributeName.value),
62
+ );
63
+ const itemOptions = computed<{ title: string; value: number }[]>(() =>
64
+ Array.from({ length: currentAttribute.value?.nb_items ?? 0 }, (_, index) => ({
65
+ title: `Item ${index + 1}`,
66
+ value: index,
67
+ })),
68
+ );
69
+
70
+ function attributeSchema(kind: string, isModel: boolean): JsonRpcSchema {
71
+ const prefix = isModel ? "model_component_" : "";
72
+ return back_schemas.opengeodeweb_back[
73
+ `${prefix}${kind}_attribute_names` as keyof typeof back_schemas.opengeodeweb_back
74
+ ] as JsonRpcSchema;
75
+ }
76
+
77
+ function attributeSource(
78
+ kind: string,
79
+ title: string,
80
+ params: Record<string, unknown>,
81
+ isModel: boolean,
82
+ ): AttributeSource {
83
+ return {
84
+ title: `${title}${kind} attribute`,
85
+ schema: attributeSchema(kind, isModel),
86
+ params,
87
+ location: kind === "vertex" ? "point" : "cell",
88
+ };
89
+ }
90
+
91
+ async function fetchSources(id: string): Promise<AttributeSource[]> {
92
+ const item = await dataStore.item(id);
93
+ if (item.viewer_type !== "model") {
94
+ return ["vertex", MESH_ELEMENT_KINDS[item.geode_object_type]]
95
+ .filter((kind): kind is string => kind !== undefined)
96
+ .map((kind) => attributeSource(kind, "", { id }, false));
97
+ }
98
+ const modelSources = await Promise.all(
99
+ Object.entries(MODEL_COMPONENT_KINDS).map(async ([type, kinds]) => {
100
+ const component_ids = await dataStore.getMeshComponentGeodeIds(id, type);
101
+ if (component_ids.length === 0) {
102
+ return [];
103
+ }
104
+ return kinds.map((kind) => attributeSource(kind, `${type} `, { id, component_ids }, true));
105
+ }),
106
+ );
107
+ return modelSources.flat();
108
+ }
109
+
110
+ async function applyThreshold(): Promise<void> {
111
+ const source = sources.value[selectedSourceIndex.value ?? -1];
112
+ if (
113
+ !selectedDatasetId.value ||
114
+ !source ||
115
+ !attributeName.value ||
116
+ minimum.value === undefined ||
117
+ maximum.value === undefined ||
118
+ minimum.value > maximum.value
119
+ ) {
120
+ return;
121
+ }
122
+ await hybridViewerStore.setThreshold([selectedDatasetId.value], {
123
+ name: attributeName.value,
124
+ location: source.location,
125
+ item: attributeItem.value,
126
+ minimum: minimum.value,
127
+ maximum: maximum.value,
128
+ });
129
+ }
130
+
131
+ const debouncedApply = useDebounceFn(() => applyThreshold(), DEBOUNCE_DELAY);
132
+
133
+ function resetRange(): void {
134
+ const { min, max } = getAttributeRange(
135
+ currentAttribute.value as Parameters<typeof getAttributeRange>[0],
136
+ attributeItem.value,
137
+ );
138
+ minimum.value = min;
139
+ maximum.value = max;
140
+ }
141
+
142
+ async function removeThreshold(): Promise<void> {
143
+ attributeName.value = undefined;
144
+ if (selectedDatasetId.value) {
145
+ await hybridViewerStore.setThreshold([selectedDatasetId.value]);
146
+ }
147
+ }
148
+
149
+ watch(selectedDatasetId, async (id, previousId) => {
150
+ if (previousId) {
151
+ await hybridViewerStore.setThreshold([previousId]);
152
+ }
153
+ selectedSourceIndex.value = undefined;
154
+ sources.value = id ? await fetchSources(id) : [];
155
+ });
156
+
157
+ watch(selectedSourceIndex, async (index) => {
158
+ attributeName.value = undefined;
159
+ attributes.value = [];
160
+ const source = sources.value[index ?? -1];
161
+ if (!source) {
162
+ return;
163
+ }
164
+ const response = (await backStore.request({
165
+ schema: source.schema,
166
+ params: source.params,
167
+ })) as { attributes: AttributeInfo[] };
168
+ attributes.value = response.attributes;
169
+ });
170
+
171
+ watch(attributeName, () => {
172
+ attributeItem.value = 0;
173
+ resetRange();
174
+ });
175
+
176
+ watch(attributeItem, () => {
177
+ resetRange();
178
+ });
179
+
180
+ watch([minimum, maximum], () => {
181
+ debouncedApply();
182
+ });
183
+
184
+ watch(show, (visible) => {
185
+ if (visible) {
186
+ applyThreshold();
187
+ }
188
+ });
189
+ </script>
190
+
191
+ <template>
192
+ <ToolPanel
193
+ v-model="show"
194
+ data-testid="thresholdFilterPanel"
195
+ title="Threshold Filter"
196
+ :width="340"
197
+ :click-outside="false"
198
+ :escapeFunction="escapeFunction"
199
+ >
200
+ <v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
201
+ <v-select
202
+ v-model="selectedDatasetId"
203
+ data-testid="thresholdDatasetSelect"
204
+ :items="availableDatasets"
205
+ label="Select a dataset"
206
+ variant="outlined"
207
+ density="compact"
208
+ hide-details
209
+ class="mb-3 text-caption"
210
+ />
211
+
212
+ <v-select
213
+ v-if="selectedDatasetId"
214
+ v-model="selectedSourceIndex"
215
+ data-testid="thresholdAttributeTypeSelect"
216
+ :items="availableSources"
217
+ label="Select an attribute type"
218
+ variant="outlined"
219
+ density="compact"
220
+ hide-details
221
+ class="mb-3 text-caption"
222
+ />
223
+
224
+ <v-select
225
+ v-if="selectedSourceIndex !== undefined"
226
+ v-model="attributeName"
227
+ data-testid="thresholdAttributeSelect"
228
+ :items="attributes.map((attribute) => attribute.attribute_name)"
229
+ label="Select an attribute"
230
+ variant="outlined"
231
+ density="compact"
232
+ hide-details
233
+ class="mb-3 text-caption"
234
+ />
235
+
236
+ <v-select
237
+ v-if="currentAttribute && currentAttribute.nb_items > 1"
238
+ v-model="attributeItem"
239
+ data-testid="thresholdItemSelect"
240
+ :items="itemOptions"
241
+ label="Select an item"
242
+ variant="outlined"
243
+ density="compact"
244
+ hide-details
245
+ class="mb-3 text-caption"
246
+ />
247
+
248
+ <template v-if="attributeName">
249
+ <v-divider class="my-3" />
250
+ <span class="text-caption font-weight-bold">Range</span>
251
+ <ViewerOptionsAttributeRangeSelector
252
+ v-model:minimum="minimum"
253
+ v-model:maximum="maximum"
254
+ class="mt-2"
255
+ @reset="resetRange"
256
+ />
257
+ </template>
258
+ </v-card-text>
259
+
260
+ <template #actions>
261
+ <v-card-actions class="justify-space-between px-3 pb-3 pt-0">
262
+ <v-btn
263
+ data-testid="removeThresholdButton"
264
+ variant="text"
265
+ size="small"
266
+ color="error"
267
+ class="text-caption text-none"
268
+ @click="removeThreshold"
269
+ >
270
+ Remove Threshold
271
+ </v-btn>
272
+ <v-btn
273
+ data-testid="resetThresholdButton"
274
+ variant="tonal"
275
+ size="small"
276
+ color="secondary"
277
+ class="text-caption text-none"
278
+ @click="resetRange"
279
+ >
280
+ Reset
281
+ </v-btn>
282
+ </v-card-actions>
283
+ </template>
284
+ </ToolPanel>
285
+ </template>
286
+
287
+ <style scoped>
288
+ .max-panel-height {
289
+ max-height: 520px;
290
+ }
291
+ </style>
@@ -7,6 +7,7 @@ import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue";
7
7
  import Ruler from "@ogw_front/components/Ruler.vue";
8
8
  import Screenshot from "@ogw_front/components/Screenshot.vue";
9
9
  import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue";
10
+ import ThresholdFilter from "@ogw_front/components/ThresholdFilter.vue";
10
11
  import ZScaling from "@ogw_front/components/ZScaling.vue";
11
12
  import { onKeyStroke } from "@vueuse/core";
12
13
  import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
@@ -15,12 +16,15 @@ import { useViewerStore } from "@ogw_front/stores/viewer";
15
16
 
16
17
  const hybridViewerStore = useHybridViewerStore();
17
18
  const viewerStore = useViewerStore();
19
+ const { is_picking, is_zoom_box_active } = storeToRefs(hybridViewerStore);
20
+ const { picking_mode } = storeToRefs(viewerStore);
18
21
  const showScreenshot = ref<boolean>(false);
19
22
  const showCameraManager = ref<boolean>(false);
20
23
  const showCameraOrientation = ref<boolean>(false);
21
24
  const showZScaling = ref<boolean>(false);
22
25
  const showClippingPlanes = ref<boolean>(false);
23
26
  const showShrinkFilter = ref<boolean>(false);
27
+ const showThresholdFilter = ref<boolean>(false);
24
28
  const showRuler = ref<boolean>(false);
25
29
  const gridScale = ref<boolean>(false);
26
30
  const zScale = ref<number>(hybridViewerStore.zScale);
@@ -58,6 +62,11 @@ onKeyStroke("Escape", () => {
58
62
  }
59
63
  });
60
64
 
65
+ function stopHoverHighlight(): void {
66
+ hybridViewerStore.is_hover_highlight = false;
67
+ hybridViewerStore.clearHoverHighlight();
68
+ }
69
+
61
70
  function closeAllToolsExcept(toolRef: Ref<boolean>): void {
62
71
  const tools = [
63
72
  showCameraOrientation,
@@ -66,13 +75,20 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
66
75
  showZScaling,
67
76
  showClippingPlanes,
68
77
  showShrinkFilter,
78
+ showThresholdFilter,
69
79
  showRuler,
80
+ is_picking,
81
+ is_zoom_box_active,
82
+ picking_mode,
70
83
  ];
71
84
  for (const tool of tools) {
72
85
  if (tool !== toolRef) {
73
86
  tool.value = false;
74
87
  }
75
88
  }
89
+ if (toolRef !== showRuler) {
90
+ void nextTick(stopHoverHighlight);
91
+ }
76
92
  }
77
93
 
78
94
  function toggleTool(toolRef: Ref<boolean>): void {
@@ -80,6 +96,19 @@ function toggleTool(toolRef: Ref<boolean>): void {
80
96
  toolRef.value = !toolRef.value;
81
97
  }
82
98
 
99
+ function toggleHoverHighlight(fieldType: string): void {
100
+ const activate =
101
+ !hybridViewerStore.is_hover_highlight ||
102
+ hybridViewerStore.hover_highlight_field_type !== fieldType;
103
+ closeAllToolsExcept(showRuler);
104
+ if (activate) {
105
+ hybridViewerStore.is_hover_highlight = true;
106
+ hybridViewerStore.hover_highlight_field_type = fieldType;
107
+ } else {
108
+ stopHoverHighlight();
109
+ }
110
+ }
111
+
83
112
  const camera_options = computed<CameraOptionAction[]>(() => [
84
113
  {
85
114
  testId: "resetCameraButton",
@@ -95,7 +124,16 @@ const camera_options = computed<CameraOptionAction[]>(() => [
95
124
  icon: "mdi-crosshairs-question",
96
125
  color: hybridViewerStore.is_picking ? "primary" : undefined,
97
126
  action: (): void => {
98
- hybridViewerStore.is_picking = !hybridViewerStore.is_picking;
127
+ toggleTool(is_picking);
128
+ },
129
+ },
130
+ {
131
+ testId: "zoomToBoxButton",
132
+ tooltip: "Zoom to box",
133
+ icon: "mdi-magnify-scan",
134
+ color: hybridViewerStore.is_zoom_box_active ? "primary" : undefined,
135
+ action: (): void => {
136
+ toggleTool(is_zoom_box_active);
99
137
  },
100
138
  },
101
139
  {
@@ -103,28 +141,14 @@ const camera_options = computed<CameraOptionAction[]>(() => [
103
141
  tooltip: "Highlight on hover",
104
142
  icon: "mdi-cursor-default-click",
105
143
  color: hybridViewerStore.is_hover_highlight ? "primary" : undefined,
106
- action: hybridViewerStore.is_hover_highlight
107
- ? (): void => {
108
- hybridViewerStore.is_hover_highlight = false;
109
- hybridViewerStore.clearHoverHighlight();
110
- }
111
- : undefined,
144
+ action: hybridViewerStore.is_hover_highlight ? stopHoverHighlight : undefined,
112
145
  menu: [
113
146
  {
114
147
  title: "Cells",
115
148
  testId: "highlightOnHoverCellsButton",
116
149
  icon: "mdi-select-all",
117
150
  action: (): void => {
118
- if (
119
- hybridViewerStore.is_hover_highlight &&
120
- hybridViewerStore.hover_highlight_field_type === "CELL"
121
- ) {
122
- hybridViewerStore.is_hover_highlight = false;
123
- hybridViewerStore.clearHoverHighlight();
124
- } else {
125
- hybridViewerStore.is_hover_highlight = true;
126
- hybridViewerStore.hover_highlight_field_type = "CELL";
127
- }
151
+ toggleHoverHighlight("CELL");
128
152
  },
129
153
  },
130
154
  {
@@ -132,16 +156,7 @@ const camera_options = computed<CameraOptionAction[]>(() => [
132
156
  testId: "highlightOnHoverPointsButton",
133
157
  icon: "mdi-select-drag",
134
158
  action: (): void => {
135
- if (
136
- hybridViewerStore.is_hover_highlight &&
137
- hybridViewerStore.hover_highlight_field_type === "POINT"
138
- ) {
139
- hybridViewerStore.is_hover_highlight = false;
140
- hybridViewerStore.clearHoverHighlight();
141
- } else {
142
- hybridViewerStore.is_hover_highlight = true;
143
- hybridViewerStore.hover_highlight_field_type = "POINT";
144
- }
159
+ toggleHoverHighlight("POINT");
145
160
  },
146
161
  },
147
162
  ],
@@ -219,6 +234,15 @@ const camera_options = computed<CameraOptionAction[]>(() => [
219
234
  toggleTool(showShrinkFilter);
220
235
  },
221
236
  },
237
+ {
238
+ testId: "thresholdFilterButton",
239
+ tooltip: "Threshold Filter",
240
+ icon: "mdi-filter-variant",
241
+ color: showThresholdFilter.value ? "primary" : undefined,
242
+ action: (): void => {
243
+ toggleTool(showThresholdFilter);
244
+ },
245
+ },
222
246
  {
223
247
  testId: "rulerButton",
224
248
  tooltip: "Ruler",
@@ -308,6 +332,10 @@ const camera_options = computed<CameraOptionAction[]>(() => [
308
332
  v-model:show="showShrinkFilter"
309
333
  :escapeFunction="() => (showShrinkFilter = false)"
310
334
  />
335
+ <ThresholdFilter
336
+ v-model:show="showThresholdFilter"
337
+ :escapeFunction="() => (showThresholdFilter = false)"
338
+ />
311
339
  <Ruler v-model:show="showRuler" :escapeFunction="() => (showRuler = false)" />
312
340
  </template>
313
341
 
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ label: string;
4
+ color: string;
5
+ icon: string;
6
+ }
7
+
8
+ const { label, color, icon } = defineProps<Props>();
9
+
10
+ const emit = defineEmits<{
11
+ close: [];
12
+ }>();
13
+
14
+ defineOptions({ inheritAttrs: false });
15
+ </script>
16
+
17
+ <template>
18
+ <div
19
+ class="tool-active-container d-flex justify-center w-100 pa-4"
20
+ :style="{ '--tool-active-color': `var(--v-theme-${color})` }"
21
+ >
22
+ <v-chip
23
+ v-bind="$attrs"
24
+ :color="color"
25
+ :prepend-icon="icon"
26
+ elevation="8"
27
+ size="large"
28
+ variant="flat"
29
+ class="tool-active-pulse"
30
+ style="pointer-events: auto"
31
+ @click="emit('close')"
32
+ >
33
+ {{ label }} &middot; Esc to stop
34
+ <v-divider vertical class="mx-2 my-1" opacity="0.3" />
35
+ <v-icon icon="mdi-close" size="small" />
36
+ </v-chip>
37
+ </div>
38
+ </template>
39
+
40
+ <style scoped>
41
+ .tool-active-container {
42
+ position: absolute;
43
+ top: 20px;
44
+ left: 0;
45
+ pointer-events: none;
46
+ z-index: 3;
47
+ }
48
+
49
+ @keyframes pulse-ring {
50
+ 0% {
51
+ box-shadow: 0 0 0 0 rgba(var(--tool-active-color), 0.7);
52
+ }
53
+ 70% {
54
+ box-shadow: 0 0 0 10px rgba(var(--tool-active-color), 0);
55
+ }
56
+ 100% {
57
+ box-shadow: 0 0 0 0 rgba(var(--tool-active-color), 0);
58
+ }
59
+ }
60
+
61
+ .tool-active-pulse {
62
+ animation: pulse-ring 1.5s ease-out infinite;
63
+ }
64
+ </style>