@geode/opengeodeweb-front 10.40.0 → 10.41.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.
@@ -4,9 +4,13 @@ import {
4
4
  type RangesPerData,
5
5
  useBatchGroup,
6
6
  } from "@ogw_front/composables/batch_style";
7
+ import {
8
+ type AttributeRangeInfo,
9
+ type AttributeSchemas,
10
+ fetchAttributeRange,
11
+ } from "@ogw_front/utils/attribute_range";
7
12
  import { getAttributeRange, intersectAttributes } from "@ogw_front/utils/attributes";
8
13
  import { DEFAULT_NO_DATA_COLOR } from "@ogw_front/utils/default_styles/constants";
9
- import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
10
14
  import ViewerOptionsAttributeColorBar from "@ogw_front/components/Viewer/Options/AttributeColorBar.vue";
11
15
  import ViewerOptionsColorPicker from "@ogw_front/components/Viewer/Options/ColorPicker.vue";
12
16
  import ViewerOptionsTimeStepSlider from "@ogw_front/components/Viewer/Options/TimeStepSlider.vue";
@@ -27,10 +31,10 @@ const attributeTimeStep = defineModel<number>("attributeTimeStep");
27
31
  interface Props {
28
32
  id: string;
29
33
  componentIds?: string[];
30
- schema: JsonRpcSchema;
34
+ schemas: AttributeSchemas;
31
35
  }
32
36
 
33
- const { id, componentIds = undefined, schema } = defineProps<Props>();
37
+ const { id, componentIds = undefined, schemas } = defineProps<Props>();
34
38
 
35
39
  interface Emits {
36
40
  "update:attributeColorMap": [colorMap: string];
@@ -43,13 +47,15 @@ const emit = defineEmits<Emits>();
43
47
  interface AttributeInfo {
44
48
  attribute_name: string;
45
49
  nb_items: number;
46
- no_data?: boolean;
47
50
  time_steps?: number[];
48
51
  [key: string]: unknown;
49
52
  }
50
53
 
51
54
  const attributes = ref<AttributeInfo[]>([]);
52
55
  let attributesPerTarget = new Map<string, AttributeInfo[]>();
56
+ const rangesPerTarget = ref(new Map<string, AttributeRangeInfo>());
57
+ let rangesName: string | undefined = undefined;
58
+ let rangesLoading: Promise<void> = Promise.resolve();
53
59
 
54
60
  const groupTargetIds = useBatchGroup(() => id);
55
61
 
@@ -57,6 +63,9 @@ const currentAttribute = computed<AttributeInfo | undefined>(() =>
57
63
  attributes.value.find((attr) => attr.attribute_name === attributeName.value),
58
64
  );
59
65
  const timeSteps = computed<number[]>(() => currentAttribute.value?.time_steps ?? []);
66
+ const noData = computed<boolean>(() =>
67
+ [...rangesPerTarget.value.values()].some((range) => range.no_data),
68
+ );
60
69
 
61
70
  let committedSeries: string | undefined = undefined;
62
71
 
@@ -72,7 +81,10 @@ function commitSeriesTimeStep(): void {
72
81
  committedSeries = name;
73
82
  const current = attributeTimeStep.value;
74
83
  const valid = current !== undefined && current >= 0 && current < timeSteps.value.length;
75
- emit("update:attributeTimeStep", valid ? current : 0);
84
+ // A valid step is already stored and applied with the series: emitting it again re-applies the attribute
85
+ if (!valid) {
86
+ emit("update:attributeTimeStep", 0);
87
+ }
76
88
  }
77
89
 
78
90
  const cssNoDataColor = computed<string>(() => {
@@ -118,68 +130,105 @@ const componentItems = computed<{ title: string; value: number }[]>(() => {
118
130
  }));
119
131
  });
120
132
 
