@geode/opengeodeweb-front 10.35.0-rc.3 → 10.35.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/Launcher.vue +5 -2
- package/app/components/ThresholdFilter.vue +291 -0
- package/app/components/ViewToolbar.vue +16 -0
- package/app/stores/back.ts +10 -6
- package/app/stores/cloud.ts +59 -15
- package/app/stores/infra.ts +7 -1
- package/app/utils/attributes.ts +26 -1
- package/app/utils/cloud.ts +35 -0
- package/internal/stores/hybrid_viewer/filters.ts +24 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import Loading from "@ogw_front/components/Loading.vue";
|
|
|
3
3
|
import Recaptcha from "@ogw_front/components/Recaptcha.vue";
|
|
4
4
|
import { Status } from "@ogw_front/utils/status";
|
|
5
5
|
import { appMode } from "@ogw_shared/app_mode";
|
|
6
|
+
import { getCloudUrlParam } from "@ogw_front/utils/cloud";
|
|
6
7
|
import { useInfraStore } from "@ogw_front/stores/infra";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -15,8 +16,10 @@ interface Props {
|
|
|
15
16
|
const { appName, email = undefined, isUserAuthenticated = false, logo = "" } = defineProps<Props>();
|
|
16
17
|
|
|
17
18
|
const infraStore = useInfraStore();
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
|
|
20
|
+
// Points at an already running service: that one is connected straight away, without authentication.
|
|
21
|
+
if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
|
|
22
|
+
infraStore.create_backend(email);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function cloudCreateBackend(): Promise<void> {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
MESH_ELEMENT_KINDS,
|
|
4
|
+
MODEL_COMPONENT_KINDS,
|
|
5
|
+
getAttributeRange,
|
|
6
|
+
} from "@ogw_front/utils/attributes";
|
|
7
|
+
import type { JsonRpcSchema } from "@ogw_shared/utils/types.js";
|
|
8
|
+
import ToolPanel from "@ogw_front/components/ToolPanel.vue";
|
|
9
|
+
import ViewerOptionsAttributeRangeSelector from "@ogw_front/components/Viewer/Options/AttributeRangeSelector.vue";
|
|
10
|
+
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
|
|
11
|
+
import { useBackStore } from "@ogw_front/stores/back";
|
|
12
|
+
import { useDataStore } from "@ogw_front/stores/data";
|
|
13
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
14
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
15
|
+
|
|
16
|
+
const DEBOUNCE_DELAY = 100;
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
escapeFunction?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { escapeFunction = undefined } = defineProps<Props>();
|
|
23
|
+
|
|
24
|
+
interface AttributeSource {
|
|
25
|
+
title: string;
|
|
26
|
+
schema: JsonRpcSchema;
|
|
27
|
+
params: Record<string, unknown>;
|
|
28
|
+
location: "point" | "cell";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AttributeInfo {
|
|
32
|
+
attribute_name: string;
|
|
33
|
+
nb_items: number;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const show = defineModel<boolean>("show", { default: false });
|
|
38
|
+
const backStore = useBackStore();
|
|
39
|
+
const dataStore = useDataStore();
|
|
40
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
41
|
+
const selectedDatasetId = ref<string>();
|
|
42
|
+
const sources = ref<AttributeSource[]>([]);
|
|
43
|
+
const selectedSourceIndex = ref<number>();
|
|
44
|
+
const attributes = ref<AttributeInfo[]>([]);
|
|
45
|
+
const attributeName = ref<string>();
|
|
46
|
+
const attributeItem = ref<number>(0);
|
|
47
|
+
const minimum = ref<number>();
|
|
48
|
+
const maximum = ref<number>();
|
|
49
|
+
|
|
50
|
+
const allItems = dataStore.refAllItems();
|
|
51
|
+
const availableDatasets = computed<{ title: string; value: string }[]>(() =>
|
|
52
|
+
allItems.value.map((item) => ({
|
|
53
|
+
title: item.name || item.id,
|
|
54
|
+
value: item.id,
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
const availableSources = computed<{ title: string; value: number }[]>(() =>
|
|
58
|
+
sources.value.map((source, index) => ({ title: source.title, value: index })),
|
|
59
|
+
);
|
|
60
|
+
const currentAttribute = computed<AttributeInfo | undefined>(() =>
|
|
61
|
+
attributes.value.find((attribute) => attribute.attribute_name === attributeName.value),
|
|
62
|
+
);
|
|
63
|
+
const itemOptions = computed<{ title: string; value: number }[]>(() =>
|
|
64
|
+
Array.from({ length: currentAttribute.value?.nb_items ?? 0 }, (_, index) => ({
|
|
65
|
+
title: `Item ${index + 1}`,
|
|
66
|
+
value: index,
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
function attributeSchema(kind: string, isModel: boolean): JsonRpcSchema {
|
|
71
|
+
const prefix = isModel ? "model_component_" : "";
|
|
72
|
+
return back_schemas.opengeodeweb_back[
|
|
73
|
+
`${prefix}${kind}_attribute_names` as keyof typeof back_schemas.opengeodeweb_back
|
|
74
|
+
] as JsonRpcSchema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function attributeSource(
|
|
78
|
+
kind: string,
|
|
79
|
+
title: string,
|
|
80
|
+
params: Record<string, unknown>,
|
|
81
|
+
isModel: boolean,
|
|
82
|
+
): AttributeSource {
|
|
83
|
+
return {
|
|
84
|
+
title: `${title}${kind} attribute`,
|
|
85
|
+
schema: attributeSchema(kind, isModel),
|
|
86
|
+
params,
|
|
87
|
+
location: kind === "vertex" ? "point" : "cell",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchSources(id: string): Promise<AttributeSource[]> {
|
|
92
|
+
const item = await dataStore.item(id);
|
|
93
|
+
if (item.viewer_type !== "model") {
|
|
94
|
+
return ["vertex", MESH_ELEMENT_KINDS[item.geode_object_type]]
|
|
95
|
+
.filter((kind): kind is string => kind !== undefined)
|
|
96
|
+
.map((kind) => attributeSource(kind, "", { id }, false));
|
|
97
|
+
}
|
|
98
|
+
const modelSources = await Promise.all(
|
|
99
|
+
Object.entries(MODEL_COMPONENT_KINDS).map(async ([type, kinds]) => {
|
|
100
|
+
const component_ids = await dataStore.getMeshComponentGeodeIds(id, type);
|
|
101
|
+
if (component_ids.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return kinds.map((kind) => attributeSource(kind, `${type} `, { id, component_ids }, true));
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
return modelSources.flat();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function applyThreshold(): Promise<void> {
|
|
111
|
+
const source = sources.value[selectedSourceIndex.value ?? -1];
|
|
112
|
+
if (
|
|
113
|
+
!selectedDatasetId.value ||
|
|
114
|
+
!source ||
|
|
115
|
+
!attributeName.value ||
|
|
116
|
+
minimum.value === undefined ||
|
|
117
|
+
maximum.value === undefined ||
|
|
118
|
+
minimum.value > maximum.value
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await hybridViewerStore.setThreshold([selectedDatasetId.value], {
|
|
123
|
+
name: attributeName.value,
|
|
124
|
+
location: source.location,
|
|
125
|
+
item: attributeItem.value,
|
|
126
|
+
minimum: minimum.value,
|
|
127
|
+
maximum: maximum.value,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const debouncedApply = useDebounceFn(() => applyThreshold(), DEBOUNCE_DELAY);
|
|
132
|
+
|
|
133
|
+
function resetRange(): void {
|
|
134
|
+
const { min, max } = getAttributeRange(
|
|
135
|
+
currentAttribute.value as Parameters<typeof getAttributeRange>[0],
|
|
136
|
+
attributeItem.value,
|
|
137
|
+
);
|
|
138
|
+
minimum.value = min;
|
|
139
|
+
maximum.value = max;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function removeThreshold(): Promise<void> {
|
|
143
|
+
attributeName.value = undefined;
|
|
144
|
+
if (selectedDatasetId.value) {
|
|
145
|
+
await hybridViewerStore.setThreshold([selectedDatasetId.value]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
watch(selectedDatasetId, async (id, previousId) => {
|
|
150
|
+
if (previousId) {
|
|
151
|
+
await hybridViewerStore.setThreshold([previousId]);
|
|
152
|
+
}
|
|
153
|
+
selectedSourceIndex.value = undefined;
|
|
154
|
+
sources.value = id ? await fetchSources(id) : [];
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
watch(selectedSourceIndex, async (index) => {
|
|
158
|
+
attributeName.value = undefined;
|
|
159
|
+
attributes.value = [];
|
|
160
|
+
const source = sources.value[index ?? -1];
|
|
161
|
+
if (!source) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const response = (await backStore.request({
|
|
165
|
+
schema: source.schema,
|
|
166
|
+
params: source.params,
|
|
167
|
+
})) as { attributes: AttributeInfo[] };
|
|
168
|
+
attributes.value = response.attributes;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
watch(attributeName, () => {
|
|
172
|
+
attributeItem.value = 0;
|
|
173
|
+
resetRange();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
watch(attributeItem, () => {
|
|
177
|
+
resetRange();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
watch([minimum, maximum], () => {
|
|
181
|
+
debouncedApply();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
watch(show, (visible) => {
|
|
185
|
+
if (visible) {
|
|
186
|
+
applyThreshold();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<template>
|
|
192
|
+
<ToolPanel
|
|
193
|
+
v-model="show"
|
|
194
|
+
data-testid="thresholdFilterPanel"
|
|
195
|
+
title="Threshold Filter"
|
|
196
|
+
:width="340"
|
|
197
|
+
:click-outside="false"
|
|
198
|
+
:escapeFunction="escapeFunction"
|
|
199
|
+
>
|
|
200
|
+
<v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
|
|
201
|
+
<v-select
|
|
202
|
+
v-model="selectedDatasetId"
|
|
203
|
+
data-testid="thresholdDatasetSelect"
|
|
204
|
+
:items="availableDatasets"
|
|
205
|
+
label="Select a dataset"
|
|
206
|
+
variant="outlined"
|
|
207
|
+
density="compact"
|
|
208
|
+
hide-details
|
|
209
|
+
class="mb-3 text-caption"
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
<v-select
|
|
213
|
+
v-if="selectedDatasetId"
|
|
214
|
+
v-model="selectedSourceIndex"
|
|
215
|
+
data-testid="thresholdAttributeTypeSelect"
|
|
216
|
+
:items="availableSources"
|
|
217
|
+
label="Select an attribute type"
|
|
218
|
+
variant="outlined"
|
|
219
|
+
density="compact"
|
|
220
|
+
hide-details
|
|
221
|
+
class="mb-3 text-caption"
|
|
222
|
+
/>
|
|
223
|
+
|
|
224
|
+
<v-select
|
|
225
|
+
v-if="selectedSourceIndex !== undefined"
|
|
226
|
+
v-model="attributeName"
|
|
227
|
+
data-testid="thresholdAttributeSelect"
|
|
228
|
+
:items="attributes.map((attribute) => attribute.attribute_name)"
|
|
229
|
+
label="Select an attribute"
|
|
230
|
+
variant="outlined"
|
|
231
|
+
density="compact"
|
|
232
|
+
hide-details
|
|
233
|
+
class="mb-3 text-caption"
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<v-select
|
|
237
|
+
v-if="currentAttribute && currentAttribute.nb_items > 1"
|
|
238
|
+
v-model="attributeItem"
|
|
239
|
+
data-testid="thresholdItemSelect"
|
|
240
|
+
:items="itemOptions"
|
|
241
|
+
label="Select an item"
|
|
242
|
+
variant="outlined"
|
|
243
|
+
density="compact"
|
|
244
|
+
hide-details
|
|
245
|
+
class="mb-3 text-caption"
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
<template v-if="attributeName">
|
|
249
|
+
<v-divider class="my-3" />
|
|
250
|
+
<span class="text-caption font-weight-bold">Range</span>
|
|
251
|
+
<ViewerOptionsAttributeRangeSelector
|
|
252
|
+
v-model:minimum="minimum"
|
|
253
|
+
v-model:maximum="maximum"
|
|
254
|
+
class="mt-2"
|
|
255
|
+
@reset="resetRange"
|
|
256
|
+
/>
|
|
257
|
+
</template>
|
|
258
|
+
</v-card-text>
|
|
259
|
+
|
|
260
|
+
<template #actions>
|
|
261
|
+
<v-card-actions class="justify-space-between px-3 pb-3 pt-0">
|
|
262
|
+
<v-btn
|
|
263
|
+
data-testid="removeThresholdButton"
|
|
264
|
+
variant="text"
|
|
265
|
+
size="small"
|
|
266
|
+
color="error"
|
|
267
|
+
class="text-caption text-none"
|
|
268
|
+
@click="removeThreshold"
|
|
269
|
+
>
|
|
270
|
+
Remove Threshold
|
|
271
|
+
</v-btn>
|
|
272
|
+
<v-btn
|
|
273
|
+
data-testid="resetThresholdButton"
|
|
274
|
+
variant="tonal"
|
|
275
|
+
size="small"
|
|
276
|
+
color="secondary"
|
|
277
|
+
class="text-caption text-none"
|
|
278
|
+
@click="resetRange"
|
|
279
|
+
>
|
|
280
|
+
Reset
|
|
281
|
+
</v-btn>
|
|
282
|
+
</v-card-actions>
|
|
283
|
+
</template>
|
|
284
|
+
</ToolPanel>
|
|
285
|
+
</template>
|
|
286
|
+
|
|
287
|
+
<style scoped>
|
|
288
|
+
.max-panel-height {
|
|
289
|
+
max-height: 520px;
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -7,6 +7,7 @@ import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue";
|
|
|
7
7
|
import Ruler from "@ogw_front/components/Ruler.vue";
|
|
8
8
|
import Screenshot from "@ogw_front/components/Screenshot.vue";
|
|
9
9
|
import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue";
|
|
10
|
+
import ThresholdFilter from "@ogw_front/components/ThresholdFilter.vue";
|
|
10
11
|
import ZScaling from "@ogw_front/components/ZScaling.vue";
|
|
11
12
|
import { onKeyStroke } from "@vueuse/core";
|
|
12
13
|
import schemas from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_schemas.json";
|
|
@@ -23,6 +24,7 @@ const showCameraOrientation = ref<boolean>(false);
|
|
|
23
24
|
const showZScaling = ref<boolean>(false);
|
|
24
25
|
const showClippingPlanes = ref<boolean>(false);
|
|
25
26
|
const showShrinkFilter = ref<boolean>(false);
|
|
27
|
+
const showThresholdFilter = ref<boolean>(false);
|
|
26
28
|
const showRuler = ref<boolean>(false);
|
|
27
29
|
const gridScale = ref<boolean>(false);
|
|
28
30
|
const zScale = ref<number>(hybridViewerStore.zScale);
|
|
@@ -73,6 +75,7 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
|
|
|
73
75
|
showZScaling,
|
|
74
76
|
showClippingPlanes,
|
|
75
77
|
showShrinkFilter,
|
|
78
|
+
showThresholdFilter,
|
|
76
79
|
showRuler,
|
|
77
80
|
is_picking,
|
|
78
81
|
is_zoom_box_active,
|
|
@@ -231,6 +234,15 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
231
234
|
toggleTool(showShrinkFilter);
|
|
232
235
|
},
|
|
233
236
|
},
|
|
237
|
+
{
|
|
238
|
+
testId: "thresholdFilterButton",
|
|
239
|
+
tooltip: "Threshold Filter",
|
|
240
|
+
icon: "mdi-filter-variant",
|
|
241
|
+
color: showThresholdFilter.value ? "primary" : undefined,
|
|
242
|
+
action: (): void => {
|
|
243
|
+
toggleTool(showThresholdFilter);
|
|
244
|
+
},
|
|
245
|
+
},
|
|
234
246
|
{
|
|
235
247
|
testId: "rulerButton",
|
|
236
248
|
tooltip: "Ruler",
|
|
@@ -320,6 +332,10 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
320
332
|
v-model:show="showShrinkFilter"
|
|
321
333
|
:escapeFunction="() => (showShrinkFilter = false)"
|
|
322
334
|
/>
|
|
335
|
+
<ThresholdFilter
|
|
336
|
+
v-model:show="showThresholdFilter"
|
|
337
|
+
:escapeFunction="() => (showThresholdFilter = false)"
|
|
338
|
+
/>
|
|
323
339
|
<Ruler v-model:show="showRuler" :escapeFunction="() => (showRuler = false)" />
|
|
324
340
|
</template>
|
|
325
341
|
|
package/app/stores/back.ts
CHANGED
|
@@ -29,12 +29,16 @@ export const useBackStore = defineStore("back", {
|
|
|
29
29
|
return getRestApiPort(this.default_local_port);
|
|
30
30
|
},
|
|
31
31
|
base_url(): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
return this.base_url_for(useInfraStore().domain_name);
|
|
33
|
+
},
|
|
34
|
+
base_url_for(): (domain_name: string) => string {
|
|
35
|
+
return (domain_name: string) => {
|
|
36
|
+
let back_url = `${this.protocol}://${domain_name}:${this.port}`;
|
|
37
|
+
if (isCloudMode()) {
|
|
38
|
+
back_url += `/geode`;
|
|
39
|
+
}
|
|
40
|
+
return back_url;
|
|
41
|
+
};
|
|
38
42
|
},
|
|
39
43
|
is_busy(): boolean {
|
|
40
44
|
return this.request_counter > 0;
|
package/app/stores/cloud.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
|
|
1
2
|
import { Status } from "@ogw_front/utils/status";
|
|
3
|
+
import { api_fetch } from "@ogw_internal/utils/api_fetch";
|
|
4
|
+
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json";
|
|
2
5
|
import { setAppBaseUrl } from "@ogw_shared/scripts";
|
|
3
6
|
import { useAPIStore } from "@ogw_front/stores/api";
|
|
7
|
+
import { useFeedbackStore } from "@ogw_front/stores/feedback";
|
|
4
8
|
|
|
5
9
|
const run_cloud_schema = {
|
|
6
10
|
$id: "/cloud/run",
|
|
@@ -20,13 +24,24 @@ export const useCloudStore = defineStore("cloud", {
|
|
|
20
24
|
status: Status.NOT_CONNECTED,
|
|
21
25
|
}),
|
|
22
26
|
actions: {
|
|
27
|
+
// Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
|
|
28
|
+
// The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
|
|
29
|
+
async start(email: string) {
|
|
30
|
+
const existing_host = getCloudUrlParam();
|
|
31
|
+
if (existing_host === undefined) {
|
|
32
|
+
await this.launch(email);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await this.connect_existing(existing_host);
|
|
37
|
+
} finally {
|
|
38
|
+
clearCloudUrlParam();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
23
41
|
async launch(email: string) {
|
|
24
42
|
this.status = Status.CONNECTING;
|
|
25
43
|
const { PROJECT, BRANCH } = useRuntimeConfig().public;
|
|
26
44
|
const params = { email, project: PROJECT, branch: BRANCH };
|
|
27
|
-
const { useAppStore } = await import("./app");
|
|
28
|
-
const { useFeedbackStore } = await import("./feedback");
|
|
29
|
-
const appStore = useAppStore();
|
|
30
45
|
const feedbackStore = useFeedbackStore();
|
|
31
46
|
const APIStore = useAPIStore();
|
|
32
47
|
const result = await APIStore.request(
|
|
@@ -45,18 +60,7 @@ export const useCloudStore = defineStore("cloud", {
|
|
|
45
60
|
) {
|
|
46
61
|
return;
|
|
47
62
|
}
|
|
48
|
-
|
|
49
|
-
feedbackStore.$patch({ server_error: false });
|
|
50
|
-
this.status = Status.CONNECTED;
|
|
51
|
-
const { useInfraStore } = await import("./infra");
|
|
52
|
-
const infraStore = useInfraStore();
|
|
53
|
-
infraStore.$patch({
|
|
54
|
-
domain_name: url,
|
|
55
|
-
});
|
|
56
|
-
setAppBaseUrl(appStore.base_url).catch(() => undefined);
|
|
57
|
-
appStore.$patch({
|
|
58
|
-
projectFolderPath: "/project",
|
|
59
|
-
});
|
|
63
|
+
await this.on_connected(response.url);
|
|
60
64
|
},
|
|
61
65
|
response_error_function: () => {
|
|
62
66
|
feedbackStore.$patch({ server_error: true });
|
|
@@ -66,6 +70,46 @@ export const useCloudStore = defineStore("cloud", {
|
|
|
66
70
|
);
|
|
67
71
|
return result;
|
|
68
72
|
},
|
|
73
|
+
async connect_existing(host: string) {
|
|
74
|
+
this.status = Status.CONNECTING;
|
|
75
|
+
const { useBackStore } = await import("./back");
|
|
76
|
+
const backStore = useBackStore();
|
|
77
|
+
// Probe the candidate host directly so domain_name is only updated once the service answers.
|
|
78
|
+
try {
|
|
79
|
+
await api_fetch(
|
|
80
|
+
{
|
|
81
|
+
$id: backStore.$id,
|
|
82
|
+
base_url: backStore.base_url_for(host),
|
|
83
|
+
start_request: () => {
|
|
84
|
+
backStore.start_request();
|
|
85
|
+
},
|
|
86
|
+
stop_request: () => {
|
|
87
|
+
backStore.stop_request();
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{ schema: back_schemas.opengeodeweb_back.ping },
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
useFeedbackStore().$patch({ server_error: true });
|
|
94
|
+
this.status = Status.NOT_CONNECTED;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
await this.on_connected(host);
|
|
98
|
+
},
|
|
99
|
+
async on_connected(url: string) {
|
|
100
|
+
const { useAppStore } = await import("./app");
|
|
101
|
+
const { useInfraStore } = await import("./infra");
|
|
102
|
+
const appStore = useAppStore();
|
|
103
|
+
useFeedbackStore().$patch({ server_error: false });
|
|
104
|
+
this.status = Status.CONNECTED;
|
|
105
|
+
useInfraStore().$patch({
|
|
106
|
+
domain_name: url,
|
|
107
|
+
});
|
|
108
|
+
appStore.$patch({
|
|
109
|
+
projectFolderPath: "/project",
|
|
110
|
+
});
|
|
111
|
+
await setAppBaseUrl(appStore.base_url);
|
|
112
|
+
},
|
|
69
113
|
connect() {
|
|
70
114
|
this.status = Status.CONNECTED;
|
|
71
115
|
},
|
package/app/stores/infra.ts
CHANGED
|
@@ -55,7 +55,13 @@ export const useInfraStore = defineStore("infra", {
|
|
|
55
55
|
this.status = Status.CREATING;
|
|
56
56
|
if (this.app_mode === appMode.CLOUD) {
|
|
57
57
|
const cloudStore = useCloudStore();
|
|
58
|
-
|
|
58
|
+
try {
|
|
59
|
+
await cloudStore.start(email ?? "");
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Back to the "Load the app" button so the user is not stuck on the loading screen.
|
|
62
|
+
this.status = Status.NOT_CREATED;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
59
65
|
} else {
|
|
60
66
|
const { useAppStore } = await import("@ogw_front/stores/app");
|
|
61
67
|
const appStore = useAppStore();
|
package/app/utils/attributes.ts
CHANGED
|
@@ -10,7 +10,7 @@ interface AttributeRange {
|
|
|
10
10
|
max: number;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function getAttributeRange(
|
|
14
14
|
currentAttribute: AttributeRangeSource | undefined | null,
|
|
15
15
|
compIndex = 0,
|
|
16
16
|
): AttributeRange {
|
|
@@ -38,3 +38,28 @@ export function getAttributeRange(
|
|
|
38
38
|
|
|
39
39
|
return { min, max };
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
const MESH_ELEMENT_KINDS: Record<string, string> = {
|
|
43
|
+
EdgedCurve2D: "edge",
|
|
44
|
+
EdgedCurve3D: "edge",
|
|
45
|
+
LightRegularGrid2D: "cell",
|
|
46
|
+
LightRegularGrid3D: "cell",
|
|
47
|
+
RegularGrid2D: "cell",
|
|
48
|
+
RegularGrid3D: "cell",
|
|
49
|
+
PolygonalSurface2D: "polygon",
|
|
50
|
+
PolygonalSurface3D: "polygon",
|
|
51
|
+
TriangulatedSurface2D: "polygon",
|
|
52
|
+
TriangulatedSurface3D: "polygon",
|
|
53
|
+
HybridSolid3D: "polyhedron",
|
|
54
|
+
PolyhedralSolid3D: "polyhedron",
|
|
55
|
+
TetrahedralSolid3D: "polyhedron",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const MODEL_COMPONENT_KINDS: Record<string, string[]> = {
|
|
59
|
+
Corner: ["vertex"],
|
|
60
|
+
Line: ["vertex", "edge"],
|
|
61
|
+
Surface: ["vertex", "polygon"],
|
|
62
|
+
Block: ["vertex", "polyhedron"],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { MESH_ELEMENT_KINDS, MODEL_COMPONENT_KINDS, getAttributeRange };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const CLOUD_URL_PARAM = "cloud_url";
|
|
2
|
+
const CLOUD_RUN_HOST_SUFFIX = ".run.app";
|
|
3
|
+
|
|
4
|
+
// Hostname of an already running Cloud Run service passed as `?cloud_url=`, restricted to Cloud Run hosts so a crafted link cannot point the app at an arbitrary server.
|
|
5
|
+
function getCloudUrlParam(): string | undefined {
|
|
6
|
+
const value = new URLSearchParams(globalThis.location.search).get(CLOUD_URL_PARAM);
|
|
7
|
+
if (value === null || value === "") {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const url_string = value.includes("://") ? value : `https://${value}`;
|
|
11
|
+
let hostname = "";
|
|
12
|
+
try {
|
|
13
|
+
({ hostname } = new URL(url_string));
|
|
14
|
+
} catch {
|
|
15
|
+
console.warn(`[Cloud] Invalid ${CLOUD_URL_PARAM}:`, value);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (!hostname.endsWith(CLOUD_RUN_HOST_SUFFIX)) {
|
|
19
|
+
console.warn(`[Cloud] Ignoring ${CLOUD_URL_PARAM}, not a Cloud Run host:`, hostname);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return hostname;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Drops `?cloud_url=` from the address bar, keeping the router's history state, so a later "Load the app" launches a new service.
|
|
26
|
+
function clearCloudUrlParam(): void {
|
|
27
|
+
const url = new URL(globalThis.location.href);
|
|
28
|
+
if (!url.searchParams.has(CLOUD_URL_PARAM)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
url.searchParams.delete(CLOUD_URL_PARAM);
|
|
32
|
+
globalThis.history.replaceState(globalThis.history.state, "", url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { clearCloudUrlParam, getCloudUrlParam };
|
|
@@ -30,13 +30,37 @@ async function setShrink(ids: string[], shrink_factor: number): Promise<void> {
|
|
|
30
30
|
});
|
|
31
31
|
await remoteRender();
|
|
32
32
|
}
|
|
33
|
+
interface ThresholdAttribute {
|
|
34
|
+
name: string;
|
|
35
|
+
location: "point" | "cell";
|
|
36
|
+
item: number;
|
|
37
|
+
minimum: number;
|
|
38
|
+
maximum: number;
|
|
39
|
+
}
|
|
40
|
+
async function setThreshold(ids: string[], attribute?: ThresholdAttribute): Promise<void> {
|
|
41
|
+
const viewerStore = useViewerStore();
|
|
42
|
+
const { remoteRender } = useHybridViewerCore();
|
|
43
|
+
const schema = viewer_schemas.opengeodeweb_viewer.viewer.threshold;
|
|
44
|
+
const params = {
|
|
45
|
+
ids,
|
|
46
|
+
attribute,
|
|
47
|
+
};
|
|
48
|
+
await viewerStore.request({
|
|
49
|
+
schema,
|
|
50
|
+
params,
|
|
51
|
+
});
|
|
52
|
+
await remoteRender();
|
|
53
|
+
}
|
|
33
54
|
function useHybridViewerFilters(): {
|
|
34
55
|
setClippingPlanes: (ids: string[], planes: unknown) => Promise<void>;
|
|
35
56
|
setShrink: (ids: string[], shrink_factor: number) => Promise<void>;
|
|
57
|
+
setThreshold: (ids: string[], attribute?: ThresholdAttribute) => Promise<void>;
|
|
36
58
|
} {
|
|
37
59
|
return {
|
|
38
60
|
setClippingPlanes,
|
|
39
61
|
setShrink,
|
|
62
|
+
setThreshold,
|
|
40
63
|
};
|
|
41
64
|
}
|
|
42
65
|
export { useHybridViewerFilters };
|
|
66
|
+
export type { ThresholdAttribute };
|
package/package.json
CHANGED