kml first commit
This commit is contained in:
4343
src-tauri/src/commands.rs
Normal file
4343
src-tauri/src/commands.rs
Normal file
File diff suppressed because it is too large
Load Diff
247
src-tauri/src/db.rs
Normal file
247
src-tauri/src/db.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use sqlx::SqlitePool;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn init_pool(db_path: &Path) -> Result<SqlitePool> {
|
||||
if let Some(parent) = db_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let url = format!("sqlite://{}", db_path.display());
|
||||
let opts = SqliteConnectOptions::from_str(&url)?
|
||||
.create_if_missing(true)
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(4)
|
||||
.connect_with(opts)
|
||||
.await?;
|
||||
|
||||
apply_schema(&pool).await?;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS assets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
row_id TEXT NOT NULL UNIQUE,
|
||||
asset_type TEXT NOT NULL CHECK (asset_type IN ('fixed','range')),
|
||||
asset_name TEXT NOT NULL,
|
||||
video_name TEXT NOT NULL,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
start_lat REAL,
|
||||
start_lng REAL,
|
||||
end_lat REAL,
|
||||
end_lng REAL,
|
||||
min_lat REAL NOT NULL,
|
||||
max_lat REAL NOT NULL,
|
||||
min_lng REAL NOT NULL,
|
||||
max_lng REAL NOT NULL,
|
||||
image_path TEXT,
|
||||
image_path1 TEXT,
|
||||
image_path2 TEXT,
|
||||
image_path3 TEXT,
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
modified INTEGER NOT NULL DEFAULT 0,
|
||||
modified_by TEXT,
|
||||
modified_at TEXT,
|
||||
merged_into INTEGER REFERENCES assets(id),
|
||||
in_scope INTEGER
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_assets_video ON assets(video_name)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_assets_type ON assets(asset_type)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_assets_name ON assets(asset_name)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
// Migration: side detected from image_path (LHS/RHS) at import time.
|
||||
let _ = sqlx::query("ALTER TABLE assets ADD COLUMN side TEXT")
|
||||
.execute(pool)
|
||||
.await;
|
||||
// Manual scope override: NULL = derived from scope polygons (default),
|
||||
// 0 = forced out-of-scope, 1 = forced in-scope. Set via lasso bulk edits.
|
||||
let _ = sqlx::query("ALTER TABLE assets ADD COLUMN manual_scope INTEGER")
|
||||
.execute(pool)
|
||||
.await;
|
||||
// Cross-side / cross-video pair links: link_pair_id is the partner asset's id.
|
||||
// link_locked = 1 marks a manual link that survives auto-match re-runs.
|
||||
let _ = sqlx::query("ALTER TABLE assets ADD COLUMN link_pair_id INTEGER")
|
||||
.execute(pool)
|
||||
.await;
|
||||
let _ = sqlx::query(
|
||||
"ALTER TABLE assets ADD COLUMN link_locked INTEGER NOT NULL DEFAULT 0",
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
// Original positions captured at import time. Never overwritten by edits or
|
||||
// re-imports; used to support "Reset to original" (single + bulk).
|
||||
for col in [
|
||||
"orig_lat", "orig_lng",
|
||||
"orig_start_lat", "orig_start_lng",
|
||||
"orig_end_lat", "orig_end_lng",
|
||||
] {
|
||||
let _ = sqlx::query(&format!("ALTER TABLE assets ADD COLUMN {} REAL", col))
|
||||
.execute(pool)
|
||||
.await;
|
||||
}
|
||||
let _ = sqlx::query("ALTER TABLE roads ADD COLUMN oneway INTEGER NOT NULL DEFAULT 0")
|
||||
.execute(pool)
|
||||
.await;
|
||||
let _ = sqlx::query("ALTER TABLE roads ADD COLUMN highway TEXT")
|
||||
.execute(pool)
|
||||
.await;
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE VIRTUAL TABLE IF NOT EXISTS assets_rtree USING rtree(
|
||||
id, min_lat, max_lat, min_lng, max_lng
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER IF NOT EXISTS assets_ai AFTER INSERT ON assets
|
||||
BEGIN
|
||||
INSERT INTO assets_rtree(id, min_lat, max_lat, min_lng, max_lng)
|
||||
VALUES (NEW.id, NEW.min_lat, NEW.max_lat, NEW.min_lng, NEW.max_lng);
|
||||
END",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER IF NOT EXISTS assets_au AFTER UPDATE ON assets
|
||||
BEGIN
|
||||
UPDATE assets_rtree
|
||||
SET min_lat = NEW.min_lat, max_lat = NEW.max_lat,
|
||||
min_lng = NEW.min_lng, max_lng = NEW.max_lng
|
||||
WHERE id = OLD.id;
|
||||
END",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER IF NOT EXISTS assets_ad AFTER DELETE ON assets
|
||||
BEGIN
|
||||
DELETE FROM assets_rtree WHERE id = OLD.id;
|
||||
END",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO assets_rtree(id, min_lat, max_lat, min_lng, max_lng)
|
||||
SELECT id, min_lat, max_lat, min_lng, max_lng FROM assets
|
||||
WHERE id NOT IN (SELECT id FROM assets_rtree)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS scope_polygons (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
geojson TEXT NOT NULL,
|
||||
geom_type TEXT NOT NULL DEFAULT 'polygon'
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
// Migration for DBs predating geom_type column.
|
||||
let _ = sqlx::query(
|
||||
"ALTER TABLE scope_polygons ADD COLUMN geom_type TEXT NOT NULL DEFAULT 'polygon'",
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS roads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
geojson TEXT NOT NULL,
|
||||
min_lat REAL NOT NULL,
|
||||
max_lat REAL NOT NULL,
|
||||
min_lng REAL NOT NULL,
|
||||
max_lng REAL NOT NULL
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"CREATE VIRTUAL TABLE IF NOT EXISTS roads_rtree USING rtree(
|
||||
id, min_lat, max_lat, min_lng, max_lng
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER IF NOT EXISTS roads_ai AFTER INSERT ON roads
|
||||
BEGIN
|
||||
INSERT INTO roads_rtree(id, min_lat, max_lat, min_lng, max_lng)
|
||||
VALUES (NEW.id, NEW.min_lat, NEW.max_lat, NEW.min_lng, NEW.max_lng);
|
||||
END",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"CREATE TRIGGER IF NOT EXISTS roads_ad AFTER DELETE ON roads
|
||||
BEGIN
|
||||
DELETE FROM roads_rtree WHERE id = OLD.id;
|
||||
END",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE IF NOT EXISTS action_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
action_type TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
asset_ids TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
before TEXT,
|
||||
after TEXT,
|
||||
ts TEXT NOT NULL,
|
||||
undone INTEGER NOT NULL DEFAULT 0
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
// Migration for DBs created before `after` column existed.
|
||||
let _ = sqlx::query("ALTER TABLE action_history ADD COLUMN after TEXT")
|
||||
.execute(pool)
|
||||
.await;
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_audit_ts ON action_history(ts DESC)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query("CREATE INDEX IF NOT EXISTS idx_audit_undone ON action_history(undone)")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
96
src-tauri/src/lib.rs
Normal file
96
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
mod commands;
|
||||
mod db;
|
||||
mod scope;
|
||||
|
||||
use commands::{
|
||||
delete_asset, delete_assets_by_video, get_assets_in_bbox, get_osm_roads,
|
||||
get_scope_polygons, import_fixed_assets, import_kml_scope, import_osm_geojson,
|
||||
import_range_assets, list_assets, list_recent_actions, merge_polylines, reset_assets_to_original,
|
||||
read_metadata_polyline, redo_action, reset_database, snap_assets_in_bbox,
|
||||
add_class, auto_link_in_polygon, bulk_translate, change_assets_side, clear_link, clear_manual_scope,
|
||||
create_osm_road, create_user, set_link,
|
||||
delete_roads_by_highway, export_osm_roads, find_duplicate_clusters,
|
||||
flip_road_direction, generate_osm_for_bbox, list_road_classes, merge_roads_by_name,
|
||||
delete_assets_bulk, delete_osm_road, delete_user, export_assets,
|
||||
list_asset_names, list_classes, list_users, list_video_names,
|
||||
rename_assets, restore_assets, set_assets_by_video_in_scope,
|
||||
set_assets_in_scope, set_current_user, snap_to_road,
|
||||
undo_action, update_asset_position, update_osm_road, AppState,
|
||||
};
|
||||
use tauri::Manager;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
let app_data = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.expect("could not resolve app data dir");
|
||||
let db_path = app_data.join("markers.db");
|
||||
|
||||
let handle = app.handle().clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = db::init_pool(&db_path)
|
||||
.await
|
||||
.expect("failed to initialize SQLite pool");
|
||||
handle.manage(AppState { pool });
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
import_fixed_assets,
|
||||
import_range_assets,
|
||||
list_assets,
|
||||
get_assets_in_bbox,
|
||||
update_asset_position,
|
||||
import_kml_scope,
|
||||
get_scope_polygons,
|
||||
delete_asset,
|
||||
delete_assets_by_video,
|
||||
undo_action,
|
||||
list_recent_actions,
|
||||
merge_polylines, reset_assets_to_original,
|
||||
reset_database,
|
||||
redo_action,
|
||||
import_osm_geojson,
|
||||
snap_to_road,
|
||||
get_osm_roads,
|
||||
read_metadata_polyline,
|
||||
snap_assets_in_bbox,
|
||||
update_osm_road,
|
||||
create_osm_road,
|
||||
delete_osm_road,
|
||||
list_asset_names,
|
||||
export_assets,
|
||||
set_assets_in_scope,
|
||||
set_assets_by_video_in_scope,
|
||||
clear_manual_scope,
|
||||
delete_assets_bulk,
|
||||
list_video_names,
|
||||
list_classes,
|
||||
add_class,
|
||||
rename_assets,
|
||||
restore_assets,
|
||||
list_users,
|
||||
create_user,
|
||||
delete_user,
|
||||
set_current_user,
|
||||
find_duplicate_clusters,
|
||||
delete_roads_by_highway,
|
||||
list_road_classes,
|
||||
merge_roads_by_name,
|
||||
export_osm_roads,
|
||||
generate_osm_for_bbox,
|
||||
flip_road_direction,
|
||||
change_assets_side,
|
||||
auto_link_in_polygon,
|
||||
bulk_translate,
|
||||
set_link,
|
||||
clear_link
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
kml_map_tool_lib::run()
|
||||
}
|
||||
270
src-tauri/src/scope.rs
Normal file
270
src-tauri/src/scope.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
use anyhow::Result;
|
||||
use geo::algorithm::Distance;
|
||||
use geo::{ClosestPoint, Closest, Contains, Coord, Geodesic, LineString, Point, Polygon as GeoPolygon};
|
||||
use kml::types::{Geometry as KmlGeom, Kml, Placemark};
|
||||
use sqlx::SqlitePool;
|
||||
use std::path::Path;
|
||||
|
||||
/// Distance threshold for "near a scope LineString" → in_scope = 1.
|
||||
const LINE_PROXIMITY_M: f64 = 30.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum ScopeGeom {
|
||||
Polygon(GeoPolygon<f64>),
|
||||
Line(LineString<f64>),
|
||||
}
|
||||
|
||||
pub struct ParsedFeature {
|
||||
pub name: Option<String>,
|
||||
pub geom: ScopeGeom,
|
||||
pub geojson: String,
|
||||
pub geom_type: &'static str,
|
||||
}
|
||||
|
||||
pub async fn parse_kml(path: &Path) -> Result<Vec<ParsedFeature>> {
|
||||
let s = tokio::fs::read_to_string(path).await?;
|
||||
let kml: Kml = s.parse()?;
|
||||
let mut out = vec![];
|
||||
walk(&kml, None, &mut out);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn walk(kml: &Kml, parent_name: Option<&str>, out: &mut Vec<ParsedFeature>) {
|
||||
match kml {
|
||||
Kml::KmlDocument(doc) => {
|
||||
for el in &doc.elements {
|
||||
walk(el, parent_name, out);
|
||||
}
|
||||
}
|
||||
Kml::Document { elements, .. } => {
|
||||
for el in elements {
|
||||
walk(el, parent_name, out);
|
||||
}
|
||||
}
|
||||
Kml::Folder(folder) => {
|
||||
for el in &folder.elements {
|
||||
walk(el, folder.name.as_deref(), out);
|
||||
}
|
||||
}
|
||||
Kml::Placemark(p) => {
|
||||
let name = p.name.clone();
|
||||
if let Some(g) = &p.geometry {
|
||||
extract(g, name.as_deref(), p, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract(g: &KmlGeom, name: Option<&str>, _p: &Placemark, out: &mut Vec<ParsedFeature>) {
|
||||
match g {
|
||||
KmlGeom::Polygon(poly) => {
|
||||
let outer: Vec<Coord<f64>> = poly
|
||||
.outer
|
||||
.coords
|
||||
.iter()
|
||||
.map(|c| Coord { x: c.x, y: c.y })
|
||||
.collect();
|
||||
if outer.len() < 4 {
|
||||
return;
|
||||
}
|
||||
let inners: Vec<LineString<f64>> = poly
|
||||
.inner
|
||||
.iter()
|
||||
.map(|ring| {
|
||||
LineString(
|
||||
ring.coords
|
||||
.iter()
|
||||
.map(|c| Coord { x: c.x, y: c.y })
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let geo_poly = GeoPolygon::new(LineString(outer), inners);
|
||||
let geojson = polygon_to_geojson(&geo_poly);
|
||||
out.push(ParsedFeature {
|
||||
name: name.map(|s| s.to_string()),
|
||||
geom: ScopeGeom::Polygon(geo_poly),
|
||||
geojson,
|
||||
geom_type: "polygon",
|
||||
});
|
||||
}
|
||||
KmlGeom::LineString(ls) => {
|
||||
let pts: Vec<Coord<f64>> = ls
|
||||
.coords
|
||||
.iter()
|
||||
.map(|c| Coord { x: c.x, y: c.y })
|
||||
.collect();
|
||||
if pts.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let line = LineString(pts);
|
||||
let geojson = linestring_to_geojson(&line);
|
||||
out.push(ParsedFeature {
|
||||
name: name.map(|s| s.to_string()),
|
||||
geom: ScopeGeom::Line(line),
|
||||
geojson,
|
||||
geom_type: "linestring",
|
||||
});
|
||||
}
|
||||
KmlGeom::MultiGeometry(mg) => {
|
||||
for sub in &mg.geometries {
|
||||
extract(sub, name, _p, out);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn polygon_to_geojson(p: &GeoPolygon<f64>) -> String {
|
||||
let mut rings: Vec<Vec<[f64; 2]>> = vec![];
|
||||
rings.push(p.exterior().0.iter().map(|c| [c.x, c.y]).collect());
|
||||
for inner in p.interiors() {
|
||||
rings.push(inner.0.iter().map(|c| [c.x, c.y]).collect());
|
||||
}
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"type": "Polygon",
|
||||
"coordinates": rings
|
||||
}))
|
||||
.unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
fn linestring_to_geojson(l: &LineString<f64>) -> String {
|
||||
let coords: Vec<[f64; 2]> = l.0.iter().map(|c| [c.x, c.y]).collect();
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"type": "LineString",
|
||||
"coordinates": coords
|
||||
}))
|
||||
.unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
pub async fn replace_scope(pool: &SqlitePool, features: &[ParsedFeature]) -> Result<()> {
|
||||
let mut tx = pool.begin().await?;
|
||||
sqlx::query("DELETE FROM scope_polygons")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
for f in features {
|
||||
sqlx::query(
|
||||
"INSERT INTO scope_polygons(name, geojson, geom_type) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(&f.name)
|
||||
.bind(&f.geojson)
|
||||
.bind(f.geom_type)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// in_scope = inside any polygon OR within LINE_PROXIMITY_M of any LineString.
|
||||
/// Range assets are tagged by midpoint only (v1 limitation).
|
||||
pub async fn apply_scope_to_assets(
|
||||
pool: &SqlitePool,
|
||||
geoms: &[ScopeGeom],
|
||||
) -> Result<usize> {
|
||||
if geoms.is_empty() {
|
||||
// Reset only assets that don't have a manual override.
|
||||
sqlx::query("UPDATE assets SET in_scope = NULL WHERE manual_scope IS NULL")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
return Ok(0);
|
||||
}
|
||||
let rows: Vec<(i64, Option<f64>, Option<f64>)> = sqlx::query_as(
|
||||
"SELECT id, lat, lng FROM assets WHERE manual_scope IS NULL",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
let mut tx = pool.begin().await?;
|
||||
let mut tagged = 0usize;
|
||||
for (id, lat, lng) in rows {
|
||||
let in_scope = match (lat, lng) {
|
||||
(Some(la), Some(lo)) => {
|
||||
let p = Point::new(lo, la);
|
||||
geoms.iter().any(|g| match g {
|
||||
ScopeGeom::Polygon(poly) => poly.contains(&p),
|
||||
ScopeGeom::Line(line) => match line.closest_point(&p) {
|
||||
Closest::SinglePoint(cp) | Closest::Intersection(cp) => {
|
||||
Geodesic.distance(p, cp) <= LINE_PROXIMITY_M
|
||||
}
|
||||
Closest::Indeterminate => false,
|
||||
},
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
sqlx::query("UPDATE assets SET in_scope = ? WHERE id = ?")
|
||||
.bind(in_scope as i64)
|
||||
.bind(id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tagged += 1;
|
||||
}
|
||||
tx.commit().await?;
|
||||
Ok(tagged)
|
||||
}
|
||||
|
||||
pub async fn load_geoms(pool: &SqlitePool) -> Result<Vec<ScopeGeom>> {
|
||||
let rows: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT geojson, geom_type FROM scope_polygons")
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
let mut out = vec![];
|
||||
for (gj, gt) in rows {
|
||||
let value = match serde_json::from_str::<serde_json::Value>(&gj) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let coords = match value.get("coordinates") {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
if gt == "polygon" {
|
||||
let arr = match coords.as_array() {
|
||||
Some(a) => a,
|
||||
None => continue,
|
||||
};
|
||||
let mut rings: Vec<LineString<f64>> = vec![];
|
||||
for ring in arr {
|
||||
if let Some(ring_arr) = ring.as_array() {
|
||||
let pts: Vec<Coord<f64>> = ring_arr
|
||||
.iter()
|
||||
.filter_map(|p| p.as_array())
|
||||
.filter_map(|p| {
|
||||
Some(Coord {
|
||||
x: p.first()?.as_f64()?,
|
||||
y: p.get(1)?.as_f64()?,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if pts.len() >= 4 {
|
||||
rings.push(LineString(pts));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(outer) = rings.first().cloned() {
|
||||
let inners: Vec<LineString<f64>> = rings.into_iter().skip(1).collect();
|
||||
out.push(ScopeGeom::Polygon(GeoPolygon::new(outer, inners)));
|
||||
}
|
||||
} else if gt == "linestring" {
|
||||
let arr = match coords.as_array() {
|
||||
Some(a) => a,
|
||||
None => continue,
|
||||
};
|
||||
let pts: Vec<Coord<f64>> = arr
|
||||
.iter()
|
||||
.filter_map(|p| p.as_array())
|
||||
.filter_map(|p| {
|
||||
Some(Coord {
|
||||
x: p.first()?.as_f64()?,
|
||||
y: p.get(1)?.as_f64()?,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if pts.len() >= 2 {
|
||||
out.push(ScopeGeom::Line(LineString(pts)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
Reference in New Issue
Block a user