121
- function resetRange(): void {
122
- if (currentAttribute.value) {
123
- const comp = attributeItem.value ?? 0;
124
- // GetAttributeRange's parameter type (AttributeRangeSource) isn't exported;
125
- // AttributeInfo's index signature covers its optional min/max fields at
126
- // Runtime (they come from the same backend attribute response shape).
127
- const { min, max } = getAttributeRange(
128
- currentAttribute.value as unknown as Parameters<typeof getAttributeRange>[0],
129
- comp,
130
- );
131
- attributeRange.value = [min, max];
133
+ function hasSelectedComponent(components: unknown): boolean {
134
+ return Array.isArray(components) && components.length > 0;
135
+ }
136
+
137
+ function requestParams(): { id: string; component_ids?: string[] } | undefined {
138
+ const schemaProperties = schemas.names.properties as Record<string, unknown> | undefined;
139
+ if (schemaProperties?.component_ids === undefined) {
140
+ return { id };
132
141
  }
142
+ return hasSelectedComponent(componentIds) ? { id, component_ids: componentIds } : undefined;
133
143
  }
134
144
 
135
- function hasSelectedComponent(components: unknown): boolean {
136
- return Array.isArray(components) && components.length > 0;
145
+ // Ranges are computed by the back for the selected attribute only, as scanning every step of every series is slow
146
+ async function requestRanges(name: string): Promise<void> {
147
+ const targets: [string, { id: string; component_ids?: string[] } | undefined][] =
148
+ groupTargetIds.value
149
+ ? [...attributesPerTarget]
150
+ .filter(([, targetAttributes]) =>
151
+ targetAttributes.some((attribute) => attribute.attribute_name === name),
152
+ )
153
+ .map(([targetId]) => [targetId, { id: targetId }])
154
+ : [[id, requestParams()]];
155
+ const entries = await Promise.all(
156
+ targets.map(async ([targetId, params]): Promise<[string, AttributeRangeInfo | undefined]> => [
157
+ targetId,
158
+ params ? await fetchAttributeRange(schemas.range, params, name) : undefined,
159
+ ]),
160
+ );
161
+ if (rangesName !== name) {
162
+ return;
163
+ }
164
+ rangesPerTarget.value = new Map(
165
+ entries.filter((entry): entry is [string, AttributeRangeInfo] => entry[1] !== undefined),
166
+ );
167
+ }
168
+
169
+ async function loadRanges(name: string): Promise<void> {
170
+ if (name !== rangesName) {
171
+ rangesName = name;
172
+ rangesPerTarget.value = new Map();
173
+ rangesLoading = requestRanges(name);
174
+ }
175
+ await rangesLoading;
176
+ }
177
+
178
+ function applyRange(): void {
179
+ const { min, max } = getAttributeRange(rangesPerTarget.value.get(id), attributeItem.value ?? 0);
180
+ attributeRange.value = [min, max];
181
+ }
182
+
183
+ async function resetRange(): Promise<void> {
184
+ if (currentAttribute.value) {
185
+ await loadRanges(currentAttribute.value.attribute_name);
186
+ applyRange();
187
+ }
137
188
  }
138
189
 
139
190
  async function getGroupAttributes(targetIds: string[]): Promise<void> {
140
- const responses = await requestForTargets<{ attributes: AttributeInfo[] }>(schema, targetIds, {
141
- attributes: [],
142
- });
191
+ const responses = await requestForTargets<{ attributes: AttributeInfo[] }>(
192
+ schemas.names,
193
+ targetIds,
194
+ {
195
+ attributes: [],
196
+ },
197
+ );
143
198
  attributesPerTarget = new Map(
144
199
  responses.map(([targetId, response]) => [targetId, response.attributes]),
145
200
  );
146
201
  attributes.value = intersectAttributes([...attributesPerTarget.values()]);
147
202
  }
148
203
 
149
- function initGroupAttribute(name: string, item: number): void {
204
+ async function initGroupAttribute(name: string, item: number): Promise<void> {
205
+ await loadRanges(name);
206
+ if (attributeName.value !== name) {
207
+ return;
208
+ }
150
209
  const ranges = new Map<string, AttributeRange>();
151
- for (const [targetId, targetAttributes] of attributesPerTarget) {
152
- const attribute = targetAttributes.find((candidate) => candidate.attribute_name === name);
153
- if (attribute) {
154
- const { min, max } = getAttributeRange(
155
- attribute as unknown as Parameters<typeof getAttributeRange>[0],
156
- item,
157
- );
158
- ranges.set(targetId, [min, max]);
159
- }
210
+ for (const [targetId, range] of rangesPerTarget.value) {
211
+ const { min, max } = getAttributeRange(range, item);
212
+ ranges.set(targetId, [min, max]);
160
213
  }
161
214
  emit("update:attributeColorMap", attributeColorMap.value ?? "batlow");
162
215
  emit("ranges_per_data", ranges);
163
216
  }
164
217
 
165
218
  async function getAttributes(): Promise<void> {
219
+ rangesName = undefined;
220
+ rangesPerTarget.value = new Map();
166
221
  if (groupTargetIds.value) {
167
222
  await getGroupAttributes(groupTargetIds.value);
168
223
  return;
169
224
  }
170
- const schemaProperties = schema.properties as Record<string, unknown> | undefined;
171
- const requiresComponent = schemaProperties?.component_ids !== undefined;
172
- if (requiresComponent && !hasSelectedComponent(componentIds)) {
225
+ const params = requestParams();
226
+ if (!params) {
173
227
  return;
174
228
  }
175
229
 
176
- const params: { id: string; component_ids?: unknown } = { id };
177
- if (requiresComponent) {
178
- params.component_ids = componentIds;
179
- }
180
-
181
230
  await backStore.request(
182
- { schema, params },
231
+ { schema: schemas.names, params },
183
232
  {
184
233
  response_function: (response: unknown) => {
185
234
  attributes.value = (response as { attributes: AttributeInfo[] }).attributes;
@@ -193,19 +242,19 @@ onMounted(async () => {
193
242
  });
194
243
 
195
244
  watch(
196
- () => [id, componentIds, schema],
245
+ [(): string => id, (): string[] | undefined => componentIds, (): AttributeSchemas => schemas],
197
246
  async () => {
198
247
  await getAttributes();
199
248
  },
200
249
  );
201
250
 
202
- watch([attributeName, attributeItem], ([name, item]) => {
251
+ watch([attributeName, attributeItem], async ([name, item]) => {
203
252
  if (groupTargetIds.value && name !== undefined) {
204
- initGroupAttribute(name, item ?? 0);
253
+ await initGroupAttribute(name, item ?? 0);
205
254
  }
206
255
  });
207
256
 
208
- watch([attributeName, attributeItem, currentAttribute], () => {
257
+ watch([attributeName, attributeItem, currentAttribute], async () => {
209
258
  commitSeriesTimeStep();
210
259
  if (groupTargetIds.value) {
211
260
  return;
@@ -216,8 +265,13 @@ watch([attributeName, attributeItem, currentAttribute], () => {
216
265
  if (attributeNoDataColor.value === undefined) {
217
266
  attributeNoDataColor.value = DEFAULT_NO_DATA_COLOR;
218
267
  }
219
- if (attributeRange.value[0] === undefined) {
220
- resetRange();
268
+ const name = currentAttribute.value?.attribute_name;
269
+ if (name === undefined) {
270
+ return;
271
+ }
272
+ await loadRanges(name);
273
+ if (name === currentAttribute.value?.attribute_name && attributeRange.value[0] === undefined) {
274
+ applyRange();
221
275
  }
222
276
  });
223
277
  </script>
@@ -247,7 +301,7 @@ watch([attributeName, attributeItem, currentAttribute], () => {
247
301
  />
248
302
  <ViewerOptionsTimeStepSlider v-model="attributeTimeStep" :time-steps="timeSteps" />
249
303
  <div
250
- v-if="currentAttribute && currentAttribute.no_data"
304
+ v-if="currentAttribute && noData"
251
305
  class="text-caption text-high-emphasis mt-1 d-flex align-center ga-1"
252
306
  data-testid="noDataInfo"
253
307
  >
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
2
+ import type { AttributeSchemas } from "@ogw_front/utils/attribute_range";
3
3
  import type { RGBAColor } from "@ogw_front/utils/default_styles/constants";
4
4
  import type { RangesPerData } from "@ogw_front/composables/batch_style";
5
5
  import ViewerOptionsAttributeSelector from "@ogw_front/components/Viewer/Options/AttributeSelector.vue";
@@ -18,11 +18,11 @@ interface CapabilityConfig {
18
18
  }
19
19
 
20
20
  interface Schemas {
21
- vertex?: JsonRpcSchema;
22
- edge?: JsonRpcSchema;
23
- cell?: JsonRpcSchema;
24
- polygon?: JsonRpcSchema;
25
- polyhedron?: JsonRpcSchema;
21
+ vertex?: AttributeSchemas;
22
+ edge?: AttributeSchemas;
23
+ cell?: AttributeSchemas;
24
+ polygon?: AttributeSchemas;
25
+ polyhedron?: AttributeSchemas;
26
26
  }
27
27
 
28
28
  const coloring_style_key = defineModel<string>("coloring_style_key");
@@ -95,12 +95,27 @@ const {
95
95
  allowRandom = false,
96
96
  } = defineProps<Props>();
97
97
 
98
- const vertexSchema = schemas.vertex || back_schemas.opengeodeweb_back.vertex_attribute_names;
99
- const edgeSchema = schemas.edge || back_schemas.opengeodeweb_back.edge_attribute_names;
100
- const cellSchema = schemas.cell || back_schemas.opengeodeweb_back.cell_attribute_names;
101
- const polygonSchema = schemas.polygon || back_schemas.opengeodeweb_back.polygon_attribute_names;
102
- const polyhedronSchema =
103
- schemas.polyhedron || back_schemas.opengeodeweb_back.polyhedron_attribute_names;
98
+ const backSchemas = back_schemas.opengeodeweb_back;
99
+ const vertexSchemas = schemas.vertex ?? {
100
+ names: backSchemas.vertex_attribute_names,
101
+ range: backSchemas.vertex_attribute_range,
102
+ };
103
+ const edgeSchemas = schemas.edge ?? {
104
+ names: backSchemas.edge_attribute_names,
105
+ range: backSchemas.edge_attribute_range,
106
+ };
107
+ const cellSchemas = schemas.cell ?? {
108
+ names: backSchemas.cell_attribute_names,
109
+ range: backSchemas.cell_attribute_range,
110
+ };
111
+ const polygonSchemas = schemas.polygon ?? {
112
+ names: backSchemas.polygon_attribute_names,
113
+ range: backSchemas.polygon_attribute_range,
114
+ };
115
+ const polyhedronSchemas = schemas.polyhedron ?? {
116
+ names: backSchemas.polyhedron_attribute_names,
117
+ range: backSchemas.polyhedron_attribute_range,
118
+ };
104
119
 
105
120
  function isAvailable(key: string): boolean {
106
121
  if (capabilities[key] && capabilities[key].available === false) {
@@ -266,7 +281,7 @@ watch(
266
281
  @ranges_per_data="emit('vertex_attribute_ranges_per_data', $event)"
267
282
  :id="id"
268
283
  :componentIds="componentIds"
269
- :schema="vertexSchema"
284
+ :schemas="vertexSchemas"
270
285
  />
271
286
  </template>
272
287
  <template v-if="active_key === edge_dict['value'] && hasColorMap('edge')">
@@ -280,7 +295,7 @@ watch(
280
295
  @ranges_per_data="emit('edge_attribute_ranges_per_data', $event)"
281
296
  :id="id"
282
297
  :componentIds="componentIds"
283
- :schema="edgeSchema"
298
+ :schemas="edgeSchemas"
284
299
  />
285
300
  </template>
286
301
  <template v-if="active_key === cell_dict['value'] && hasColorMap('cell')">
@@ -294,7 +309,7 @@ watch(
294
309
  @ranges_per_data="emit('cell_attribute_ranges_per_data', $event)"
295
310
  :id="id"
296
311
  :componentIds="componentIds"
297
- :schema="cellSchema"
312
+ :schemas="cellSchemas"
298
313
  />
299
314
  </template>
300
315
  <template v-if="active_key === polygon_dict['value'] && hasColorMap('polygon')">
@@ -308,7 +323,7 @@ watch(
308
323
  @ranges_per_data="emit('polygon_attribute_ranges_per_data', $event)"
309
324
  :id="id"
310
325
  :componentIds="componentIds"
311
- :schema="polygonSchema"
326
+ :schemas="polygonSchemas"
312
327
  />
313
328
  </template>
314
329
  <template v-if="active_key === polyhedron_dict['value'] && hasColorMap('polyhedron')">
@@ -322,7 +337,7 @@ watch(
322
337
  @ranges_per_data="emit('polyhedron_attribute_ranges_per_data', $event)"
323
338
  :id="id"
324
339
  :componentIds="componentIds"
325
- :schema="polyhedronSchema"
340
+ :schemas="polyhedronSchemas"
326
341
  />
327
342
  </template>
328
343
  </v-col>
@@ -26,7 +26,7 @@ const emit = defineEmits<Emits>();
26
26
  const separators = [
27
27
  { title: "Comma (,)", value: "," },
28
28
  { title: "Semicolon (;)", value: ";" },
29
- { title: "Tab (\\t)", value: "\t" },
29
+ { title: String.raw`Tab (\t)`, value: "\t" },
30
30
  { title: "Space ( )", value: " " },
31
31
  { title: "Pipe (|)", value: "|" },
32
32
  { title: "Custom", value: "custom" },
@@ -1,5 +1,6 @@
1
1
  import type { JsonRpcSchema } from "@ogw_shared/utils/types";
2
2
  import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
3
+ import { fetchAttributeRange } from "@ogw_front/utils/attribute_range";
3
4
  import { getAttributeRange } from "@ogw_front/utils/attributes";
4
5
  import { useBackStore } from "@ogw_front/stores/back";
5
6
  import { useDataStyleStore } from "@ogw_front/stores/data_style";
@@ -35,10 +36,6 @@ interface AttributeStyleComponent {
35
36
  interface AttributeResponse {
36
37
  readonly attributes?: readonly {
37
38
  readonly attribute_name?: string;
38
- readonly min_values?: readonly number[];
39
- readonly max_values?: readonly number[];
40
- readonly min_value?: number;
41
- readonly max_value?: number;
42
39
  readonly time_steps?: readonly number[];
43
40
  }[];
44
41
  }
@@ -259,39 +256,29 @@ export function useGlobalAttributeStyle(dataIdRef: Ref<string | undefined>): {
259
256
  continue;
260
257
  }
261
258
 
262
- const schemaName = `${activeColoring}_attribute_names`;
263
- const backSchemas = back_schemas.opengeodeweb_back as Record<string, unknown>;
264
- const schema = backSchemas[schemaName];
265
- if (schema === undefined || !isJsonRpcSchema(schema)) {
259
+ const rangeSchema = (back_schemas.opengeodeweb_back as Record<string, unknown>)[
260
+ `${activeColoring}_attribute_range`
261
+ ];
262
+ if (!isJsonRpcSchema(rangeSchema)) {
266
263
  continue;
267
264
  }
268
265
 
269
266
  requestPromises.push(
270
- backStore.request(
271
- {
272
- schema,
273
- params: { id: targetId },
274
- },
275
- {
276
- response_function: async (response: unknown): Promise<void> => {
277
- if (!isAttributeResponse(response)) {
278
- return;
279
- }
280
- const attributes = response.attributes ?? [];
281
- const currentAttribute = attributes.find((attr) => attr.attribute_name === attrName);
282
- if (currentAttribute) {
283
- const { min, max } = getAttributeRange(currentAttribute, attrItem);
284
-
285
- const setterName = `set${setterKey}${attributeType}Range`;
286
- const setter = getDynamicStoreMethod(dataStyleStore, setterName);
287
- if (setter) {
288
- setter(targetId, min, max);
289
- await hybridViewerStore.remoteRender();
290
- }
291
- }
292
- },
293
- },
294
- ),
267
+ (async (): Promise<void> => {
268
+ const range = await fetchAttributeRange(rangeSchema, { id: targetId }, attrName);
269
+ if (!range) {
270
+ return;
271
+ }
272
+ const { min, max } = getAttributeRange(range, attrItem);
273
+ const setter = getDynamicStoreMethod(
274
+ dataStyleStore,
275
+ `set${setterKey}${attributeType}Range`,
276
+ );
277
+ if (setter) {
278
+ setter(targetId, min, max);
279
+ await hybridViewerStore.remoteRender();
280
+ }
281
+ })(),
295
282
  );
296
283
  }
297
284
 
@@ -0,0 +1,56 @@
1
+ import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
2
+ import { useBackStore } from "@ogw_front/stores/back";
3
+
4
+ interface AttributeRangeInfo {
5
+ min_values: number[];
6
+ max_values: number[];
7
+ no_data: boolean;
8
+ }
9
+
10
+ // The *_attribute_names route listing the attributes, and its *_attribute_range route
11
+ interface AttributeSchemas {
12
+ names: JsonRpcSchema;
13
+ range: JsonRpcSchema;
14
+ }
15
+
16
+ interface AttributeRangeTarget {
17
+ id: string;
18
+ component_ids?: string[];
19
+ }
20
+
21
+ // A data never changes once imported: selectors showing the same attribute share one request
22
+ const rangeRequests = new Map<string, Promise<AttributeRangeInfo | undefined>>();
23
+
24
+ async function requestAttributeRange(
25
+ rangeSchema: JsonRpcSchema,
26
+ params: Record<string, unknown>,
27
+ key: string,
28
+ ): Promise<AttributeRangeInfo | undefined> {
29
+ const backStore = useBackStore();
30
+ try {
31
+ const range = await backStore.request({ schema: rangeSchema, params });
32
+ // oxlint-disable-next-line no-unsafe-type-assertion -- every *_attribute_range schema shares this response.
33
+ return range as AttributeRangeInfo;
34
+ } catch {
35
+ rangeRequests.delete(key);
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ async function fetchAttributeRange(
41
+ rangeSchema: JsonRpcSchema,
42
+ target: AttributeRangeTarget,
43
+ attributeName: string,
44
+ ): Promise<AttributeRangeInfo | undefined> {
45
+ const params = { ...target, attribute_name: attributeName };
46
+ const key = JSON.stringify([rangeSchema.$id, params]);
47
+ let request = rangeRequests.get(key);
48
+ if (request === undefined) {
49
+ request = requestAttributeRange(rangeSchema, params, key);
50
+ rangeRequests.set(key, request);
51
+ }
52
+ const range = await request;
53
+ return range;
54
+ }
55
+
56
+ export { type AttributeRangeInfo, type AttributeSchemas, fetchAttributeRange };
@@ -96,18 +96,26 @@ async function importWorkflow(files: readonly FileToImport[]): Promise<string[]>
96
96
  }
97
97
 
98
98
  const results: string[] = [];
99
+ const errors: unknown[] = [];
99
100
  async function processChunk(chunkIndex: number): Promise<void> {
100
101
  if (chunkIndex >= chunks.length) {
101
102
  return;
102
103
  }
103
104
  const chunk = chunks[chunkIndex] ?? [];
104
- const chunk_results = await Promise.all(
105
+ // A failing file does not prevent the other files from being imported
106
+ const chunk_results = await Promise.allSettled(
105
107
  chunk.map(async ({ filename, geode_object_type }) => {
106
108
  const id = await importFile(filename, geode_object_type);
107
109
  return id;
108
110
  }),
109
111
  );
110
- results.push(...chunk_results);
112
+ for (const result of chunk_results) {
113
+ if (result.status === "fulfilled") {
114
+ results.push(result.value);
115
+ } else {
116
+ errors.push(result.reason);
117
+ }
118
+ }
111
119
  await processChunk(chunkIndex + 1);
112
120
  }
113
121
  await processChunk(0);
@@ -116,6 +124,9 @@ async function importWorkflow(files: readonly FileToImport[]): Promise<string[]>
116
124
  hybridViewerStore.resetCamera();
117
125
  }
118
126
  await hybridViewerStore.remoteRender();
127
+ if (errors.length > 0) {
128
+ throw new AggregateError(errors, `${errors.length} file(s) could not be imported`);
129
+ }
119
130
  return results;
120
131
  }
121
132