@geode/opengeodeweb-front 10.38.0-rc.3 → 10.38.0-rc.5
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/ExplodeFilter.vue +34 -0
- package/app/components/FactorFilterPanel.vue +211 -0
- package/app/components/Launcher.vue +10 -5
- package/app/components/ShrinkFilter.vue +18 -165
- package/app/components/ViewToolbar.vue +16 -0
- package/app/stores/api.ts +12 -11
- package/app/stores/cloud.ts +14 -18
- package/app/stores/infra.ts +2 -2
- package/internal/stores/hybrid_viewer/filters.ts +29 -38
- package/package.json +1 -1
- package/app/components/Recaptcha.vue +0 -89
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import FactorFilterPanel from "@ogw_front/components/FactorFilterPanel.vue";
|
|
3
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_EXPLODE_VALUE = 0.2;
|
|
6
|
+
const MIN_EXPLODE_VALUE = 0;
|
|
7
|
+
const MAX_EXPLODE_VALUE = 1;
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
escapeFunction?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { escapeFunction = undefined } = defineProps<Props>();
|
|
14
|
+
|
|
15
|
+
const show = defineModel<boolean>("show", { default: false });
|
|
16
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<FactorFilterPanel
|
|
21
|
+
v-model:show="show"
|
|
22
|
+
title="Exploded View"
|
|
23
|
+
label="Explode Factor"
|
|
24
|
+
test-id-prefix="explode"
|
|
25
|
+
:set-factor="hybridViewerStore.setExplode"
|
|
26
|
+
:factor-range="{
|
|
27
|
+
min: MIN_EXPLODE_VALUE,
|
|
28
|
+
max: MAX_EXPLODE_VALUE,
|
|
29
|
+
defaultValue: DEFAULT_EXPLODE_VALUE,
|
|
30
|
+
neutralValue: MIN_EXPLODE_VALUE,
|
|
31
|
+
}"
|
|
32
|
+
:escapeFunction="escapeFunction"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import ToolPanel from "@ogw_front/components/ToolPanel.vue";
|
|
3
|
+
import { useDataStore } from "@ogw_front/stores/data";
|
|
4
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
5
|
+
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
6
|
+
|
|
7
|
+
const DEBOUNCE_DELAY = 100;
|
|
8
|
+
|
|
9
|
+
interface FactorRange {
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
defaultValue: number;
|
|
13
|
+
neutralValue: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
title: string;
|
|
18
|
+
label: string;
|
|
19
|
+
testIdPrefix: string;
|
|
20
|
+
setFactor: (ids: string[], factor: number) => Promise<void>;
|
|
21
|
+
factorRange: FactorRange;
|
|
22
|
+
formatValue?: (value: number) => string;
|
|
23
|
+
escapeFunction?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
title,
|
|
28
|
+
label,
|
|
29
|
+
testIdPrefix,
|
|
30
|
+
setFactor,
|
|
31
|
+
factorRange,
|
|
32
|
+
formatValue = (value: number): string => value.toFixed(2),
|
|
33
|
+
escapeFunction = undefined,
|
|
34
|
+
} = defineProps<Props>();
|
|
35
|
+
|
|
36
|
+
const capitalizedPrefix = computed<string>(
|
|
37
|
+
() => `${testIdPrefix.charAt(0).toUpperCase()}${testIdPrefix.slice(1)}`,
|
|
38
|
+
);
|
|
39
|
+
const show = defineModel<boolean>("show", { default: false });
|
|
40
|
+
const dataStore = useDataStore();
|
|
41
|
+
const hybridViewerStore = useHybridViewerStore();
|
|
42
|
+
const targetAllVisible = ref<boolean>(true);
|
|
43
|
+
const selectedDatasetIds = ref<string[]>([]);
|
|
44
|
+
const factor = ref<number>(factorRange.defaultValue);
|
|
45
|
+
|
|
46
|
+
const allItems = dataStore.refAllItems();
|
|
47
|
+
const availableDatasets = computed<{ title: string; value: string }[]>(() =>
|
|
48
|
+
allItems.value.map((item) => ({
|
|
49
|
+
title: item.name || item.id,
|
|
50
|
+
value: item.id,
|
|
51
|
+
})),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let appliedFactor: number | undefined = undefined;
|
|
55
|
+
|
|
56
|
+
async function applyFactor(): Promise<void> {
|
|
57
|
+
appliedFactor = factor.value;
|
|
58
|
+
const allIds = allItems.value.map((item) => item.id);
|
|
59
|
+
if (allIds.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const targetIds = targetAllVisible.value ? allIds : selectedDatasetIds.value;
|
|
63
|
+
const untargetedIds = allIds.filter((id) => !targetIds.includes(id));
|
|
64
|
+
|
|
65
|
+
if (targetIds.length > 0) {
|
|
66
|
+
await setFactor(targetIds, Number(factor.value));
|
|
67
|
+
}
|
|
68
|
+
if (untargetedIds.length > 0) {
|
|
69
|
+
await setFactor(untargetedIds, factorRange.neutralValue);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const debouncedApply = useDebounceFn(() => applyFactor(), DEBOUNCE_DELAY);
|
|
74
|
+
|
|
75
|
+
async function resetFactor(): Promise<void> {
|
|
76
|
+
factor.value = factorRange.defaultValue;
|
|
77
|
+
await applyFactor();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function removeFactor(): Promise<void> {
|
|
81
|
+
factor.value = factorRange.neutralValue;
|
|
82
|
+
await applyFactor();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
watch(factor, (value) => {
|
|
86
|
+
if (value !== appliedFactor) {
|
|
87
|
+
debouncedApply();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
watch(show, (visible) => {
|
|
92
|
+
if (visible) {
|
|
93
|
+
applyFactor();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
watch(
|
|
98
|
+
[targetAllVisible, selectedDatasetIds],
|
|
99
|
+
() => {
|
|
100
|
+
if (show.value) {
|
|
101
|
+
applyFactor();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{ deep: true },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
watch(allItems, () => {
|
|
108
|
+
if (show.value) {
|
|
109
|
+
applyFactor();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
watch(
|
|
114
|
+
() => Object.values(hybridViewerStore.hybridDb).filter((entry) => entry && entry.actor).length,
|
|
115
|
+
(actorCount) => {
|
|
116
|
+
if (show.value && actorCount > 0) {
|
|
117
|
+
applyFactor();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<ToolPanel
|
|
125
|
+
v-model="show"
|
|
126
|
+
:title="title"
|
|
127
|
+
:width="340"
|
|
128
|
+
:click-outside="false"
|
|
129
|
+
:escapeFunction="escapeFunction"
|
|
130
|
+
>
|
|
131
|
+
<v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
|
|
132
|
+
<v-switch
|
|
133
|
+
v-model="targetAllVisible"
|
|
134
|
+
:data-testid="`${testIdPrefix}TargetAllVisibleSwitch`"
|
|
135
|
+
label="Apply to all visible datasets"
|
|
136
|
+
color="primary"
|
|
137
|
+
density="compact"
|
|
138
|
+
hide-details
|
|
139
|
+
class="mb-2 text-caption"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<v-select
|
|
143
|
+
v-if="!targetAllVisible"
|
|
144
|
+
v-model="selectedDatasetIds"
|
|
145
|
+
:data-testid="`${testIdPrefix}SelectedDatasetsSelect`"
|
|
146
|
+
:items="availableDatasets"
|
|
147
|
+
label="Select datasets"
|
|
148
|
+
multiple
|
|
149
|
+
chips
|
|
150
|
+
closable-chips
|
|
151
|
+
variant="outlined"
|
|
152
|
+
density="compact"
|
|
153
|
+
hide-details
|
|
154
|
+
class="mb-3 text-caption"
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
<v-divider class="my-3" />
|
|
158
|
+
|
|
159
|
+
<div class="d-flex align-center justify-space-between mb-1">
|
|
160
|
+
<span class="text-caption font-weight-bold">{{ label }}</span>
|
|
161
|
+
<span class="text-caption text-primary font-weight-bold">
|
|
162
|
+
{{ formatValue(factor) }}
|
|
163
|
+
</span>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<v-slider
|
|
167
|
+
v-model="factor"
|
|
168
|
+
:data-testid="`${testIdPrefix}FactorSlider`"
|
|
169
|
+
:min="factorRange.min"
|
|
170
|
+
:max="factorRange.max"
|
|
171
|
+
step="0.01"
|
|
172
|
+
color="primary"
|
|
173
|
+
track-color="grey-lighten-2"
|
|
174
|
+
density="compact"
|
|
175
|
+
hide-details
|
|
176
|
+
class="my-2 px-1"
|
|
177
|
+
/>
|
|
178
|
+
</v-card-text>
|
|
179
|
+
|
|
180
|
+
<template #actions>
|
|
181
|
+
<v-card-actions class="justify-space-between px-3 pb-3 pt-0">
|
|
182
|
+
<v-btn
|
|
183
|
+
:data-testid="`remove${capitalizedPrefix}Button`"
|
|
184
|
+
variant="text"
|
|
185
|
+
size="small"
|
|
186
|
+
color="error"
|
|
187
|
+
class="text-caption text-none"
|
|
188
|
+
@click="removeFactor"
|
|
189
|
+
>
|
|
190
|
+
Remove {{ capitalizedPrefix }}
|
|
191
|
+
</v-btn>
|
|
192
|
+
<v-btn
|
|
193
|
+
:data-testid="`reset${capitalizedPrefix}Button`"
|
|
194
|
+
variant="tonal"
|
|
195
|
+
size="small"
|
|
196
|
+
color="secondary"
|
|
197
|
+
class="text-caption text-none"
|
|
198
|
+
@click="resetFactor"
|
|
199
|
+
>
|
|
200
|
+
Reset
|
|
201
|
+
</v-btn>
|
|
202
|
+
</v-card-actions>
|
|
203
|
+
</template>
|
|
204
|
+
</ToolPanel>
|
|
205
|
+
</template>
|
|
206
|
+
|
|
207
|
+
<style scoped>
|
|
208
|
+
.max-panel-height {
|
|
209
|
+
max-height: 520px;
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import Loading from "@ogw_front/components/Loading.vue";
|
|
3
|
-
import Recaptcha from "@ogw_front/components/Recaptcha.vue";
|
|
4
3
|
import { Status } from "@ogw_front/utils/status";
|
|
5
4
|
import { appMode } from "@ogw_shared/app_mode";
|
|
6
5
|
import { getCloudUrlParam } from "@ogw_front/utils/cloud";
|
|
@@ -8,22 +7,28 @@ import { useInfraStore } from "@ogw_front/stores/infra";
|
|
|
8
7
|
|
|
9
8
|
interface Props {
|
|
10
9
|
appName: string;
|
|
11
|
-
|
|
10
|
+
// Firebase ID token for the cloud launch; required in cloud mode.
|
|
11
|
+
authToken?: string;
|
|
12
12
|
isUserAuthenticated?: boolean;
|
|
13
13
|
logo?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
appName,
|
|
18
|
+
authToken = undefined,
|
|
19
|
+
isUserAuthenticated = false,
|
|
20
|
+
logo = "",
|
|
21
|
+
} = defineProps<Props>();
|
|
17
22
|
|
|
18
23
|
const infraStore = useInfraStore();
|
|
19
24
|
// In cloud mode, wait for the user to authenticate and load the app, unless a `?cloud_url=` link
|
|
20
25
|
// Points at an already running service: that one is connected straight away, without authentication.
|
|
21
26
|
if (infraStore.app_mode !== appMode.CLOUD || getCloudUrlParam() !== undefined) {
|
|
22
|
-
infraStore.create_backend(
|
|
27
|
+
infraStore.create_backend(authToken);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function cloudCreateBackend(): Promise<void> {
|
|
26
|
-
return infraStore.create_backend(
|
|
31
|
+
return infraStore.create_backend(authToken);
|
|
27
32
|
}
|
|
28
33
|
</script>
|
|
29
34
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import { useDataStore } from "@ogw_front/stores/data";
|
|
4
|
-
import { useDebounceFn } from "@vueuse/core";
|
|
2
|
+
import FactorFilterPanel from "@ogw_front/components/FactorFilterPanel.vue";
|
|
5
3
|
import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
|
|
6
4
|
|
|
7
5
|
const DEFAULT_SHRINK_VALUE = 0.8;
|
|
6
|
+
const MIN_SHRINK_VALUE = 0;
|
|
8
7
|
const MAX_SHRINK_VALUE = 1;
|
|
9
|
-
const
|
|
8
|
+
const PERCENT = 100;
|
|
10
9
|
|
|
11
10
|
interface Props {
|
|
12
11
|
escapeFunction?: () => void;
|
|
@@ -15,173 +14,27 @@ interface Props {
|
|
|
15
14
|
const { escapeFunction = undefined } = defineProps<Props>();
|
|
16
15
|
|
|
17
16
|
const show = defineModel<boolean>("show", { default: false });
|
|
18
|
-
const dataStore = useDataStore();
|
|
19
17
|
const hybridViewerStore = useHybridViewerStore();
|
|
20
|
-
const targetAllVisible = ref<boolean>(true);
|
|
21
|
-
const selectedDatasetIds = ref<string[]>([]);
|
|
22
|
-
const shrinkFactor = ref<number>(DEFAULT_SHRINK_VALUE);
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
allItems.value.map((item) => ({
|
|
27
|
-
title: item.name || item.id,
|
|
28
|
-
value: item.id,
|
|
29
|
-
})),
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
async function applyShrink(): Promise<void> {
|
|
33
|
-
const allIds = allItems.value.map((item) => item.id);
|
|
34
|
-
if (allIds.length === 0) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const targetIds = targetAllVisible.value ? allIds : selectedDatasetIds.value;
|
|
38
|
-
const untargetedIds = allIds.filter((id) => !targetIds.includes(id));
|
|
39
|
-
|
|
40
|
-
if (targetIds.length > 0) {
|
|
41
|
-
await hybridViewerStore.setShrink(targetIds, Number(shrinkFactor.value));
|
|
42
|
-
}
|
|
43
|
-
if (untargetedIds.length > 0) {
|
|
44
|
-
await hybridViewerStore.setShrink(untargetedIds, MAX_SHRINK_VALUE);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const debouncedApply = useDebounceFn(() => applyShrink(), DEBOUNCE_DELAY);
|
|
49
|
-
|
|
50
|
-
async function resetShrink(): Promise<void> {
|
|
51
|
-
shrinkFactor.value = DEFAULT_SHRINK_VALUE;
|
|
52
|
-
await applyShrink();
|
|
19
|
+
function formatShrinkFactor(value: number): string {
|
|
20
|
+
return `${(value * PERCENT).toFixed(0)}% (${value.toFixed(2)})`;
|
|
53
21
|
}
|
|
54
|
-
|
|
55
|
-
async function removeShrink(): Promise<void> {
|
|
56
|
-
shrinkFactor.value = MAX_SHRINK_VALUE;
|
|
57
|
-
const allIds = allItems.value.map((item) => item.id);
|
|
58
|
-
if (allIds.length > 0) {
|
|
59
|
-
await hybridViewerStore.setShrink(allIds, MAX_SHRINK_VALUE);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
watch(shrinkFactor, () => {
|
|
64
|
-
debouncedApply();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
watch(show, (visible) => {
|
|
68
|
-
if (visible) {
|
|
69
|
-
applyShrink();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
watch(
|
|
74
|
-
[targetAllVisible, selectedDatasetIds],
|
|
75
|
-
() => {
|
|
76
|
-
if (show.value) {
|
|
77
|
-
applyShrink();
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
{ deep: true },
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
watch(allItems, () => {
|
|
84
|
-
if (show.value) {
|
|
85
|
-
applyShrink();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
watch(
|
|
90
|
-
() => Object.values(hybridViewerStore.hybridDb).filter((entry) => entry && entry.actor).length,
|
|
91
|
-
(actorCount) => {
|
|
92
|
-
if (show.value && actorCount > 0) {
|
|
93
|
-
applyShrink();
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
22
|
</script>
|
|
98
23
|
|
|
99
24
|
<template>
|
|
100
|
-
<
|
|
101
|
-
v-model="show"
|
|
25
|
+
<FactorFilterPanel
|
|
26
|
+
v-model:show="show"
|
|
102
27
|
title="Shrink Filter"
|
|
103
|
-
|
|
104
|
-
|
|
28
|
+
label="Shrink Factor"
|
|
29
|
+
test-id-prefix="shrink"
|
|
30
|
+
:set-factor="hybridViewerStore.setShrink"
|
|
31
|
+
:factor-range="{
|
|
32
|
+
min: MIN_SHRINK_VALUE,
|
|
33
|
+
max: MAX_SHRINK_VALUE,
|
|
34
|
+
defaultValue: DEFAULT_SHRINK_VALUE,
|
|
35
|
+
neutralValue: MAX_SHRINK_VALUE,
|
|
36
|
+
}"
|
|
37
|
+
:format-value="formatShrinkFactor"
|
|
105
38
|
:escapeFunction="escapeFunction"
|
|
106
|
-
|
|
107
|
-
<v-card-text class="pa-3 max-panel-height overflow-y-auto overflow-x-hidden">
|
|
108
|
-
<v-switch
|
|
109
|
-
v-model="targetAllVisible"
|
|
110
|
-
data-testid="shrinkTargetAllVisibleSwitch"
|
|
111
|
-
label="Apply to all visible datasets"
|
|
112
|
-
color="primary"
|
|
113
|
-
density="compact"
|
|
114
|
-
hide-details
|
|
115
|
-
class="mb-2 text-caption"
|
|
116
|
-
/>
|
|
117
|
-
|
|
118
|
-
<v-select
|
|
119
|
-
v-if="!targetAllVisible"
|
|
120
|
-
v-model="selectedDatasetIds"
|
|
121
|
-
data-testid="shrinkSelectedDatasetsSelect"
|
|
122
|
-
:items="availableDatasets"
|
|
123
|
-
label="Select datasets"
|
|
124
|
-
multiple
|
|
125
|
-
chips
|
|
126
|
-
closable-chips
|
|
127
|
-
variant="outlined"
|
|
128
|
-
density="compact"
|
|
129
|
-
hide-details
|
|
130
|
-
class="mb-3 text-caption"
|
|
131
|
-
/>
|
|
132
|
-
|
|
133
|
-
<v-divider class="my-3" />
|
|
134
|
-
|
|
135
|
-
<div class="d-flex align-center justify-space-between mb-1">
|
|
136
|
-
<span class="text-caption font-weight-bold">Shrink Factor</span>
|
|
137
|
-
<span class="text-caption text-primary font-weight-bold">
|
|
138
|
-
{{ (shrinkFactor * 100).toFixed(0) }}% ({{ shrinkFactor.toFixed(2) }})
|
|
139
|
-
</span>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<v-slider
|
|
143
|
-
v-model="shrinkFactor"
|
|
144
|
-
data-testid="shrinkFactorSlider"
|
|
145
|
-
min="0.0"
|
|
146
|
-
max="1.0"
|
|
147
|
-
step="0.01"
|
|
148
|
-
color="primary"
|
|
149
|
-
track-color="grey-lighten-2"
|
|
150
|
-
density="compact"
|
|
151
|
-
hide-details
|
|
152
|
-
class="my-2 px-1"
|
|
153
|
-
/>
|
|
154
|
-
</v-card-text>
|
|
155
|
-
|
|
156
|
-
<template #actions>
|
|
157
|
-
<v-card-actions class="justify-space-between px-3 pb-3 pt-0">
|
|
158
|
-
<v-btn
|
|
159
|
-
data-testid="removeShrinkButton"
|
|
160
|
-
variant="text"
|
|
161
|
-
size="small"
|
|
162
|
-
color="error"
|
|
163
|
-
class="text-caption text-none"
|
|
164
|
-
@click="removeShrink"
|
|
165
|
-
>
|
|
166
|
-
Remove Shrink
|
|
167
|
-
</v-btn>
|
|
168
|
-
<v-btn
|
|
169
|
-
data-testid="resetShrinkButton"
|
|
170
|
-
variant="tonal"
|
|
171
|
-
size="small"
|
|
172
|
-
color="secondary"
|
|
173
|
-
class="text-caption text-none"
|
|
174
|
-
@click="resetShrink"
|
|
175
|
-
>
|
|
176
|
-
Reset
|
|
177
|
-
</v-btn>
|
|
178
|
-
</v-card-actions>
|
|
179
|
-
</template>
|
|
180
|
-
</ToolPanel>
|
|
39
|
+
/>
|
|
181
40
|
</template>
|
|
182
|
-
|
|
183
|
-
<style scoped>
|
|
184
|
-
.max-panel-height {
|
|
185
|
-
max-height: 520px;
|
|
186
|
-
}
|
|
187
|
-
</style>
|
|
@@ -7,6 +7,7 @@ import CameraBookmarkIcon from "@ogw_front/assets/viewer_svgs/camera-bookmark.sv
|
|
|
7
7
|
import CameraManager from "@ogw_front/components/CameraManager.vue";
|
|
8
8
|
import CameraOrientation from "@ogw_front/components/CameraOrientation.vue";
|
|
9
9
|
import ClippingPlanes from "@ogw_front/components/ClippingPlanes.vue";
|
|
10
|
+
import ExplodeFilter from "@ogw_front/components/ExplodeFilter.vue";
|
|
10
11
|
import Ruler from "@ogw_front/components/Ruler.vue";
|
|
11
12
|
import Screenshot from "@ogw_front/components/Screenshot.vue";
|
|
12
13
|
import ShrinkFilter from "@ogw_front/components/ShrinkFilter.vue";
|
|
@@ -26,6 +27,7 @@ const showCameraOrientation = ref<boolean>(false);
|
|
|
26
27
|
const showZScaling = ref<boolean>(false);
|
|
27
28
|
const showClippingPlanes = ref<boolean>(false);
|
|
28
29
|
const showShrinkFilter = ref<boolean>(false);
|
|
30
|
+
const showExplodeFilter = ref<boolean>(false);
|
|
29
31
|
const showThresholdFilter = ref<boolean>(false);
|
|
30
32
|
const showRuler = ref<boolean>(false);
|
|
31
33
|
const gridScale = ref<boolean>(false);
|
|
@@ -77,6 +79,7 @@ function closeAllToolsExcept(toolRef: Ref<boolean>): void {
|
|
|
77
79
|
showZScaling,
|
|
78
80
|
showClippingPlanes,
|
|
79
81
|
showShrinkFilter,
|
|
82
|
+
showExplodeFilter,
|
|
80
83
|
showThresholdFilter,
|
|
81
84
|
showRuler,
|
|
82
85
|
is_picking,
|
|
@@ -236,6 +239,15 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
236
239
|
toggleTool(showShrinkFilter);
|
|
237
240
|
},
|
|
238
241
|
},
|
|
242
|
+
{
|
|
243
|
+
testId: "explodeFilterButton",
|
|
244
|
+
tooltip: "Exploded View",
|
|
245
|
+
icon: "mdi-arrow-expand-all",
|
|
246
|
+
color: showExplodeFilter.value ? "primary" : undefined,
|
|
247
|
+
action: (): void => {
|
|
248
|
+
toggleTool(showExplodeFilter);
|
|
249
|
+
},
|
|
250
|
+
},
|
|
239
251
|
{
|
|
240
252
|
testId: "thresholdFilterButton",
|
|
241
253
|
tooltip: "Threshold Filter",
|
|
@@ -334,6 +346,10 @@ const camera_options = computed<CameraOptionAction[]>(() => [
|
|
|
334
346
|
v-model:show="showShrinkFilter"
|
|
335
347
|
:escapeFunction="() => (showShrinkFilter = false)"
|
|
336
348
|
/>
|
|
349
|
+
<ExplodeFilter
|
|
350
|
+
v-model:show="showExplodeFilter"
|
|
351
|
+
:escapeFunction="() => (showExplodeFilter = false)"
|
|
352
|
+
/>
|
|
337
353
|
<ThresholdFilter
|
|
338
354
|
v-model:show="showThresholdFilter"
|
|
339
355
|
:escapeFunction="() => (showThresholdFilter = false)"
|
package/app/stores/api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RequestHandlers } from "@ogw_shared/utils/types";
|
|
1
|
+
import type { ParamsOf, RequestHandlers, ResponseOf } from "@ogw_shared/utils/types";
|
|
2
2
|
import { api_fetch } from "@ogw_internal/utils/api_fetch";
|
|
3
3
|
import { consola } from "consola";
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ interface ApiSchema {
|
|
|
8
8
|
[key: string]: unknown;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
type ApiCallbacks = RequestHandlers & {
|
|
11
|
+
type ApiCallbacks<Response = unknown> = RequestHandlers<Response> & {
|
|
12
12
|
skip_feedback_error?: boolean;
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -26,21 +26,21 @@ export const useAPIStore = defineStore("api", () => {
|
|
|
26
26
|
request_counter.value -= 1;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
async function request<
|
|
29
|
+
// The response type comes from the schema's generated `response` type (see the Cloud API typed schemas), asserted at the API boundary below, not verified
|
|
30
|
+
async function request<Schema extends ApiSchema>(
|
|
31
31
|
{
|
|
32
32
|
schema,
|
|
33
|
-
params
|
|
33
|
+
params,
|
|
34
34
|
headers = {},
|
|
35
|
-
}: { schema:
|
|
36
|
-
callbacks: ApiCallbacks = {},
|
|
37
|
-
): Promise<
|
|
35
|
+
}: { schema: Schema; params?: ParamsOf<Schema>; headers?: Record<string, string> },
|
|
36
|
+
callbacks: ApiCallbacks<ResponseOf<Schema>> = {},
|
|
37
|
+
): Promise<ResponseOf<Schema>> {
|
|
38
38
|
consola.info("[API] Request:", schema.$id);
|
|
39
39
|
const start = Date.now();
|
|
40
40
|
|
|
41
41
|
const result = await api_fetch(
|
|
42
42
|
{ $id: schema.$id, base_url: base_url.value, start_request, stop_request },
|
|
43
|
-
{ schema, params, headers },
|
|
43
|
+
{ schema, params: params ?? {}, headers },
|
|
44
44
|
{
|
|
45
45
|
...callbacks,
|
|
46
46
|
response_function: async (response: unknown) => {
|
|
@@ -52,13 +52,14 @@ export const useAPIStore = defineStore("api", () => {
|
|
|
52
52
|
"s",
|
|
53
53
|
);
|
|
54
54
|
if (callbacks.response_function) {
|
|
55
|
-
|
|
55
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
|
|
56
|
+
await callbacks.response_function(response as ResponseOf<Schema>);
|
|
56
57
|
}
|
|
57
58
|
},
|
|
58
59
|
},
|
|
59
60
|
);
|
|
60
61
|
// oxlint-disable-next-line no-unsafe-type-assertion -- trusted API boundary; see comment above.
|
|
61
|
-
return result as
|
|
62
|
+
return result as ResponseOf<Schema>;
|
|
62
63
|
}
|
|
63
64
|
return {
|
|
64
65
|
base_url,
|
package/app/stores/cloud.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { clearCloudUrlParam, getCloudUrlParam } from "@ogw_front/utils/cloud";
|
|
2
|
-
import type { RunCloudResponse } from "@geode/cloud-api/types";
|
|
3
2
|
import { Status } from "@ogw_front/utils/status";
|
|
4
3
|
import { api_fetch } from "@ogw_internal/utils/api_fetch";
|
|
5
4
|
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
|
|
6
|
-
import cloud_api_schemas from "@geode/cloud-api/
|
|
5
|
+
import cloud_api_schemas from "@geode/cloud-api/cloud_api_typed_schemas.js";
|
|
7
6
|
import { setAppBaseUrl } from "@ogw_shared/scripts";
|
|
8
7
|
import { useAPIStore } from "@ogw_front/stores/api";
|
|
9
8
|
import { useFeedbackStore } from "@ogw_front/stores/feedback";
|
|
@@ -17,10 +16,13 @@ export const useCloudStore = defineStore("cloud", {
|
|
|
17
16
|
actions: {
|
|
18
17
|
// Reuses the running service given by `?cloud_url=` when present, otherwise launches a new one.
|
|
19
18
|
// The param is single use: it is cleared whatever the outcome, so a retry launches a new service.
|
|
20
|
-
async start(
|
|
19
|
+
async start(authToken?: string) {
|
|
21
20
|
const existing_host = getCloudUrlParam();
|
|
22
21
|
if (existing_host === undefined) {
|
|
23
|
-
|
|
22
|
+
if (authToken === undefined) {
|
|
23
|
+
throw new Error("Launching a cloud service requires an authenticated user");
|
|
24
|
+
}
|
|
25
|
+
await this.launch(authToken);
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
28
|
try {
|
|
@@ -29,29 +31,23 @@ export const useCloudStore = defineStore("cloud", {
|
|
|
29
31
|
clearCloudUrlParam();
|
|
30
32
|
}
|
|
31
33
|
},
|
|
32
|
-
|
|
34
|
+
// `authToken` is the user's Firebase ID token, checked by the Cloud API
|
|
35
|
+
async launch(authToken: string) {
|
|
33
36
|
this.status = Status.CONNECTING;
|
|
34
37
|
const { PROJECT, BRANCH } = useRuntimeConfig().public;
|
|
35
|
-
const params = {
|
|
38
|
+
const params = { project: PROJECT, branch: BRANCH };
|
|
39
|
+
const headers = { Authorization: `Bearer ${authToken}` };
|
|
36
40
|
const feedbackStore = useFeedbackStore();
|
|
37
41
|
const APIStore = useAPIStore();
|
|
38
|
-
const result = await APIStore.request
|
|
39
|
-
{ schema: run_cloud_schema, params },
|
|
42
|
+
const result = await APIStore.request(
|
|
43
|
+
{ schema: run_cloud_schema, params, headers },
|
|
40
44
|
{
|
|
41
45
|
request_error_function: () => {
|
|
42
46
|
feedbackStore.$patch({ server_error: true });
|
|
43
47
|
this.status = Status.NOT_CONNECTED;
|
|
44
48
|
},
|
|
45
|
-
response_function: async (
|
|
46
|
-
|
|
47
|
-
typeof response !== "object" ||
|
|
48
|
-
response === null ||
|
|
49
|
-
!("url" in response) ||
|
|
50
|
-
typeof response.url !== "string"
|
|
51
|
-
) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
await this.on_connected(response.url);
|
|
49
|
+
response_function: async ({ url }) => {
|
|
50
|
+
await this.on_connected(url);
|
|
55
51
|
},
|
|
56
52
|
response_error_function: () => {
|
|
57
53
|
feedbackStore.$patch({ server_error: true });
|
package/app/stores/infra.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const useInfraStore = defineStore("infra", {
|
|
|
44
44
|
(microservice: Microservice) => microservice.$id !== microserviceId,
|
|
45
45
|
);
|
|
46
46
|
},
|
|
47
|
-
async create_backend(
|
|
47
|
+
async create_backend(authToken?: string) {
|
|
48
48
|
if (this.status === Status.CREATED) {
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
@@ -56,7 +56,7 @@ export const useInfraStore = defineStore("infra", {
|
|
|
56
56
|
if (this.app_mode === appMode.CLOUD) {
|
|
57
57
|
const cloudStore = useCloudStore();
|
|
58
58
|
try {
|
|
59
|
-
await cloudStore.start(
|
|
59
|
+
await cloudStore.start(authToken);
|
|
60
60
|
} catch (error) {
|
|
61
61
|
// Back to the "Load the app" button so the user is not stuck on the loading screen.
|
|
62
62
|
this.status = Status.NOT_CREATED;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonRpcSchema, ParamsOf, ResponseOf } from "@ogw_shared/utils/types.js";
|
|
1
2
|
import viewer_schemas, {
|
|
2
3
|
type ViewerClippingPlanesParams,
|
|
3
4
|
} from "@geode/opengeodeweb-viewer/opengeodeweb_viewer_typed_schemas.js";
|
|
@@ -5,55 +6,51 @@ import type { SliceAxis } from "@ogw_front/utils/slice";
|
|
|
5
6
|
import { useHybridViewerCore } from "./core";
|
|
6
7
|
import { useViewerStore } from "@ogw_front/stores/viewer";
|
|
7
8
|
|
|
9
|
+
async function requestAndRender<Schema extends JsonRpcSchema>(
|
|
10
|
+
schema: Schema,
|
|
11
|
+
params: ParamsOf<Schema>,
|
|
12
|
+
): Promise<ResponseOf<Schema>> {
|
|
13
|
+
const viewerStore = useViewerStore();
|
|
14
|
+
const { remoteRender } = useHybridViewerCore();
|
|
15
|
+
const response = await viewerStore.request({
|
|
16
|
+
schema,
|
|
17
|
+
params,
|
|
18
|
+
});
|
|
19
|
+
await remoteRender();
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
async function setClippingPlanes(
|
|
9
24
|
ids: string[],
|
|
10
25
|
planes: ViewerClippingPlanesParams["planes"],
|
|
11
26
|
): Promise<void> {
|
|
12
|
-
|
|
13
|
-
const { remoteRender } = useHybridViewerCore();
|
|
14
|
-
const schema = viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes;
|
|
15
|
-
const params = {
|
|
27
|
+
await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.clipping_planes, {
|
|
16
28
|
ids,
|
|
17
29
|
planes,
|
|
18
|
-
};
|
|
19
|
-
await viewerStore.request({
|
|
20
|
-
schema,
|
|
21
|
-
params,
|
|
22
30
|
});
|
|
23
|
-
await remoteRender();
|
|
24
31
|
}
|
|
25
32
|
async function setShrink(ids: string[], shrink_factor: number): Promise<void> {
|
|
26
|
-
|
|
27
|
-
const { remoteRender } = useHybridViewerCore();
|
|
28
|
-
const schema = viewer_schemas.opengeodeweb_viewer.viewer.shrink;
|
|
29
|
-
const params = {
|
|
33
|
+
await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.shrink, {
|
|
30
34
|
ids,
|
|
31
35
|
shrink_factor,
|
|
32
|
-
};
|
|
33
|
-
await viewerStore.request({
|
|
34
|
-
schema,
|
|
35
|
-
params,
|
|
36
36
|
});
|
|
37
|
-
|
|
37
|
+
}
|
|
38
|
+
async function setExplode(ids: string[], explode_factor: number): Promise<void> {
|
|
39
|
+
await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.explode, {
|
|
40
|
+
ids,
|
|
41
|
+
explode_factor,
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
async function setSlice(
|
|
40
45
|
ids: string[],
|
|
41
46
|
slices: { axis: SliceAxis; index: number }[],
|
|
42
47
|
): Promise<[number, number, number]> {
|
|
43
|
-
const viewerStore = useViewerStore();
|
|
44
|
-
const { remoteRender } = useHybridViewerCore();
|
|
45
|
-
const schema = viewer_schemas.opengeodeweb_viewer.viewer.slice;
|
|
46
|
-
const params = {
|
|
47
|
-
ids,
|
|
48
|
-
slices,
|
|
49
|
-
};
|
|
50
48
|
const {
|
|
51
49
|
max_indices: [max_i = 0, max_j = 0, max_k = 0],
|
|
52
|
-
} = await
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
} = await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.slice, {
|
|
51
|
+
ids,
|
|
52
|
+
slices,
|
|
55
53
|
});
|
|
56
|
-
await remoteRender();
|
|
57
54
|
return [max_i, max_j, max_k];
|
|
58
55
|
}
|
|
59
56
|
interface ThresholdAttribute {
|
|
@@ -64,22 +61,15 @@ interface ThresholdAttribute {
|
|
|
64
61
|
maximum: number;
|
|
65
62
|
}
|
|
66
63
|
async function setThreshold(ids: string[], attribute?: ThresholdAttribute): Promise<void> {
|
|
67
|
-
|
|
68
|
-
const { remoteRender } = useHybridViewerCore();
|
|
69
|
-
const schema = viewer_schemas.opengeodeweb_viewer.viewer.threshold;
|
|
70
|
-
const params = {
|
|
64
|
+
await requestAndRender(viewer_schemas.opengeodeweb_viewer.viewer.threshold, {
|
|
71
65
|
ids,
|
|
72
66
|
attribute,
|
|
73
|
-
};
|
|
74
|
-
await viewerStore.request({
|
|
75
|
-
schema,
|
|
76
|
-
params,
|
|
77
67
|
});
|
|
78
|
-
await remoteRender();
|
|
79
68
|
}
|
|
80
69
|
function useHybridViewerFilters(): {
|
|
81
70
|
setClippingPlanes: (ids: string[], planes: ViewerClippingPlanesParams["planes"]) => Promise<void>;
|
|
82
71
|
setShrink: (ids: string[], shrink_factor: number) => Promise<void>;
|
|
72
|
+
setExplode: (ids: string[], explode_factor: number) => Promise<void>;
|
|
83
73
|
setSlice: (
|
|
84
74
|
ids: string[],
|
|
85
75
|
slices: { axis: SliceAxis; index: number }[],
|
|
@@ -89,6 +79,7 @@ function useHybridViewerFilters(): {
|
|
|
89
79
|
return {
|
|
90
80
|
setClippingPlanes,
|
|
91
81
|
setShrink,
|
|
82
|
+
setExplode,
|
|
92
83
|
setSlice,
|
|
93
84
|
setThreshold,
|
|
94
85
|
};
|
package/package.json
CHANGED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { useInfraStore } from "@ogw_front/stores/infra";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
buttonLabel?: string;
|
|
6
|
-
buttonColor?: string;
|
|
7
|
-
color?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const {
|
|
11
|
-
buttonLabel = "Load the app",
|
|
12
|
-
buttonColor = "white",
|
|
13
|
-
color = undefined,
|
|
14
|
-
} = defineProps<Props>();
|
|
15
|
-
|
|
16
|
-
const name = ref("");
|
|
17
|
-
const email = ref("");
|
|
18
|
-
const launch = ref(false);
|
|
19
|
-
const valid = ref(false);
|
|
20
|
-
const emailRules = [
|
|
21
|
-
(value: string): boolean | string => {
|
|
22
|
-
if (value) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
return "E-mail is required.";
|
|
26
|
-
},
|
|
27
|
-
(value: string): boolean | string => {
|
|
28
|
-
if (/.+@.+\..+/u.test(value)) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return "E-mail must be valid.";
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
function submit(): Promise<void> {
|
|
36
|
-
const infraStore = useInfraStore();
|
|
37
|
-
return infraStore.create_backend(email.value);
|
|
38
|
-
}
|
|
39
|
-
</script>
|
|
40
|
-
|
|
41
|
-
<template>
|
|
42
|
-
<VRow align="center" justify="center" style="display: none">
|
|
43
|
-
<VCol cols="4">
|
|
44
|
-
<VForm v-model="valid">
|
|
45
|
-
<VContainer>
|
|
46
|
-
<VRow>
|
|
47
|
-
<VCol>
|
|
48
|
-
<VTextField v-model="name" label="Name" required />
|
|
49
|
-
</VCol>
|
|
50
|
-
</VRow>
|
|
51
|
-
<VRow>
|
|
52
|
-
<VCol>
|
|
53
|
-
<VTextField v-model="email" :rules="emailRules" label="E-mail" required />
|
|
54
|
-
</VCol>
|
|
55
|
-
</VRow>
|
|
56
|
-
<VRow>
|
|
57
|
-
<VCol>
|
|
58
|
-
<VCheckbox label="Load the app" v-model="launch" />
|
|
59
|
-
</VCol>
|
|
60
|
-
</VRow>
|
|
61
|
-
</VContainer>
|
|
62
|
-
</VForm>
|
|
63
|
-
</VCol>
|
|
64
|
-
</VRow>
|
|
65
|
-
<VRow align="center" justify="center">
|
|
66
|
-
<VCol cols="auto" class="d-flex justify-center align-center">
|
|
67
|
-
<VBtn class="load-btn" :text="buttonLabel" :color="color || buttonColor" @click="submit" />
|
|
68
|
-
</VCol>
|
|
69
|
-
</VRow>
|
|
70
|
-
</template>
|
|
71
|
-
|
|
72
|
-
<style scoped>
|
|
73
|
-
.load-btn {
|
|
74
|
-
padding: 0 40px !important;
|
|
75
|
-
height: 50px !important;
|
|
76
|
-
border-radius: 8px;
|
|
77
|
-
text-transform: none !important;
|
|
78
|
-
font-weight: 600;
|
|
79
|
-
letter-spacing: 0.5px;
|
|
80
|
-
transition:
|
|
81
|
-
transform 0.2s ease,
|
|
82
|
-
box-shadow 0.2s ease !important;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.load-btn:hover {
|
|
86
|
-
transform: translateY(-2px);
|
|
87
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
|
88
|
-
}
|
|
89
|
-
</style>
|