osm merge, optmisation
This commit is contained in:
@@ -104,7 +104,7 @@ Coordinates are `[lat, lng]`; the importer swaps to `[lng, lat]` internally. Gar
|
|||||||
- Non-finite or out-of-range lat/lng.
|
- Non-finite or out-of-range lat/lng.
|
||||||
- Jumps > 5 km between consecutive points.
|
- Jumps > 5 km between consecutive points.
|
||||||
|
|
||||||
Stored only in memory (cleared by the **Loaded data → Clear** button or app restart).
|
**Persisted in the SQLite database** (`video_metadata` table, keyed by `video_name`). Once imported, the polylines hydrate automatically on every app start — you don't have to re-pick the file each session. Cleared by the **Loaded data → Clear** button on the Per-video metadata row, or by **Reset DB** (which wipes the whole database).
|
||||||
|
|
||||||
### Metadata polyline (legacy single-track)
|
### Metadata polyline (legacy single-track)
|
||||||
Single-video legacy format used by the old FastAPI pipeline.
|
Single-video legacy format used by the old FastAPI pipeline.
|
||||||
|
|||||||
154
README.md
154
README.md
@@ -12,7 +12,7 @@ Tauri 2 desktop app for reviewing, correcting and exporting fixed-asset / range-
|
|||||||
- **Range assets (JSON)** — linear assets with start / mid / end (e.g., crash barriers).
|
- **Range assets (JSON)** — linear assets with start / mid / end (e.g., crash barriers).
|
||||||
- **KML scope polygon** — defines which assets are "in scope". Points outside the polygon are hidden by default.
|
- **KML scope polygon** — defines which assets are "in scope". Points outside the polygon are hidden by default.
|
||||||
- **OSM roads (GeoJSON)** — imported road network for snapping / direction lookups.
|
- **OSM roads (GeoJSON)** — imported road network for snapping / direction lookups.
|
||||||
- **Metadata polyline (KML)** — vehicle GPS track from the source video; takes priority over OSM for direction inference.
|
- **Per-video metadata (JSON)** — vehicle GPS track per video; takes priority over OSM for direction inference. **Persists in the SQLite database**, so you only pick the file once — every subsequent app start hydrates it automatically. Cleared only by the **Clear** button on the Per-video metadata row, or by **Reset DB**.
|
||||||
3. The map auto-centers on the first imported asset. Pan / zoom triggers viewport-bounded fetches (debounced) to keep the deck.gl rendering snappy.
|
3. The map auto-centers on the first imported asset. Pan / zoom triggers viewport-bounded fetches (debounced) to keep the deck.gl rendering snappy.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -51,6 +51,23 @@ Tauri 2 desktop app for reviewing, correcting and exporting fixed-asset / range-
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Single-video focus mode
|
||||||
|
|
||||||
|
When you want to inspect / clean one video's worth of data in isolation:
|
||||||
|
|
||||||
|
- **Tap that video's metadata polyline on the map**, or
|
||||||
|
- **Leave exactly one video checked in the Filters panel**.
|
||||||
|
|
||||||
|
Either path triggers focus mode:
|
||||||
|
|
||||||
|
- Other videos' metadata polylines are hidden — only the focused track renders.
|
||||||
|
- OSM roads are spatially filtered to those within ~75 m of the focused track (cheap spatial-hash buffer; recomputed on each switch).
|
||||||
|
- Clicking the metadata line again, or unchecking the lone video filter, restores the full view.
|
||||||
|
|
||||||
|
Focus mode also unlocks the bulk OSM road tools (multi-select / hide / merge — see **OSM road editing**). Selection and hidden state are session-scoped: they reset whenever you exit OSM edit mode or change the focused video.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-side / cross-video pair linking
|
## Cross-side / cross-video pair linking
|
||||||
|
|
||||||
Common situation: one physical asset (e.g., a median streetlight) is captured in two videos (LHS and RHS) and shows up as two near-duplicate points. Pair linking marks them as the same physical thing without deleting either.
|
Common situation: one physical asset (e.g., a median streetlight) is captured in two videos (LHS and RHS) and shows up as two near-duplicate points. Pair linking marks them as the same physical thing without deleting either.
|
||||||
@@ -97,45 +114,70 @@ Range assets (start / mid / end) translate as a rigid body — all three vertice
|
|||||||
## OSM road editing
|
## OSM road editing
|
||||||
|
|
||||||
### Modes
|
### Modes
|
||||||
- **OSM tools…** modal — generate roads for the visible bbox (Overpass), import existing GeoJSON, export current roads, prune small road classes.
|
- **OSM tools…** modal — generate roads for the visible bbox or for a buffer around imported assets (Overpass), import existing GeoJSON, export current roads, prune small road classes.
|
||||||
- **Edit OSM roads** button — toggles OSM edit mode. While on, every other layer is dimmed and only OSM roads are interactive.
|
- **Edit OSM roads** button — toggles OSM edit mode. While on, every other layer is dimmed and only OSM roads are interactive.
|
||||||
|
- **Magnifier** button — cursor-following circular lens that zooms +3 levels around the cursor; renders satellite + OSM cyan + dashed-green metadata + red asset dots. Toggle on, hover anywhere, then ESC or click again to exit.
|
||||||
- **Lane offset (m)** — controls the parallel offset used by snap-to-road. Range 0.1–30 m.
|
- **Lane offset (m)** — controls the parallel offset used by snap-to-road. Range 0.1–30 m.
|
||||||
|
|
||||||
### Picking a road to edit
|
### Two click models
|
||||||
1. Turn on **Edit OSM roads** (right panel → Data section).
|
|
||||||
2. Zoom in to **z ≥ 14** — vertex handles only render at that zoom or deeper, to keep the map responsive.
|
|
||||||
3. Click any road. Its yellow `•` vertex pins appear; other roads stay un-pinned. The selected road's id is shown in the OSM-edit panel.
|
|
||||||
|
|
||||||
### Modifying a road
|
OSM edit mode behaves differently depending on whether a single video is focused (see **Single-video focus mode**):
|
||||||
- **Drag a yellow `•` pin** — moves that vertex to wherever you drop it. The change is saved immediately.
|
|
||||||
- **Click a yellow `+` ghost pin** between two vertices — inserts a new vertex at that midpoint. Useful when an OSM segment is too sparse for the curve.
|
- **Non-focus** — click a road to enter the legacy single-road vertex editor (the only road remains visible; vertex pins appear). One road at a time.
|
||||||
- **Flip direction** — cycles `oneway` through forward (`1`) → reverse (`-1`) → undirected (`0`). Use when OSM has the heading wrong on a divided road.
|
- **Focus mode** — click a road to **toggle multi-select** (yellow highlight). All roads stay visible. When the selection narrows to **exactly one** road, the vertex editor automatically engages on it (Flip / Simplify / Delete this road / Ignore for snap appear inline). Click the road again to drop back to multi-select.
|
||||||
|
|
||||||
|
### Modifying a road's vertices (single road active)
|
||||||
|
- **Drag a yellow `•` pin** — moves that vertex. Saved immediately.
|
||||||
|
- **Click a yellow `+` ghost pin** between two vertices — inserts a new vertex at that midpoint.
|
||||||
|
- **Flip direction** — cycles `oneway` through forward (`1`) → reverse (`-1`) → undirected (`0`).
|
||||||
|
- Vertex handles render at z ≥ 14; zoom in if you don't see them.
|
||||||
|
|
||||||
### Deleting vertices
|
### Deleting vertices
|
||||||
Two tools, depending on scope:
|
|
||||||
|
|
||||||
**A. Surgical — Shift-click + Del**
|
**A. Surgical — Shift-click + Del**
|
||||||
1. Hold **Shift** and click a vertex pin → it turns red with an `✕`.
|
1. **Shift-click** a vertex pin → turns red with `✕`. Repeat to mark more.
|
||||||
2. Repeat to mark more.
|
2. Press **Del** → all marked vertices removed in one shot.
|
||||||
3. Press **Del** (or Backspace) → all marked vertices are removed in one shot.
|
3. **Clear marks** unselects without deleting. Shift-click a marked vertex to unmark.
|
||||||
4. The "Currently marked: N" label in the panel shows how many you've flagged. **Clear marks** unselects without deleting.
|
4. The road must keep ≥ 2 vertices.
|
||||||
5. Shift-click a marked vertex again to unmark it individually.
|
|
||||||
6. The road must keep ≥ 2 vertices; the backend rejects deletes that would break that.
|
|
||||||
|
|
||||||
**B. Sweeping — Simplify by tolerance**
|
**B. Sweeping — Simplify by tolerance**
|
||||||
1. Use the **Simplify (m)** slider in the panel (0.5–20 m, default 2 m). Higher = more aggressive.
|
1. **Simplify (m)** slider (0.5–20 m, default 2 m). Higher = more aggressive.
|
||||||
2. Click **Simplify this road**. Runs Douglas-Peucker on the polyline: any vertex within tolerance of the simplified line is dropped.
|
2. Click **Simplify this road** → Douglas-Peucker on the polyline.
|
||||||
3. Status bar reports `before → after` vertex counts.
|
3. Status bar reports `before → after` vertex counts.
|
||||||
4. Try 1–2 m for cleanup, 5+ m for heavy decimation. Re-running with the same tolerance is idempotent.
|
|
||||||
|
|
||||||
### Deleting / flipping the whole road
|
### Bulk road tools (focus mode only)
|
||||||
- **Delete this road** — removes the road outright (with confirmation).
|
|
||||||
- **Deselect road** — exits the per-road editing tool but keeps OSM edit mode on.
|
|
||||||
|
|
||||||
### Notes
|
A "Bulk road tools — focus: <video>" panel appears when OSM edit mode is on inside single-video focus. Workflow:
|
||||||
- OSM road edits are **not** undoable via the Recent actions panel — they're treated like reference-data tweaks. Be deliberate, especially with delete and simplify.
|
|
||||||
- Dragging a vertex triggers a single `update_osm_road` write per drag. Bulk delete and simplify are also single writes.
|
- **Click** roads to build a selection (yellow + thicker stroke).
|
||||||
- After a vertex change, the road line refreshes in real time but the surrounding draggable pins re-render — slight visual flicker is normal.
|
- **Right-click** any road → instantly hides it (no selection needed).
|
||||||
|
- **Del** → moves all currently-selected roads into the hidden bucket and clears the selection.
|
||||||
|
- **Hide selected / Clear selection / Restore hidden** buttons in the panel mirror those shortcuts.
|
||||||
|
- **Merge selected** (≥ 2 picked) — concatenates connected ways into one. Endpoints within ~8 m of each other are clustered first, so OSM ways with near-but-not-touching endpoints still merge — both ends are snapped to the cluster centroid so the merged geometry has no visible gap. **Disconnected clusters become independent merged roads** (no false bridging).
|
||||||
|
- **Merge all visible** — same logic over every non-hidden road in the focused area.
|
||||||
|
- Branched clusters (any node with > 2 incident ways) are skipped rather than corrupted; the toast tells you how many.
|
||||||
|
|
||||||
|
Both **hidden** and **selection** sets are session-scoped — cleared on **Exit OSM edit mode** or focused-video change, never persisted.
|
||||||
|
|
||||||
|
### Ignore-for-snap (persistent)
|
||||||
|
|
||||||
|
Per-road toggle that keeps a road on the map for visual reference but excludes it from snap candidates. Useful for service roads / driveways that the snap should never pick.
|
||||||
|
|
||||||
|
- With **one road selected** in the vertex editor, the **Ignore for snap** button appears next to **Flip direction**.
|
||||||
|
- Ignored roads render in **grey** (vs. the default cyan) and are skipped by both single-asset and bulk snap.
|
||||||
|
- Stored as a `snap_ignored` column on the `roads` table — survives app restarts. Toggle again to re-enable.
|
||||||
|
|
||||||
|
### Whole-road actions
|
||||||
|
- **Delete this road** — removes the road (with confirmation).
|
||||||
|
- **Deselect road** — exits the per-road editor (non-focus only; in focus mode, click the road again).
|
||||||
|
|
||||||
|
### Per-session OSM edit undo
|
||||||
|
|
||||||
|
A 3-deep ring buffer scoped to the current OSM edit session captures the last edits so you can revert them without affecting the global asset audit log.
|
||||||
|
|
||||||
|
- **↶ Undo last edit (N)** button right under "Exit OSM edit mode" — visible in both focused and non-focused mode. Tooltip names the action it will undo.
|
||||||
|
- Covers: **flip direction, simplify road, delete vertices, drag vertex, ignore-for-snap toggle, hide road (right-click + Del), restore hidden**.
|
||||||
|
- **Not** undoable via this stack: merge selected / merge all visible (they consume source rows; this stack doesn't recreate them), creating new roads, deleting whole roads.
|
||||||
|
- Cleared on **Exit OSM edit mode**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -149,6 +191,31 @@ Two tools, depending on scope:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Quality dashboard
|
||||||
|
|
||||||
|
A `<details>` panel in the right sidebar that scans the live asset set for likely-bad rows. Click **Run scan** the first time, **Refresh** thereafter.
|
||||||
|
|
||||||
|
Three checks (cap 500 hits each; totals shown even when capped):
|
||||||
|
|
||||||
|
- **Moved far from import** — modified rows whose current position is > 50 m from the original (import-time) lat/lng. Flags accidental drags.
|
||||||
|
- **Off track (far from metadata polyline)** — perpendicular distance to the asset's video metadata polyline exceeds the configured threshold (default **30 m**, tunable in Settings). Flags assets that were assigned to a video / side they don't actually belong to. Skipped silently for videos with no metadata loaded.
|
||||||
|
- **Missing side** — `side IS NULL`. Flags assets the labeller didn't tag.
|
||||||
|
|
||||||
|
For any non-zero count, **View** in the row launches a step-through:
|
||||||
|
- Zooms the map to z=18 on the first issue.
|
||||||
|
- **Prev / Next** in the same panel walks through the rest, one at a time at z=18.
|
||||||
|
|
||||||
|
### Post-snap quality check
|
||||||
|
|
||||||
|
Every snap entrypoint (single asset, bulk visible, snap by video, snap to road) runs a quality scan immediately after and surfaces a toast:
|
||||||
|
|
||||||
|
- **Green** — "No quality issues."
|
||||||
|
- **Red** — "N quality issues found (M moved · O off track · P no side)."
|
||||||
|
|
||||||
|
The scan reuses your configured off-track threshold. Toast auto-dismisses after 8 s; click `×` to dismiss sooner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Centerline override
|
## Centerline override
|
||||||
|
|
||||||
Some asset classes (expansion joint, vms gantry) belong on the road centerline, not the LHS/RHS offset. Settings → **Snap to centerline (instead of offset lane)** lets you tick those class names.
|
Some asset classes (expansion joint, vms gantry) belong on the road centerline, not the LHS/RHS offset. Settings → **Snap to centerline (instead of offset lane)** lets you tick those class names.
|
||||||
@@ -161,6 +228,10 @@ Some asset classes (expansion joint, vms gantry) belong on the road centerline,
|
|||||||
- On / off (when off, marker pins still appear but no image opens on click).
|
- On / off (when off, marker pins still appear but no image opens on click).
|
||||||
- Width: S / M / L / XL.
|
- Width: S / M / L / XL.
|
||||||
- Aspect ratio: 16:9 / 4:3 / 3:2 / 1:1.
|
- Aspect ratio: 16:9 / 4:3 / 3:2 / 1:1.
|
||||||
|
- **Quality thresholds**
|
||||||
|
- **Off-track distance (m)** — perpendicular distance from metadata polyline beyond which an asset is flagged "off track" by the Quality dashboard and post-snap scan. Default 30 m. **Reset to 30 m** button restores the default. Persisted in localStorage. Lowering below ~25 m starts producing noise on curved / sparse polylines (chord cuts a corner the asset is on).
|
||||||
|
- **Diagnostics**
|
||||||
|
- **Performance mode** — while panning / zooming, render only basemap + asset markers + range paths (hide roads / scope / metadata / links / labels). Diagnostic; leave off for normal use.
|
||||||
- **Snap to centerline names** — list of asset_names that should snap to centerline.
|
- **Snap to centerline names** — list of asset_names that should snap to centerline.
|
||||||
- **Image folder (offline)** — a local folder used to resolve `image_path` URLs to disk (HTTPS URLs always pass through unchanged).
|
- **Image folder (offline)** — a local folder used to resolve `image_path` URLs to disk (HTTPS URLs always pass through unchanged).
|
||||||
|
|
||||||
@@ -181,17 +252,27 @@ OSM roads can be exported separately via **OSM tools… → Export current roads
|
|||||||
|
|
||||||
## Keyboard shortcuts
|
## Keyboard shortcuts
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Context | Action |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `[` / `]` | Step prev / next asset in the filtered list |
|
| `[` / `]` | filtered list | Step prev / next asset |
|
||||||
| `ESC` | Cancel: lasso / link-pick / draw-road / road-edit / image popup |
|
| `ESC` | various | Cancel: lasso / link-pick / draw-road / road-edit / image popup / metadata focus / magnifier |
|
||||||
| `Delete` / `Backspace` | Unlink the currently-selected link line |
|
| `Delete` / `Backspace` | link selected | Unlink the currently-selected link line |
|
||||||
|
| `Shift` + click | OSM vertex | Mark / unmark a road vertex (red ✕) |
|
||||||
|
| `Delete` / `Backspace` | OSM edit, marked vertices | Delete all marked vertices on the active road |
|
||||||
|
| `Delete` / `Backspace` | OSM edit, focus + selection | Hide all currently-selected roads (session-only) |
|
||||||
|
| Right-click | OSM edit, focus | Hide that road instantly |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Undo / Redo
|
## Undo / Redo
|
||||||
|
|
||||||
Every state-changing action writes an audit row. The **Recent actions** panel shows the last N. Click **Undo** on any row to revert; **Redo** on a previously undone row replays it. Multi-asset operations (bulk delete, distribute, auto-link) revert as a single atomic step. Road edits (OSM vertex drag, create / delete / flip) are not audited.
|
Two independent stacks:
|
||||||
|
|
||||||
|
**Asset audit log** — every state-changing asset action writes an audit row. The **Recent actions** panel shows the last N. **Undo** on any row reverts; **Redo** replays. Multi-asset operations (bulk delete, distribute, auto-link, snap) revert atomically as a single step.
|
||||||
|
|
||||||
|
**OSM edit ring buffer** — separate 3-deep, in-session undo for road edits, with its own **↶ Undo last edit** button in the OSM edit panel. Covers flip / simplify / delete vertices / drag vertex / ignore-for-snap toggle / hide / restore-hidden. Cleared on Exit OSM edit mode. Merge and whole-road creation / deletion are **not** captured here — be deliberate with those.
|
||||||
|
|
||||||
|
Road edits do not appear in the asset audit log; the asset Undo and the OSM Undo never collide.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -210,3 +291,8 @@ Every state-changing action writes an audit row. The **Recent actions** panel sh
|
|||||||
- **Streetlights not visible on satellite** — switch to **Google Satellite (deep zoom)** basemap and zoom to z≥22.
|
- **Streetlights not visible on satellite** — switch to **Google Satellite (deep zoom)** basemap and zoom to z≥22.
|
||||||
- **Snap moves assets to the wrong side** — load the metadata polyline (gives true vehicle heading), or fix the OSM road's `oneway` direction.
|
- **Snap moves assets to the wrong side** — load the metadata polyline (gives true vehicle heading), or fix the OSM road's `oneway` direction.
|
||||||
- **Re-importing wipes my edits** — re-import preserves rows where `modified=1` (manual moves) and rows the user deleted; only `modified=0` geometry is re-stamped from source. If you need a fresh start, use **Reset DB** in the Data section first.
|
- **Re-importing wipes my edits** — re-import preserves rows where `modified=1` (manual moves) and rows the user deleted; only `modified=0` geometry is re-stamped from source. If you need a fresh start, use **Reset DB** in the Data section first.
|
||||||
|
- **Per-video metadata didn't auto-load on startup** — confirm the **Per-video metadata** row in the Loaded data section shows a non-zero count. If it does and the polylines aren't drawing, toggle **Metadata** in the Layers picker. If the count is zero after a previous import, the DB was wiped (Reset DB or manual `markers.db` delete) — re-pick the JSON.
|
||||||
|
- **Quality "Off track" flags too many / too few rows** — adjust **Settings → Quality thresholds → Off-track distance**. Default 30 m. Going below ~25 m starts producing noise on curved / sparse polylines (chord cuts a corner the asset is on).
|
||||||
|
- **Off-track flagged assets that look fine on the map** — check that the asset's `video_name` has metadata loaded. If the metadata is for a different video the chord-distance calc compares against the wrong track.
|
||||||
|
- **Bulk road tools panel doesn't show** — needs OSM edit mode AND a focused video. Tap a metadata polyline on the map (or check exactly one video in Filters) to focus, then press **Edit OSM roads**.
|
||||||
|
- **Magnifier shows a black circle** — basemap tile fetch hadn't completed when the lens map mounted; move the cursor a couple of pixels to trigger a re-render, or close and re-open the magnifier.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -104,6 +104,10 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
|||||||
let _ = sqlx::query("ALTER TABLE roads ADD COLUMN highway TEXT")
|
let _ = sqlx::query("ALTER TABLE roads ADD COLUMN highway TEXT")
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
|
let _ =
|
||||||
|
sqlx::query("ALTER TABLE roads ADD COLUMN snap_ignored INTEGER NOT NULL DEFAULT 0")
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"CREATE TABLE IF NOT EXISTS users (
|
"CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -189,7 +193,8 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
|||||||
min_lng REAL NOT NULL,
|
min_lng REAL NOT NULL,
|
||||||
max_lng REAL NOT NULL,
|
max_lng REAL NOT NULL,
|
||||||
oneway INTEGER NOT NULL DEFAULT 0,
|
oneway INTEGER NOT NULL DEFAULT 0,
|
||||||
highway TEXT
|
highway TEXT,
|
||||||
|
snap_ignored INTEGER NOT NULL DEFAULT 0
|
||||||
)",
|
)",
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
@@ -219,6 +224,18 @@ async fn apply_schema(pool: &SqlitePool) -> Result<()> {
|
|||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Per-video metadata polylines persisted across sessions; survives app
|
||||||
|
// restarts, wiped only by reset_database or explicit clear.
|
||||||
|
sqlx::query(
|
||||||
|
"CREATE TABLE IF NOT EXISTS video_metadata (
|
||||||
|
video_name TEXT PRIMARY KEY,
|
||||||
|
track_json TEXT NOT NULL,
|
||||||
|
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)",
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"CREATE TABLE IF NOT EXISTS action_history (
|
"CREATE TABLE IF NOT EXISTS action_history (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ mod scope;
|
|||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
assets_bbox, delete_asset, delete_assets_by_video, get_assets_in_bbox, get_osm_roads,
|
assets_bbox, 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,
|
get_scope_polygons, import_assets_auto, import_fixed_assets, import_kml_scope,
|
||||||
|
import_osm_geojson,
|
||||||
import_range_assets, list_assets, read_video_polylines_json, list_recent_actions, merge_polylines, reset_assets_to_original,
|
import_range_assets, list_assets, read_video_polylines_json, list_recent_actions, merge_polylines, reset_assets_to_original,
|
||||||
read_metadata_polyline, redo_action, reset_database, snap_assets_in_bbox,
|
read_metadata_polyline, redo_action, reset_database, snap_assets_in_bbox,
|
||||||
add_class, auto_link_in_polygon, bulk_translate, clear_assets_by_type,
|
add_class, auto_link_in_polygon, bulk_translate, clear_assets_by_type,
|
||||||
clear_osm_roads, clear_scope_polygons, data_source_counts, change_assets_side, clear_link, clear_manual_scope,
|
clear_osm_roads, clear_scope_polygons, data_source_counts, change_assets_side, clear_link, clear_manual_scope,
|
||||||
create_osm_road, create_user, delete_road_vertices, set_link, simplify_road,
|
create_osm_road, create_user, delete_road_vertices, set_link, simplify_road,
|
||||||
delete_roads_by_highway, export_osm_roads, find_duplicate_clusters,
|
delete_roads_by_highway, export_osm_roads, find_duplicate_clusters,
|
||||||
|
save_video_metadata, get_video_metadata, clear_video_metadata,
|
||||||
flip_road_direction, generate_osm_for_bbox, generate_osm_near_assets,
|
flip_road_direction, generate_osm_for_bbox, generate_osm_near_assets,
|
||||||
list_road_classes, merge_roads_by_name,
|
set_road_snap_ignored, osm_road_snapshot, restore_road_state,
|
||||||
|
list_road_classes, merge_roads_by_name, merge_roads_by_ids, quality_report, assets_coords_for_ids,
|
||||||
delete_assets_bulk, delete_osm_road, delete_user, export_assets,
|
delete_assets_bulk, delete_osm_road, delete_user, export_assets,
|
||||||
list_asset_names, list_classes, list_users, list_video_names,
|
list_asset_names, list_classes, list_users, list_video_names,
|
||||||
rename_assets, restore_assets, set_assets_by_video_in_scope,
|
rename_assets, restore_assets, set_assets_by_video_in_scope,
|
||||||
@@ -45,6 +48,7 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
import_fixed_assets,
|
import_fixed_assets,
|
||||||
import_range_assets,
|
import_range_assets,
|
||||||
|
import_assets_auto,
|
||||||
list_assets,
|
list_assets,
|
||||||
get_assets_in_bbox,
|
get_assets_in_bbox,
|
||||||
update_asset_position,
|
update_asset_position,
|
||||||
@@ -87,11 +91,17 @@ pub fn run() {
|
|||||||
delete_roads_by_highway,
|
delete_roads_by_highway,
|
||||||
list_road_classes,
|
list_road_classes,
|
||||||
merge_roads_by_name,
|
merge_roads_by_name,
|
||||||
|
merge_roads_by_ids,
|
||||||
export_osm_roads,
|
export_osm_roads,
|
||||||
generate_osm_for_bbox,
|
generate_osm_for_bbox,
|
||||||
generate_osm_near_assets,
|
generate_osm_near_assets,
|
||||||
assets_bbox,
|
assets_bbox,
|
||||||
|
quality_report,
|
||||||
|
assets_coords_for_ids,
|
||||||
flip_road_direction,
|
flip_road_direction,
|
||||||
|
set_road_snap_ignored,
|
||||||
|
osm_road_snapshot,
|
||||||
|
restore_road_state,
|
||||||
change_assets_side,
|
change_assets_side,
|
||||||
auto_link_in_polygon,
|
auto_link_in_polygon,
|
||||||
bulk_translate,
|
bulk_translate,
|
||||||
@@ -99,6 +109,9 @@ pub fn run() {
|
|||||||
clear_osm_roads,
|
clear_osm_roads,
|
||||||
clear_scope_polygons,
|
clear_scope_polygons,
|
||||||
data_source_counts,
|
data_source_counts,
|
||||||
|
save_video_metadata,
|
||||||
|
get_video_metadata,
|
||||||
|
clear_video_metadata,
|
||||||
set_link,
|
set_link,
|
||||||
clear_link
|
clear_link
|
||||||
])
|
])
|
||||||
|
|||||||
1293
src/App.tsx
1293
src/App.tsx
File diff suppressed because it is too large
Load Diff
272
src/LensView.tsx
Normal file
272
src/LensView.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import maplibregl, { Map as MapLibreMap } from "maplibre-gl";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
import { basemapById } from "./basemaps";
|
||||||
|
import type { Asset } from "./types";
|
||||||
|
import type { OsmRoadFC } from "./MapView";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
basemapId: string;
|
||||||
|
centerLng: number;
|
||||||
|
centerLat: number;
|
||||||
|
zoom: number;
|
||||||
|
diameterPx: number;
|
||||||
|
osmRoads?: OsmRoadFC;
|
||||||
|
assets: Asset[];
|
||||||
|
metadataTrack?: Array<[number, number]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cursor-following magnifier: a circular MapLibre instance that re-projects
|
||||||
|
// to whatever the user's cursor is hovering on the main map, at +3 zoom. No
|
||||||
|
// deck.gl — assets and OSM roads render as native MapLibre layers from
|
||||||
|
// GeoJSON sources so each cursor move is a cheap setCenter() call instead of
|
||||||
|
// a full layer rebuild.
|
||||||
|
export function LensView({
|
||||||
|
basemapId,
|
||||||
|
centerLng,
|
||||||
|
centerLat,
|
||||||
|
zoom,
|
||||||
|
diameterPx,
|
||||||
|
osmRoads,
|
||||||
|
assets,
|
||||||
|
metadataTrack = [],
|
||||||
|
}: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mapRef = useRef<MapLibreMap | null>(null);
|
||||||
|
const styleReadyRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Hold refs to current data so installLayers can push the live state right
|
||||||
|
// after creating the empty sources — without this, the lens mounts blank
|
||||||
|
// until one of the data deps below changes (which often never happens).
|
||||||
|
const osmRoadsRef = useRef(osmRoads);
|
||||||
|
osmRoadsRef.current = osmRoads;
|
||||||
|
const assetsRef = useRef(assets);
|
||||||
|
assetsRef.current = assets;
|
||||||
|
const metadataTrackRef = useRef(metadataTrack);
|
||||||
|
metadataTrackRef.current = metadataTrack;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: basemapById(basemapId).style,
|
||||||
|
center: [centerLng, centerLat],
|
||||||
|
zoom,
|
||||||
|
interactive: false,
|
||||||
|
attributionControl: false,
|
||||||
|
});
|
||||||
|
mapRef.current = map;
|
||||||
|
map.on("load", () => {
|
||||||
|
styleReadyRef.current = true;
|
||||||
|
installLayers(map);
|
||||||
|
pushAllData(map);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
mapRef.current = null;
|
||||||
|
styleReadyRef.current = false;
|
||||||
|
map.remove();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Re-install layers when basemap changes (style swap drops sources/layers).
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
styleReadyRef.current = false;
|
||||||
|
map.setStyle(basemapById(basemapId).style);
|
||||||
|
map.once("styledata", () => {
|
||||||
|
styleReadyRef.current = true;
|
||||||
|
installLayers(map);
|
||||||
|
pushAllData(map);
|
||||||
|
});
|
||||||
|
}, [basemapId]);
|
||||||
|
|
||||||
|
function pushAllData(map: MapLibreMap) {
|
||||||
|
const osmSrc = map.getSource("lens-osm-roads") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (osmSrc && osmRoadsRef.current) {
|
||||||
|
osmSrc.setData(
|
||||||
|
osmRoadsRef.current as unknown as GeoJSON.FeatureCollection<GeoJSON.Geometry>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const assetSrc = map.getSource("lens-assets") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (assetSrc) {
|
||||||
|
const features = assetsRef.current
|
||||||
|
.filter(
|
||||||
|
(a) => typeof a.lat === "number" && typeof a.lng === "number",
|
||||||
|
)
|
||||||
|
.map((a) => ({
|
||||||
|
type: "Feature" as const,
|
||||||
|
geometry: {
|
||||||
|
type: "Point" as const,
|
||||||
|
coordinates: [a.lng as number, a.lat as number],
|
||||||
|
},
|
||||||
|
properties: { id: a.id, side: a.side ?? "" },
|
||||||
|
}));
|
||||||
|
assetSrc.setData({ type: "FeatureCollection", features });
|
||||||
|
}
|
||||||
|
const trackSrc = map.getSource("lens-metadata") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (trackSrc) {
|
||||||
|
const t = metadataTrackRef.current;
|
||||||
|
if (t.length < 2) {
|
||||||
|
trackSrc.setData({ type: "FeatureCollection", features: [] });
|
||||||
|
} else {
|
||||||
|
trackSrc.setData({
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: "Feature" as const,
|
||||||
|
geometry: {
|
||||||
|
type: "LineString" as const,
|
||||||
|
coordinates: t.slice(),
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push live data into the GeoJSON sources whenever they change.
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map || !styleReadyRef.current) return;
|
||||||
|
const src = map.getSource("lens-osm-roads") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (src && osmRoads) {
|
||||||
|
src.setData(
|
||||||
|
osmRoads as unknown as GeoJSON.FeatureCollection<GeoJSON.Geometry>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [osmRoads]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map || !styleReadyRef.current) return;
|
||||||
|
const src = map.getSource("lens-assets") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (!src) return;
|
||||||
|
const features = assets
|
||||||
|
.filter((a) => typeof a.lat === "number" && typeof a.lng === "number")
|
||||||
|
.map((a) => ({
|
||||||
|
type: "Feature" as const,
|
||||||
|
geometry: {
|
||||||
|
type: "Point" as const,
|
||||||
|
coordinates: [a.lng as number, a.lat as number],
|
||||||
|
},
|
||||||
|
properties: { id: a.id, side: a.side ?? "" },
|
||||||
|
}));
|
||||||
|
src.setData({ type: "FeatureCollection", features });
|
||||||
|
}, [assets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map || !styleReadyRef.current) return;
|
||||||
|
const src = map.getSource("lens-metadata") as
|
||||||
|
| maplibregl.GeoJSONSource
|
||||||
|
| undefined;
|
||||||
|
if (!src) return;
|
||||||
|
if (metadataTrack.length < 2) {
|
||||||
|
src.setData({ type: "FeatureCollection", features: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
src.setData({
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
type: "Feature" as const,
|
||||||
|
geometry: {
|
||||||
|
type: "LineString" as const,
|
||||||
|
coordinates: metadataTrack.slice(),
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}, [metadataTrack]);
|
||||||
|
|
||||||
|
// Track the cursor world coordinate via prop changes.
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
if (!Number.isFinite(centerLng) || !Number.isFinite(centerLat)) return;
|
||||||
|
map.jumpTo({ center: [centerLng, centerLat], zoom });
|
||||||
|
}, [centerLng, centerLat, zoom]);
|
||||||
|
|
||||||
|
function installLayers(map: MapLibreMap) {
|
||||||
|
if (!map.getSource("lens-osm-roads")) {
|
||||||
|
map.addSource("lens-osm-roads", {
|
||||||
|
type: "geojson",
|
||||||
|
data: { type: "FeatureCollection", features: [] },
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: "lens-osm-roads",
|
||||||
|
type: "line",
|
||||||
|
source: "lens-osm-roads",
|
||||||
|
paint: {
|
||||||
|
"line-color": "#00e5ff",
|
||||||
|
"line-width": 2,
|
||||||
|
"line-opacity": 0.85,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!map.getSource("lens-metadata")) {
|
||||||
|
map.addSource("lens-metadata", {
|
||||||
|
type: "geojson",
|
||||||
|
data: { type: "FeatureCollection", features: [] },
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: "lens-metadata",
|
||||||
|
type: "line",
|
||||||
|
source: "lens-metadata",
|
||||||
|
paint: {
|
||||||
|
"line-color": "#27ae60",
|
||||||
|
"line-width": 2,
|
||||||
|
"line-opacity": 0.9,
|
||||||
|
"line-dasharray": [2, 2],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!map.getSource("lens-assets")) {
|
||||||
|
map.addSource("lens-assets", {
|
||||||
|
type: "geojson",
|
||||||
|
data: { type: "FeatureCollection", features: [] },
|
||||||
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: "lens-assets",
|
||||||
|
type: "circle",
|
||||||
|
source: "lens-assets",
|
||||||
|
paint: {
|
||||||
|
"circle-radius": 5,
|
||||||
|
"circle-color": "#e74c3c",
|
||||||
|
"circle-stroke-color": "#ffffff",
|
||||||
|
"circle-stroke-width": 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
width: diameterPx,
|
||||||
|
height: diameterPx,
|
||||||
|
borderRadius: "50%",
|
||||||
|
overflow: "hidden",
|
||||||
|
boxShadow: "0 4px 16px rgba(0,0,0,0.45)",
|
||||||
|
border: "2px solid #fff",
|
||||||
|
background: "#000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/MapView.tsx
102
src/MapView.tsx
@@ -27,7 +27,7 @@ export type OsmRoadFC = {
|
|||||||
features: Array<{
|
features: Array<{
|
||||||
type: "Feature";
|
type: "Feature";
|
||||||
geometry: unknown;
|
geometry: unknown;
|
||||||
properties: { id: number; name: string | null };
|
properties: { id: number; name: string | null; snap_ignored?: boolean };
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,6 +76,9 @@ type Props = {
|
|||||||
osmEditMode?: boolean;
|
osmEditMode?: boolean;
|
||||||
editingRoadId?: number | null;
|
editingRoadId?: number | null;
|
||||||
onPickRoad?: (id: number) => void;
|
onPickRoad?: (id: number) => void;
|
||||||
|
onRoadRightClick?: (id: number) => void;
|
||||||
|
selectedRoadIds?: Set<number>;
|
||||||
|
multiSelectActive?: boolean;
|
||||||
onUpdateRoad?: (id: number, coords: Array<[number, number]>) => void;
|
onUpdateRoad?: (id: number, coords: Array<[number, number]>) => void;
|
||||||
roadOffsetMeters?: number;
|
roadOffsetMeters?: number;
|
||||||
drawRoadMode?: boolean;
|
drawRoadMode?: boolean;
|
||||||
@@ -89,6 +92,12 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
highlightedIds?: number[];
|
highlightedIds?: number[];
|
||||||
anchorIds?: number[];
|
anchorIds?: number[];
|
||||||
|
onCursorWorld?: (
|
||||||
|
lng: number,
|
||||||
|
lat: number,
|
||||||
|
screenX: number,
|
||||||
|
screenY: number,
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filled rightward-pointing arrow head, white stroke for legibility on any
|
// Filled rightward-pointing arrow head, white stroke for legibility on any
|
||||||
@@ -138,6 +147,9 @@ export function MapView({
|
|||||||
osmEditMode = false,
|
osmEditMode = false,
|
||||||
editingRoadId = null,
|
editingRoadId = null,
|
||||||
onPickRoad,
|
onPickRoad,
|
||||||
|
onRoadRightClick,
|
||||||
|
selectedRoadIds,
|
||||||
|
multiSelectActive = false,
|
||||||
onUpdateRoad,
|
onUpdateRoad,
|
||||||
roadOffsetMeters = 4,
|
roadOffsetMeters = 4,
|
||||||
drawRoadMode = false,
|
drawRoadMode = false,
|
||||||
@@ -148,6 +160,7 @@ export function MapView({
|
|||||||
onLassoComplete,
|
onLassoComplete,
|
||||||
highlightedIds = [],
|
highlightedIds = [],
|
||||||
anchorIds = [],
|
anchorIds = [],
|
||||||
|
onCursorWorld,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// Diagnostic counters — visible in DevTools console. Should stay flat
|
// Diagnostic counters — visible in DevTools console. Should stay flat
|
||||||
// during pure pan/zoom; spikes on filter or import.
|
// during pure pan/zoom; spikes on filter or import.
|
||||||
@@ -179,6 +192,10 @@ export function MapView({
|
|||||||
osmEditModeRef.current = osmEditMode;
|
osmEditModeRef.current = osmEditMode;
|
||||||
const onPickRoadRef = useRef(onPickRoad);
|
const onPickRoadRef = useRef(onPickRoad);
|
||||||
onPickRoadRef.current = onPickRoad;
|
onPickRoadRef.current = onPickRoad;
|
||||||
|
const onRoadRightClickRef = useRef(onRoadRightClick);
|
||||||
|
onRoadRightClickRef.current = onRoadRightClick;
|
||||||
|
const onCursorWorldRef = useRef(onCursorWorld);
|
||||||
|
onCursorWorldRef.current = onCursorWorld;
|
||||||
const drawRoadModeRef = useRef(drawRoadMode);
|
const drawRoadModeRef = useRef(drawRoadMode);
|
||||||
drawRoadModeRef.current = drawRoadMode;
|
drawRoadModeRef.current = drawRoadMode;
|
||||||
const onDrawRoadAddPointRef = useRef(onDrawRoadAddPoint);
|
const onDrawRoadAddPointRef = useRef(onDrawRoadAddPoint);
|
||||||
@@ -208,20 +225,46 @@ export function MapView({
|
|||||||
|
|
||||||
map.on("contextmenu", (e) => {
|
map.on("contextmenu", (e) => {
|
||||||
const overlay = overlayRef.current;
|
const overlay = overlayRef.current;
|
||||||
if (!overlay || !onAssetRightClickRef.current) return;
|
if (!overlay) return;
|
||||||
const { x, y } = e.point;
|
const { x, y } = e.point;
|
||||||
const info = overlay.pickObject({ x, y, radius: 8 }) as
|
const info = overlay.pickObject({ x, y, radius: 8 }) as
|
||||||
| { object?: unknown; layer?: { id?: string } }
|
| {
|
||||||
|
object?: unknown;
|
||||||
|
layer?: { id?: string };
|
||||||
|
sourceLayer?: { id?: string };
|
||||||
|
}
|
||||||
| null;
|
| null;
|
||||||
if (!info || !info.object) return;
|
if (!info || !info.object) return;
|
||||||
// An asset record carries `row_id` and `asset_type`; ignore picks that
|
// Road right-click takes priority when in OSM edit mode.
|
||||||
// landed on roads/scopes/metadata.
|
if (
|
||||||
|
osmEditModeRef.current &&
|
||||||
|
info.layer?.id === "osm-roads" &&
|
||||||
|
onRoadRightClickRef.current
|
||||||
|
) {
|
||||||
|
const f = info.object as { properties?: { id?: number } };
|
||||||
|
const rid = f.properties?.id;
|
||||||
|
if (typeof rid === "number") {
|
||||||
|
e.preventDefault();
|
||||||
|
onRoadRightClickRef.current(rid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!onAssetRightClickRef.current) return;
|
||||||
const obj = info.object as Partial<Asset>;
|
const obj = info.object as Partial<Asset>;
|
||||||
if (typeof obj.row_id !== "string" || typeof obj.id !== "number") return;
|
if (typeof obj.row_id !== "string" || typeof obj.id !== "number") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onAssetRightClickRef.current(obj as Asset);
|
onAssetRightClickRef.current(obj as Asset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
map.on("mousemove", (e) => {
|
||||||
|
const cb = onCursorWorldRef.current;
|
||||||
|
if (!cb) return;
|
||||||
|
cb(e.lngLat.lng, e.lngLat.lat, e.point.x, e.point.y);
|
||||||
|
});
|
||||||
|
map.on("mouseleave", () => {
|
||||||
|
onCursorWorldRef.current?.(NaN, NaN, NaN, NaN);
|
||||||
|
});
|
||||||
|
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
setIsInteracting(false);
|
setIsInteracting(false);
|
||||||
// Sync the React viewport mirror once per gesture so programmatic
|
// Sync the React viewport mirror once per gesture so programmatic
|
||||||
@@ -448,8 +491,14 @@ export function MapView({
|
|||||||
|
|
||||||
const layers: unknown[] = [];
|
const layers: unknown[] = [];
|
||||||
|
|
||||||
|
// Vertex-edit mode collapses to the single road being dragged so other
|
||||||
|
// roads don't clutter the editor. But when multi-select is active (focused
|
||||||
|
// video + bulk select / hide / merge workflow), keep all roads visible.
|
||||||
const visibleOsm: OsmRoadFC | undefined =
|
const visibleOsm: OsmRoadFC | undefined =
|
||||||
osmEditMode && editingRoadId !== null && osmRoads
|
osmEditMode &&
|
||||||
|
editingRoadId !== null &&
|
||||||
|
!multiSelectActive &&
|
||||||
|
osmRoads
|
||||||
? {
|
? {
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: osmRoads.features.filter(
|
features: osmRoads.features.filter(
|
||||||
@@ -480,10 +529,28 @@ export function MapView({
|
|||||||
data: visibleOsm,
|
data: visibleOsm,
|
||||||
stroked: true,
|
stroked: true,
|
||||||
filled: false,
|
filled: false,
|
||||||
getLineColor: [0, 229, 255, 255],
|
getLineColor: (f: unknown) => {
|
||||||
getLineWidth: 3,
|
const p = (f as { properties?: { id?: number; snap_ignored?: boolean } })
|
||||||
|
.properties;
|
||||||
|
if (p && selectedRoadIds && typeof p.id === "number" && selectedRoadIds.has(p.id)) {
|
||||||
|
return [255, 196, 0, 255];
|
||||||
|
}
|
||||||
|
if (p?.snap_ignored) return [120, 120, 120, 180];
|
||||||
|
return [0, 229, 255, 255];
|
||||||
|
},
|
||||||
|
getLineWidth: (f: unknown) => {
|
||||||
|
const p = (f as { properties?: { id?: number } }).properties;
|
||||||
|
if (p && selectedRoadIds && typeof p.id === "number" && selectedRoadIds.has(p.id)) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
},
|
||||||
lineWidthUnits: "pixels",
|
lineWidthUnits: "pixels",
|
||||||
pickable: osmEditMode,
|
pickable: osmEditMode,
|
||||||
|
updateTriggers: {
|
||||||
|
getLineColor: [selectedRoadIds],
|
||||||
|
getLineWidth: [selectedRoadIds],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if (osmEditMode && roadOffsetMeters > 0) {
|
if (osmEditMode && roadOffsetMeters > 0) {
|
||||||
@@ -1406,6 +1473,8 @@ export function MapView({
|
|||||||
zoomBand,
|
zoomBand,
|
||||||
isInteracting,
|
isInteracting,
|
||||||
perfMode,
|
perfMode,
|
||||||
|
selectedRoadIds,
|
||||||
|
multiSelectActive,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Manage draggable HTML markers + per-vertex popup for the actively-edited asset.
|
// Manage draggable HTML markers + per-vertex popup for the actively-edited asset.
|
||||||
@@ -1476,7 +1545,22 @@ export function MapView({
|
|||||||
}
|
}
|
||||||
cap.appendChild(document.createElement("br"));
|
cap.appendChild(document.createElement("br"));
|
||||||
const small = document.createElement("small");
|
const small = document.createElement("small");
|
||||||
small.textContent = selectedAsset.row_id;
|
// row_id · video_name on a single line so users can read both
|
||||||
|
// at a glance without the popup growing taller.
|
||||||
|
const rowSpan = document.createElement("span");
|
||||||
|
rowSpan.textContent = selectedAsset.row_id;
|
||||||
|
small.appendChild(rowSpan);
|
||||||
|
if (selectedAsset.video_name && selectedAsset.video_name !== "Not available") {
|
||||||
|
const sep = document.createElement("span");
|
||||||
|
sep.style.color = "#888";
|
||||||
|
sep.style.margin = "0 6px";
|
||||||
|
sep.textContent = "·";
|
||||||
|
small.appendChild(sep);
|
||||||
|
const vidSpan = document.createElement("span");
|
||||||
|
vidSpan.style.color = "#666";
|
||||||
|
vidSpan.textContent = selectedAsset.video_name;
|
||||||
|
small.appendChild(vidSpan);
|
||||||
|
}
|
||||||
cap.appendChild(small);
|
cap.appendChild(small);
|
||||||
root.appendChild(cap);
|
root.appendChild(cap);
|
||||||
|
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ const STUBS: Record<string, Stub> = {
|
|||||||
data_source_counts: () => ({
|
data_source_counts: () => ({
|
||||||
fixed_assets: FAKE_ASSETS.length,
|
fixed_assets: FAKE_ASSETS.length,
|
||||||
range_assets: 0,
|
range_assets: 0,
|
||||||
scope_polygons: 0,
|
|
||||||
osm_roads: 0,
|
osm_roads: 0,
|
||||||
metadata_polylines: 0,
|
scope_features: 0,
|
||||||
|
video_metadata: 0,
|
||||||
}),
|
}),
|
||||||
list_video_names: () =>
|
list_video_names: () =>
|
||||||
Array.from(new Set(FAKE_ASSETS.map((a) => a.video_name))),
|
Array.from(new Set(FAKE_ASSETS.map((a) => a.video_name))),
|
||||||
@@ -145,7 +145,15 @@ const STUBS: Record<string, Stub> = {
|
|||||||
get_scope_polygons: () => [],
|
get_scope_polygons: () => [],
|
||||||
read_video_polylines_json: () => ({}),
|
read_video_polylines_json: () => ({}),
|
||||||
read_metadata_kml: () => [],
|
read_metadata_kml: () => [],
|
||||||
list_osm_roads: () => ({ type: "FeatureCollection", features: [] }),
|
// Backend rename — App invokes get_osm_roads; the legacy list_osm_roads
|
||||||
|
// alias is kept so older browser fixtures don't crash.
|
||||||
|
get_osm_roads: () => [],
|
||||||
|
list_osm_roads: () => [],
|
||||||
|
get_video_metadata: () => ({}),
|
||||||
|
save_video_metadata: () => 0,
|
||||||
|
clear_video_metadata: () => 0,
|
||||||
|
osm_road_snapshot: () => [],
|
||||||
|
restore_road_state: () => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function invoke<T = unknown>(
|
export function invoke<T = unknown>(
|
||||||
|
|||||||
Reference in New Issue
Block a user