@geode/opengeodeweb-front 10.41.0-rc.2 → 10.41.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.
- package/app/components/HybridRenderingView.vue +2 -0
- package/app/components/RemoteRenderingView.vue +2 -0
- package/app/components/Viewer/Options/ColormapQuickPicker.vue +11 -2
- package/app/composables/attribute_coloring_style.ts +182 -0
- package/app/composables/global_attribute_style.ts +55 -366
- package/app/composables/model_attribute_style.ts +79 -0
- package/app/composables/use_quick_colormap.ts +4 -1
- package/app/stores/data.ts +12 -0
- package/app/utils/attribute_range.ts +15 -15
- package/package.json +1 -1
|
@@ -98,10 +98,12 @@ async function handleClick(event: PointerEvent): Promise<void> {
|
|
|
98
98
|
<ClientOnly>
|
|
99
99
|
<div data-testid="hybridViewer" class="fill-height" style="position: relative; height: 100%">
|
|
100
100
|
<ColormapQuickPicker
|
|
101
|
+
:key="quickColormap.data_id"
|
|
101
102
|
v-model:show="quickColormap.show"
|
|
102
103
|
:x="quickColormap.x"
|
|
103
104
|
:y="quickColormap.y"
|
|
104
105
|
:data-id="quickColormap.data_id"
|
|
106
|
+
:block-ids="quickColormap.block_ids"
|
|
105
107
|
/>
|
|
106
108
|
<ZoomBoxOverlay v-if="hybridViewerStore.is_zoom_box_active" />
|
|
107
109
|
<ViewToolbar />
|
|
@@ -116,10 +116,12 @@ onMounted(async () => {
|
|
|
116
116
|
<ClientOnly>
|
|
117
117
|
<div style="position: relative; width: 100%; height: 100%">
|
|
118
118
|
<ColormapQuickPicker
|
|
119
|
+
:key="quickColormap.data_id"
|
|
119
120
|
v-model:show="quickColormap.show"
|
|
120
121
|
:x="quickColormap.x"
|
|
121
122
|
:y="quickColormap.y"
|
|
122
123
|
:data-id="quickColormap.data_id"
|
|
124
|
+
:block-ids="quickColormap.block_ids"
|
|
123
125
|
/>
|
|
124
126
|
<ViewToolbar />
|
|
125
127
|
<slot name="ui"></slot>
|
|
@@ -5,18 +5,21 @@ import TimeStepSlider from "@ogw_front/components/Viewer/Options/TimeStepSlider.
|
|
|
5
5
|
|
|
6
6
|
import { getPresetsWithCurrentAtTop } from "@ogw_front/utils/colormap";
|
|
7
7
|
import { useGlobalAttributeStyle } from "@ogw_front/composables/global_attribute_style";
|
|
8
|
+
import { useModelAttributeStyle } from "@ogw_front/composables/model_attribute_style";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
dataId?: string;
|
|
12
|
+
blockIds?: number[];
|
|
11
13
|
x: number;
|
|
12
14
|
y: number;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const { dataId = undefined, x, y } = defineProps<Props>();
|
|
17
|
+
const { dataId = undefined, blockIds = undefined, x, y } = defineProps<Props>();
|
|
16
18
|
|
|
17
19
|
const show = defineModel<boolean>("show", { default: false });
|
|
18
20
|
|
|
19
21
|
const dataIdRef = computed<string>(() => dataId);
|
|
22
|
+
// The parent keys this picker on the picked data: a mesh or a model, so the style is chosen once here
|
|
20
23
|
const {
|
|
21
24
|
currentColormap,
|
|
22
25
|
currentRange,
|
|
@@ -25,7 +28,13 @@ const {
|
|
|
25
28
|
currentTimeSteps,
|
|
26
29
|
currentTimeStep,
|
|
27
30
|
loadTimeSteps,
|
|
28
|
-
} =
|
|
31
|
+
} =
|
|
32
|
+
blockIds === undefined
|
|
33
|
+
? useGlobalAttributeStyle(dataIdRef)
|
|
34
|
+
: useModelAttributeStyle(
|
|
35
|
+
dataIdRef,
|
|
36
|
+
computed<number[]>(() => blockIds),
|
|
37
|
+
);
|
|
29
38
|
|
|
30
39
|
watch(show, async (isShown) => {
|
|
31
40
|
if (isShown) {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { type AttributeSchemas, fetchAttributeRange } from "@ogw_front/utils/attribute_range";
|
|
2
|
+
import type { JsonRpcSchema } from "@ogw_shared/utils/types";
|
|
3
|
+
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
|
|
4
|
+
import { getAttributeRange } from "@ogw_front/utils/attributes";
|
|
5
|
+
import { useBackStore } from "@ogw_front/stores/back";
|
|
6
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
7
|
+
|
|
8
|
+
// Elements colored by the same attribute, with the store and back accesses of their data
|
|
9
|
+
interface AttributeColoring {
|
|
10
|
+
// Reads a style property ("Name", "Range", ...) of the attribute coloring
|
|
11
|
+
read: (property: string) => unknown;
|
|
12
|
+
write: (property: string, ...values: unknown[]) => Promise<void>;
|
|
13
|
+
schemas: AttributeSchemas;
|
|
14
|
+
params: { id: string; component_ids?: string[] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface AttributeNamesResponse {
|
|
18
|
+
attributes: { attribute_name: string; time_steps?: number[] }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AttributeColoringStyle {
|
|
22
|
+
currentColormap: ComputedRef<string>;
|
|
23
|
+
currentRange: WritableComputedRef<readonly [number, number]>;
|
|
24
|
+
applyGlobalColormap: (colorMap: string) => Promise<void>;
|
|
25
|
+
resetGlobalRange: () => Promise<void>;
|
|
26
|
+
currentTimeSteps: Ref<number[]>;
|
|
27
|
+
currentTimeStep: WritableComputedRef<number>;
|
|
28
|
+
loadTimeSteps: () => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type StoreMethod = (...args: readonly unknown[]) => unknown;
|
|
32
|
+
|
|
33
|
+
// The data style store methods are named after the element and attribute types, e.g. "meshPointsVertexAttributeRange"
|
|
34
|
+
function storeMethod(store: object, name: string): StoreMethod {
|
|
35
|
+
const method: unknown = Reflect.get(store, name);
|
|
36
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- the style methods exist for every element type and attribute coloring
|
|
37
|
+
return method as StoreMethod;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// "Polyhedron" in the store method names of the "polyhedron" coloring
|
|
41
|
+
function attributeType(activeColoring: string): string {
|
|
42
|
+
return `${activeColoring.charAt(0).toUpperCase()}${activeColoring.slice(1)}Attribute`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function attributeSchema(prefix: string, activeColoring: string, route: string): JsonRpcSchema {
|
|
46
|
+
const schema: unknown = Reflect.get(
|
|
47
|
+
back_schemas.opengeodeweb_back,
|
|
48
|
+
`${prefix}${activeColoring}_attribute_${route}`,
|
|
49
|
+
);
|
|
50
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- these schemas exist for every attribute coloring
|
|
51
|
+
return schema as JsonRpcSchema;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The `${prefix}${activeColoring}_attribute_names` and `_attribute_range` schemas
|
|
55
|
+
function attributeSchemas(prefix: string, activeColoring: string): AttributeSchemas {
|
|
56
|
+
return {
|
|
57
|
+
names: attributeSchema(prefix, activeColoring, "names"),
|
|
58
|
+
range: attributeSchema(prefix, activeColoring, "range"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Colormap, range and time step of the attributes colored by a scalar bar
|
|
63
|
+
function useAttributeColoringStyle(colorings: Ref<AttributeColoring[]>): AttributeColoringStyle {
|
|
64
|
+
const backStore = useBackStore();
|
|
65
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
66
|
+
|
|
67
|
+
const currentTimeSteps = ref<number[]>([]);
|
|
68
|
+
const temporalColoring = shallowRef<AttributeColoring | undefined>(undefined);
|
|
69
|
+
|
|
70
|
+
async function writeAll(property: string, ...values: unknown[]): Promise<void> {
|
|
71
|
+
await Promise.all(
|
|
72
|
+
colorings.value.map(async (coloring) => {
|
|
73
|
+
await coloring.write(property, ...values);
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
await hybridViewerStore.remoteRender();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const currentColormap = computed((): string => {
|
|
80
|
+
const [coloring] = colorings.value;
|
|
81
|
+
return coloring ? String(coloring.read("ColorMap")) : "batlow";
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
async function applyGlobalColormap(colorMap: string): Promise<void> {
|
|
85
|
+
await writeAll("ColorMap", colorMap);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const currentRange = computed<readonly [number, number]>({
|
|
89
|
+
get(): readonly [number, number] {
|
|
90
|
+
const [coloring] = colorings.value;
|
|
91
|
+
if (!coloring) {
|
|
92
|
+
return [0, 1];
|
|
93
|
+
}
|
|
94
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- the range getters return [minimum, maximum].
|
|
95
|
+
const [minimum = 0, maximum = 1] = coloring.read("Range") as (number | undefined)[];
|
|
96
|
+
return [minimum, maximum];
|
|
97
|
+
},
|
|
98
|
+
// Vue's WritableComputedOptions setter is strictly `(v: T) => void`: the async write is delegated.
|
|
99
|
+
set: (range: readonly [number, number]): void => {
|
|
100
|
+
/* oxlint-disable-next-line promise/prefer-await-to-then -- setter cannot be async; see comment above. */
|
|
101
|
+
writeAll("Range", range[0], range[1]).catch(() => undefined);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
async function resetGlobalRange(): Promise<void> {
|
|
106
|
+
await Promise.all(
|
|
107
|
+
colorings.value.map(async (coloring) => {
|
|
108
|
+
const range = await fetchAttributeRange(
|
|
109
|
+
coloring.schemas.range,
|
|
110
|
+
coloring.params,
|
|
111
|
+
String(coloring.read("Name")),
|
|
112
|
+
);
|
|
113
|
+
const { min, max } = getAttributeRange(range, Number(coloring.read("Item") ?? 0));
|
|
114
|
+
await coloring.write("Range", min, max);
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
await hybridViewerStore.remoteRender();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function requestTimeSteps(coloring: AttributeColoring): Promise<number[]> {
|
|
121
|
+
const name = coloring.read("Name");
|
|
122
|
+
const response = await backStore.request({
|
|
123
|
+
schema: coloring.schemas.names,
|
|
124
|
+
params: coloring.params,
|
|
125
|
+
});
|
|
126
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- every *_attribute_names schema shares this response.
|
|
127
|
+
const { attributes } = response as AttributeNamesResponse;
|
|
128
|
+
return attributes.find((attribute) => attribute.attribute_name === name)?.time_steps ?? [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadTimeSteps(): Promise<void> {
|
|
132
|
+
temporalColoring.value = undefined;
|
|
133
|
+
currentTimeSteps.value = [];
|
|
134
|
+
for (const coloring of colorings.value) {
|
|
135
|
+
// oxlint-disable-next-line no-await-in-loop -- first temporal coloring wins, in order.
|
|
136
|
+
const timeSteps = await requestTimeSteps(coloring);
|
|
137
|
+
if (timeSteps.length > 0) {
|
|
138
|
+
temporalColoring.value = coloring;
|
|
139
|
+
currentTimeSteps.value = timeSteps;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function applyTimeStep(step: number): Promise<void> {
|
|
146
|
+
if (temporalColoring.value) {
|
|
147
|
+
await temporalColoring.value.write("TimeStep", step);
|
|
148
|
+
await hybridViewerStore.remoteRender();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const currentTimeStep = computed<number>({
|
|
153
|
+
get(): number {
|
|
154
|
+
const coloring = temporalColoring.value;
|
|
155
|
+
return coloring ? Number(coloring.read("TimeStep") ?? 0) : 0;
|
|
156
|
+
},
|
|
157
|
+
// Setter cannot be async (see `currentRange`), so the write is delegated.
|
|
158
|
+
set: (step: number): void => {
|
|
159
|
+
/* oxlint-disable-next-line promise/prefer-await-to-then -- setter cannot be async; see comment above. */
|
|
160
|
+
applyTimeStep(step).catch(() => undefined);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
currentColormap,
|
|
166
|
+
currentRange,
|
|
167
|
+
applyGlobalColormap,
|
|
168
|
+
resetGlobalRange,
|
|
169
|
+
currentTimeSteps,
|
|
170
|
+
currentTimeStep,
|
|
171
|
+
loadTimeSteps,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
type AttributeColoring,
|
|
177
|
+
type AttributeColoringStyle,
|
|
178
|
+
attributeSchemas,
|
|
179
|
+
attributeType,
|
|
180
|
+
storeMethod,
|
|
181
|
+
useAttributeColoringStyle,
|
|
182
|
+
};
|
|
@@ -1,384 +1,73 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AttributeColoring,
|
|
3
|
+
type AttributeColoringStyle,
|
|
4
|
+
attributeSchemas,
|
|
5
|
+
attributeType,
|
|
6
|
+
storeMethod,
|
|
7
|
+
useAttributeColoringStyle,
|
|
8
|
+
} from "@ogw_front/composables/attribute_coloring_style";
|
|
6
9
|
import { useDataStyleStore } from "@ogw_front/stores/data_style";
|
|
7
|
-
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
8
10
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
interface MeshElement {
|
|
12
|
+
// Element type of the store methods, e.g. "MeshPoints" for "setMeshPointsVertexAttributeRange"
|
|
13
|
+
storeName: string;
|
|
14
|
+
styleKey: "points" | "edges" | "polygons" | "cells" | "polyhedra";
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function isDynamicStoreMethod(value: unknown): value is DynamicStoreMethod {
|
|
24
|
-
return typeof value === "function";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getDynamicStoreMethod(store: object, key: string): DynamicStoreMethod | undefined {
|
|
28
|
-
const value: unknown = Reflect.get(store, key);
|
|
29
|
-
return isDynamicStoreMethod(value) ? value : undefined;
|
|
30
|
-
}
|
|
17
|
+
const MESH_ELEMENTS: MeshElement[] = [
|
|
18
|
+
{ storeName: "MeshPoints", styleKey: "points" },
|
|
19
|
+
{ storeName: "MeshEdges", styleKey: "edges" },
|
|
20
|
+
{ storeName: "MeshPolygons", styleKey: "polygons" },
|
|
21
|
+
{ storeName: "MeshCells", styleKey: "cells" },
|
|
22
|
+
{ storeName: "MeshPolyhedra", styleKey: "polyhedra" },
|
|
23
|
+
];
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
coloring?: { active?: string };
|
|
34
|
-
}
|
|
25
|
+
const ATTRIBUTE_COLORINGS = new Set(["vertex", "edge", "polygon", "cell", "polyhedron"]);
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
readonly attributes?: readonly {
|
|
38
|
-
readonly attribute_name?: string;
|
|
39
|
-
readonly time_steps?: readonly number[];
|
|
40
|
-
}[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isColoringValue(
|
|
44
|
-
value: unknown,
|
|
45
|
-
): value is NonNullable<AttributeStyleComponent["coloring"]> {
|
|
27
|
+
function isColoring(value: unknown): value is { active?: string } {
|
|
46
28
|
return typeof value === "object" && value !== null;
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isAttributeResponse(value: unknown): value is AttributeResponse {
|
|
56
|
-
return typeof value === "object" && value !== null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isNumberArray(value: unknown): value is number[] {
|
|
60
|
-
return Array.isArray(value) && value.every((item) => typeof item === "number");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isStringOrUndefined(value: unknown): value is string | undefined {
|
|
64
|
-
return value === undefined || typeof value === "string";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isNumberOrUndefined(value: unknown): value is number | undefined {
|
|
68
|
-
return value === undefined || typeof value === "number";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function useGlobalAttributeStyle(dataIdRef: Ref<string | undefined>): {
|
|
72
|
-
currentColormap: typeof currentColormap;
|
|
73
|
-
currentRange: typeof currentRange;
|
|
74
|
-
applyGlobalColormap: typeof applyGlobalColormap;
|
|
75
|
-
resetGlobalRange: typeof resetGlobalRange;
|
|
76
|
-
currentTimeSteps: typeof currentTimeSteps;
|
|
77
|
-
currentTimeStep: typeof currentTimeStep;
|
|
78
|
-
loadTimeSteps: typeof loadTimeSteps;
|
|
79
|
-
} {
|
|
31
|
+
// Styles the mesh elements colored by an attribute
|
|
32
|
+
export function useGlobalAttributeStyle(
|
|
33
|
+
dataIdRef: Ref<string | undefined>,
|
|
34
|
+
): AttributeColoringStyle {
|
|
80
35
|
const dataStyleStore = useDataStyleStore();
|
|
81
|
-
const hybridViewerStore = useHybridViewerStore();
|
|
82
|
-
const backStore = useBackStore();
|
|
83
|
-
|
|
84
|
-
const componentNames: ComponentNameEntry[] = [
|
|
85
|
-
{ getterKey: "meshPoints", setterKey: "MeshPoints", key: "points" },
|
|
86
|
-
{ getterKey: "meshEdges", setterKey: "MeshEdges", key: "edges" },
|
|
87
|
-
{ getterKey: "meshPolygons", setterKey: "MeshPolygons", key: "polygons" },
|
|
88
|
-
{ getterKey: "meshCells", setterKey: "MeshCells", key: "cells" },
|
|
89
|
-
{ getterKey: "meshPolyhedra", setterKey: "MeshPolyhedra", key: "polyhedra" },
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
function getActiveComponents(targetId: string): ActiveComponent[] {
|
|
93
|
-
const style = dataStyleStore.getStyle(targetId);
|
|
94
|
-
const activeComponents: ActiveComponent[] = [];
|
|
95
|
-
for (const { key, getterKey, setterKey } of componentNames) {
|
|
96
|
-
let componentStyle: Record<string, unknown> | undefined = undefined;
|
|
97
|
-
switch (key) {
|
|
98
|
-
case "points": {
|
|
99
|
-
componentStyle = style.points;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
case "edges": {
|
|
103
|
-
componentStyle = style.edges;
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
case "polygons": {
|
|
107
|
-
componentStyle = style.polygons;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
case "cells": {
|
|
111
|
-
componentStyle = style.cells;
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
case "polyhedra": {
|
|
115
|
-
componentStyle = style.polyhedra;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
default: {
|
|
119
|
-
componentStyle = undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (componentStyle === undefined) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const { coloring } = componentStyle;
|
|
127
|
-
if (!isColoringValue(coloring)) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const { active: activeColoring } = coloring;
|
|
132
|
-
if (
|
|
133
|
-
activeColoring === undefined ||
|
|
134
|
-
!["vertex", "edge", "polygon", "cell", "polyhedron"].includes(activeColoring)
|
|
135
|
-
) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const attributeType = `${activeColoring.charAt(0).toUpperCase()}${activeColoring.slice(1)}Attribute`;
|
|
140
|
-
activeComponents.push({ activeColoring, attributeType, getterKey, setterKey, key });
|
|
141
|
-
}
|
|
142
|
-
return activeComponents;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const currentColormap = computed((): string => {
|
|
146
|
-
const targetId = dataIdRef.value;
|
|
147
|
-
if (targetId === undefined || targetId === "") {
|
|
148
|
-
return "batlow";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
152
|
-
const getterName = `${comp.getterKey}${comp.attributeType}ColorMap`;
|
|
153
|
-
const getter = getDynamicStoreMethod(dataStyleStore, getterName);
|
|
154
|
-
if (getter) {
|
|
155
|
-
const colorMap = getter(targetId);
|
|
156
|
-
if (typeof colorMap === "string" && colorMap.length > 0) {
|
|
157
|
-
return colorMap;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return "batlow";
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
async function applyCurrentRange(newValue: readonly [number, number]): Promise<void> {
|
|
166
|
-
const targetId = dataIdRef.value;
|
|
167
|
-
if (targetId === undefined || targetId === "") {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let updated = false;
|
|
172
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
173
|
-
const setterName = `set${comp.setterKey}${comp.attributeType}Range`;
|
|
174
|
-
const setter = getDynamicStoreMethod(dataStyleStore, setterName);
|
|
175
|
-
if (setter) {
|
|
176
|
-
setter(targetId, newValue[0], newValue[1]);
|
|
177
|
-
updated = true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (updated) {
|
|
181
|
-
await hybridViewerStore.remoteRender();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const currentRange = computed<readonly [number, number]>({
|
|
186
|
-
get(): readonly [number, number] {
|
|
187
|
-
const targetId = dataIdRef.value;
|
|
188
|
-
if (targetId === undefined || targetId === "") {
|
|
189
|
-
return [0, 1];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
193
|
-
const getterName = `${comp.getterKey}${comp.attributeType}Range`;
|
|
194
|
-
const getter = getDynamicStoreMethod(dataStyleStore, getterName);
|
|
195
|
-
if (getter) {
|
|
196
|
-
const rawRange = getter(targetId);
|
|
197
|
-
const range = isNumberArray(rawRange) ? rawRange : undefined;
|
|
198
|
-
if (range && range.length === 2) {
|
|
199
|
-
return [range[0] ?? 0, range[1] ?? 1];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return [0, 1];
|
|
204
|
-
},
|
|
205
|
-
// Vue's WritableComputedOptions setter is strictly `(v: T) => void`, so the async render call is delegated to a helper (see the same trade-off in `app/composables/hover_highlight.ts`'s `onHoverEnter`).
|
|
206
|
-
set: (newValue: readonly [number, number]): void => {
|
|
207
|
-
/* oxlint-disable-next-line promise/prefer-await-to-then -- setter cannot be async; see comment above. */
|
|
208
|
-
applyCurrentRange(newValue).catch(() => undefined);
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
async function applyGlobalColormap(newMap: string): Promise<void> {
|
|
213
|
-
const targetId = dataIdRef.value;
|
|
214
|
-
if (targetId === undefined || targetId === "") {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const promises: Promise<unknown>[] = [];
|
|
219
|
-
|
|
220
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
221
|
-
const setterName = `set${comp.setterKey}${comp.attributeType}ColorMap`;
|
|
222
|
-
const setter = getDynamicStoreMethod(dataStyleStore, setterName);
|
|
223
|
-
|
|
224
|
-
if (setter) {
|
|
225
|
-
promises.push(Promise.resolve(setter(targetId, newMap)));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
await Promise.all(promises);
|
|
230
|
-
await hybridViewerStore.remoteRender();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function resetGlobalRange(): Promise<void> {
|
|
234
|
-
const targetId = dataIdRef.value;
|
|
235
|
-
if (targetId === undefined || targetId === "") {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const requestPromises: Promise<unknown>[] = [];
|
|
240
|
-
|
|
241
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
242
|
-
const { activeColoring, attributeType, getterKey, setterKey } = comp;
|
|
243
|
-
|
|
244
|
-
const nameGetter = getDynamicStoreMethod(dataStyleStore, `${getterKey}${attributeType}Name`);
|
|
245
|
-
const itemGetter = getDynamicStoreMethod(dataStyleStore, `${getterKey}${attributeType}Item`);
|
|
246
|
-
if (!nameGetter || !itemGetter) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const rawAttrName = nameGetter(targetId);
|
|
251
|
-
const attrName = isStringOrUndefined(rawAttrName) ? rawAttrName : undefined;
|
|
252
|
-
const rawAttrItem = itemGetter(targetId);
|
|
253
|
-
const attrItem = (isNumberOrUndefined(rawAttrItem) ? rawAttrItem : undefined) ?? 0;
|
|
254
36
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
requestPromises.push(
|
|
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
|
-
})(),
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await Promise.all(requestPromises);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const currentTimeSteps = ref<number[]>([]);
|
|
289
|
-
const temporalComponent = shallowRef<ActiveComponent | undefined>(undefined);
|
|
290
|
-
|
|
291
|
-
async function requestTimeSteps(targetId: string, comp: ActiveComponent): Promise<number[]> {
|
|
292
|
-
const { activeColoring, attributeType, getterKey } = comp;
|
|
293
|
-
const nameGetter = getDynamicStoreMethod(dataStyleStore, `${getterKey}${attributeType}Name`);
|
|
294
|
-
const rawAttrName = nameGetter?.(targetId);
|
|
295
|
-
const schema = (back_schemas.opengeodeweb_back as Record<string, unknown>)[
|
|
296
|
-
`${activeColoring}_attribute_names`
|
|
297
|
-
];
|
|
298
|
-
if (
|
|
299
|
-
!isStringOrUndefined(rawAttrName) ||
|
|
300
|
-
rawAttrName === undefined ||
|
|
301
|
-
rawAttrName === "" ||
|
|
302
|
-
!isJsonRpcSchema(schema)
|
|
303
|
-
) {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
let timeSteps: number[] = [];
|
|
307
|
-
await backStore.request(
|
|
308
|
-
{ schema, params: { id: targetId } },
|
|
309
|
-
{
|
|
310
|
-
response_function: (response: unknown): void => {
|
|
311
|
-
if (!isAttributeResponse(response)) {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const found = response.attributes?.find((attr) => attr.attribute_name === rawAttrName);
|
|
315
|
-
timeSteps = [...(found?.time_steps ?? [])];
|
|
316
|
-
},
|
|
37
|
+
function meshColoring(
|
|
38
|
+
dataId: string,
|
|
39
|
+
{ storeName }: MeshElement,
|
|
40
|
+
activeColoring: string,
|
|
41
|
+
): AttributeColoring {
|
|
42
|
+
const methodName = `${storeName}${attributeType(activeColoring)}`;
|
|
43
|
+
const getterName = `${methodName.charAt(0).toLowerCase()}${methodName.slice(1)}`;
|
|
44
|
+
return {
|
|
45
|
+
read: (property): unknown => storeMethod(dataStyleStore, `${getterName}${property}`)(dataId),
|
|
46
|
+
write: async (property, ...values): Promise<void> => {
|
|
47
|
+
await storeMethod(dataStyleStore, `set${methodName}${property}`)(dataId, ...values);
|
|
317
48
|
},
|
|
318
|
-
|
|
319
|
-
|
|
49
|
+
schemas: attributeSchemas("", activeColoring),
|
|
50
|
+
params: { id: dataId },
|
|
51
|
+
};
|
|
320
52
|
}
|
|
321
53
|
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
for (const comp of getActiveComponents(targetId)) {
|
|
330
|
-
// oxlint-disable-next-line no-await-in-loop -- first temporal component wins, in order.
|
|
331
|
-
const timeSteps = await requestTimeSteps(targetId, comp);
|
|
332
|
-
if (timeSteps.length > 0) {
|
|
333
|
-
temporalComponent.value = comp;
|
|
334
|
-
currentTimeSteps.value = timeSteps;
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
watch(dataIdRef, loadTimeSteps, { immediate: true });
|
|
341
|
-
|
|
342
|
-
async function applyCurrentTimeStep(step: number): Promise<void> {
|
|
343
|
-
const targetId = dataIdRef.value;
|
|
344
|
-
const comp = temporalComponent.value;
|
|
345
|
-
if (targetId === undefined || targetId === "" || !comp) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const { attributeType, setterKey } = comp;
|
|
349
|
-
const setter = getDynamicStoreMethod(dataStyleStore, `set${setterKey}${attributeType}TimeStep`);
|
|
350
|
-
if (setter) {
|
|
351
|
-
await setter(targetId, step);
|
|
352
|
-
await hybridViewerStore.remoteRender();
|
|
54
|
+
const colorings = computed<AttributeColoring[]>(() => {
|
|
55
|
+
const dataId = dataIdRef.value;
|
|
56
|
+
// The picker exists before any scalar bar is picked
|
|
57
|
+
if (dataId === undefined) {
|
|
58
|
+
return [];
|
|
353
59
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
const { attributeType, getterKey } = comp;
|
|
364
|
-
const getter = getDynamicStoreMethod(dataStyleStore, `${getterKey}${attributeType}TimeStep`);
|
|
365
|
-
const rawStep = getter?.(targetId);
|
|
366
|
-
return typeof rawStep === "number" ? rawStep : 0;
|
|
367
|
-
},
|
|
368
|
-
// Setter cannot be async (see `currentRange`), so the work is delegated to a helper.
|
|
369
|
-
set: (step: number): void => {
|
|
370
|
-
/* oxlint-disable-next-line promise/prefer-await-to-then -- setter cannot be async; see comment above. */
|
|
371
|
-
applyCurrentTimeStep(step).catch(() => undefined);
|
|
372
|
-
},
|
|
60
|
+
const style = dataStyleStore.getStyle(dataId);
|
|
61
|
+
return MESH_ELEMENTS.flatMap((element) => {
|
|
62
|
+
const coloring: unknown = style[element.styleKey]?.coloring;
|
|
63
|
+
const activeColoring = isColoring(coloring) ? coloring.active : undefined;
|
|
64
|
+
return activeColoring !== undefined && ATTRIBUTE_COLORINGS.has(activeColoring)
|
|
65
|
+
? [meshColoring(dataId, element, activeColoring)]
|
|
66
|
+
: [];
|
|
67
|
+
});
|
|
373
68
|
});
|
|
374
69
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
applyGlobalColormap,
|
|
379
|
-
resetGlobalRange,
|
|
380
|
-
currentTimeSteps,
|
|
381
|
-
currentTimeStep,
|
|
382
|
-
loadTimeSteps,
|
|
383
|
-
};
|
|
70
|
+
const style = useAttributeColoringStyle(colorings);
|
|
71
|
+
watch(dataIdRef, style.loadTimeSteps, { immediate: true });
|
|
72
|
+
return style;
|
|
384
73
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AttributeColoring,
|
|
3
|
+
type AttributeColoringStyle,
|
|
4
|
+
attributeSchemas,
|
|
5
|
+
attributeType,
|
|
6
|
+
storeMethod,
|
|
7
|
+
useAttributeColoringStyle,
|
|
8
|
+
} from "@ogw_front/composables/attribute_coloring_style";
|
|
9
|
+
import { useDataStore } from "@ogw_front/stores/data";
|
|
10
|
+
import { useDataStyleStore } from "@ogw_front/stores/data_style";
|
|
11
|
+
|
|
12
|
+
// Components of the same type colored by the same attribute share the store methods, e.g. "modelBlocksPolyhedronAttributeName"
|
|
13
|
+
interface ComponentGroup {
|
|
14
|
+
// Component type of the store methods, e.g. "Block"
|
|
15
|
+
type: string;
|
|
16
|
+
activeColoring: string;
|
|
17
|
+
componentIds: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function modelColoring(
|
|
21
|
+
dataStyleStore: object,
|
|
22
|
+
modelId: string,
|
|
23
|
+
{ type, activeColoring, componentIds }: ComponentGroup,
|
|
24
|
+
): AttributeColoring {
|
|
25
|
+
const methodName = `${type}s${attributeType(activeColoring)}`;
|
|
26
|
+
return {
|
|
27
|
+
// Every component of the coloring shares its style: reading the first one is enough
|
|
28
|
+
read: (property): unknown =>
|
|
29
|
+
storeMethod(dataStyleStore, `model${methodName}${property}`)(modelId, componentIds[0]),
|
|
30
|
+
write: async (property, ...values): Promise<void> => {
|
|
31
|
+
await storeMethod(dataStyleStore, `setModel${methodName}${property}`)(
|
|
32
|
+
modelId,
|
|
33
|
+
componentIds,
|
|
34
|
+
...values,
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
schemas: attributeSchemas("model_component_", activeColoring),
|
|
38
|
+
params: { id: modelId, component_ids: componentIds },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Styles the model components colored by the picked scalar bar
|
|
43
|
+
export function useModelAttributeStyle(
|
|
44
|
+
modelIdRef: Ref<string>,
|
|
45
|
+
blockViewerIdsRef: Ref<number[]>,
|
|
46
|
+
): AttributeColoringStyle {
|
|
47
|
+
const dataStyleStore = useDataStyleStore();
|
|
48
|
+
const dataStore = useDataStore();
|
|
49
|
+
|
|
50
|
+
const colorings = shallowRef<AttributeColoring[]>([]);
|
|
51
|
+
const style = useAttributeColoringStyle(colorings);
|
|
52
|
+
|
|
53
|
+
async function loadColorings(): Promise<void> {
|
|
54
|
+
const modelId = modelIdRef.value;
|
|
55
|
+
const blockViewerIds = blockViewerIdsRef.value;
|
|
56
|
+
const components = await dataStore.getComponentsByViewerIds(modelId, blockViewerIds);
|
|
57
|
+
// A newer pick replaced this one while the components were read
|
|
58
|
+
if (modelId !== modelIdRef.value || blockViewerIds !== blockViewerIdsRef.value) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const groups = new Map<string, ComponentGroup>();
|
|
62
|
+
for (const { type, geode_id } of components) {
|
|
63
|
+
const activeColoring = String(
|
|
64
|
+
storeMethod(dataStyleStore, `model${type}ActiveColoring`)(modelId, geode_id),
|
|
65
|
+
);
|
|
66
|
+
const key = `${type}_${activeColoring}`;
|
|
67
|
+
const group = groups.get(key) ?? { type, activeColoring, componentIds: [] };
|
|
68
|
+
group.componentIds.push(geode_id);
|
|
69
|
+
groups.set(key, group);
|
|
70
|
+
}
|
|
71
|
+
colorings.value = [...groups.values()].map((group) =>
|
|
72
|
+
modelColoring(dataStyleStore, modelId, group),
|
|
73
|
+
);
|
|
74
|
+
await style.loadTimeSteps();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
watch([modelIdRef, blockViewerIdsRef], loadColorings, { immediate: true });
|
|
78
|
+
return style;
|
|
79
|
+
}
|
|
@@ -9,11 +9,13 @@ export function useQuickColormap(): {
|
|
|
9
9
|
const viewerStore = useViewerStore();
|
|
10
10
|
const quickColormap = reactive<{
|
|
11
11
|
data_id: string | undefined;
|
|
12
|
+
block_ids: number[] | undefined;
|
|
12
13
|
show: boolean;
|
|
13
14
|
x: number;
|
|
14
15
|
y: number;
|
|
15
16
|
}>({
|
|
16
17
|
data_id: undefined,
|
|
18
|
+
block_ids: undefined,
|
|
17
19
|
show: false,
|
|
18
20
|
x: 0,
|
|
19
21
|
y: 0,
|
|
@@ -28,9 +30,10 @@ export function useQuickColormap(): {
|
|
|
28
30
|
try {
|
|
29
31
|
const schema = viewer_schemas.opengeodeweb_viewer.viewer.pick_colormap;
|
|
30
32
|
const params = { x: offsetX, y: offsetY };
|
|
31
|
-
const { data_id } = await viewerStore.request({ schema, params });
|
|
33
|
+
const { data_id, block_ids } = await viewerStore.request({ schema, params });
|
|
32
34
|
if (data_id !== undefined && data_id !== "") {
|
|
33
35
|
quickColormap.data_id = data_id;
|
|
36
|
+
quickColormap.block_ids = block_ids;
|
|
34
37
|
quickColormap.x = clientX;
|
|
35
38
|
quickColormap.y = clientY;
|
|
36
39
|
quickColormap.show = true;
|
package/app/stores/data.ts
CHANGED
|
@@ -285,6 +285,17 @@ export const useDataStore = defineStore("data", () => {
|
|
|
285
285
|
.first();
|
|
286
286
|
return component;
|
|
287
287
|
}
|
|
288
|
+
async function getComponentsByViewerIds(
|
|
289
|
+
modelId: string,
|
|
290
|
+
viewer_ids: number[],
|
|
291
|
+
): Promise<ModelComponentRecord[]> {
|
|
292
|
+
const components = await model_components_db
|
|
293
|
+
.where("viewer_id")
|
|
294
|
+
.anyOf(viewer_ids)
|
|
295
|
+
.and((model_component: ModelComponentRecord) => model_component.id === modelId)
|
|
296
|
+
.toArray();
|
|
297
|
+
return components;
|
|
298
|
+
}
|
|
288
299
|
async function deleteModelComponents(modelId: string): Promise<void> {
|
|
289
300
|
await model_components_db.where("id").equals(modelId).delete();
|
|
290
301
|
await model_components_relation_db.where("id").equals(modelId).delete();
|
|
@@ -365,6 +376,7 @@ export const useDataStore = defineStore("data", () => {
|
|
|
365
376
|
getAllModelComponentsViewerIds,
|
|
366
377
|
getMeshComponentsViewerIds,
|
|
367
378
|
getComponentByViewerId,
|
|
379
|
+
getComponentsByViewerIds,
|
|
368
380
|
exportStores,
|
|
369
381
|
importStores,
|
|
370
382
|
clear,
|
|
@@ -19,38 +19,38 @@ interface AttributeRangeTarget {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// A data never changes once imported: selectors showing the same attribute share one request
|
|
22
|
-
const rangeRequests = new Map<string, Promise<AttributeRangeInfo
|
|
22
|
+
const rangeRequests = new Map<string, Promise<AttributeRangeInfo>>();
|
|
23
23
|
|
|
24
24
|
async function requestAttributeRange(
|
|
25
25
|
rangeSchema: JsonRpcSchema,
|
|
26
26
|
params: Record<string, unknown>,
|
|
27
|
-
|
|
28
|
-
): Promise<AttributeRangeInfo | undefined> {
|
|
27
|
+
): Promise<AttributeRangeInfo> {
|
|
29
28
|
const backStore = useBackStore();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return range as AttributeRangeInfo;
|
|
34
|
-
} catch {
|
|
35
|
-
rangeRequests.delete(key);
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
29
|
+
const range = await backStore.request({ schema: rangeSchema, params });
|
|
30
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- every *_attribute_range schema shares this response.
|
|
31
|
+
return range as AttributeRangeInfo;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
async function fetchAttributeRange(
|
|
41
35
|
rangeSchema: JsonRpcSchema,
|
|
42
36
|
target: AttributeRangeTarget,
|
|
43
37
|
attributeName: string,
|
|
44
|
-
): Promise<AttributeRangeInfo
|
|
38
|
+
): Promise<AttributeRangeInfo> {
|
|
45
39
|
const params = { ...target, attribute_name: attributeName };
|
|
46
40
|
const key = JSON.stringify([rangeSchema.$id, params]);
|
|
47
41
|
let request = rangeRequests.get(key);
|
|
48
42
|
if (request === undefined) {
|
|
49
|
-
request = requestAttributeRange(rangeSchema, params
|
|
43
|
+
request = requestAttributeRange(rangeSchema, params);
|
|
50
44
|
rangeRequests.set(key, request);
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
try {
|
|
47
|
+
const range = await request;
|
|
48
|
+
return range;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// A failed request is not kept: the next call retries it
|
|
51
|
+
rangeRequests.delete(key);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export { type AttributeRangeInfo, type AttributeSchemas, fetchAttributeRange };
|
package/package.json
CHANGED