@geode/opengeodeweb-front 10.18.0-rc.2 → 10.18.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.
@@ -1,36 +1,59 @@
1
1
  <script setup>
2
2
  import { useGeodeStore } from "@ogw_front/stores/geode";
3
- import { useTemplateRef } from "vue";
4
3
 
4
+ import CsvPreviewer from "@ogw_front/components/csv-preview/CsvPreviewer";
5
5
  import DragAndDrop from "@ogw_front/components/DragAndDrop";
6
6
 
7
7
  const emit = defineEmits(["files_uploaded", "decrement_step", "reset_values"]);
8
8
 
9
- const {
10
- multiple,
11
- accept,
12
- files,
13
- auto_upload,
14
- mini,
15
- show_overlay: showOverlay,
16
- } = defineProps({
17
- multiple: { type: Boolean, required: true },
18
- accept: { type: String, required: true },
19
- files: { type: Array, required: false, default: [] },
20
- auto_upload: { type: Boolean, required: false, default: false },
21
- mini: { type: Boolean, required: false, default: false },
22
- show_overlay: { type: Boolean, required: false, default: true },
9
+ const { multiple, accept, files, auto_upload, showOverlay, mini } = defineProps({
10
+ multiple: { type: Boolean, default: false },
11
+ accept: { type: String, default: "" },
12
+ files: { type: Array, default: () => [] },
13
+ auto_upload: { type: Boolean, default: true },
14
+ showOverlay: { type: Boolean, default: false },
15
+ mini: { type: Boolean, default: false },
23
16
  });
24
17
 
25
18
  const geodeStore = useGeodeStore();
26
-
27
19
  const internal_files = ref(files);
20
+ const dragAndDropRef = useTemplateRef("dragAndDropRef");
21
+ const csv_dialog = ref(false);
22
+ const current_csv_file = ref(undefined);
23
+ const current_csv_index = ref(-1);
28
24
  const loading = ref(false);
29
25
  const files_uploaded = ref(false);
30
- const dragAndDropRef = useTemplateRef("dragAndDropRef");
31
-
32
26
  const toggle_loading = useToggle(loading);
33
27
 
28
+ function isCsv(file) {
29
+ return file.name.toLowerCase().endsWith(".csv");
30
+ }
31
+
32
+ function openCsvPreviewer(file, index) {
33
+ current_csv_file.value = file;
34
+ current_csv_index.value = index;
35
+ csv_dialog.value = true;
36
+ }
37
+
38
+ async function onCsvConfirm(result) {
39
+ const json_content = JSON.stringify(result, undefined, 2);
40
+ const base_name = current_csv_file.value.name.slice(
41
+ 0,
42
+ current_csv_file.value.name.lastIndexOf("."),
43
+ );
44
+ const json_filename = `${base_name}.json`;
45
+
46
+ const blob = new Blob([json_content], { type: "application/json" });
47
+ const json_file = new File([blob], json_filename, {
48
+ type: "application/json",
49
+ });
50
+
51
+ current_csv_file.value.isConfigured = true;
52
+ await geodeStore.upload(json_file);
53
+ internal_files.value = [...internal_files.value];
54
+ csv_dialog.value = false;
55
+ }
56
+
34
57
  function processSelectedFiles(selected_files) {
35
58
  if (multiple) {
36
59
  internal_files.value = [...internal_files.value, ...selected_files];
@@ -52,11 +75,32 @@ async function upload_files() {
52
75
  const promise_array = internal_files.value.map((file) => geodeStore.upload(file));
53
76
  await Promise.all(promise_array);
54
77
  files_uploaded.value = true;
55
- emit("files_uploaded", internal_files.value);
56
-
57
78
  toggle_loading();
79
+ emit("files_uploaded", internal_files.value);
58
80
  }
59
81
 
82
+ watch(
83
+ () => internal_files.value,
84
+ async (newFiles) => {
85
+ if (newFiles.length === 0) {
86
+ return;
87
+ }
88
+ const unconfiguredCsv = newFiles.find((file) => isCsv(file) && !file.isConfigured);
89
+
90
+ if (unconfiguredCsv) {
91
+ openCsvPreviewer(unconfiguredCsv, internal_files.value.indexOf(unconfiguredCsv));
92
+ return;
93
+ }
94
+
95
+ const allConfigured = newFiles.every((file) => !isCsv(file) || file.isConfigured);
96
+
97
+ if (auto_upload && allConfigured) {
98
+ await upload_files();
99
+ }
100
+ },
101
+ { deep: true },
102
+ );
103
+
60
104
  if (files.length > 0) {
61
105
  internal_files.value = files;
62
106
  if (auto_upload) {
@@ -71,33 +115,26 @@ watch(
71
115
  },
72
116
  { deep: true },
73
117
  );
74
-
75
- watch(internal_files, (value) => {
76
- files_uploaded.value = false;
77
- if (auto_upload && value.length > 0) {
78
- upload_files();
79
- }
80
- });
81
118
  </script>
82
119
 
83
120
  <template>
84
- <DragAndDrop
85
- v-if="!internal_files.length"
86
- ref="dragAndDropRef"
87
- :multiple
88
- :accept
89
- :loading
90
- :show-extensions="false"
91
- :inline="true"
92
- :show-overlay="showOverlay"
93
- :texts="{
94
- idle: 'Select files',
95
- drop: 'Drop files here',
96
- loading: 'Loading...',
97
- }"
98
- @files-selected="processSelectedFiles"
99
- />
100
-
121
+ <template v-if="mini">
122
+ <v-btn
123
+ icon="mdi-plus"
124
+ variant="text"
125
+ color="primary"
126
+ :loading="loading"
127
+ class="mt-2"
128
+ @click="dragAndDropRef?.triggerFileDialog"
129
+ />
130
+ <DragAndDrop
131
+ ref="dragAndDropRef"
132
+ class="d-none"
133
+ :multiple
134
+ :accept
135
+ @files-selected="processSelectedFiles"
136
+ />
137
+ </template>
101
138
  <DragAndDrop
102
139
  v-else
103
140
  ref="dragAndDropRef"
@@ -105,7 +142,7 @@ watch(internal_files, (value) => {
105
142
  :accept
106
143
  :loading
107
144
  :show-extensions="false"
108
- :inline="false"
145
+ :inline="!internal_files.length"
109
146
  :show-overlay="showOverlay"
110
147
  @files-selected="processSelectedFiles"
111
148
  />
@@ -132,23 +169,50 @@ watch(internal_files, (value) => {
132
169
  </v-sheet>
133
170
 
134
171
  <v-sheet class="d-flex flex-wrap ga-2" color="transparent">
135
- <v-chip
136
- v-for="(file, index) in internal_files"
137
- :key="index"
138
- closable
139
- size="default"
140
- color="white"
141
- variant="outlined"
142
- class="font-weight-medium glass-ui border-opacity-10 px-4"
143
- style="background: rgba(255, 255, 255, 0.05) !important"
144
- @click:close="removeFile(index)"
145
- >
146
- <v-icon start size="18" color="primary">mdi-file-outline</v-icon>
147
- <span class="text-white">{{ file.name }}</span>
148
- <template #close>
149
- <v-icon size="16" class="ml-2 opacity-60 hover-opacity-100">mdi-close-circle</v-icon>
150
- </template>
151
- </v-chip>
172
+ <template v-for="(file, index) in internal_files" :key="index">
173
+ <v-chip
174
+ closable
175
+ size="default"
176
+ color="white"
177
+ variant="outlined"
178
+ class="font-weight-medium glass-ui border-opacity-10 px-4"
179
+ style="background: rgba(255, 255, 255, 0.05) !important"
180
+ @click:close="removeFile(index)"
181
+ >
182
+ <v-icon start size="18" :color="isCsv(file) && file.isConfigured ? 'success' : 'primary'">
183
+ {{
184
+ isCsv(file)
185
+ ? file.isConfigured
186
+ ? "mdi-file-check"
187
+ : "mdi-file-table"
188
+ : "mdi-file-outline"
189
+ }}
190
+ </v-icon>
191
+ <span class="text-white">{{ file.displayName || file.name }}</span>
192
+
193
+ <v-tooltip v-if="isCsv(file)" text="Configure CSV" location="bottom">
194
+ <template #activator="{ props: tooltipProps }">
195
+ <v-btn
196
+ v-bind="tooltipProps"
197
+ icon="mdi-cog"
198
+ variant="flat"
199
+ :color="file.isConfigured ? 'success' : 'primary'"
200
+ :class="['ml-2', { 'pulse-animation': !file.isConfigured }]"
201
+ width="24"
202
+ height="24"
203
+ density="compact"
204
+ @click.stop="openCsvPreviewer(file, index)"
205
+ >
206
+ <v-icon size="14">mdi-cog</v-icon>
207
+ </v-btn>
208
+ </template>
209
+ </v-tooltip>
210
+
211
+ <template #close>
212
+ <v-icon size="16" class="ml-2 opacity-60 hover-opacity-100">mdi-close-circle</v-icon>
213
+ </template>
214
+ </v-chip>
215
+ </template>
152
216
  </v-sheet>
153
217
  </v-card-text>
154
218
 
@@ -164,24 +228,40 @@ watch(internal_files, (value) => {
164
228
  @click="upload_files"
165
229
  >
166
230
  <v-icon start size="22">mdi-cloud-upload</v-icon>
167
- Upload {{ internal_files.length }} file<span v-if="internal_files.length > 1">s</span>
231
+ Upload
232
+ {{ internal_files.length }}
233
+ file<span v-if="internal_files.length > 1">s</span>
168
234
  </v-btn>
169
235
  </v-card-actions>
236
+
237
+ <CsvPreviewer
238
+ v-if="current_csv_file"
239
+ v-model="csv_dialog"
240
+ :file="current_csv_file"
241
+ @confirm="onCsvConfirm"
242
+ />
170
243
  </template>
171
244
 
172
245
  <style scoped>
173
- .border-dashed {
174
- border: 2px dashed rgba(255, 255, 255, 0.1) !important;
175
- transition: all 0.3s ease;
246
+ .glass-ui {
247
+ background: rgba(255, 255, 255, 0.05) !important;
248
+ backdrop-filter: blur(10px);
176
249
  }
177
-
178
- .border-dashed:hover {
179
- border-color: rgba(var(--v-theme-primary), 0.4) !important;
180
- background: rgba(var(--v-theme-primary), 0.02) !important;
250
+ .hover-opacity-100:hover {
251
+ opacity: 1 !important;
181
252
  }
182
-
183
- .custom-upload-btn {
184
- letter-spacing: 0.5px;
185
- box-shadow: 0 4px 15px rgba(var(--v-theme-primary), 0.3);
253
+ .pulse-animation {
254
+ animation: pulse 2s infinite;
255
+ }
256
+ @keyframes pulse {
257
+ 0% {
258
+ box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0.4);
259
+ }
260
+ 70% {
261
+ box-shadow: 0 0 0 10px rgba(var(--v-theme-primary), 0);
262
+ }
263
+ 100% {
264
+ box-shadow: 0 0 0 0 rgba(var(--v-theme-primary), 0);
265
+ }
186
266
  }
187
267
  </style>
@@ -9,10 +9,11 @@ const schema = schemas.opengeodeweb_back.missing_files;
9
9
 
10
10
  const emit = defineEmits(["update_values", "increment_step", "decrement_step"]);
11
11
 
12
- const { multiple, geode_object_type, filenames } = defineProps({
12
+ const { multiple, geode_object_type, filenames, files } = defineProps({
13
13
  multiple: { type: Boolean, required: true },
14
14
  geode_object_type: { type: String, required: true },
15
15
  filenames: { type: Array, required: true },
16
+ files: { type: Array, required: false, default: () => [] },
16
17
  });
17
18
 
18
19
  const accept = ref("");
@@ -34,12 +35,17 @@ async function missing_files() {
34
35
  additional_files.value = [];
35
36
  const geodeStore = useGeodeStore();
36
37
 
37
- const promise_array = filenames.map(async (filename) => {
38
- const response = await geodeStore.request(schema, {
39
- geode_object_type,
40
- filename,
41
- });
42
- return response;
38
+ const promise_array = filenames.map((filename) => {
39
+ const isCsvFile =
40
+ filename.toLowerCase().endsWith(".csv") || filename.toLowerCase().endsWith(".csv.json");
41
+ if (isCsvFile) {
42
+ return Promise.resolve({
43
+ has_missing_files: false,
44
+ mandatory_files: [],
45
+ additional_files: [],
46
+ });
47
+ }
48
+ return geodeStore.request(schema, { geode_object_type, filename });
43
49
  });
44
50
  const values = await Promise.all(promise_array);
45
51
  for (const value of values) {
@@ -49,11 +55,19 @@ async function missing_files() {
49
55
  mandatory_files.value = [...mandatory_files.value, ...value.mandatory_files];
50
56
  additional_files.value = [...additional_files.value, ...value.additional_files];
51
57
  }
52
- if (has_missing_files.value) {
53
- accept.value = [...mandatory_files.value, ...additional_files.value]
54
- .map((filename) => `.${filename.split(".").pop()}`)
55
- .join(",");
56
- } else {
58
+ const unconfigured_csvs = files.filter(
59
+ (file) =>
60
+ (file.name.toLowerCase().endsWith(".csv") || file.name.toLowerCase().endsWith(".csv.json")) &&
61
+ !file.isConfigured,
62
+ );
63
+ if (unconfigured_csvs.length > 0) {
64
+ has_missing_files.value = true;
65
+ if (accept.value === "") {
66
+ accept.value = ".json";
67
+ }
68
+ }
69
+
70
+ if (!has_missing_files.value) {
57
71
  emit("increment_step");
58
72
  }
59
73
  toggle_loading();
@@ -85,7 +99,10 @@ await missing_files();
85
99
  </v-row>
86
100
  <v-row>
87
101
  <v-col cols="12">
88
- <FileUploader v-bind="{ multiple, accept }" @files_uploaded="files_uploaded_event" />
102
+ <FileUploader
103
+ v-bind="{ multiple, accept, files, auto_upload: false }"
104
+ @files_uploaded="files_uploaded_event"
105
+ />
89
106
  </v-col>
90
107
  </v-row>
91
108
  <v-row>
@@ -62,12 +62,20 @@ async function get_allowed_objects() {
62
62
  const load_scores = allowed_objects_list.map((obj) => obj[key].is_loadable);
63
63
  const priorities = allowed_objects_list
64
64
  .map((obj) => obj[key].object_priority)
65
- .filter((priority) => priority !== undefined && priority !== null);
65
+ .filter((priority) => priority !== undefined);
66
66
  final_object[key] = { is_loadable: Math.min(...load_scores) };
67
67
  if (priorities.length > 0) {
68
68
  final_object[key].object_priority = Math.max(...priorities);
69
69
  }
70
70
  }
71
+ const isCsv = filenames.some(
72
+ (filename) =>
73
+ filename.toLowerCase().endsWith(".csv") || filename.toLowerCase().endsWith(".csv.json"),
74
+ );
75
+ if (isCsv && !final_object["PointSet3D"]) {
76
+ final_object["PointSet3D"] = { is_loadable: 1, object_priority: 100 };
77
+ }
78
+
71
79
  allowed_objects.value = final_object;
72
80
  const selected_object = select_geode_object(final_object);
73
81
  if (selected_object) {
@@ -137,11 +137,19 @@ function handleHoverLeave({ item }) {
137
137
  </template>
138
138
 
139
139
  <template #append="{ item }">
140
+ <v-btn
141
+ v-if="item.viewer_type"
142
+ icon="mdi-target"
143
+ size="medium"
144
+ variant="text"
145
+ v-tooltip="'Focus camera on object'"
146
+ @click.stop="hybridViewerStore.focusCameraOnObject(item.id)"
147
+ />
140
148
  <v-btn
141
149
  v-if="isModel(item)"
142
150
  icon="mdi-magnify-expand"
143
151
  size="medium"
144
- class="ml-8"
152
+ class="ml-2"
145
153
  variant="text"
146
154
  v-tooltip="'Model\'s mesh components'"
147
155
  @click.stop="
@@ -5,11 +5,13 @@ import FetchingData from "@ogw_front/components/FetchingData.vue";
5
5
  import ObjectTreeControls from "@ogw_front/components/Viewer/ObjectTree/Base/Controls.vue";
6
6
  import ObjectTreeItemLabel from "@ogw_front/components/Viewer/ObjectTree/Base/ItemLabel.vue";
7
7
  import { useHoverhighlight } from "@ogw_front/composables/hover_highlight";
8
+ import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
8
9
  import { useModelComponents } from "@ogw_front/composables/model_components";
9
10
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
10
11
 
11
12
  const { id } = defineProps({ id: { type: String, required: true } });
12
13
  const { onHoverEnter, onHoverLeave } = useHoverhighlight();
14
+ const hybridViewerStore = useHybridViewerStore();
13
15
  const emit = defineEmits(["show-menu"]);
14
16
 
15
17
  const treeviewStore = useTreeviewStore();
@@ -150,6 +152,22 @@ function handleHoverLeave() {
150
152
  @contextmenu.prevent.stop="showContextMenu($event, item)"
151
153
  />
152
154
  </template>
155
+
156
+ <template #append="{ item }">
157
+ <v-btn
158
+ v-if="item.category || (item.children && item.children.length > 0)"
159
+ icon="mdi-target"
160
+ size="medium"
161
+ variant="text"
162
+ v-tooltip="'Focus camera on object'"
163
+ @click.stop="
164
+ hybridViewerStore.focusCameraOnObject(
165
+ id,
166
+ item.category ? [item.viewer_id] : item.children.map((child) => child.viewer_id),
167
+ )
168
+ "
169
+ />
170
+ </template>
153
171
  </CommonTreeView>
154
172
  </div>
155
173
  </template>
@@ -0,0 +1,277 @@
1
+ <script setup>
2
+ import CsvSettings from "./CsvSettings.vue";
3
+ import CsvTable from "./CsvTable.vue";
4
+ import { useToggle } from "@vueuse/core";
5
+
6
+ const { file, modelValue } = defineProps({
7
+ file: { type: Object, required: true },
8
+ modelValue: { type: Boolean, default: false },
9
+ });
10
+
11
+ const MAX_CONTENT_SLICE = 1000;
12
+ const MAX_LINES_FOR_DETECTION = 5;
13
+ const MIN_AVG_COUNT = 1.5;
14
+ const MAX_VARIANCE = 0.5;
15
+ const PREVIEW_ROWS_LIMIT = 101;
16
+
17
+ const emit = defineEmits(["update:modelValue", "confirm"]);
18
+
19
+ const separator = ref(",");
20
+ const headerRow = ref(0);
21
+ const firstRow = ref(1);
22
+
23
+ const xColumn = ref(undefined);
24
+ const yColumn = ref(undefined);
25
+ const zColumn = ref(undefined);
26
+
27
+ const rawContent = ref("");
28
+ const previewRows = ref([]);
29
+ const previewHeaders = ref([]);
30
+ const loading = ref(false);
31
+ const toggleLoading = useToggle(loading);
32
+
33
+ function autoDetectSeparator(content) {
34
+ const lines = content
35
+ .slice(0, MAX_CONTENT_SLICE)
36
+ .split(/\r?\n/u)
37
+ .slice(0, MAX_LINES_FOR_DETECTION);
38
+ const candidates = [",", ";", "\t", "|"];
39
+ let best = ",";
40
+ let maxCount = -1;
41
+
42
+ for (const candidate of candidates) {
43
+ const counts = lines.map((line) => line.split(candidate).length);
44
+ const average = counts.reduce((total, count) => total + count, 0) / counts.length;
45
+ const variance =
46
+ counts.reduce((total, count) => total + (count - average) ** 2, 0) / counts.length;
47
+
48
+ if (average > MIN_AVG_COUNT && variance < MAX_VARIANCE && average > maxCount) {
49
+ maxCount = average;
50
+ best = candidate;
51
+ }
52
+ }
53
+ return best;
54
+ }
55
+
56
+ function readAndParse() {
57
+ if (!file) {
58
+ return;
59
+ }
60
+ toggleLoading(true);
61
+
62
+ const reader = new FileReader();
63
+ reader.addEventListener("load", (event) => {
64
+ rawContent.value = event.target.result;
65
+ if (!separator.value || separator.value === ",") {
66
+ separator.value = autoDetectSeparator(rawContent.value);
67
+ }
68
+ parseContent();
69
+ toggleLoading(false);
70
+ });
71
+ reader.addEventListener("error", () => {
72
+ toggleLoading(false);
73
+ });
74
+ reader.readAsText(file, "utf8");
75
+ }
76
+
77
+ function parseContent() {
78
+ if (!rawContent.value) {
79
+ return;
80
+ }
81
+
82
+ const allLines = rawContent.value.split(/\r?\n/u).filter((line) => line.trim() !== "");
83
+
84
+ function splitLine(line) {
85
+ if (!separator.value) {
86
+ return [line];
87
+ }
88
+ const result = [];
89
+ let current = "";
90
+ let inQuotes = false;
91
+ for (let index = 0; index < line.length; index += 1) {
92
+ const char = line[index];
93
+ if (char === '"') {
94
+ inQuotes = !inQuotes;
95
+ } else if (char === separator.value && !inQuotes) {
96
+ result.push(current.trim());
97
+ current = "";
98
+ } else {
99
+ current += char;
100
+ }
101
+ }
102
+ result.push(current.trim());
103
+ return result;
104
+ }
105
+
106
+ const headerLine = allLines[headerRow.value];
107
+ const rawHeaders = headerLine ? splitLine(headerLine) : [];
108
+
109
+ previewHeaders.value = rawHeaders.map((header, index) => ({
110
+ title: header || `Column ${index + 1}`,
111
+ key: `col${index}`,
112
+ align: "start",
113
+ sortable: true,
114
+ }));
115
+
116
+ const dataLines = allLines.slice(firstRow.value, firstRow.value + PREVIEW_ROWS_LIMIT);
117
+ previewRows.value = dataLines.map((line) => {
118
+ const row = splitLine(line);
119
+ const obj = {};
120
+ for (let index = 0; index < row.length; index += 1) {
121
+ obj[`col${index}`] = row[index];
122
+ }
123
+ return obj;
124
+ });
125
+ }
126
+ const computedResult = computed(() => {
127
+ const xIndex = previewHeaders.value.findIndex((header) => header.key === xColumn.value);
128
+ const yIndex = previewHeaders.value.findIndex((header) => header.key === yColumn.value);
129
+ const zIndex = previewHeaders.value.findIndex((header) => header.key === zColumn.value);
130
+
131
+ return {
132
+ firstRow: firstRow.value,
133
+ headerRow: headerRow.value,
134
+ separator: separator.value,
135
+ xColumn: xIndex === -1 ? 0 : xIndex,
136
+ yColumn: yIndex === -1 ? 1 : yIndex,
137
+ zColumn: zIndex === -1 ? 2 : zIndex,
138
+ };
139
+ });
140
+
141
+ watch([separator, headerRow, firstRow], () => {
142
+ parseContent();
143
+ xColumn.value = undefined;
144
+ yColumn.value = undefined;
145
+ zColumn.value = undefined;
146
+ });
147
+
148
+ watch(
149
+ () => modelValue,
150
+ (val) => {
151
+ if (val) {
152
+ readAndParse();
153
+ }
154
+ },
155
+ { immediate: true },
156
+ );
157
+
158
+ watch(
159
+ () => file,
160
+ (newFile) => {
161
+ if (newFile && modelValue) {
162
+ readAndParse();
163
+ }
164
+ },
165
+ );
166
+
167
+ function onConfirm() {
168
+ emit("confirm", computedResult.value);
169
+ emit("update:modelValue", false);
170
+ }
171
+ </script>
172
+
173
+ <template>
174
+ <v-dialog
175
+ :model-value="modelValue"
176
+ @update:model-value="emit('update:modelValue', $event)"
177
+ max-width="1200px"
178
+ >
179
+ <v-card class="glass-ui rounded-xl overflow-hidden border-opacity-10" color="grey-darken-4">
180
+ <v-toolbar color="transparent" flat class="px-4">
181
+ <v-icon icon="mdi-file-table" size="32" color="primary" class="ml-1" />
182
+ <v-toolbar-title class="text-h6 font-weight-bold text-white">
183
+ CSV Previewer & Configuration
184
+ </v-toolbar-title>
185
+ <v-spacer />
186
+ <v-btn
187
+ icon="mdi-close"
188
+ variant="text"
189
+ color="white"
190
+ @click="emit('update:modelValue', false)"
191
+ />
192
+ </v-toolbar>
193
+
194
+ <v-divider class="border-opacity-10" />
195
+
196
+ <div class="previewer-grid">
197
+ <CsvSettings
198
+ v-model:separator="separator"
199
+ v-model:header-row="headerRow"
200
+ v-model:first-row="firstRow"
201
+ v-model:x-column="xColumn"
202
+ v-model:y-column="yColumn"
203
+ v-model:z-column="zColumn"
204
+ :headers="previewHeaders"
205
+ />
206
+
207
+ <CsvTable
208
+ :headers="previewHeaders"
209
+ :rows="previewRows"
210
+ :loading="loading"
211
+ :coordinates="{ x: xColumn, y: yColumn, z: zColumn }"
212
+ :separator="separator"
213
+ :header-row="headerRow"
214
+ :first-row="firstRow"
215
+ />
216
+ </div>
217
+
218
+ <v-divider class="border-opacity-10" />
219
+
220
+ <v-card-actions class="pa-4 bg-white-opacity-5">
221
+ <v-spacer />
222
+ <v-btn
223
+ variant="text"
224
+ class="text-none px-6"
225
+ color="white"
226
+ @click="emit('update:modelValue', false)"
227
+ >
228
+ Cancel
229
+ </v-btn>
230
+ <v-btn
231
+ variant="flat"
232
+ color="primary"
233
+ class="text-none px-8 rounded-lg font-weight-bold"
234
+ @click="onConfirm"
235
+ :disabled="!previewHeaders.length"
236
+ >
237
+ Confirm
238
+ </v-btn>
239
+ </v-card-actions>
240
+ </v-card>
241
+ </v-dialog>
242
+ </template>
243
+
244
+ <style scoped>
245
+ .glass-ui {
246
+ background: rgba(30, 30, 30, 0.8) !important;
247
+ backdrop-filter: blur(20px);
248
+ -webkit-backdrop-filter: blur(20px);
249
+ border: 1px solid rgba(255, 255, 255, 0.1);
250
+ }
251
+
252
+ .previewer-grid {
253
+ display: grid;
254
+ grid-template-columns: 350px 1fr;
255
+ height: 70vh;
256
+ overflow: hidden;
257
+ }
258
+
259
+ .bg-white-opacity-5 {
260
+ background: rgba(255, 255, 255, 0.03);
261
+ }
262
+
263
+ /* Custom scrollbar */
264
+ ::-webkit-scrollbar {
265
+ width: 6px;
266
+ }
267
+ ::-webkit-scrollbar-track {
268
+ background: transparent;
269
+ }
270
+ ::-webkit-scrollbar-thumb {
271
+ background: rgba(255, 255, 255, 0.1);
272
+ border-radius: 10px;
273
+ }
274
+ ::-webkit-scrollbar-thumb:hover {
275
+ background: rgba(255, 255, 255, 0.2);
276
+ }
277
+ </style>
@@ -0,0 +1,170 @@
1
+ <script setup>
2
+ const { separator, headerRow, firstRow, xColumn, yColumn, zColumn, headers } = defineProps({
3
+ separator: { type: String, required: true },
4
+ headerRow: { type: Number, required: true },
5
+ firstRow: { type: Number, required: true },
6
+ xColumn: { type: String, default: undefined },
7
+ yColumn: { type: String, default: undefined },
8
+ zColumn: { type: String, default: undefined },
9
+ headers: { type: Array, default: () => [] },
10
+ });
11
+
12
+ const emit = defineEmits([
13
+ "update:separator",
14
+ "update:headerRow",
15
+ "update:firstRow",
16
+ "update:xColumn",
17
+ "update:yColumn",
18
+ "update:zColumn",
19
+ ]);
20
+
21
+ const separators = [
22
+ { title: "Comma (,)", value: "," },
23
+ { title: "Semicolon (;)", value: ";" },
24
+ { title: "Tab (\\t)", value: "\t" },
25
+ { title: "Pipe (|)", value: "|" },
26
+ ];
27
+
28
+ const internalSeparator = computed({
29
+ get: () => separator,
30
+ set: (value) => emit("update:separator", value),
31
+ });
32
+
33
+ const internalHeaderRow = computed({
34
+ get: () => headerRow,
35
+ set: (value) => emit("update:headerRow", value),
36
+ });
37
+
38
+ const internalFirstRow = computed({
39
+ get: () => firstRow,
40
+ set: (value) => emit("update:firstRow", value),
41
+ });
42
+
43
+ const internalXColumn = computed({
44
+ get: () => xColumn,
45
+ set: (value) => emit("update:xColumn", value),
46
+ });
47
+
48
+ const internalYColumn = computed({
49
+ get: () => yColumn,
50
+ set: (value) => emit("update:yColumn", value),
51
+ });
52
+
53
+ const internalZColumn = computed({
54
+ get: () => zColumn,
55
+ set: (value) => emit("update:zColumn", value),
56
+ });
57
+ </script>
58
+
59
+ <template>
60
+ <div class="pa-6 overflow-y-auto border-e border-opacity-10 bg-white-opacity-5">
61
+ <div class="text-overline mb-4 text-primary font-weight-bold">Parser Settings</div>
62
+
63
+ <v-select
64
+ v-model="internalSeparator"
65
+ :items="separators"
66
+ label="Separator"
67
+ variant="outlined"
68
+ density="compact"
69
+ class="mb-4"
70
+ rounded="lg"
71
+ />
72
+
73
+ <v-text-field
74
+ v-if="!separators.find((s) => s.value === separator)"
75
+ v-model="internalSeparator"
76
+ label="Custom Separator"
77
+ variant="outlined"
78
+ density="compact"
79
+ placeholder="Enter character"
80
+ class="mb-4"
81
+ rounded="lg"
82
+ />
83
+
84
+ <v-divider class="my-6 border-opacity-10" />
85
+
86
+ <div class="text-overline mb-4 text-primary font-weight-bold">Row Configuration</div>
87
+
88
+ <v-text-field
89
+ v-model.number="internalHeaderRow"
90
+ type="number"
91
+ label="Header Row"
92
+ variant="outlined"
93
+ density="compact"
94
+ hint="Index of the row containing headers"
95
+ persistent-hint
96
+ class="mb-4"
97
+ min="0"
98
+ rounded="lg"
99
+ />
100
+
101
+ <v-text-field
102
+ v-model.number="internalFirstRow"
103
+ type="number"
104
+ label="First Data Row"
105
+ variant="outlined"
106
+ density="compact"
107
+ hint="Index of the first row containing data"
108
+ persistent-hint
109
+ class="mb-6"
110
+ min="0"
111
+ rounded="lg"
112
+ />
113
+
114
+ <v-divider class="my-6 border-opacity-10" />
115
+
116
+ <div class="text-overline mb-4 text-primary font-weight-bold">Spatial Mapping</div>
117
+
118
+ <v-select
119
+ v-model="internalXColumn"
120
+ :items="headers"
121
+ item-title="title"
122
+ item-value="key"
123
+ label="X Column"
124
+ variant="outlined"
125
+ density="compact"
126
+ prepend-inner-icon="mdi-axis-x-arrow"
127
+ color="light-blue-accent-3"
128
+ class="mb-2"
129
+ clearable
130
+ rounded="lg"
131
+ />
132
+
133
+ <v-select
134
+ v-model="internalYColumn"
135
+ :items="headers"
136
+ item-title="title"
137
+ item-value="key"
138
+ label="Y Column"
139
+ variant="outlined"
140
+ density="compact"
141
+ prepend-inner-icon="mdi-axis-y-arrow"
142
+ color="light-green-accent-3"
143
+ class="mb-2"
144
+ clearable
145
+ rounded="lg"
146
+ />
147
+
148
+ <v-select
149
+ v-model="internalZColumn"
150
+ :items="headers"
151
+ item-title="title"
152
+ item-value="key"
153
+ label="Z Column"
154
+ variant="outlined"
155
+ density="compact"
156
+ prepend-inner-icon="mdi-axis-z-arrow"
157
+ color="red-accent-3"
158
+ class="mb-8"
159
+ clearable
160
+ rounded="lg"
161
+ />
162
+ <div class="pb-12" />
163
+ </div>
164
+ </template>
165
+
166
+ <style scoped>
167
+ .bg-white-opacity-5 {
168
+ background: rgba(255, 255, 255, 0.03);
169
+ }
170
+ </style>
@@ -0,0 +1,112 @@
1
+ <script setup>
2
+ const { headers, rows, loading, coordinates, separator, headerRow, firstRow } = defineProps({
3
+ headers: { type: Array, required: true },
4
+ rows: { type: Array, required: true },
5
+ loading: { type: Boolean, default: false },
6
+ coordinates: { type: Object, default: () => ({ x: undefined, y: undefined, z: undefined }) },
7
+ separator: { type: String, default: "," },
8
+ headerRow: { type: Number, default: 0 },
9
+ firstRow: { type: Number, default: 1 },
10
+ });
11
+
12
+ function getColumnClass(key) {
13
+ if (key === coordinates.x) {
14
+ return "x-col-highlight";
15
+ }
16
+ if (key === coordinates.y) {
17
+ return "y-col-highlight";
18
+ }
19
+ if (key === coordinates.z) {
20
+ return "z-col-highlight";
21
+ }
22
+ return "";
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="d-flex flex-column bg-black-opacity-20 overflow-hidden">
28
+ <div class="pa-4 d-flex align-center border-b border-opacity-10">
29
+ <v-icon icon="mdi-table-eye" size="small" class="mr-2 opacity-60" />
30
+ <span class="text-subtitle-2 font-weight-medium opacity-80">
31
+ Live Preview (First 100 rows)
32
+ </span>
33
+ <v-spacer />
34
+ <v-chip v-if="loading" size="x-small" color="primary" variant="flat">
35
+ <v-progress-circular indeterminate size="12" width="2" class="mr-2" />
36
+ Parsing...
37
+ </v-chip>
38
+ </div>
39
+
40
+ <v-data-table
41
+ :key="`${separator}-${headerRow}-${firstRow}`"
42
+ :headers="headers"
43
+ :items="rows"
44
+ class="bg-transparent"
45
+ density="compact"
46
+ hover
47
+ hide-default-footer
48
+ :loading="loading"
49
+ fixed-header
50
+ height="100%"
51
+ >
52
+ <template v-for="header in headers" v-slot:[`item.${header.key}`]="{ item }">
53
+ <div :class="getColumnClass(header.key)" class="px-2 py-1 rounded text-truncate">
54
+ {{ item[header.key] }}
55
+ </div>
56
+ </template>
57
+
58
+ <template #no-data>
59
+ <div class="d-flex flex-column align-center justify-center h-100 py-12 opacity-40">
60
+ <v-icon size="64" icon="mdi-table-off" />
61
+ <div class="text-h6 mt-2">No preview available</div>
62
+ <div class="text-caption">Check your parser settings</div>
63
+ </div>
64
+ </template>
65
+ </v-data-table>
66
+ </div>
67
+ </template>
68
+
69
+ <style scoped>
70
+ .bg-black-opacity-20 {
71
+ background: rgba(0, 0, 0, 0.2);
72
+ }
73
+
74
+ :deep(.v-data-table__th) {
75
+ background: #1e1e1e !important; /* Solid background to prevent overlap transparency */
76
+ color: rgba(var(--v-theme-primary), 0.9) !important;
77
+ font-weight: bold !important;
78
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
79
+ text-transform: uppercase;
80
+ font-size: 0.75rem;
81
+ letter-spacing: 0.5px;
82
+ min-width: 150px !important;
83
+ white-space: nowrap !important;
84
+ }
85
+
86
+ :deep(.v-data-table__td) {
87
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important;
88
+ color: rgba(255, 255, 255, 0.8) !important;
89
+ }
90
+
91
+ :deep(.v-data-table__tr:hover td) {
92
+ background: rgba(var(--v-theme-primary), 0.05) !important;
93
+ }
94
+
95
+ .x-col-highlight {
96
+ background: rgba(0, 176, 255, 0.2) !important;
97
+ color: #00b0ff !important;
98
+ font-weight: bold;
99
+ }
100
+
101
+ .y-col-highlight {
102
+ background: rgba(100, 221, 23, 0.2) !important;
103
+ color: #64dd17 !important;
104
+ font-weight: bold;
105
+ }
106
+
107
+ .z-col-highlight {
108
+ background: rgba(255, 23, 68, 0.2) !important;
109
+ color: #ff1744 !important;
110
+ font-weight: bold;
111
+ }
112
+ </style>
@@ -7,6 +7,7 @@ import {
7
7
  getCameraOptions,
8
8
  performCameraOrientation,
9
9
  performClickPicking,
10
+ performFocusCameraOnObject,
10
11
  performSetCamera,
11
12
  } from "@ogw_internal/stores/hybrid_viewer";
12
13
  import { newInstance as vtkActor } from "@kitware/vtk.js/Rendering/Core/Actor";
@@ -144,9 +145,13 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
144
145
  syncRemoteCamera();
145
146
  }
146
147
 
147
- function setCamera(new_camera_options) {
148
- performSetCamera(new_camera_options, {
148
+ async function focusCameraOnObject(id, block_ids = []) {
149
+ await performFocusCameraOnObject(id, {
150
+ hybridDb,
151
+ viewerStore,
152
+ viewer_schemas,
149
153
  genericRenderWindow: genericRenderWindow.value,
154
+ block_ids,
150
155
  is_moving,
151
156
  imageStyle,
152
157
  syncRemoteCamera,
@@ -162,6 +167,15 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
162
167
  });
163
168
  }
164
169
 
170
+ function setCamera(targetCameraOptions) {
171
+ performSetCamera(targetCameraOptions, {
172
+ genericRenderWindow: genericRenderWindow.value,
173
+ is_moving,
174
+ imageStyle,
175
+ syncRemoteCamera,
176
+ });
177
+ }
178
+
165
179
  function syncRemoteCamera() {
166
180
  const camera = genericRenderWindow.value.getRenderer().getActiveCamera();
167
181
  const options = getCameraOptions(camera);
@@ -217,6 +231,9 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
217
231
  is_moving.value = false;
218
232
  syncRemoteCamera();
219
233
  }
234
+ is_moving.value = false;
235
+ genericRenderWindow.value.getRenderer().resetCameraClippingRange();
236
+ syncRemoteCamera();
220
237
  },
221
238
  });
222
239
  useEventListener(container, "wheel", () => {
@@ -227,6 +244,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
227
244
  clearTimeout(wheelEventEndTimeout);
228
245
  wheelEventEndTimeout = setTimeout(() => {
229
246
  is_moving.value = false;
247
+ genericRenderWindow.value.getRenderer().resetCameraClippingRange();
230
248
  syncRemoteCamera();
231
249
  }, WHEEL_TIME_OUT_MS);
232
250
  });
@@ -294,6 +312,7 @@ export const useHybridViewerStore = defineStore("hybridViewer", () => {
294
312
  remoteRender,
295
313
  resize,
296
314
  resetCamera,
315
+ focusCameraOnObject,
297
316
  setCameraOrientation,
298
317
  setContainer,
299
318
  zScale,
@@ -9,26 +9,16 @@ import { useGeodeStore } from "@ogw_front/stores/geode";
9
9
  import { useHybridViewerStore } from "@ogw_front/stores/hybrid_viewer";
10
10
  import { useTreeviewStore } from "@ogw_front/stores/treeview";
11
11
 
12
- const SECOND = 1000;
13
-
14
12
  async function importWorkflow(files) {
15
- console.log("importWorkflow", { files });
16
- const start = Date.now();
17
- const promise_array = [];
18
- for (const file of files) {
19
- const { filename, geode_object_type } = file;
20
- console.log({ filename }, { geode_object_type });
21
- promise_array.push(importFile(filename, geode_object_type));
22
- }
23
- const results = await Promise.all(promise_array);
13
+ const results = await Promise.all(
14
+ files.map(({ filename, geode_object_type }) => importFile(filename, geode_object_type)),
15
+ );
24
16
  const hybridViewerStore = useHybridViewerStore();
25
17
  hybridViewerStore.remoteRender();
26
- console.log("importWorkflow completed in", (Date.now() - start) / SECOND);
27
18
  return results;
28
19
  }
29
20
 
30
21
  function buildImportItemFromPayloadApi(value, geode_object_type) {
31
- console.log("buildImportItemFromPayloadApi", { value, geode_object_type });
32
22
  return {
33
23
  geode_object_type,
34
24
  ...value,
@@ -235,6 +235,47 @@ function performCameraOrientation(orientation, options) {
235
235
  });
236
236
  }
237
237
 
238
+ async function performFocusCameraOnObject(id, options) {
239
+ const {
240
+ hybridDb,
241
+ viewerStore,
242
+ viewer_schemas,
243
+ genericRenderWindow,
244
+ block_ids = [],
245
+ is_moving,
246
+ imageStyle,
247
+ syncRemoteCamera,
248
+ } = options;
249
+
250
+ if (!hybridDb[id]) {
251
+ return;
252
+ }
253
+
254
+ let bounds = [];
255
+ if (block_ids.length > 0) {
256
+ bounds = await viewerStore.request(viewer_schemas.opengeodeweb_viewer.model.get_blocks_bounds, {
257
+ id,
258
+ block_ids,
259
+ });
260
+ } else {
261
+ bounds = hybridDb[id].actor.getBounds();
262
+ }
263
+
264
+ const renderer = genericRenderWindow.getRenderer();
265
+ const camera = renderer.getActiveCamera();
266
+ const startOptions = getCameraOptions(camera);
267
+ renderer.resetCamera(bounds);
268
+ const targetOptions = getCameraOptions(camera);
269
+ applyCameraOptions(camera, startOptions);
270
+
271
+ performSetCamera(targetOptions, {
272
+ genericRenderWindow,
273
+ is_moving,
274
+ imageStyle,
275
+ syncRemoteCamera,
276
+ });
277
+ }
278
+
238
279
  export {
239
280
  BACKGROUND_COLOR,
240
281
  ACTOR_COLOR,
@@ -250,5 +291,6 @@ export {
250
291
  getCameraOptions,
251
292
  performCameraOrientation,
252
293
  performClickPicking,
294
+ performFocusCameraOnObject,
253
295
  performSetCamera,
254
296
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.18.0-rc.2",
3
+ "version": "10.18.0-rc.4",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -17,7 +17,7 @@ describe("crs selector", () => {
17
17
  beforeEach(() => {
18
18
  pinia = setupActivePinia();
19
19
  geodeStore = useGeodeStore();
20
- geodeStore.base_url = "";
20
+ geodeStore.base_url = "/";
21
21
  });
22
22
 
23
23
  test("default behavior", async () => {
@@ -21,7 +21,7 @@ const geodeStore = useGeodeStore();
21
21
 
22
22
  describe("extension selector", () => {
23
23
  beforeEach(() => {
24
- geodeStore.base_url = "";
24
+ geodeStore.base_url = "/";
25
25
 
26
26
  geodeStore.request = vi.fn(() => {
27
27
  const response = {
@@ -21,7 +21,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
21
21
  describe("file selector", () => {
22
22
  const pinia = setupActivePinia();
23
23
  const geodeStore = useGeodeStore();
24
- geodeStore.base_url = "";
24
+ geodeStore.base_url = "/";
25
25
 
26
26
  test("select file", async () => {
27
27
  registerEndpoint(allowed_files_schema.$id, {
@@ -52,6 +52,7 @@ describe("file selector", () => {
52
52
  writable: true,
53
53
  });
54
54
  await v_file_input.trigger("change");
55
+ await flushPromises();
55
56
  const v_btn = wrapper.findComponent(components.VBtn);
56
57
  await v_btn.trigger("click");
57
58
  await flushPromises();
@@ -18,7 +18,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
18
18
  describe("file uploader", () => {
19
19
  const pinia = setupActivePinia();
20
20
  const geodeStore = useGeodeStore();
21
- geodeStore.base_url = "";
21
+ geodeStore.base_url = "/";
22
22
 
23
23
  registerEndpoint(upload_file_schema.$id, {
24
24
  method: upload_file_schema.methods[FIRST_INDEX],
@@ -37,7 +37,7 @@ describe("file uploader", () => {
37
37
  global: {
38
38
  plugins: [vuetify, pinia],
39
39
  },
40
- props: { multiple: false, accept: "*.txt" },
40
+ props: { multiple: false, accept: "*.txt", auto_upload: false },
41
41
  });
42
42
 
43
43
  const v_file_input = wrapper.find('input[type="file"]');
@@ -46,6 +46,7 @@ describe("file uploader", () => {
46
46
  writable: true,
47
47
  });
48
48
  await v_file_input.trigger("change");
49
+ await flushPromises();
49
50
  const v_btn = wrapper.findComponent(components.VBtn);
50
51
 
51
52
  await v_btn.trigger("click");
@@ -12,7 +12,7 @@ import { useGeodeStore } from "@ogw_front/stores/geode";
12
12
  describe("inspector inspection button", () => {
13
13
  const pinia = setupActivePinia();
14
14
  const geodeStore = useGeodeStore();
15
- geodeStore.base_url = "";
15
+ geodeStore.base_url = "/";
16
16
 
17
17
  test("with issues", async () => {
18
18
  const inspection_result = {
@@ -20,7 +20,7 @@ const upload_file_schema = schemas.opengeodeweb_back.upload_file;
20
20
  describe("missing files selector", () => {
21
21
  const pinia = setupActivePinia();
22
22
  const geodeStore = useGeodeStore();
23
- geodeStore.base_url = "";
23
+ geodeStore.base_url = "/";
24
24
 
25
25
  test("select file", async () => {
26
26
  geodeStore.request = vi.fn((schema, params, callbacks) => {
@@ -57,6 +57,7 @@ describe("missing files selector", () => {
57
57
  writable: true,
58
58
  });
59
59
  await v_file_input.trigger("change");
60
+ await flushPromises();
60
61
  const v_btn = file_uploader.findComponent(components.VBtn);
61
62
 
62
63
  registerEndpoint(upload_file_schema.$id, {
@@ -19,7 +19,7 @@ const { allowed_objects } = schemas.opengeodeweb_back;
19
19
  describe("object selector", () => {
20
20
  const pinia = setupActivePinia();
21
21
  const geodeStore = useGeodeStore();
22
- geodeStore.base_url = "";
22
+ geodeStore.base_url = "/";
23
23
 
24
24
  test("loadable with one class", async () => {
25
25
  const response = {
@@ -11,7 +11,7 @@ describe("packages versions", () => {
11
11
  test("mount", async () => {
12
12
  const pinia = setupActivePinia();
13
13
  const geodeStore = useGeodeStore();
14
- geodeStore.base_url = "";
14
+ geodeStore.base_url = "/";
15
15
 
16
16
  const schema = {
17
17
  $id: "/versions",