@geode/opengeodeweb-front 10.32.2 → 10.33.0-rc.1

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.
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import AttributeRangeSelector from "./AttributeRangeSelector.vue";
2
3
  import ColorMapPicker from "./ColorMapPicker.vue";
3
4
 
4
5
  const emit = defineEmits(["reset"]);
@@ -11,43 +12,12 @@ const colorMap = defineModel("colorMap", { type: String });
11
12
  <template>
12
13
  <div class="attribute-colorbar mt-3">
13
14
  <ColorMapPicker v-model:selected-preset-name="colorMap" :min="minimum" :max="maximum" />
14
- <v-row dense align="center" class="mt-2" no-gutters>
15
- <v-col cols="5" class="pe-1">
16
- <v-text-field
17
- data-testid="attributeMinInput"
18
- :model-value="minimum"
19
- @update:model-value="(value) => (minimum = Number(value))"
20
- label="Min"
21
- type="number"
22
- :max="maximum"
23
- density="compact"
24
- hide-details
25
- variant="outlined"
26
- />
27
- </v-col>
28
- <v-col cols="2" class="d-flex justify-center">
29
- <v-btn
30
- icon="mdi-arrow-left-right"
31
- size="x-small"
32
- variant="text"
33
- @click="emit('reset')"
34
- v-tooltip="'Reset range'"
35
- />
36
- </v-col>
37
- <v-col cols="5" class="ps-1">
38
- <v-text-field
39
- data-testid="attributeMaxInput"
40
- :model-value="maximum"
41
- @update:model-value="(value) => (maximum = Number(value))"
42
- label="Max"
43
- type="number"
44
- :min="minimum"
45
- density="compact"
46
- hide-details
47
- variant="outlined"
48
- />
49
- </v-col>
50
- </v-row>
15
+ <AttributeRangeSelector
16
+ v-model:minimum="minimum"
17
+ v-model:maximum="maximum"
18
+ class="mt-2"
19
+ @reset="emit('reset')"
20
+ />
51
21
  </div>
52
22
  </template>
53
23
 
@@ -0,0 +1,48 @@
1
+ <script setup>
2
+ const emit = defineEmits(["reset"]);
3
+
4
+ const minimum = defineModel("minimum", { type: Number });
5
+ const maximum = defineModel("maximum", { type: Number });
6
+ </script>
7
+
8
+ <template>
9
+ <v-row dense align="center" no-gutters>
10
+ <v-col cols="5" class="pe-1">
11
+ <v-text-field
12
+ data-testid="attributeMinInput"
13
+ :model-value="minimum"
14
+ @update:model-value="(value) => (minimum = Number(value))"
15
+ label="Min"
16
+ type="number"
17
+ :max="maximum"
18
+ density="compact"
19
+ hide-details
20
+ variant="outlined"
21
+ />
22
+ </v-col>
23
+ <v-col cols="2" class="d-flex justify-center">
24
+ <v-btn
25
+ data-testid="resetRangeButton"
26
+ aria-label="Reset range"
27
+ icon="mdi-arrow-left-right"
28
+ size="x-small"
29
+ variant="text"
30
+ @click="emit('reset')"
31
+ v-tooltip="'Reset range'"
32
+ />
33
+ </v-col>
34
+ <v-col cols="5" class="ps-1">
35
+ <v-text-field
36
+ data-testid="attributeMaxInput"
37
+ :model-value="maximum"
38
+ @update:model-value="(value) => (maximum = Number(value))"
39
+ label="Max"
40
+ type="number"
41
+ :min="minimum"
42
+ density="compact"
43
+ hide-details
44
+ variant="outlined"
45
+ />
46
+ </v-col>
47
+ </v-row>
48
+ </template>
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import ViewerOptionsAttributeColorBar from "@ogw_front/components/Viewer/Options/AttributeColorBar.vue";
3
+ import { getAttributeRange } from "@ogw_front/utils/attributes";
3
4
  import { useBackStore } from "@ogw_front/stores/back";
4
5
 
5
6
  const backStore = useBackStore();
