1use anyhow::{Context, Result};
6use arrow::array::{Array, Float64Array, Int64Array, StringArray, TimestampSecondArray, TimestampMicrosecondArray, TimestampMillisecondArray, TimestampNanosecondArray};
7use arrow::datatypes::DataType;
8use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
9use std::path::{Path, PathBuf};
10use stt_core::timestamp::{normalize_timestamp_to_ms, TimestampUnit};
11use stt_core::types::{BoundingBox, TimeRange};
12
13#[derive(Debug, Clone)]
15pub enum DataSource {
16 GeoParquet {
17 path: PathBuf,
18 time_field: String,
19 time_format: String,
20 },
21}
22
23impl DataSource {
24 pub fn display_name(&self) -> String {
25 match self {
26 DataSource::GeoParquet { path, .. } => {
27 path.file_name()
28 .map(|n| n.to_string_lossy().to_string())
29 .unwrap_or_else(|| "unknown".to_string())
30 }
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct AnalyzableFeature {
38 pub lon: f64,
40 pub lat: f64,
42 pub timestamp: u64,
44 pub geometry_type: GeometryType,
46 pub vertex_count: usize,
48 pub estimated_size: usize,
50 pub property_count: usize,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum GeometryType {
57 Point,
58 LineString,
59 Polygon,
60 MultiPoint,
61 MultiLineString,
62 MultiPolygon,
63 Unknown,
64}
65
66impl std::fmt::Display for GeometryType {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 GeometryType::Point => write!(f, "Point"),
70 GeometryType::LineString => write!(f, "LineString"),
71 GeometryType::Polygon => write!(f, "Polygon"),
72 GeometryType::MultiPoint => write!(f, "MultiPoint"),
73 GeometryType::MultiLineString => write!(f, "MultiLineString"),
74 GeometryType::MultiPolygon => write!(f, "MultiPolygon"),
75 GeometryType::Unknown => write!(f, "Unknown"),
76 }
77 }
78}
79
80#[derive(Debug)]
82pub struct LoadedData {
83 pub features: Vec<AnalyzableFeature>,
84 pub bounds: BoundingBox,
85 pub time_range: TimeRange,
86}
87
88pub fn load_data(source: &DataSource) -> Result<LoadedData> {
90 match source {
91 DataSource::GeoParquet { path, time_field, time_format } => {
92 load_geoparquet(path, time_field, time_format)
93 }
94 }
95}
96
97fn load_geoparquet(path: &Path, time_field: &str, time_format: &str) -> Result<LoadedData> {
99 use indicatif::{ProgressBar, ProgressStyle};
100
101 let pb = ProgressBar::new_spinner();
102 pb.set_style(
103 ProgressStyle::default_spinner()
104 .template("{spinner:.green} {msg}")
105 .unwrap(),
106 );
107 pb.set_message("Loading GeoParquet file...");
108
109 let file = std::fs::File::open(path).context("Failed to open GeoParquet file")?;
110 let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
111 let schema = builder.schema().clone();
112
113 let geom_col_name = find_geometry_column(&schema)?;
115 let time_col_idx = schema.fields().iter().position(|f| f.name() == time_field)
116 .ok_or_else(|| anyhow::anyhow!("Time field '{}' not found", time_field))?;
117
118 let reader = builder.build()?;
119
120 let mut features = Vec::new();
121 let mut min_lon = f64::MAX;
122 let mut max_lon = f64::MIN;
123 let mut min_lat = f64::MAX;
124 let mut max_lat = f64::MIN;
125 let mut min_time = u64::MAX;
126 let mut max_time = u64::MIN;
127
128 for batch_result in reader {
129 let batch = batch_result.context("Failed to read Parquet batch")?;
130
131 let geometries = extract_geometries_from_batch(&batch, &geom_col_name)?;
132 let timestamps = extract_timestamps_from_batch(&batch, time_col_idx, time_format)?;
133
134 let property_count = schema.fields().len() - 2; for i in 0..batch.num_rows() {
138 let (geom_type, vertex_count, lon, lat) = geometries.get(i)
139 .cloned()
140 .unwrap_or((GeometryType::Unknown, 0, 0.0, 0.0));
141 let timestamp = timestamps.get(i).copied().unwrap_or(0);
142
143 min_lon = min_lon.min(lon);
145 max_lon = max_lon.max(lon);
146 min_lat = min_lat.min(lat);
147 max_lat = max_lat.max(lat);
148 min_time = min_time.min(timestamp);
149 max_time = max_time.max(timestamp);
150
151 let estimated_size = 100 + (vertex_count * 16) + (property_count * 20);
153
154 features.push(AnalyzableFeature {
155 lon,
156 lat,
157 timestamp,
158 geometry_type: geom_type,
159 vertex_count,
160 estimated_size,
161 property_count,
162 });
163 }
164
165 if features.len() % 100_000 == 0 {
166 pb.set_message(format!("Loaded {} features...", features.len()));
167 }
168 }
169
170 pb.finish_with_message(format!("Loaded {} features", features.len()));
171
172 Ok(LoadedData {
173 features,
174 bounds: BoundingBox::new(min_lon, min_lat, max_lon, max_lat),
175 time_range: TimeRange::new(min_time, max_time),
176 })
177}
178
179fn find_geometry_column(schema: &arrow::datatypes::Schema) -> Result<String> {
186 let common_names = ["geometry", "geom", "wkb_geometry", "the_geom", "shape"];
187
188 for name in common_names {
189 if schema.field_with_name(name).is_ok() {
190 return Ok(name.to_string());
191 }
192 }
193
194 for field in schema.fields() {
196 if matches!(field.data_type(), DataType::Binary | DataType::LargeBinary) {
197 return Ok(field.name().clone());
198 }
199 }
200
201 for field in schema.fields() {
203 if matches!(field.data_type(), DataType::Struct(_)) {
204 return Ok(field.name().clone());
205 }
206 }
207
208 let has_lon = schema.field_with_name("lon").is_ok()
210 || schema.field_with_name("longitude").is_ok()
211 || schema.field_with_name("x").is_ok();
212 let has_lat = schema.field_with_name("lat").is_ok()
213 || schema.field_with_name("latitude").is_ok()
214 || schema.field_with_name("y").is_ok();
215
216 if has_lon && has_lat {
217 return Ok("__lon_lat__".to_string());
218 }
219
220 anyhow::bail!("Could not find geometry column in Parquet schema")
221}
222
223fn extract_geometries_from_batch(
225 batch: &arrow::record_batch::RecordBatch,
226 geom_col_name: &str,
227) -> Result<Vec<(GeometryType, usize, f64, f64)>> {
228 let mut results = Vec::with_capacity(batch.num_rows());
229
230 if geom_col_name == "__lon_lat__" {
232 let lon_col = batch.column_by_name("lon")
233 .or_else(|| batch.column_by_name("longitude"))
234 .or_else(|| batch.column_by_name("x"));
235 let lat_col = batch.column_by_name("lat")
236 .or_else(|| batch.column_by_name("latitude"))
237 .or_else(|| batch.column_by_name("y"));
238
239 if let (Some(lon), Some(lat)) = (lon_col, lat_col) {
240 if let (Some(lon_arr), Some(lat_arr)) = (
241 lon.as_any().downcast_ref::<Float64Array>(),
242 lat.as_any().downcast_ref::<Float64Array>(),
243 ) {
244 for i in 0..batch.num_rows() {
245 if lon_arr.is_valid(i) && lat_arr.is_valid(i) {
246 results.push((GeometryType::Point, 1, lon_arr.value(i), lat_arr.value(i)));
247 } else {
248 results.push((GeometryType::Unknown, 0, 0.0, 0.0));
249 }
250 }
251 return Ok(results);
252 }
253 }
254 anyhow::bail!("Expected lon/lat columns but could not read them");
255 }
256
257 let geom_col = batch.column_by_name(geom_col_name)
258 .ok_or_else(|| anyhow::anyhow!("Geometry column '{}' not found", geom_col_name))?;
259
260 if let Some(struct_array) = geom_col.as_any().downcast_ref::<arrow::array::StructArray>() {
262 let x_col = struct_array.column_by_name("x")
263 .or_else(|| struct_array.column_by_name("longitude"))
264 .or_else(|| struct_array.column_by_name("lon"));
265 let y_col = struct_array.column_by_name("y")
266 .or_else(|| struct_array.column_by_name("latitude"))
267 .or_else(|| struct_array.column_by_name("lat"));
268
269 if let (Some(x), Some(y)) = (x_col, y_col) {
270 if let (Some(x_arr), Some(y_arr)) = (
271 x.as_any().downcast_ref::<Float64Array>(),
272 y.as_any().downcast_ref::<Float64Array>(),
273 ) {
274 for i in 0..batch.num_rows() {
275 if x_arr.is_valid(i) && y_arr.is_valid(i) {
276 results.push((GeometryType::Point, 1, x_arr.value(i), y_arr.value(i)));
277 } else {
278 results.push((GeometryType::Unknown, 0, 0.0, 0.0));
279 }
280 }
281 return Ok(results);
282 }
283 }
284 }
285
286 if let Some(binary_array) = geom_col.as_any().downcast_ref::<arrow::array::BinaryArray>() {
288 for i in 0..batch.num_rows() {
289 if binary_array.is_valid(i) {
290 let wkb = binary_array.value(i);
291 if let Some((geom_type, vertex_count, lon, lat)) = parse_wkb_info(wkb) {
292 results.push((geom_type, vertex_count, lon, lat));
293 } else {
294 results.push((GeometryType::Unknown, 0, 0.0, 0.0));
295 }
296 } else {
297 results.push((GeometryType::Unknown, 0, 0.0, 0.0));
298 }
299 }
300 return Ok(results);
301 }
302
303 let lon_col = batch.column_by_name("lon")
305 .or_else(|| batch.column_by_name("longitude"))
306 .or_else(|| batch.column_by_name("x"));
307 let lat_col = batch.column_by_name("lat")
308 .or_else(|| batch.column_by_name("latitude"))
309 .or_else(|| batch.column_by_name("y"));
310
311 if let (Some(lon), Some(lat)) = (lon_col, lat_col) {
312 if let (Some(lon_arr), Some(lat_arr)) = (
313 lon.as_any().downcast_ref::<Float64Array>(),
314 lat.as_any().downcast_ref::<Float64Array>(),
315 ) {
316 for i in 0..batch.num_rows() {
317 if lon_arr.is_valid(i) && lat_arr.is_valid(i) {
318 results.push((GeometryType::Point, 1, lon_arr.value(i), lat_arr.value(i)));
319 } else {
320 results.push((GeometryType::Unknown, 0, 0.0, 0.0));
321 }
322 }
323 return Ok(results);
324 }
325 }
326
327 anyhow::bail!("Could not extract geometries from column '{}'", geom_col_name)
328}
329
330fn extract_timestamps_from_batch(
332 batch: &arrow::record_batch::RecordBatch,
333 col_idx: usize,
334 time_format: &str,
335) -> Result<Vec<u64>> {
336 let column = batch.column(col_idx);
337 let mut timestamps = Vec::with_capacity(batch.num_rows());
338
339 macro_rules! push_ts_column {
346 ($arr:expr, $unit:expr) => {{
347 for i in 0..batch.num_rows() {
348 if $arr.is_valid(i) {
349 timestamps.push(normalize_timestamp_to_ms(i, $arr.value(i), $unit)?);
350 } else {
351 timestamps.push(0);
352 }
353 }
354 return Ok(timestamps);
355 }};
356 }
357
358 if let Some(ts_array) = column.as_any().downcast_ref::<TimestampSecondArray>() {
359 push_ts_column!(ts_array, TimestampUnit::Second);
360 }
361 if let Some(ts_array) = column.as_any().downcast_ref::<TimestampMillisecondArray>() {
362 push_ts_column!(ts_array, TimestampUnit::Millisecond);
363 }
364 if let Some(ts_array) = column.as_any().downcast_ref::<TimestampMicrosecondArray>() {
365 push_ts_column!(ts_array, TimestampUnit::Microsecond);
366 }
367 if let Some(ts_array) = column.as_any().downcast_ref::<TimestampNanosecondArray>() {
368 push_ts_column!(ts_array, TimestampUnit::Nanosecond);
369 }
370
371 if let Some(int_array) = column.as_any().downcast_ref::<Int64Array>() {
373 let unit = match time_format {
374 "unix-sec" => TimestampUnit::Second,
375 _ => TimestampUnit::Millisecond,
376 };
377 for i in 0..batch.num_rows() {
378 if int_array.is_valid(i) {
379 timestamps.push(normalize_timestamp_to_ms(i, int_array.value(i), unit)?);
380 } else {
381 timestamps.push(0);
382 }
383 }
384 return Ok(timestamps);
385 }
386
387 if let Some(str_array) = column.as_any().downcast_ref::<StringArray>() {
389 for i in 0..batch.num_rows() {
390 if str_array.is_valid(i) {
391 let s = str_array.value(i);
392 let ts = parse_iso8601(s).unwrap_or(0);
393 timestamps.push(ts);
394 } else {
395 timestamps.push(0);
396 }
397 }
398 return Ok(timestamps);
399 }
400
401 anyhow::bail!("Unsupported timestamp column type")
402}
403
404fn parse_iso8601(s: &str) -> Result<u64> {
406 use chrono::{DateTime, NaiveDateTime};
407
408 if let Ok(dt) = s.parse::<DateTime<chrono::Utc>>() {
409 return Ok(dt.timestamp_millis() as u64);
410 }
411
412 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
413 return Ok(dt.and_utc().timestamp_millis() as u64);
414 }
415
416 if let Ok(date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
417 let dt = date.and_hms_opt(0, 0, 0).unwrap().and_utc();
418 return Ok(dt.timestamp_millis() as u64);
419 }
420
421 anyhow::bail!("Failed to parse timestamp: {}", s)
422}
423
424const WKB_POINT: u32 = 1;
429const WKB_LINESTRING: u32 = 2;
430const WKB_POLYGON: u32 = 3;
431const WKB_MULTIPOINT: u32 = 4;
432const WKB_MULTILINESTRING: u32 = 5;
433const WKB_MULTIPOLYGON: u32 = 6;
434
435fn parse_wkb_info(wkb: &[u8]) -> Option<(GeometryType, usize, f64, f64)> {
436 if wkb.len() < 5 {
437 return None;
438 }
439
440 let little_endian = wkb[0] == 1;
441 let geom_type = if little_endian {
442 u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]) % 1000
443 } else {
444 u32::from_be_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]) % 1000
445 };
446
447 match geom_type {
448 WKB_POINT => parse_wkb_point_info(wkb, little_endian),
449 WKB_LINESTRING => parse_wkb_linestring_info(wkb, little_endian),
450 WKB_POLYGON => parse_wkb_polygon_info(wkb, little_endian),
451 WKB_MULTIPOINT => parse_wkb_multipoint_info(wkb, little_endian),
452 WKB_MULTILINESTRING => parse_wkb_multilinestring_info(wkb, little_endian),
453 WKB_MULTIPOLYGON => parse_wkb_multipolygon_info(wkb, little_endian),
454 _ => None,
455 }
456}
457
458fn read_f64(wkb: &[u8], offset: usize, little_endian: bool) -> Option<f64> {
459 if offset + 8 > wkb.len() {
460 return None;
461 }
462 let bytes: [u8; 8] = wkb[offset..offset+8].try_into().ok()?;
463 Some(if little_endian {
464 f64::from_le_bytes(bytes)
465 } else {
466 f64::from_be_bytes(bytes)
467 })
468}
469
470fn read_u32(wkb: &[u8], offset: usize, little_endian: bool) -> Option<u32> {
471 if offset + 4 > wkb.len() {
472 return None;
473 }
474 let bytes: [u8; 4] = wkb[offset..offset+4].try_into().ok()?;
475 Some(if little_endian {
476 u32::from_le_bytes(bytes)
477 } else {
478 u32::from_be_bytes(bytes)
479 })
480}
481
482fn parse_wkb_point_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
483 let x = read_f64(wkb, 5, little_endian)?;
484 let y = read_f64(wkb, 13, little_endian)?;
485 Some((GeometryType::Point, 1, x, y))
486}
487
488fn parse_wkb_linestring_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
489 let num_points = read_u32(wkb, 5, little_endian)? as usize;
490 if num_points == 0 {
491 return None;
492 }
493
494 let mut sum_x = 0.0;
495 let mut sum_y = 0.0;
496 let mut offset = 9;
497
498 for _ in 0..num_points {
499 let x = read_f64(wkb, offset, little_endian)?;
500 let y = read_f64(wkb, offset + 8, little_endian)?;
501 sum_x += x;
502 sum_y += y;
503 offset += 16;
504 }
505
506 Some((GeometryType::LineString, num_points, sum_x / num_points as f64, sum_y / num_points as f64))
507}
508
509fn parse_wkb_polygon_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
510 let num_rings = read_u32(wkb, 5, little_endian)? as usize;
511 if num_rings == 0 {
512 return None;
513 }
514
515 let mut total_points = 0usize;
516 let mut sum_x = 0.0;
517 let mut sum_y = 0.0;
518 let mut offset = 9;
519
520 for ring_idx in 0..num_rings {
521 let num_points = read_u32(wkb, offset, little_endian)? as usize;
522 offset += 4;
523
524 for _ in 0..num_points {
525 let x = read_f64(wkb, offset, little_endian)?;
526 let y = read_f64(wkb, offset + 8, little_endian)?;
527
528 if ring_idx == 0 {
529 sum_x += x;
530 sum_y += y;
531 total_points += 1;
532 }
533
534 offset += 16;
535 }
536 }
537
538 let vertex_count: usize = total_points;
539 let centroid_x = if total_points > 0 { sum_x / total_points as f64 } else { 0.0 };
540 let centroid_y = if total_points > 0 { sum_y / total_points as f64 } else { 0.0 };
541
542 Some((GeometryType::Polygon, vertex_count, centroid_x, centroid_y))
543}
544
545fn parse_wkb_multipoint_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
546 let num_geoms = read_u32(wkb, 5, little_endian)? as usize;
547 if num_geoms == 0 {
548 return None;
549 }
550
551 let mut sum_x = 0.0;
552 let mut sum_y = 0.0;
553 let mut offset = 9;
554
555 for _ in 0..num_geoms {
556 offset += 5; let x = read_f64(wkb, offset, little_endian)?;
558 let y = read_f64(wkb, offset + 8, little_endian)?;
559 sum_x += x;
560 sum_y += y;
561 offset += 16;
562 }
563
564 Some((GeometryType::MultiPoint, num_geoms, sum_x / num_geoms as f64, sum_y / num_geoms as f64))
565}
566
567fn parse_wkb_multilinestring_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
568 let num_geoms = read_u32(wkb, 5, little_endian)? as usize;
569 if num_geoms == 0 {
570 return None;
571 }
572
573 let mut total_points = 0usize;
574 let mut sum_x = 0.0;
575 let mut sum_y = 0.0;
576 let mut offset = 9;
577
578 for _ in 0..num_geoms {
579 offset += 5; let num_points = read_u32(wkb, offset, little_endian)? as usize;
581 offset += 4;
582
583 for _ in 0..num_points {
584 let x = read_f64(wkb, offset, little_endian)?;
585 let y = read_f64(wkb, offset + 8, little_endian)?;
586 sum_x += x;
587 sum_y += y;
588 total_points += 1;
589 offset += 16;
590 }
591 }
592
593 let centroid_x = if total_points > 0 { sum_x / total_points as f64 } else { 0.0 };
594 let centroid_y = if total_points > 0 { sum_y / total_points as f64 } else { 0.0 };
595
596 Some((GeometryType::MultiLineString, total_points, centroid_x, centroid_y))
597}
598
599fn parse_wkb_multipolygon_info(wkb: &[u8], little_endian: bool) -> Option<(GeometryType, usize, f64, f64)> {
600 let num_geoms = read_u32(wkb, 5, little_endian)? as usize;
601 if num_geoms == 0 {
602 return None;
603 }
604
605 let mut total_points = 0usize;
606 let mut sum_x = 0.0;
607 let mut sum_y = 0.0;
608 let mut offset = 9;
609
610 for poly_idx in 0..num_geoms {
611 offset += 5; let num_rings = read_u32(wkb, offset, little_endian)? as usize;
613 offset += 4;
614
615 for ring_idx in 0..num_rings {
616 let num_points = read_u32(wkb, offset, little_endian)? as usize;
617 offset += 4;
618
619 for _ in 0..num_points {
620 let x = read_f64(wkb, offset, little_endian)?;
621 let y = read_f64(wkb, offset + 8, little_endian)?;
622
623 if poly_idx == 0 && ring_idx == 0 {
624 sum_x += x;
625 sum_y += y;
626 total_points += 1;
627 }
628
629 offset += 16;
630 }
631 }
632 }
633
634 let centroid_x = if total_points > 0 { sum_x / total_points as f64 } else { 0.0 };
635 let centroid_y = if total_points > 0 { sum_y / total_points as f64 } else { 0.0 };
636
637 Some((GeometryType::MultiPolygon, total_points, centroid_x, centroid_y))
638}
639