@@ -17,6 +18,9 @@ const { id, componentIds, schema } = defineProps({
17
18
 
18
19
  const attributes = ref([]);
19
20
 
21
+ const currentAttribute = computed(() =>
22
+ attributes.value.find((attr) => attr.attribute_name === attributeName.value),
23
+ );
20
24
  const rangeMin = computed({
21
25
  get: () => (attributeRange.value ? attributeRange.value[0] : undefined),
22
26
  set: (val) => {
@@ -40,10 +44,6 @@ const rangeMax = computed({
40
44
  },
41
45
  });
42
46
 
43
- const currentAttribute = computed(() =>
44
- attributes.value.find((attr) => attr.attribute_name === attributeName.value),
45
- );
46
-
47
47
  const componentItems = computed(() => {
48
48
  if (!currentAttribute.value) {
49
49
  return [];
@@ -57,10 +57,8 @@ const componentItems = computed(() => {
57
57
  function resetRange() {
58
58
  if (currentAttribute.value) {
59
59
  const comp = attributeItem.value ?? 0;
60
- attributeRange.value = [
61
- currentAttribute.value.min_values[comp],
62
- currentAttribute.value.max_values[comp],
63
- ];
60
+ const { min, max } = getAttributeRange(currentAttribute.value, comp);
61
+ attributeRange.value = [min, max];
64
62
  }
65
63
  }
66
64
 
@@ -168,6 +168,7 @@ watch(filteredPresets, drawAllCanvases);
168
168
  </v-list-item>
169
169
  </template>
170
170
  </v-list>
171
+ <slot></slot>
171
172
  </GlassCard>
172
173
  </template>
173
174
 
@@ -1,9 +1,9 @@
1
1
  <script setup>
2
+ import AttributeRangeSelector from "@ogw_front/components/Viewer/Options/AttributeRangeSelector.vue";
2
3
  import ColorMapList from "@ogw_front/components/Viewer/Options/ColorMapList.vue";
3
- import { getPresetsWithCurrentAtTop } from "@ogw_front/utils/colormap";
4
4
 
5
- import { useDataStyleStore } from "@ogw_front/stores/data_style";
6
- import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
5
+ import { getPresetsWithCurrentAtTop } from "@ogw_front/utils/colormap";
6
+ import { useGlobalAttributeStyle } from "@ogw_front/composables/global_attribute_style";
7
7
 
8
8
  const { dataId, x, y } = defineProps({
9
9
  dataId: { required: false, type: String, default: undefined },
@@ -13,94 +13,28 @@ const { dataId, x, y } = defineProps({
13
13
 
14
14
  const show = defineModel("show", { type: Boolean, default: false });
15
15
 
16
- const dataStyleStore = useDataStyleStore();
17
- const hybridViewerStore = useHybridViewerStore();
18
-
19
- const componentNames = [
20
- { getterKey: "meshPoints", setterKey: "MeshPoints", key: "points" },
21
- { getterKey: "meshEdges", setterKey: "MeshEdges", key: "edges" },
22
- { getterKey: "meshPolygons", setterKey: "MeshPolygons", key: "polygons" },
23
- { getterKey: "meshCells", setterKey: "MeshCells", key: "cells" },
24
- { getterKey: "meshPolyhedra", setterKey: "MeshPolyhedra", key: "polyhedra" },
25
- ];
26
-
27
- const current = computed(() => {
28
- const targetId = dataId;
29
- if (!targetId) {
30
- return "batlow";
31
- }
32
-
33
- const style = dataStyleStore.getStyle(targetId);
34
- if (!style) {
35
- return "batlow";
36
- }
37
-
38
- for (const { key, getterKey } of componentNames) {
39
- if (!style[key] || !style[key].coloring) {
40
- continue;
41
- }
42
-
43
- const activeColoring = style[key].coloring.active;
44
- if (!["vertex", "edge", "polygon", "cell", "polyhedron"].includes(activeColoring)) {
45
- continue;
46
- }
47
-
48
- const attributeType = `${activeColoring.charAt(0).toUpperCase()}${activeColoring.slice(1)}Attribute`;
49
- const getterName = `${getterKey}${attributeType}ColorMap`;
50
- const getter = dataStyleStore[getterName];
51
-
52
- if (!getter) {
53
- continue;
54
- }
16
+ const dataIdRef = computed(() => dataId);
17
+ const { currentColormap, currentRange, applyGlobalColormap, resetGlobalRange } =
18
+ useGlobalAttributeStyle(dataIdRef);
55
19
 
56
- const colorMap = getter(targetId);
57
- if (colorMap) {
58
- return colorMap;
59
- }
60
- }
20
+ const minimum = computed({
21
+ get: () => currentRange.value[0],
22
+ set: (val) => {
23
+ currentRange.value = [val, currentRange.value[1]];
24
+ },
25
+ });
61
26
 
62
- return "batlow";
27
+ const maximum = computed({
28
+ get: () => currentRange.value[1],
29
+ set: (val) => {
30
+ currentRange.value = [currentRange.value[0], val];
31
+ },
63
32
  });
64
33
 
65
- const quickColormapPresets = computed(() => getPresetsWithCurrentAtTop(current.value));
34
+ const quickColormapPresets = computed(() => getPresetsWithCurrentAtTop(currentColormap.value));
66
35
 
67
36
  async function onQuickColormapSelect(preset) {
68
- show.value = false;
69
- const newMap = preset.Name;
70
- const targetId = dataId;
71
- if (!targetId) {
72
- return;
73
- }
74
-
75
- const style = dataStyleStore.getStyle(targetId);
76
- if (!style) {
77
- return;
78
- }
79
-
80
- const promises = [];
81
-
82
- for (const { key, setterKey } of componentNames) {
83
- if (!style[key] || !style[key].coloring) {
84
- continue;
85
- }
86
-
87
- const activeColoring = style[key].coloring.active;
88
- if (!["vertex", "edge", "polygon", "cell", "polyhedron"].includes(activeColoring)) {
89
- continue;
90
- }
91
-
92
- const attributeType = `${activeColoring.charAt(0).toUpperCase() + activeColoring.slice(1)}Attribute`;
93
- const setterName = `set${setterKey}${attributeType}ColorMap`;
94
- const setter = dataStyleStore[setterName];
95
-
96
- if (setter) {
97
- promises.push(setter(targetId, newMap));
98
- }
99
- }
100
-
101
- await Promise.all(promises);
102
-
103
- hybridViewerStore.remoteRender();
37
+ await applyGlobalColormap(preset.Name);
104
38
  }
105
39
  </script>
106
40
 
@@ -114,8 +48,15 @@ async function onQuickColormapSelect(preset) {
114
48
  >
115
49
  <ColorMapList
116
50
  :presets="quickColormapPresets"
117
- :selected-preset-name="current"
51
+ :selected-preset-name="currentColormap"
118
52
  @select="onQuickColormapSelect"
119
- />
53
+ >
54
+ <v-divider class="my-2"></v-divider>
55
+ <AttributeRangeSelector
56
+ v-model:minimum="minimum"
57
+ v-model:maximum="maximum"
58
+ @reset="resetGlobalRange"
59
+ />
60
+ </ColorMapList>
120
61
  </v-menu>
121
62
  </template>
@@ -0,0 +1,181 @@
1
+ import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
2
+ import { computed } from "vue";
3
+
4
+ import { getAttributeRange } from "@ogw_front/utils/attributes";
5
+ import { useBackStore } from "@ogw_front/stores/back";
6
+ import { useDataStyleStore } from "@ogw_front/stores/data_style";
7
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
8
+
9
+ export function useGlobalAttributeStyle(dataIdRef) {
10
+ const dataStyleStore = useDataStyleStore();
11
+ const hybridViewerStore = useHybridViewerStore();
12
+ const backStore = useBackStore();
13
+
14
+ const componentNames = [
15
+ { getterKey: "meshPoints", setterKey: "MeshPoints", key: "points" },
16
+ { getterKey: "meshEdges", setterKey: "MeshEdges", key: "edges" },
17
+ { getterKey: "meshPolygons", setterKey: "MeshPolygons", key: "polygons" },
18
+ { getterKey: "meshCells", setterKey: "MeshCells", key: "cells" },
19
+ { getterKey: "meshPolyhedra", setterKey: "MeshPolyhedra", key: "polyhedra" },
20
+ ];
21
+
22
+ function getActiveComponents(targetId) {
23
+ const style = dataStyleStore.getStyle(targetId);
24
+ const activeComponents = [];
25
+ if (!style) {
26
+ return activeComponents;
27
+ }
28
+ for (const { key, getterKey, setterKey } of componentNames) {
29
+ if (!style[key] || !style[key].coloring) {
30
+ continue;
31
+ }
32
+
33
+ const activeColoring = style[key].coloring.active;
34
+ if (!["vertex", "edge", "polygon", "cell", "polyhedron"].includes(activeColoring)) {
35
+ continue;
36
+ }
37
+
38
+ const attributeType = `${activeColoring.charAt(0).toUpperCase()}${activeColoring.slice(1)}Attribute`;
39
+ activeComponents.push({ activeColoring, attributeType, getterKey, setterKey });
40
+ }
41
+ return activeComponents;
42
+ }
43
+
44
+ const currentColormap = computed(() => {
45
+ const targetId = dataIdRef.value;
46
+ if (!targetId) {
47
+ return "batlow";
48
+ }
49
+
50
+ for (const comp of getActiveComponents(targetId)) {
51
+ const getterName = `${comp.getterKey}${comp.attributeType}ColorMap`;
52
+ const getter = dataStyleStore[getterName];
53
+ if (getter) {
54
+ const colorMap = getter(targetId);
55
+ if (colorMap) {
56
+ return colorMap;
57
+ }
58
+ }
59
+ }
60
+
61
+ return "batlow";
62
+ });
63
+
64
+ const currentRange = computed({
65
+ get() {
66
+ const targetId = dataIdRef.value;
67
+ if (!targetId) {
68
+ return [0, 1];
69
+ }
70
+
71
+ for (const comp of getActiveComponents(targetId)) {
72
+ const getterName = `${comp.getterKey}${comp.attributeType}Range`;
73
+ const getter = dataStyleStore[getterName];
74
+ if (getter) {
75
+ const range = getter(targetId);
76
+ if (range && range.length === 2) {
77
+ return range;
78
+ }
79
+ }
80
+ }
81
+ return [0, 1];
82
+ },
83
+ set(newValue) {
84
+ const targetId = dataIdRef.value;
85
+ if (!targetId) {
86
+ return;
87
+ }
88
+
89
+ let updated = false;
90
+ for (const comp of getActiveComponents(targetId)) {
91
+ const setterName = `set${comp.setterKey}${comp.attributeType}Range`;
92
+ const setter = dataStyleStore[setterName];
93
+ if (setter) {
94
+ setter(targetId, newValue[0], newValue[1]);
95
+ updated = true;
96
+ }
97
+ }
98
+ if (updated) {
99
+ hybridViewerStore.remoteRender();
100
+ }
101
+ },
102
+ });
103
+
104
+ async function applyGlobalColormap(newMap) {
105
+ const targetId = dataIdRef.value;
106
+ if (!targetId) {
107
+ return;
108
+ }
109
+
110
+ const promises = [];
111
+
112
+ for (const comp of getActiveComponents(targetId)) {
113
+ const setterName = `set${comp.setterKey}${comp.attributeType}ColorMap`;
114
+ const setter = dataStyleStore[setterName];
115
+
116
+ if (setter) {
117
+ promises.push(setter(targetId, newMap));
118
+ }
119
+ }
120
+
121
+ await Promise.all(promises);
122
+ hybridViewerStore.remoteRender();
123
+ }
124
+
125
+ function resetGlobalRange() {
126
+ const targetId = dataIdRef.value;
127
+ if (!targetId) {
128
+ return;
129
+ }
130
+
131
+ for (const comp of getActiveComponents(targetId)) {
132
+ const { activeColoring, attributeType, getterKey, setterKey } = comp;
133
+
134
+ const nameGetter = dataStyleStore[`${getterKey}${attributeType}Name`];
135
+ const itemGetter = dataStyleStore[`${getterKey}${attributeType}Item`];
136
+ if (!nameGetter || !itemGetter) {
137
+ continue;
138
+ }
139
+
140
+ const attrName = nameGetter(targetId);
141
+ const attrItem = itemGetter(targetId) ?? 0;
142
+
143
+ if (!attrName) {
144
+ continue;
145
+ }
146
+
147
+ const schemaName = `${activeColoring}_attribute_names`;
148
+ const schema = back_schemas.opengeodeweb_back[schemaName];
149
+ if (!schema) {
150
+ continue;
151
+ }
152
+
153
+ backStore.request(
154
+ { schema, params: { id: targetId } },
155
+ {
156
+ response_function: (response) => {
157
+ const attributes = response.attributes || [];
158
+ const currentAttribute = attributes.find((attr) => attr.attribute_name === attrName);
159
+ if (currentAttribute) {
160
+ const { min, max } = getAttributeRange(currentAttribute, attrItem);
161
+
162
+ const setterName = `set${setterKey}${attributeType}Range`;
163
+ const setter = dataStyleStore[setterName];
164
+ if (setter) {
165
+ setter(targetId, min, max);
166
+ hybridViewerStore.remoteRender();
167
+ }
168
+ }
169
+ },
170
+ },
171
+ );
172
+ }
173
+ }
174
+
175
+ return {
176
+ currentColormap,
177
+ currentRange,
178
+ applyGlobalColormap,
179
+ resetGlobalRange,
180
+ };
181
+ }
@@ -1,11 +1,9 @@
1
1
  // Third party imports
2
- import vtkWSLinkClient, { newInstance } from "@kitware/vtk.js/IO/Core/WSLinkClient";
3
2
  import _ from "lodash";
4
3
  // oxlint-disable-next-line no-unassigned-import
5
4
  import "@kitware/vtk.js/Rendering/OpenGL/Profiles/Geometry";
6
- import SmartConnect from "wslink/src/SmartConnect";
7
5
  import { connectImageStream } from "@kitware/vtk.js/Rendering/Misc/RemoteView";
8
-
6
+ import { initWebSocketClient } from "@ogw_internal/utils/ws_client";
9
7
  import opengeodeweb_front_schemas from "@geode/opengeodeweb-front/opengeodeweb_front_schemas.json" with { type: "json" };
10
8
  import opengeodeweb_viewer_schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json" with { type: "json" };
11
9
 
@@ -37,7 +35,6 @@ export const useViewerStore = defineStore(
37
35
  const request_counter = ref(0);
38
36
  const status = ref(Status.NOT_CONNECTED);
39
37
  const version = ref("0.0.0");
40
- const busy = ref(0);
41
38
 
42
39
  const protocol = computed(() => getWebsocketApiProtocol());
43
40
 
@@ -77,29 +74,10 @@ export const useViewerStore = defineStore(
77
74
  try {
78
75
  console.log("VIEWER LOCK GRANTED !", lock);
79
76
  status.value = Status.CONNECTING;
80
- vtkWSLinkClient.setSmartConnectClass(SmartConnect);
81
-
82
- if (_.isEmpty(client.value)) {
83
- client.value = newInstance();
84
- }
85
-
86
- client.value.onBusyChange((count) => {
87
- busy.value = count;
88
- });
89
- client.value.onConnectionError((httpReq) => {
90
- const message = httpReq?.response?.error || `Connection error`;
91
- console.error(message);
92
- });
93
- client.value.onConnectionClose((httpReq) => {
94
- const message = httpReq?.response?.error || `Connection close`;
95
- status.value = Status.NOT_CONNECTED;
96
- console.error(message);
97
- });
98
-
99
- client.value.beginBusy();
100
- await client.value.connect({
101
- application: "Viewer",
102
- sessionURL: base_url.value,
77
+ client.value = await initWebSocketClient(base_url.value, client.value, {
78
+ onConnectionClose: () => {
79
+ status.value = Status.NOT_CONNECTED;
80
+ },
103
81
  });
104
82
  connectImageStream(client.value.getConnection().getSession());
105
83
  client.value.endBusy();
@@ -0,0 +1,23 @@
1
+ export function getAttributeRange(currentAttribute, compIndex = 0) {
2
+ if (!currentAttribute) {
3
+ return { min: 0, max: 1 };
4
+ }
5
+
6
+ const { min_values, max_values, min_value, max_value } = currentAttribute;
7
+ let min = 0;
8
+ let max = 1;
9
+
10
+ if (min_values && min_values[compIndex] !== undefined) {
11
+ min = min_values[compIndex];
12
+ } else if (compIndex === 0 && min_value !== undefined) {
13
+ min = min_value;
14
+ }
15
+
16
+ if (max_values && max_values[compIndex] !== undefined) {
17
+ max = max_values[compIndex];
18
+ } else if (compIndex === 0 && max_value !== undefined) {
19
+ max = max_value;
20
+ }
21
+
22
+ return { min, max };
23
+ }
@@ -9,6 +9,10 @@ export function useMeshPointsCommonStyle() {
9
9
  });
10
10
  }
11
11
 
12
+ function mutateMeshPointsVisibility(response) {
13
+ return mutateMeshPointsStyle(response.id, { visibility: response.visibility });
14
+ }
15
+
12
16
  function meshPointsStyle(id) {
13
17
  return dataStyleState.getStyle(id).points;
14
18
  }
@@ -26,7 +30,8 @@ export function useMeshPointsCommonStyle() {
26
30
  return {
27
31
  meshPointsStyle,
28
32
  meshPointsColoring,
29
- mutateMeshPointsStyle,
30
33
  mutateMeshPointsColoring,
34
+ mutateMeshPointsStyle,
35
+ mutateMeshPointsVisibility,
31
36
  };
32
37
  }
@@ -23,7 +23,9 @@ export function useMeshPointsVisibilityStyle() {
23
23
  params,
24
24
  },
25
25
  {
26
- response_function: () => meshPointsCommonStyle.mutateMeshPointsStyle(id, { visibility }),
26
+ response_function(response) {
27
+ return meshPointsCommonStyle.mutateMeshPointsVisibility(response);
28
+ },
27
29
  },
28
30
  );
29
31
  }
@@ -15,8 +15,8 @@ export function api_fetch(
15
15
  return fetchSchema(
16
16
  {
17
17
  schema,
18
- baseURL: microservice.base_url,
19
18
  params,
19
+ baseURL: microservice.base_url,
20
20
  headers,
21
21
  max_retry: schema.max_retry,
22
22
  timeout,
@@ -1,12 +1,6 @@
1
- // Third party imports
2
- import pTimeout from "p-timeout";
3
-
4
- // Local imports
5
1
  import { endRequestLog, startRequestLog } from "@ogw_front/utils/log";
2
+ import { callSchema } from "@ogw_shared/utils/call_schema";
6
3
  import { useFeedbackStore } from "@ogw_front/stores/feedback";
7
- import { validate_schema } from "@ogw_shared/utils/validate_schema";
8
-
9
- const ERROR_400 = 400;
10
4
 
11
5
  export function viewer_call(
12
6
  microservice,
@@ -14,52 +8,42 @@ export function viewer_call(
14
8
  { request_error_function, response_function, response_error_function } = {},
15
9
  ) {
16
10
  const feedbackStore = useFeedbackStore();
17
-
18
- const { valid, error: schema_error } = validate_schema(schema, params);
19
-
20
- if (!valid) {
21
- if (process.env.NODE_ENV !== "production") {
22
- console.log("Bad request", schema_error, schema, params);
23
- }
24
- feedbackStore.add_error(ERROR_400, schema.$id, "Bad request", schema_error);
25
- throw new Error(`${schema.$id}: ${schema_error}`);
26
- }
27
-
28
11
  const { client } = microservice;
29
12
 
30
- async function performCall() {
31
- if (!client.getConnection) {
32
- return;
33
- }
34
- microservice.start_request();
35
- const requestStart = startRequestLog(microservice, schema);
36
- try {
37
- const value = await client.getConnection().getSession().call(schema.$id, [params]);
38
- endRequestLog(microservice, schema, requestStart);
39
- if (response_function) {
40
- await response_function(value);
41
- }
42
- return value;
43
- } catch (error) {
44
- feedbackStore.add_error(error.code, schema.$id, error.message, error.message);
45
- if (request_error_function) {
46
- request_error_function(error);
47
- }
48
- if (response_error_function) {
49
- response_error_function(error);
50
- }
51
- throw error;
52
- } finally {
53
- microservice.stop_request();
54
- }
55
- }
56
-
57
- if (timeout > 0) {
58
- return pTimeout(performCall(), {
59
- milliseconds: timeout,
60
- message: `${schema.$id}: Timed out after ${timeout}ms`,
61
- });
62
- }
63
-
64
- return performCall();
13
+ const requestStartingTime = startRequestLog(microservice, schema);
14
+ return callSchema(
15
+ {
16
+ schema,
17
+ params,
18
+ client,
19
+ timeout,
20
+ },
21
+ {
22
+ request_error_function(error) {
23
+ microservice.stop_request();
24
+ feedbackStore.add_error(error.code, schema.$id, error.message, error.message);
25
+ if (request_error_function) {
26
+ request_error_function(error);
27
+ }
28
+ },
29
+ response_function(data) {
30
+ endRequestLog(microservice, schema, requestStartingTime);
31
+ microservice.stop_request();
32
+ if (response_function) {
33
+ response_function(data);
34
+ }
35
+ },
36
+ response_error_function(response) {
37
+ microservice.stop_request();
38
+ feedbackStore.add_error(error.code, schema.$id, error.message, error.message);
39
+ if (response_error_function) {
40
+ response_error_function(response);
41
+ }
42
+ },
43
+ validation_error_function({ code, name, error }) {
44
+ microservice.stop_request();
45
+ feedbackStore.add_error(code, schema.$id, name, error);
46
+ },
47
+ },
48
+ );
65
49
  }
@@ -0,0 +1,29 @@
1
+ // Third party imports
2
+ import vtkWSLinkClient, { newInstance } from "@kitware/vtk.js/IO/Core/WSLinkClient";
3
+ import SmartConnect from "wslink/src/SmartConnect";
4
+ import _ from "lodash";
5
+
6
+ async function initWebSocketClient(baseUrl, initialClient = {}, { onConnectionClose } = {}) {
7
+ vtkWSLinkClient.setSmartConnectClass(SmartConnect);
8
+ const client = _.isEmpty(initialClient) ? newInstance() : initialClient;
9
+
10
+ client.onConnectionError((httpReq) => {
11
+ const message = httpReq?.response?.error || `Connection error`;
12
+ console.error(message);
13
+ });
14
+ client.onConnectionClose((httpReq) => {
15
+ const message = httpReq?.response?.error || `Connection close`;
16
+ onConnectionClose();
17
+ console.error(message);
18
+ });
19
+
20
+ client.beginBusy();
21
+ await client.connect({
22
+ application: "Viewer",
23
+ sessionURL: baseUrl,
24
+ });
25
+
26
+ return client;
27
+ }
28
+
29
+ export { initWebSocketClient };
package/nuxt.config.js CHANGED
@@ -62,13 +62,36 @@ export default defineNuxtConfig({
62
62
  vite: {
63
63
  optimizeDeps: {
64
64
  include: [
65
+ "@kitware/vtk.js",
66
+ "@kitware/vtk.js/Common/Core/Math",
67
+ "@kitware/vtk.js/IO/Core/WSLinkClient",
68
+ "@kitware/vtk.js/IO/XML/XMLPolyDataReader",
69
+ "@kitware/vtk.js/Rendering/Core/Actor",
70
+ "@kitware/vtk.js/Rendering/Core/AnnotatedCubeActor",
71
+ "@kitware/vtk.js/Rendering/Core/ColorTransferFunction",
72
+ "@kitware/vtk.js/Rendering/Core/Mapper",
73
+ "@kitware/vtk.js/Rendering/Misc/GenericRenderWindow",
74
+ "@kitware/vtk.js/Rendering/Misc/RemoteView",
75
+ "@kitware/vtk.js/Rendering/OpenGL/Profiles/Geometry",
76
+ "@kitware/vtk.js/Widgets/Core/WidgetManager",
77
+ "@kitware/vtk.js/Widgets/Widgets3D/ImplicitPlaneWidget",
78
+ "@vue/devtools-core",
79
+ "@vue/devtools-kit",
65
80
  "ajv",
66
- "fast-deep-equal",
81
+ "broadcast-channel",
82
+ "dexie",
67
83
  "globalthis",
68
84
  "h3",
69
85
  "js-file-download",
70
86
  "lodash",
87
+ "lodash/merge",
88
+ "p-timeout",
71
89
  "seedrandom",
90
+ "spark-md5",
91
+ "uuid",
92
+ "wslink",
93
+ "wslink/src/SmartConnect",
94
+ "xmlbuilder2",
72
95
  ],
73
96
  },
74
97
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.32.2",
3
+ "version": "10.33.0-rc.1",
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": {
@@ -39,8 +39,8 @@
39
39
  "build": ""
40
40
  },
41
41
  "dependencies": {
42
- "@geode/opengeodeweb-back": "latest",
43
- "@geode/opengeodeweb-viewer": "latest",
42
+ "@geode/opengeodeweb-back": "next",
43
+ "@geode/opengeodeweb-viewer": "next",
44
44
  "@google-cloud/run": "3.2.0",
45
45
  "@kitware/vtk.js": "33.3.0",
46
46
  "@mdi/font": "7.4.47",
@@ -1,3 +1,5 @@
1
+ import { createServerWsRpcClient } from "./ws_client.js";
2
+
1
3
  const storage = new Map();
2
4
 
3
5
  function getAppBaseUrl() {
@@ -25,13 +27,36 @@ function setIsAppReady(isAppReady) {
25
27
  return storage.set("IS_APP_READY", isAppReady);
26
28
  }
27
29
 
30
+ async function getViewerWebSocketClient() {
31
+ const viewerClient = storage.get("VIEWER_CLIENT") ?? undefined;
32
+ if (viewerClient?.isOpen()) {
33
+ return viewerClient;
34
+ }
35
+ const viewerBaseUrl = await getViewerBaseUrl();
36
+ return setViewerWebSocketClient(viewerBaseUrl);
37
+ }
38
+
39
+ async function setViewerWebSocketClient(baseUrl) {
40
+ const client = createServerWsRpcClient(baseUrl);
41
+ client.onConnectionClose(() => {
42
+ if (viewerClient === client) {
43
+ viewerClient = undefined;
44
+ }
45
+ });
46
+ await client.ready;
47
+ storage.set("VIEWER_CLIENT", client);
48
+ return client;
49
+ }
50
+
28
51
  export {
29
52
  getAppBaseUrl,
30
53
  getBackBaseUrl,
31
54
  getIsAppReady,
32
55
  getViewerBaseUrl,
56
+ getViewerWebSocketClient,
33
57
  setAppBaseUrl,
34
58
  setBackBaseUrl,
35
59
  setIsAppReady,
36
60
  setViewerBaseUrl,
61
+ setViewerWebSocketClient,
37
62
  };
@@ -0,0 +1,112 @@
1
+ // Third party imports
2
+ import { WebSocket } from "ws";
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ // Local imports
6
+
7
+ const HELLO_ID = "system:hello";
8
+ const HELLO_SECRET = "wslink-secret";
9
+
10
+ //oxlint-disable-next-line max-lines-per-function
11
+ function createServerWsRpcClient(baseUrl) {
12
+ const socket = new WebSocket(baseUrl);
13
+ const pending = new Map();
14
+ let onCloseCallback = undefined;
15
+ let onErrorCallback = undefined;
16
+
17
+ //oxlint-disable-next-line promise/avoid-new
18
+ const ready = new Promise((resolve, reject) => {
19
+ socket.on("open", () => {
20
+ socket.send(
21
+ JSON.stringify({
22
+ id: HELLO_ID,
23
+ method: "wslink.hello",
24
+ args: [{ secret: HELLO_SECRET }],
25
+ }),
26
+ );
27
+ });
28
+
29
+ socket.on("message", (raw) => {
30
+ console.log("RAW WS MESSAGE:", raw.toString());
31
+ let message = undefined;
32
+ try {
33
+ message = JSON.parse(raw.toString());
34
+ } catch {
35
+ return;
36
+ }
37
+
38
+ if (message.id === HELLO_ID) {
39
+ resolve();
40
+ return;
41
+ }
42
+
43
+ if (typeof message.id === "string" && message.id.startsWith("publish:")) {
44
+ return;
45
+ }
46
+
47
+ const entry = pending.get(message.id);
48
+ if (!entry) {
49
+ return;
50
+ }
51
+ pending.delete(message.id);
52
+ if (message.error) {
53
+ entry.reject(new Error(message.error.message || "wslink RPC error"));
54
+ } else {
55
+ entry.resolve(message.result);
56
+ }
57
+ });
58
+
59
+ socket.on("error", (error) => {
60
+ onErrorCallback?.(error);
61
+ reject(error);
62
+ });
63
+
64
+ socket.on("close", () => {
65
+ onCloseCallback?.();
66
+ for (const { reject: rejectPending } of pending.values()) {
67
+ rejectPending(new Error("WebSocket closed"));
68
+ }
69
+ pending.clear();
70
+ });
71
+ });
72
+
73
+ async function call(rpc, params = {}) {
74
+ await ready;
75
+ const id = uuidv4();
76
+ //oxlint-disable-next-line promise/avoid-new
77
+ return new Promise((resolve, reject) => {
78
+ pending.set(id, { resolve, reject });
79
+ socket.send(
80
+ JSON.stringify({
81
+ wslink: "1.0",
82
+ id,
83
+ method: rpc,
84
+ args: [params],
85
+ kwargs: { stream: true },
86
+ }),
87
+ );
88
+ });
89
+ }
90
+
91
+ function close() {
92
+ socket.close();
93
+ }
94
+
95
+ function isOpen() {
96
+ return socket.readyState === WebSocket.OPEN;
97
+ }
98
+
99
+ //oxlint-disable-next-line promise/prefer-await-to-callbacks
100
+ function onConnectionClose(callback) {
101
+ onCloseCallback = callback;
102
+ }
103
+
104
+ //oxlint-disable-next-line promise/prefer-await-to-callbacks
105
+ function onConnectionError(callback) {
106
+ onErrorCallback = callback;
107
+ }
108
+
109
+ return { call, close, isOpen, onConnectionClose, onConnectionError, ready };
110
+ }
111
+
112
+ export { createServerWsRpcClient };
@@ -0,0 +1,46 @@
1
+ // Third party imports
2
+ import _ from "lodash";
3
+ import pTimeout from "p-timeout";
4
+
5
+ // Local imports
6
+
7
+ function callClient({ rpc, params = {}, client }) {
8
+ if (globalThis.window !== undefined) {
9
+ return client.getConnection().getSession().call(rpc, [params]);
10
+ }
11
+ return client.call(rpc, params);
12
+ }
13
+
14
+ function callRaw(
15
+ { rpc, params = {}, client, timeout },
16
+ { request_error_function, response_function, response_error_function } = {},
17
+ ) {
18
+ async function performCall() {
19
+ try {
20
+ const response = await callClient({ rpc, params, client });
21
+ if (response_function) {
22
+ await response_function(response);
23
+ }
24
+ return response;
25
+ } catch (error) {
26
+ if (request_error_function) {
27
+ request_error_function(error);
28
+ }
29
+ if (response_error_function) {
30
+ response_error_function(error);
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ if (timeout > 0) {
37
+ return pTimeout(performCall(), {
38
+ milliseconds: timeout,
39
+ message: `${rpc}: Timed out after ${timeout}ms`,
40
+ });
41
+ }
42
+
43
+ return performCall();
44
+ }
45
+
46
+ export { callRaw };
@@ -0,0 +1,45 @@
1
+ // Third party imports
2
+
3
+ // Local imports
4
+ import { callRaw } from "./call_raw.js";
5
+ import { validateSchema } from "./validate_schema.js";
6
+
7
+ const ERROR_400 = 400;
8
+
9
+ function callSchema(
10
+ { schema, params = {}, client, timeout },
11
+ {
12
+ request_error_function,
13
+ response_function,
14
+ response_error_function,
15
+ validation_error_function,
16
+ } = {},
17
+ ) {
18
+ const { valid, error: schema_error } = validateSchema(schema, params);
19
+
20
+ if (!valid) {
21
+ if (process.env.NODE_ENV !== "production") {
22
+ console.log("Bad request", schema_error, schema, params);
23
+ }
24
+ if (validation_error_function) {
25
+ validation_error_function({ code: ERROR_400, name: "Bad request", error: schema_error });
26
+ }
27
+ throw new Error(`${schema.$id}: ${schema_error}`);
28
+ }
29
+
30
+ return callRaw(
31
+ {
32
+ rpc: schema.$id,
33
+ params,
34
+ client,
35
+ timeout,
36
+ },
37
+ {
38
+ request_error_function,
39
+ response_function,
40
+ response_error_function,
41
+ },
42
+ );
43
+ }
44
+
45
+ export { callSchema };
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Local imports
4
4
  import { fetchRaw } from "./fetch_raw.js";
5
- import { validate_schema } from "./validate_schema.js";
5
+ import { validateSchema } from "./validate_schema.js";
6
6
 
7
7
  const ERROR_400 = 400;
8
8
 
@@ -15,8 +15,7 @@ function fetchSchema(
15
15
  validation_error_function,
16
16
  } = {},
17
17
  ) {
18
- console.log("fetchSchema", { schema, baseURL, params, headers, timeout });
19
- const { valid, error: schema_error } = validate_schema(schema, params);
18
+ const { valid, error: schema_error } = validateSchema(schema, params);
20
19
 
21
20
  if (!valid) {
22
21
  if (process.env.NODE_ENV !== "production") {
@@ -39,7 +38,11 @@ function fetchSchema(
39
38
  timeout,
40
39
  expectEvent,
41
40
  },
42
- { request_error_function, response_function, response_error_function },
41
+ {
42
+ request_error_function,
43
+ response_function,
44
+ response_error_function,
45
+ },
43
46
  );
44
47
  }
45
48
 
@@ -0,0 +1,16 @@
1
+ const TRUTHY_VALUES = new Set([true, 1, "1", "true", "yes"]);
2
+ const FALSY_VALUES = new Set([false, 0, "0", "false", "no"]);
3
+
4
+ function parseBoolean(value) {
5
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
6
+
7
+ if (TRUTHY_VALUES.has(normalized)) {
8
+ return true;
9
+ }
10
+ if (FALSY_VALUES.has(normalized)) {
11
+ return false;
12
+ }
13
+ throw new Error(`Cannot parse boolean from: ${value}`);
14
+ }
15
+
16
+ export { parseBoolean };
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
 
3
- function validate_schema(schema, body) {
3
+ function validateSchema(schema, body) {
4
4
  const ajv = new Ajv();
5
5
  const list_keywords = ["methods", "route", "max_retry", "rpc"];
6
6
  for (const keyword of list_keywords) {
@@ -10,4 +10,4 @@ function validate_schema(schema, body) {
10
10
  return { valid, error: ajv.errorsText() };
11
11
  }
12
12
 
13
- export { validate_schema };
13
+ export { validateSchema };
@@ -2,7 +2,7 @@
2
2
  import { describe, expect, test } from "vitest";
3
3
 
4
4
  // Local imports
5
- import { validate_schema } from "@ogw_shared/utils/validate_schema";
5
+ import { validateSchema } from "@ogw_shared/utils/validate_schema";
6
6
 
7
7
  // CONSTANTS
8
8
  const MIN_0 = 0;
@@ -24,14 +24,14 @@ describe("validate schema", () => {
24
24
 
25
25
  test("ajv wrong params", () => {
26
26
  const params = {};
27
- const { valid, error } = validate_schema(schema, params);
27
+ const { valid, error } = validateSchema(schema, params);
28
28
  expect(valid).toBe(false);
29
29
  expect(error).toBe("data must have required property 'var_1'");
30
30
  });
31
31
 
32
32
  test("good params", () => {
33
33
  const params = { var_1: "test", var_2: VAL_5 };
34
- const { valid, error } = validate_schema(schema, params);
34
+ const { valid, error } = validateSchema(schema, params);
35
35
  expect(valid).toBe(true);
36
36
  expect(error).toBe("No errors");
37
37
  });