diff --git a/GEOLIBRE_PARITY.md b/GEOLIBRE_PARITY.md index 15a803d0d..125f07c96 100644 --- a/GEOLIBRE_PARITY.md +++ b/GEOLIBRE_PARITY.md @@ -29,8 +29,8 @@ _GeoLibre: docs/data-formats.md, docs/features.md › Adding data, SQL and datab footprints). Still missing: DWG (GDAL only) and File Geodatabase in the web app. - [x] **Excel, LandXML, encoded polylines, GeoRSS** ([#118](https://github.com/opengeos/RustGIS/issues/118)). - [x] **Delimited text with a projected CRS or address geocoding**: x/y columns in any coordinate - system, WKT columns and any separator ([#115](https://github.com/opengeos/RustGIS/issues/115)); Find Place and address-table geocoding ([#127](https://github.com/opengeos/RustGIS/issues/127)). -- [x] **Remote vector and COG by URL**: Add Data › From URL ([#116](https://github.com/opengeos/RustGIS/issues/116)); COGs ([#132](https://github.com/opengeos/RustGIS/issues/132)), FlatGeobuf and + system, WKT columns and any separator ([#115](https://github.com/opengeos/RustGIS/issues/115)); place search and address-table geocoding ([#127](https://github.com/opengeos/RustGIS/issues/127)). +- [x] **Remote vector and COG by URL**: Home › Add › Add Data › From URL ([#116](https://github.com/opengeos/RustGIS/issues/116)); COGs ([#132](https://github.com/opengeos/RustGIS/issues/132)), FlatGeobuf and GeoParquet ([#152](https://github.com/opengeos/RustGIS/issues/152)) read in place by HTTP range on the desktop and the web, from `s3://` too ([#150](https://github.com/opengeos/RustGIS/issues/150)). - [x] **PMTiles and MBTiles** ([#74](https://github.com/opengeos/RustGIS/issues/74), [#113](https://github.com/opengeos/RustGIS/issues/113)): raster and vector archives, desktop and web. @@ -87,7 +87,7 @@ _GeoLibre: docs/features.md › Map workspace, Rendering engines and 3D globe; d - [ ] **Multi-map grid**: N synchronized panes. Partial: Side by Side with two maps and linked views and cursors (_Roadmap: any time_). - [ ] **Coordinate grids and readouts**: UTM and MGRS/USNG grids, UTM/MGRS/DDM readouts, - grid-reference search, pointer elevation. Partial: graticule, decimal/DMS and Go To XY + grid-reference search, pointer elevation. Partial: graticule, decimal/DMS and coordinate navigation (_Roadmap: any time_). - [ ] **Live data overlays**: RainViewer radar, a Timelapse of annual cloudless imagery, live feeds (quakes, satellites, fires, AIS). Missing. @@ -166,7 +166,7 @@ model-builder.md, segmentation.md, obia.md_ - [ ] **Routing services**: isochrones, service areas, OD matrix, multi-stop routing, turn-by-turn (Valhalla/OSRM). Partial: shortest path over a line layer (_Roadmap: analysis_). - [ ] **Geocoding**: forward, batch and reverse, with multiple providers and a place search box. - Partial: Find Place and address tables (Nominatim, Photon or your own server), a local locator, + Partial: place search and address tables (Nominatim, Photon or your own server), a local locator, a search box. Missing: reverse geocoding (_Roadmap: analysis_). - [ ] **Spatial statistics**: Emerging Hot Spot (Getis-Ord Gi\*), composite score builder. Partial: Suitability Modeler (_Roadmap: analysis_ for hot spots). diff --git a/README.md b/README.md index 352766b08..9c631fcda 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Every screenshot is RustGIS itself, rendered offscreen by `cargo run -p rustgis- |---|---| | **Data** | GeoJSON, CSV/TSV with x/y columns in any coordinate system or WKT geometry, Excel and OpenDocument spreadsheets, GeoRSS, LandXML, encoded polylines, Shapefile (zipped too, in the browser as well), GeoPackage, GeoParquet, FlatGeobuf, KML/KMZ (with styles), GPX, GML, MapInfo MIF/MID, OpenStreetMap PBF, TopoJSON, AutoCAD DXF, LiDAR point clouds (LAS, LAZ, COPC) and geotagged photos (large files are read in place), files by their web address, every vector format GDAL reads when it is installed, Esri File Geodatabases without GDAL (desktop), PostGIS, MySQL, MariaDB and SQL Server tables read live (desktop), and SQL queries and DuckDB databases through DuckDB Spatial when DuckDB is installed; PMTiles archives (web or file) and MBTiles files; GeoJSON, CSV, GeoPackage, GeoParquet, Shapefile, FlatGeobuf, KML/KMZ, GPX and WKT out; standalone tables; reusable connections; supported QGIS projects and layer styles; layer and map files, styles, templates and packages | | **Rasters** | GeoTIFF and cloud-optimised GeoTIFF (any pixel type, tiled or striped, compressed), ASCII grids, Zarr 2 and 3 stores (a variable at a time step, by folder or web address), and JPEG 2000, NetCDF, HDF, ERDAS Imagine and the other rasters GDAL reads when it is installed (desktop); reprojection from any EPSG code; Stretch, RGB, Classify, Unique Values, Colormap and Shaded Relief; DRA, band combinations, brightness/contrast/gamma; raster functions, indices, change detection, classification, georeferencing; cloud-optimised GeoTIFF export; data cubes, point time-series charts, temporal summaries and trends | -| **Maps** | Multiple maps and layouts per project, renamed and duplicated from the Browser pane; tilt, rotation, terrain and a 3D globe; maps of the Moon, Mars and other celestial bodies; Web Mercator or any projection (Equal Earth, Robinson, Winkel Tripel, polar and regional presets, or an EPSG code); bookmarks, extent history, Go To XY; five offline basemaps, with borders in two worldviews | +| **Maps** | Multiple maps and layouts per project, renamed and duplicated from the Browser pane; tilt, rotation, terrain and a 3D globe; maps of the Moon, Mars and other celestial bodies; Web Mercator or any projection (Equal Earth, Robinson, Winkel Tripel, polar and regional presets, or an EPSG code); bookmarks, extent history, coordinate navigation; five offline basemaps, with borders in two worldviews | | **Web services** | WMS, WMTS, WFS, OGC API Features and ArcGIS REST (FeatureServer, MapServer, ImageServer, VectorTileServer) from their capabilities | | **Web tiles** | XYZ/TMS/WMS raster tiles, Mapbox Vector Tiles and MapLibre styles (sprites, patterns, expressions), cached and fetched in the background | | **Cartography** | Single symbol, unique values, graduated colours, proportional symbols; label classes with SQL queries, straight and curved line placement, installed font families, scale ranges and annotation; stacked and dashed line strokes, symbol levels, reference scales and polygons without fill; masking, display filters, aggregation bins, 2.5D extrusion; graphics and dynamic text | diff --git a/crates/data/src/source/duckdb.rs b/crates/data/src/source/duckdb.rs index b8b00d504..71f13bda3 100644 --- a/crates/data/src/source/duckdb.rs +++ b/crates/data/src/source/duckdb.rs @@ -251,7 +251,7 @@ pub fn available() -> bool { /// A live-only database named for a query: add its tables instead. fn live_not_here() -> DataError { DataError::Unsupported( - "SQL queries don't run on MySQL, MariaDB or SQL Server databases: add their tables with Add Data › Database…".into(), + "SQL queries don't run on MySQL, MariaDB or SQL Server databases: add their tables with Home › Add › Add Data › Database…".into(), ) } diff --git a/crates/data/src/source/mysql.rs b/crates/data/src/source/mysql.rs index d9755ae01..73e4333d7 100644 --- a/crates/data/src/source/mysql.rs +++ b/crates/data/src/source/mysql.rs @@ -257,7 +257,7 @@ impl Conn { // A connection that was encrypted isn't continued in the clear. if plain && !self.plain { return Err(DataError::Format( - "the server no longer offers TLS: connect again with Add Data › Database… to accept that".into(), + "the server no longer offers TLS: connect again with Home › Add › Add Data › Database… to accept that".into(), )); } *guard = Some(client); diff --git a/crates/engine/src/cmd/catalogs.rs b/crates/engine/src/cmd/catalogs.rs index 546ae6475..3e13c7d13 100644 --- a/crates/engine/src/cmd/catalogs.rs +++ b/crates/engine/src/cmd/catalogs.rs @@ -163,5 +163,5 @@ fn fetch(url: &str) -> Result, String> { #[cfg(target_arch = "wasm32")] fn fetch(_url: &str) -> Result, String> { - Err("in the web app, use Home › Web Data › Overture Maps… or Source Cooperative…".into()) + Err("in the web app, use Home › Add › Web Data › Overture Maps… or Source Cooperative…".into()) } diff --git a/crates/engine/src/cmd/earthdata.rs b/crates/engine/src/cmd/earthdata.rs index fc8b96e59..b1680c91c 100644 --- a/crates/engine/src/cmd/earthdata.rs +++ b/crates/engine/src/cmd/earthdata.rs @@ -118,5 +118,5 @@ fn fetch(url: &str) -> Result, String> { #[cfg(target_arch = "wasm32")] fn fetch(_url: &str) -> Result, String> { - Err("in the web app, search NASA Earthdata with Home › Web Data › NASA Earthaccess".into()) + Err("in the web app, search NASA Earthdata with Home › Add › Web Data › NASA Earthaccess".into()) } diff --git a/crates/engine/src/cmd/embeddings.rs b/crates/engine/src/cmd/embeddings.rs index 3c8635ae4..33e1beb4b 100644 --- a/crates/engine/src/cmd/embeddings.rs +++ b/crates/engine/src/cmd/embeddings.rs @@ -1,4 +1,4 @@ -//! Satellite embeddings (Home › Web Data › Satellite Embeddings): the catalog of embedding +//! Satellite embeddings (Home › Add › Web Data › Satellite Embeddings): the catalog of embedding //! datasets, a search for a dataset's tiles where the map is, and a tile shown on the map — //! AlphaEarth's as a cloud-optimised GeoTIFF of three bands, Earth Index's as points coloured by //! their vectors' principal components, Tessera's as a raster of three bands of the view. @@ -100,7 +100,7 @@ fn search(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (year, area); - Err("in the web app, search with Home › Web Data › Satellite Embeddings".into()) + Err("in the web app, search with Home › Add › Web Data › Satellite Embeddings".into()) } #[cfg(not(target_arch = "wasm32"))] { @@ -192,7 +192,7 @@ fn add_points(s: &mut Session, p: &Value, name: Option) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = url; - return Err("in the web app, add Earth Index with Home › Web Data › Satellite Embeddings".into()); + return Err("in the web app, add Earth Index with Home › Add › Web Data › Satellite Embeddings".into()); } #[cfg(not(target_arch = "wasm32"))] { @@ -251,7 +251,7 @@ fn add_tessera(s: &mut Session, p: &Value, d: &emb::EmbeddingDataset, name: Opti #[cfg(target_arch = "wasm32")] { let _ = area; - return Err("in the web app, add Tessera with Home › Web Data › Satellite Embeddings".into()); + return Err("in the web app, add Tessera with Home › Add › Web Data › Satellite Embeddings".into()); } #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/engine/src/cmd/geocode.rs b/crates/engine/src/cmd/geocode.rs index 0b33ce56a..5bc5992e8 100644 --- a/crates/engine/src/cmd/geocode.rs +++ b/crates/engine/src/cmd/geocode.rs @@ -10,7 +10,7 @@ use crate::{CmdResult, CommandSpec as C, Session}; pub fn commands() -> Vec { vec![ - C::new("map.findPlace", "Find Place", "Home › Navigate", find_place).params( + C::new("map.findPlace", "By Place or Address…", "Home › Navigate › Locate", find_place).params( r#"{"query": "a place or an address", "provider"?: "nominatim" (OpenStreetMap, default) | "photon", "url"?: "a geocoding server of your own", "limit"?: 1–20, "go"?: bool (zooms to the first, default true)}"#, ), C::new("layer.geocode", "Geocode Addresses", "Home › Add › Add Data", geocode).params( @@ -46,7 +46,7 @@ fn find_place(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (s, query, g, limit); - Err("in the web app, find places with Home › Navigate › Find Place".into()) + Err("in the web app, find places with Home › Navigate › Locate › By Place or Address…".into()) } #[cfg(not(target_arch = "wasm32"))] { @@ -272,5 +272,5 @@ fn lookup(g: &Geocoder, distinct: &[String]) -> Result { #[cfg(target_arch = "wasm32")] fn lookup(_: &Geocoder, _: &[String]) -> Result { - Err("in the web app, geocode addresses with Home › Add Data › Geocode Addresses".into()) + Err("in the web app, geocode addresses with Home › Add › Add Data › Geocode Addresses".into()) } diff --git a/crates/engine/src/cmd/items.rs b/crates/engine/src/cmd/items.rs index ed4aa8bcc..2e554bb5c 100644 --- a/crates/engine/src/cmd/items.rs +++ b/crates/engine/src/cmd/items.rs @@ -1,6 +1,6 @@ //! Project items: folder connections, map and layer files, styles, project templates, packages, -//! layer templates, tasks, and 3D export (Home › Project; Data › Layer Templates and Styles; Share › -//! Package, Save As and Output). +//! layer templates, tasks, and 3D export (Home › Add; Data › Layer Templates and Styles; Publish › +//! Package, Save As and Output; Command Search for map/template imports). use std::fmt::Write as _; use std::path::Path; @@ -21,7 +21,7 @@ const MAX_FILE: u64 = 512 << 20; pub fn commands() -> Vec { vec![ // Folder connections. - C::new("project.addFolder", "Add Folder", "Home › Project", |s, p| { + C::new("project.addFolder", "Add Folder", "Home › Add › Add Data", |s, p| { let path = ps(p, "path").map(str::trim).filter(|p| !p.is_empty()).ok_or("path is required")?; if !cfg!(target_arch = "wasm32") && !Path::new(path).is_dir() { return Err(format!("{path} isn't a folder")); @@ -56,7 +56,7 @@ pub fn commands() -> Vec { C::new("map.save", "Save Map File", "Publish › Save As", |s, p| save_item(s, "map", p)) .view() .params(r#"{"path": "*.rgismap", "map"?: name}"#), - C::new("map.import", "Import", "Home › Project", import_maps).params(r#"{"path": "*.rgismap|*.rgis|*.rgispkg" | "text": map file JSON | "data": base64 file}"#), + C::new("map.import", "Import", "Command Search", import_maps).params(r#"{"path": "*.rgismap|*.rgis|*.rgispkg" | "text": map file JSON | "data": base64 file}"#), C::new("layer.save", "Save Layer File", "Publish › Save As", |s, p| save_item(s, "layer", p)) .view() .when(need_layer) @@ -172,10 +172,10 @@ pub fn commands() -> Vec { C::new("project.saveTemplate", "Save Project Template", "Publish › Save As", |s, p| save_item(s, "template", p)) .view() .params(r#"{"path": "*.rgistemplate", "name"?: template name}"#), - C::new("project.newTemplate", "New Custom Template", "Home › Project", |s, p| save_item(s, "template", p)) + C::new("project.newTemplate", "New Custom Template", "Publish › Save As", |s, p| save_item(s, "template", p)) .view() .params(r#"{"path": "*.rgistemplate", "name"?: template name}"#), - C::new("project.fromTemplate", "Open Custom Template", "Home › Project", |s, p| { + C::new("project.fromTemplate", "Open Custom Template", "Command Search", |s, p| { let text = read_text(p)?; super::connections::clear_active(); let mut proj = match Project::from_import_json(&text, None) { diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 4b2a6fdce..20dcfe5db 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -21,27 +21,27 @@ pub fn commands() -> Vec { C::new("info.fileLayers", "List Layers in File", "Home › Add › Add Data", file_layers) .view() .params(r#"{"path": string}"#), - C::new("info.fileColumns", "List Columns in File", "Home › Add › Delimited Text", file_columns).view().params( + C::new("info.fileColumns", "List Columns in File", "Home › Add › Add Data › Delimited Text", file_columns).view().params( r#"{"path": "a delimited text file (CSV, TSV, TXT) or a spreadsheet", "delimiter"?: ","|";"|"\\t"|"|", "layer"?: "a spreadsheet's sheet (the first by default)"}"#, ), C::new("layer.addSql", "Add Data From SQL", "Home › Add", add_sql).params( r#"{"sql": "one DuckDB SELECT; DuckDB Spatial's ST_ functions, read_parquet('…'), web addresses", "name"?: string, "database"?: "a .duckdb file or a PostgreSQL connection (postgres:host=… dbname=…) whose tables the query names", "password"?: "the connection's, kept for this session only", "crs"?: "EPSG:… of a geometry that doesn't say", "stream"?: bool, "ran"?: "a result the app ran (the SQL pane)"}"#, ), - C::new("layer.addDatabase", "Add Data From Database", "Home › Add", add_database).params( + C::new("layer.addDatabase", "Add Data From Database", "Home › Add › Add Data", add_database).params( r#"{"database": "postgres:host=… dbname=… user=… (or a postgresql:// address; PostGIS tables are read live), a mysql://user@host:port/database address (MySQL or MariaDB, read live; mariadb:// too), an mssql://user@host:1433/database address (SQL Server or Azure SQL, read live; ?encrypt=strict|false, ?trust-server-certificate=true for a self-signed certificate, ?instance=NAME; on Windows DOMAIN\\user or no user signs in with Windows), or a .duckdb file", "password"?: "kept for this session only, never saved", "layer"?: "a table, as table or schema.table (table:column for one of several geometry columns; default: every table with a geometry column)", "crs"?: "EPSG:… of a geometry that doesn't say", "name"?: string, "stream"?: bool}"#, ), - C::new("info.databaseTables", "List Database Tables", "Home › Add › Add Data From Database", database_tables) + C::new("info.databaseTables", "List Database Tables", "Home › Add › Add Data › Database", database_tables) .view() .params(r#"{"database": "as layer.addDatabase", "password"?: string}"#), C::new("layer.defineProjection", "Define Projection", "Layers", define_projection) .when(need_feature_layer) .params(r#"{"layer"?: name|id, "crs": "EPSG:… or WKT; reinterpret native coordinates, without rewriting the source file"}"#), C::new("layer.loadAll", "Load All Features", "Layers", load_all).when(need_streamed).params(r#"{"layer"?: name|id}"#), - C::new("layer.addUrl", "Add Data From URL", "Home › Add", add_url).params( + C::new("layer.addUrl", "Add Data From URL", "Home › Add › Add Data", add_url).params( r#"{"url": "https://…/data.geojson|.csv|.parquet|.fgb|.gpkg|.zip|.kml|.kmz|.gpx|.tif|.jp2 (JPEG 2000: desktop, through GDAL) (the format from the address, else the contents) | a .pmtiles archive", "name"?: text, "layer"?: "one layer of a zip, KML or GPX", "zoom"?: "bool: true zooms to what was added; false keeps the view of a GeoParquet or FlatGeobuf read in place (zoomed to only when the map was empty)", "x"?, "y"?, "wkt"?, "crs"?, "delimiter"?, "paperSpace"?, "stream"?, "as"? (as layer.addData), "whole"?: "bool: download a cloud-optimised GeoTIFF whole (as a raster layer) instead of reading it in place", "start"?, "end"?, "unit"?, "interval"?, "dates"?: "a GeoTIFF address with date placeholders ({date:YYYY}, {YYYY}{MM}{DD}), one file per date: the dates it spans and steps through (as time.set), turning the time slider on"}"#, ), C::new("layer.addDownload", "Add Downloaded Data", "Home › Add", add_download) - .params(r#"{"id": a download the app finished (Add Data › From URL), "url": its address, "name"?: text, "layer"?: text, "zoom"?: bool, "x"?, "y"?, "wkt"?, "crs"?, "delimiter"?, "paperSpace"?, "stream"?, "as"? (as layer.addData), "service"?: bool (a service's features: their axes and paging are checked), "coverage"?: bool (a WCS coverage: a GeoTIFF, else the service's error)}"#), + .params(r#"{"id": a download the app finished (Home › Add › Add Data › From URL), "url": its address, "name"?: text, "layer"?: text, "zoom"?: bool, "x"?, "y"?, "wkt"?, "crs"?, "delimiter"?, "paperSpace"?, "stream"?, "as"? (as layer.addData), "service"?: bool (a service's features: their axes and paging are checked), "coverage"?: bool (a WCS coverage: a GeoTIFF, else the service's error)}"#), C::new("layer.addEncodedPolyline", "Add Encoded Polyline", "Home › Add", add_encoded_polyline).params( r#"{"polyline": "encoded polyline text" | [texts] (one line each), "precision"?: 5 (Google) | 6 (OSRM, Valhalla), "name"?: text}"#, ), @@ -900,7 +900,7 @@ fn add_data(s: &mut Session, p: &Value) -> CmdResult { // So is an MBTiles archive, read in place a tile at a time. if rustgis_data::mbtiles::is_mbtiles_name(path) { if cfg!(target_arch = "wasm32") { - return Err(format!("{path}: in the web app, add the MBTiles file from this computer (Home › Add Data)")); + return Err(format!("{path}: in the web app, add the MBTiles file from this computer (Home › Add › Add Data)")); } let full = std::path::absolute(path).map(|p| p.display().to_string()).map_err(|e| format!("{path}: {e}"))?; // Read now, so a file that isn't an archive is said at once rather than when it draws. @@ -1554,7 +1554,8 @@ pub fn database_param(p: &Value) -> Option Result, String> { match database_param(p) { Some(rustgis_data::source::duckdb::Database::Mysql(_) | rustgis_data::source::duckdb::Database::Sqlserver(_)) => { - Err("SQL queries don't run on MySQL, MariaDB or SQL Server databases: add their tables with Add Data › Database…".into()) + Err("SQL queries don't run on MySQL, MariaDB or SQL Server databases: add their tables with Home › Add › Add Data › Database…" + .into()) } db => Ok(db), } @@ -2327,7 +2328,7 @@ pub fn url_file_name(url: &str, bytes: &[u8]) -> Result { /// Downloads data from a web address and adds it (the command line, MCP and the assistant; the /// app downloads off the frame and adds the bytes). In the browser, where nothing may wait, the /// request is kept for the app ([`Session::request_url`]), which downloads and adds it as its own -/// Add Data › From URL does, so a plugin, a script or the assistant can add data by address too. +/// Home › Add › Add Data › From URL does, so a plugin, a script or the assistant can add data by address too. pub(crate) fn add_url(s: &mut Session, p: &Value) -> CmdResult { let url = ps(p, "url").map(str::trim).filter(|u| !u.is_empty()).ok_or("url is required")?; // An address read through a saved S3 connection is judged by the address it wraps; the whole diff --git a/crates/engine/src/cmd/navigate.rs b/crates/engine/src/cmd/navigate.rs index 03bc3549f..f690d92d1 100644 --- a/crates/engine/src/cmd/navigate.rs +++ b/crates/engine/src/cmd/navigate.rs @@ -40,7 +40,7 @@ pub fn commands() -> Vec { C::new("map.zoomToExtent", "Zoom To Extent", "Home › Navigate", zoom_to_extent) .view() .params(r#"{"extent": [minLon, minLat, maxLon, maxLat]}"#), - C::new("map.goToXY", "Go To XY", "Home › Navigate", go_to_xy) + C::new("map.goToXY", "By Coordinates…", "Home › Navigate › Locate", go_to_xy) .view() .params(r#"{"lon" | "x": number, "lat" | "y": number, "scale"?: number, "bearing"?: degrees clockwise from north, "pitch"?: degrees of tilt (0 to 85)}"#), C::new("map.goToLink", "Go To Map Link", "Home › Navigate", go_to_link) diff --git a/crates/engine/src/cmd/nldi.rs b/crates/engine/src/cmd/nldi.rs index 55fa5012f..4f7bb71d2 100644 --- a/crates/engine/src/cmd/nldi.rs +++ b/crates/engine/src/cmd/nldi.rs @@ -204,10 +204,10 @@ fn fetch_post(url: &str, body: &str) -> Result, String> { #[cfg(target_arch = "wasm32")] fn fetch(_url: &str) -> Result, String> { - Err("in the web app, use Home › Web Data › United States › USGS NLDI".into()) + Err("in the web app, use Home › Add › Web Data › United States › USGS NLDI".into()) } #[cfg(target_arch = "wasm32")] fn fetch_post(_url: &str, _body: &str) -> Result, String> { - Err("in the web app, use Home › Web Data › United States › USGS NLDI".into()) + Err("in the web app, use Home › Add › Web Data › United States › USGS NLDI".into()) } diff --git a/crates/engine/src/cmd/portals.rs b/crates/engine/src/cmd/portals.rs index 3db49d421..2e0a07d7e 100644 --- a/crates/engine/src/cmd/portals.rs +++ b/crates/engine/src/cmd/portals.rs @@ -261,7 +261,7 @@ fn fetch(url: &str) -> Result, String> { #[cfg(target_arch = "wasm32")] fn fetch(_url: &str) -> Result, String> { - Err("in the web app, use Home › Web Data › OpenStreetMap…, Boundaries… or Search Open Data…".into()) + Err("in the web app, use Home › Add › Web Data › OpenStreetMap…, Boundaries… or Search Open Data…".into()) } #[cfg(test)] diff --git a/crates/engine/src/cmd/qgis.rs b/crates/engine/src/cmd/qgis.rs index 4ab560396..5c6e70d6a 100644 --- a/crates/engine/src/cmd/qgis.rs +++ b/crates/engine/src/cmd/qgis.rs @@ -188,7 +188,7 @@ fn add_layer(s: &mut Session, layer: &qgis::ProjectLayer, has_dir: bool, report: return Ok(Some(name.to_string())); } Source::Wfs { url, layer: typename } => { - report.skipped(name, format!("the WFS layer {typename} at {url} (add it with Add Data › Web Data)")); + report.skipped(name, format!("the WFS layer {typename} at {url} (add it with Home › Add › Web Data)")); return Ok(None); } Source::Postgres { connection, table } => { diff --git a/crates/engine/src/cmd/s3.rs b/crates/engine/src/cmd/s3.rs index 81ea0a3f6..974cb13d4 100644 --- a/crates/engine/src/cmd/s3.rs +++ b/crates/engine/src/cmd/s3.rs @@ -165,7 +165,7 @@ pub(super) fn list(_s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (bucket, prefix, named, limit); - Err("in the web app, browse buckets with Home › Add Data › S3…".into()) + Err("in the web app, browse buckets with Home › Add › Add Data › S3…".into()) } #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/engine/src/cmd/service.rs b/crates/engine/src/cmd/service.rs index ba947a8e5..42bde2ae1 100644 --- a/crates/engine/src/cmd/service.rs +++ b/crates/engine/src/cmd/service.rs @@ -188,7 +188,7 @@ fn capabilities(p: &Value) -> Result { #[cfg(target_arch = "wasm32")] fn fetch_capabilities(url: &str, _caps: &str) -> Result { - Err(format!("{url}: in the web app, use Home › Add Data › From Service")) + Err(format!("{url}: in the web app, use Home › Add › Add Data › From Service")) } #[cfg(not(target_arch = "wasm32"))] @@ -408,7 +408,7 @@ pub(crate) fn add(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = get; - Err(format!("{url}: in the web app, Home › Add Data › From Service downloads the coverage")) + Err(format!("{url}: in the web app, Home › Add › Add Data › From Service downloads the coverage")) } #[cfg(not(target_arch = "wasm32"))] { @@ -423,7 +423,7 @@ pub(crate) fn add(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (items, asked); - Err(format!("{url}: in the web app, Home › Add Data › From Service downloads the features")) + Err(format!("{url}: in the web app, Home › Add › Add Data › From Service downloads the features")) } #[cfg(not(target_arch = "wasm32"))] { @@ -601,5 +601,5 @@ fn fetch_records(url: &str, request: &str) -> Result Result { - Err(format!("{url}: in the web app, use Home › Add Data › From Service › Search Catalogue")) + Err(format!("{url}: in the web app, use Home › Add › Add Data › From Service › Search Catalogue")) } diff --git a/crates/engine/src/cmd/stac.rs b/crates/engine/src/cmd/stac.rs index 8b8c3a751..74b8cddda 100644 --- a/crates/engine/src/cmd/stac.rs +++ b/crates/engine/src/cmd/stac.rs @@ -112,7 +112,7 @@ pub(super) fn collections(_s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = api; - Err("in the web app, browse STAC with Home › Web Data › STAC Search".into()) + Err("in the web app, browse STAC with Home › Add › Web Data › STAC Search".into()) } #[cfg(not(target_arch = "wasm32"))] { @@ -147,7 +147,7 @@ fn search(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (s, url); - Err("in the web app, search STAC with Home › Web Data › STAC Search".into()) + Err("in the web app, search STAC with Home › Add › Web Data › STAC Search".into()) } #[cfg(not(target_arch = "wasm32"))] { @@ -191,7 +191,7 @@ fn search_static(s: &mut Session, p: &Value, root: &str) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (s, filter, root, note); - Err("in the web app, search STAC with Home › Web Data › STAC Search".into()) + Err("in the web app, search STAC with Home › Add › Web Data › STAC Search".into()) } #[cfg(not(target_arch = "wasm32"))] { @@ -228,7 +228,7 @@ fn item(p: &Value) -> Result { #[cfg(target_arch = "wasm32")] { let _ = url; - Err("in the web app, add STAC assets with Home › Web Data › STAC Search".into()) + Err("in the web app, add STAC assets with Home › Add › Web Data › STAC Search".into()) } #[cfg(not(target_arch = "wasm32"))] { diff --git a/crates/engine/src/cmd/webdata.rs b/crates/engine/src/cmd/webdata.rs index 99ba9c5c5..eb81ca575 100644 --- a/crates/engine/src/cmd/webdata.rs +++ b/crates/engine/src/cmd/webdata.rs @@ -1,4 +1,4 @@ -//! Home › Web Data services that are a fixed set of web addresses rather than a service to +//! Home › Add › Web Data services that are a fixed set of web addresses rather than a service to //! browse: USGS The National Map's map services (a catalog by category, each added as its //! ArcGIS tiles or exported images) and Fields of the World's field boundaries (PMTiles archives //! on Source Cooperative, one a year, drawn by score). The others (NASA GIBS, EnviroAtlas, FEMA's diff --git a/crates/engine/src/cmd/zarr.rs b/crates/engine/src/cmd/zarr.rs index 54793a405..fc04b26e1 100644 --- a/crates/engine/src/cmd/zarr.rs +++ b/crates/engine/src/cmd/zarr.rs @@ -153,7 +153,7 @@ fn info(_s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = p; - Err("in the web app, open Zarr stores with Home › Add Data › Zarr…".into()) + Err("in the web app, open Zarr stores with Home › Add › Add Data › Zarr…".into()) } #[cfg(not(target_arch = "wasm32"))] match source(p)? { @@ -231,7 +231,7 @@ pub(crate) fn add(s: &mut Session, p: &Value) -> CmdResult { #[cfg(target_arch = "wasm32")] { let _ = (crs_parsed, src); - Err("in the web app, add Zarr variables with Home › Add Data › Zarr…".into()) + Err("in the web app, add Zarr variables with Home › Add › Add Data › Zarr…".into()) } #[cfg(not(target_arch = "wasm32"))] match src { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0b716e92c..37791e20d 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -600,7 +600,7 @@ pub struct Session { /// them (it keeps nothing between calls). Not saved with the project: a plugin's state /// command reads what it needs from the map when the panel opens. pub plugin_panels: std::collections::BTreeMap, - /// Data downloads of this project under way (Add Data › From URL), by id. + /// Data downloads of this project under way (Home › Add › Add Data › From URL), by id. downloading: Vec, /// Downloads that landed, waiting for `layer.addDownload`. pub(crate) downloaded: Vec<(u64, Vec)>, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 6b32a618c..a3df4a485 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -13124,7 +13124,7 @@ fn postgis_layers_reopen_without_secrets() { })) .unwrap(); let e = src.open().unwrap_err(); - assert!(e.contains("connect again with Add Data › Database"), "{e}"); + assert!(e.contains("connect again with Home › Add › Add Data › Database"), "{e}"); assert!(!src.is_open()); // Saved again, it keeps its table and never a password. let saved = serde_json::to_string(&src).unwrap(); @@ -13182,7 +13182,7 @@ fn mysql_layers_reopen_without_secrets() { })) .unwrap(); let e = src.open().unwrap_err(); - assert!(e.contains("connect again with Add Data › Database"), "{e}"); + assert!(e.contains("connect again with Home › Add › Add Data › Database"), "{e}"); assert!(!src.is_open()); let saved = serde_json::to_string(&src).unwrap(); assert!(saved.contains("\"mysql\":{") && saved.contains("\"table\":\"roads\"") && !saved.contains("password"), "{saved}"); @@ -13248,7 +13248,7 @@ fn sqlserver_layers_reopen_without_secrets() { })) .unwrap(); let e = src.open().unwrap_err(); - assert!(e.contains("connect again with Add Data › Database"), "{e}"); + assert!(e.contains("connect again with Home › Add › Add Data › Database"), "{e}"); assert!(!src.is_open()); let saved = serde_json::to_string(&src).unwrap(); assert!(saved.contains("\"sqlserver\":{") && saved.contains("\"table\":\"roads\"") && !saved.contains("password"), "{saved}"); diff --git a/crates/map/src/source.rs b/crates/map/src/source.rs index d11863962..2cfb866ab 100644 --- a/crates/map/src/source.rs +++ b/crates/map/src/source.rs @@ -211,7 +211,7 @@ fn rerun(d: &DuckdbOrigin) -> rustgis_data::Result { // own PostgreSQL credentials (PGPASSWORD, ~/.pgpass) to whatever host the file says. (Some(_), Some(duckdb::Database::Postgres(_))) => { return Err(rustgis_data::DataError::Format( - "the table's result is no longer in the cache: add it again (Add Data › Database…)".into(), + "the table's result is no longer in the cache: add it again (Home › Add › Add Data › Database…)".into(), )); } (Some(t), Some(db)) => duckdb::convert_table_of(&db, &duckdb::find_table_of(&db, t)?, d.crs.as_deref())?, @@ -233,9 +233,9 @@ fn rerun(d: &DuckdbOrigin) -> rustgis_data::Result { fn database_open_error(e: rustgis_data::DataError) -> rustgis_data::DataError { match e { rustgis_data::DataError::Login(message) => { - rustgis_data::DataError::Login(format!("{message} (connect again with Add Data › Database…, giving its password)")) + rustgis_data::DataError::Login(format!("{message} (connect again with Home › Add › Add Data › Database…, giving its password)")) } - e => rustgis_data::DataError::Format(format!("{e} (connect again with Add Data › Database…, giving its password)")), + e => rustgis_data::DataError::Format(format!("{e} (connect again with Home › Add › Add Data › Database…, giving its password)")), } } diff --git a/crates/tiles/src/cache.rs b/crates/tiles/src/cache.rs index fdaf9caeb..5af3e4854 100644 --- a/crates/tiles/src/cache.rs +++ b/crates/tiles/src/cache.rs @@ -558,7 +558,7 @@ pub fn download_detailed(url: &str, max: usize, done: DoneDetailed) { } /// How long a download the caller waits for may take: commands run where the app waits too (the -/// AI Assistant's), so a large file comes through Add Data › From URL, which downloads off the +/// AI Assistant's), so a large file comes through Home › Add › Add Data › From URL, which downloads off the /// frame. #[cfg(not(target_arch = "wasm32"))] pub const BLOCKING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); @@ -574,7 +574,7 @@ pub fn download_blocking(url: &str) -> Result, String> { pub fn download_blocking_limited(url: &str, max: usize) -> Result, String> { download_blocking_within(url, max, BLOCKING_TIMEOUT).map_err(|e| { if e.contains("timeout") || e.contains("timed out") { - format!("{e} (a large file: add it with Add Data › From URL, which downloads in the background)") + format!("{e} (a large file: add it with Home › Add › Add Data › From URL, which downloads in the background)") } else { e } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index f2bf21192..976a0f513 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -43,3 +43,7 @@ rustgis-plugins = { workspace = true, features = ["testing"] } egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } image = { workspace = true } pollster = { workspace = true } + +[[example]] +name = "ui_shot" +test = true diff --git a/crates/ui-egui/examples/ui_shot.rs b/crates/ui-egui/examples/ui_shot.rs index 33bd23e54..e92a0ca16 100644 --- a/crates/ui-egui/examples/ui_shot.rs +++ b/crates/ui-egui/examples/ui_shot.rs @@ -20,9 +20,11 @@ //! --plugin` does), and `{"testPlugin": "panel"}` the plugin tests use, with a panel, //! `{"titlebar": {"left_inset": 80, "height": 34}}` //! draws the header as a macOS unified title bar (`null` turns it off), and -//! `{"width": w, "height": h}` on the first line sets the size (default -//! 1480×900 at 2×; `"dt": seconds` there sets each frame's length, default 0.25, and a -//! short one such as 0.016 catches the map mid-motion). The window opens the World Atlas sample. +//! `{"width": w, "height": h}` on the first line sets the size (default 1480×900), +//! and `"pixelsPerPoint": scale` sets the output scale (default 2). The scale must be finite and +//! positive, with physical dimensions from 1 pixel through the GPU's maximum 2D texture size. +//! `"dt": seconds` sets each frame's length, default 0.25; a short one such as 0.016 catches the map mid-motion. +//! The window opens the World Atlas sample. //! `"recovery": "/abs/dir"` on the first line gives the run that recovery folder, as the desktop //! app has one (Settings › Autosave; a copy left there is offered by the recovery dialog). //! @@ -121,6 +123,19 @@ impl RibbonSplitShowcase { } } +/// Matches the renderer's rounded physical target size and the active device's texture limit. +fn screenshot_scale(size: egui::Vec2, value: f64, max_dimension: u32) -> Result { + let scale = value as f32; + if !scale.is_finite() || scale <= 0.0 { + return Err("pixelsPerPoint must be finite, positive, and representable as f32".into()); + } + let physical = size * scale; + if [physical.x, physical.y].into_iter().any(|v| !v.is_finite() || v.round() < 1.0 || v.round() > max_dimension as f32) { + return Err(format!("scaled width and height must each round to 1..={max_dimension} pixels")); + } + Ok(scale) +} + fn main() { let path = std::env::args().nth(1).expect("usage: ui_shot script.jsonl"); let script = std::fs::read_to_string(&path).expect("read script"); @@ -128,6 +143,7 @@ fn main() { script.lines().filter(|l| !l.trim().is_empty()).map(|l| serde_json::from_str(l).expect("json line")).collect(); let w = lines.iter().find_map(|l| l.get("width").and_then(|v| v.as_f64())).unwrap_or(1480.0) as f32; let h = lines.iter().find_map(|l| l.get("height").and_then(|v| v.as_f64())).unwrap_or(900.0) as f32; + let pixels_per_point = lines.iter().find_map(|l| l.get("pixelsPerPoint").and_then(|v| v.as_f64())).unwrap_or(2.0); let dt = lines.iter().find_map(|l| l.get("dt").and_then(|v| v.as_f64())).unwrap_or(0.25) as f32; let ribbon_split = lines.first().and_then(|l| l.get("ribbonSplit")).and_then(Value::as_bool).unwrap_or(false); let mut split_showcase = RibbonSplitShowcase::default(); @@ -149,10 +165,17 @@ fn main() { )) .expect("render state") }; + let pixels_per_point = match screenshot_scale(egui::vec2(w, h), pixels_per_point, rs.device.limits().max_texture_dimension_2d) { + Ok(scale) => scale, + Err(error) => { + eprintln!("invalid screenshot settings: {error}"); + std::process::exit(2); + } + }; app.set_render_state(Some(rs.clone())); let mut harness = egui_kittest::Harness::builder() .with_size(egui::vec2(w, h)) - .with_pixels_per_point(2.0) + .with_pixels_per_point(pixels_per_point) .with_max_steps(10_000) .with_step_dt(dt) .renderer(egui_kittest::wgpu::WgpuTestRenderer::from_render_state(rs)) @@ -392,3 +415,27 @@ fn main() { } } } + +#[cfg(test)] +mod tests { + use super::screenshot_scale; + use egui::vec2; + + #[test] + fn screenshot_scale_rejects_invalid_values_and_target_sizes() { + for scale in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MAX, 1e-300, 1e-30, 8.01] { + assert!(screenshot_scale(vec2(1024.0, 512.0), scale, 8192).is_err(), "{scale}"); + } + assert!(screenshot_scale(vec2(512.0, 1024.0), 8.01, 8192).is_err()); + } + + #[test] + fn screenshot_scale_accepts_default_and_rounded_renderer_boundaries() { + assert_eq!(screenshot_scale(vec2(1366.0, 768.0), 2.0, 8192), Ok(2.0)); + assert_eq!(screenshot_scale(vec2(1024.0, 512.0), 8.0, 8192), Ok(8.0)); + assert_eq!(screenshot_scale(vec2(1.0, 1.0), 0.5, 8192), Ok(0.5)); + assert!(screenshot_scale(vec2(1.0, 1.0), 0.49, 8192).is_err()); + assert_eq!(screenshot_scale(vec2(8192.25, 512.0), 1.0, 8192), Ok(1.0)); + assert!(screenshot_scale(vec2(8192.5, 512.0), 1.0, 8192).is_err()); + } +} diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 9a6951acf..37c574b22 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -633,18 +633,17 @@ pub fn shortcuts(app: &mut RustGisApp, ctx: &egui::Context) { /// The `ui.*` commands Command Search lists: (id, label, ribbon location). pub(crate) const UI_COMMANDS: &[(&str, &str, &str)] = &[ ("ui.addData", "Add Data…", "Home › Add"), - ("ui.addDelimited", "Add Delimited Text…", "Home › Add"), - ("ui.addUrl", "Add Data From URL…", "Home › Add"), - ("ui.addZarr", "Add Zarr Variable…", "Home › Add"), - ("ui.addService", "Add From Service…", "Home › Add"), - ("ui.openSources", "Connections…", "Home › Add Data"), - ("ui.searchOpenData", "Search Open Data…", "Home › Web Data"), - ("ui.stac.planet", "Planet Open Data…", "Home › Web Data"), - ("ui.stac.portolan", "Portolan…", "Home › Web Data"), - ("ui.stac.sentinel2", "Sentinel-2 Explorer…", "Home › Web Data"), - ("ui.stac.vantor", "Vantor…", "Home › Web Data"), - ("ui.stac.usgsLidar", "USGS LiDAR…", "Home › Web Data › United States"), - ("ui.pane.sources", "Data Sources Pane", "View › Windows"), + ("ui.addDelimited", "Add Delimited Text…", "Home › Add › Add Data"), + ("ui.addUrl", "Add Data From URL…", "Home › Add › Add Data"), + ("ui.addZarr", "Add Zarr Variable…", "Home › Add › Add Data"), + ("ui.addService", "Add From Service…", "Home › Add › Add Data"), + ("ui.searchOpenData", "Search Open Data…", "Home › Add › Web Data"), + ("ui.stac.planet", "Planet Open Data…", "Home › Add › Web Data"), + ("ui.stac.portolan", "Portolan…", "Home › Add › Web Data"), + ("ui.stac.sentinel2", "Sentinel-2 Explorer…", "Home › Add › Web Data"), + ("ui.stac.vantor", "Vantor…", "Home › Add › Web Data"), + ("ui.stac.usgsLidar", "USGS LiDAR…", "Home › Add › Web Data › United States"), + ("ui.pane.sources", "Data Sources", "Home › Add"), ("ui.plugins", "Plugin Manager…", "Plugins"), ("ui.mcpRelay", "MCP Relay", "Help › Help"), ("ui.pane.geoprocessing", "Tools Pane", "View › Windows"), @@ -690,6 +689,7 @@ fn search_action(id: &str) -> Option { "map.package" => SaveItem("mapPackage"), "map.rename" => Dialog("renameMap"), "map.goToXY" => Dialog("goToXY"), + "map.findPlace" => Dialog("findPlace"), "map.camera" | "map.setScale" => Dialog("camera"), "map.basemap" => Dialog("basemap"), "map.body" => Dialog("body"), @@ -802,6 +802,18 @@ fn search_action(id: &str) -> Option { }) } +/// Desktop-only interactive actions stay visible but disabled in web Command Search. +fn search_platform_disabled(id: &str, web: bool) -> Option<&'static str> { + if !web { + return None; + } + match id { + "project.addFolder" => Some("folder connections need the desktop app"), + "layer.addDatabase" => Some("databases are read through DuckDB, a program the browser can't run"), + _ => None, + } +} + /// Availability of the interactive entry point, not the parameterized engine operation. fn search_disabled(app: &RustGisApp, id: &str) -> Option<&'static str> { if id.starts_with("gp:") || id.starts_with("panel:") || id.starts_with("pane:") { @@ -810,9 +822,8 @@ fn search_disabled(app: &RustGisApp, id: &str) -> Option<&'static str> { if matches!(search_action(id), Some(SearchAction::Workflow(_))) { return None; // The workflow lets the user choose its inputs, including the layer. } - // The ribbon hides the Database dialog in the browser for the same reason. - if id == "layer.addDatabase" && cfg!(target_arch = "wasm32") { - return Some("databases are read through DuckDB, a program the browser can't run"); + if let Some(reason) = search_platform_disabled(id, cfg!(target_arch = "wasm32")) { + return Some(reason); } why_disabled(app, id).or_else(|| { (search_action(id).is_none() && app.session.registry.get(id).is_some_and(rustgis_engine::CommandSpec::needs_input)) @@ -1180,6 +1191,15 @@ pub fn notices(app: &mut RustGisApp, ctx: &egui::Context) { mod tests { use super::*; + #[test] + fn desktop_only_search_actions_are_disabled_only_on_the_web() { + for id in ["project.addFolder", "layer.addDatabase"] { + assert!(search_platform_disabled(id, true).is_some(), "{id} must explain its web restriction"); + assert_eq!(search_platform_disabled(id, false), None, "{id} stays available on desktop"); + } + assert_eq!(search_platform_disabled("ui.addData", true), None, "browser file uploads remain available"); + } + #[test] fn typed_scales_read_as_people_write_them() { for (text, want) in [ @@ -1299,23 +1319,6 @@ mod tests { } } - #[test] - fn connections_entry_opens_sources_without_ever_closing_it() { - let mut app = RustGisApp::with_sample(); - app.run("ui.pane.sources", serde_json::Value::Null).unwrap(); - app.run("ui.pane.sources", serde_json::Value::Null).unwrap(); - assert_eq!(app.ui.left, None, "the View entry still toggles"); - app.run("ui.openSources", serde_json::Value::Null).unwrap(); - assert_eq!(app.ui.left, Some(crate::LeftPane::Sources), "opens from closed"); - app.run("ui.openSources", serde_json::Value::Null).unwrap(); - assert_eq!(app.ui.left, Some(crate::LeftPane::Sources), "idempotent when already open"); - app.ui.left = Some(crate::LeftPane::Catalog); - app.run("ui.openSources", serde_json::Value::Null).unwrap(); - assert_eq!(app.ui.left, Some(crate::LeftPane::Sources), "replaces another left pane"); - app.run("ui.pane.sources", serde_json::Value::Null).unwrap(); - assert_eq!(app.ui.left, None, "toggle closes it again"); - } - #[test] fn every_new_connection_entry_opens_its_form() { for (kind, _) in crate::connectionui::KINDS { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index ad1842dce..85900f3e7 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -161,7 +161,7 @@ pub fn show(app: &mut RustGisApp, ctx: &egui::Context) { } let title = match which { "selectByAttributes" => "Select By Attributes", - "goToXY" => "Go To XY", + "goToXY" => "By Coordinates", "bookmark" => "New Bookmark", "addField" => "Add Field", "calculateField" => "Calculate Field", @@ -177,7 +177,7 @@ pub fn show(app: &mut RustGisApp, ctx: &egui::Context) { "layerTemplate" => "Layer Template", "addPolyline" => "Add Encoded Polyline", "addCsv" => "Add Delimited Text", - "findPlace" => "Find Place", + "findPlace" => "By Place or Address", "geocode" => "Geocode Addresses", "addUrl" => "Add Data From URL", "addService" => "Add From Service", diff --git a/crates/ui-egui/src/embeddingsui.rs b/crates/ui-egui/src/embeddingsui.rs index d644e3a2e..63e14f98c 100644 --- a/crates/ui-egui/src/embeddingsui.rs +++ b/crates/ui-egui/src/embeddingsui.rs @@ -1,4 +1,4 @@ -//! Home › Web Data › Satellite Embeddings: a dataset picked from the catalog (its provider, model, +//! Home › Add › Web Data › Satellite Embeddings: a dataset picked from the catalog (its provider, model, //! resolution, years and links), its tiles found where the map is (footprints drawn), and one //! shown on the map: AlphaEarth's three chosen bands as red, green and blue with a stretch, //! Earth Index's points coloured by their vectors' principal components, Tessera's three bands of diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 4c34eea8b..0ff1b9eac 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -452,7 +452,7 @@ pub enum RightPane { Format, /// The selected feature layer's labels (#617). Labeling, - /// A Home › Web Data service: the one `UiState::web_data` names. + /// A Home › Add › Web Data service: the one `UiState::web_data` names. WebData, /// A plugin's panel: the one `UiState::plugin_panel` names. Plugin, @@ -573,7 +573,7 @@ pub struct UiState { pub pending_csv: Option, /// CRS override for the next Shapefile picked by Import Shapefile with Projection. pub pending_shapefile: Option, - /// Data downloading from web addresses (Add Data › From URL): the address, the parameters to + /// Data downloading from web addresses (Home › Add › Add Data › From URL): the address, the parameters to /// add it with, and where the download lands. pub downloads: Vec<(u64, String, Value, Slot, String>>)>, /// A project downloading from its web address (`ui.openUrl`, a `?project=` link): the @@ -647,7 +647,7 @@ pub struct UiState { pub connections_saved: u64, /// The default raster style last kept in the preferences. pub raster_defaults_saved: rustgis_engine::map::RasterDefaults, - /// Add Data › Zarr…: its store, choices and fetches. + /// Home › Add › Add Data › Zarr…: its store, choices and fetches. pub zarr: zarrui::State, /// A layout picture waiting for its image file (web). pub pending_layout_picture: bool, @@ -1817,8 +1817,6 @@ impl RustGisApp { "ui.pane.contents" => self.ui.left = if self.ui.left == Some(LeftPane::Contents) { None } else { Some(LeftPane::Contents) }, "ui.pane.catalog" => self.ui.left = if self.ui.left == Some(LeftPane::Catalog) { None } else { Some(LeftPane::Catalog) }, "ui.pane.sources" => self.ui.left = if self.ui.left == Some(LeftPane::Sources) { None } else { Some(LeftPane::Sources) }, - // Unlike `ui.pane.sources` this never closes the pane: a menu entry that opens it. - "ui.openSources" => self.ui.left = Some(LeftPane::Sources), "ui.pane.geoprocessing" => toggle_right(&mut self.ui, RightPane::Geoprocessing), "ui.pane.symbology" => { // Showing its tab already open needs no layer. @@ -2314,7 +2312,7 @@ impl RustGisApp { "arcgis" => "arcgis", _ => "auto", }; - // Opened from a menu (Home › Web Data): the dialog with it. + // Opened from a menu (Home › Add › Web Data): the dialog with it. if self.dialogs.open != Some("addService") { self.run("ui.dialog", json!({"name": "addService"}))?; } @@ -3153,7 +3151,7 @@ impl RustGisApp { Ok(Value::Null) } - /// Adds the data downloads that have landed (Add Data › From URL). The bytes go to the session + /// Adds the data downloads that have landed (Home › Add › Add Data › From URL). The bytes go to the session /// as they are; a download of a project that has since been closed is dropped. fn poll_downloads(&mut self) { let landed = self.ui.project_download.as_ref().and_then(|(id, url, slot)| { @@ -3439,7 +3437,7 @@ impl RustGisApp { self.ui.dynamic_text = None; } // Addresses commands asked for where they couldn't download themselves (the browser): - // fetched off the frame and added when they land, as Add Data › From URL does. + // fetched off the frame and added when they land, as Home › Add › Add Data › From URL does. for p in self.session.take_url_requests() { if let Err(e) = self.run("ui.addUrl", p) { self.session.notify(e, true); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 22b9fd520..02549694d 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -338,7 +338,7 @@ fn clipboard_group(app: &mut RustGisApp, ui: &mut Ui) { } fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { - // New maps and layouts and the project's items first: where work starts (#328). + // New maps and layouts first: where work starts (#328). project_group(app, ui); group(ui, "Navigate", |ui| { tool_btn( @@ -391,7 +391,16 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { col(ui, |ui| { small(ui, app, "prev_extent", "Previous", "Go back to the previous extent.", "map.prevExtent", Value::Null, false); small(ui, app, "next_extent", "Next", "Go forward to the next extent.", "map.nextExtent", Value::Null, false); - small(ui, app, "goto_xy", "Go To XY", "Pan to a longitude/latitude.", "ui.dialog", json!({"name": "goToXY"}), false); + small( + ui, + app, + "zoom_box", + "Zoom Box", + "Drag a box to zoom into.", + "tool.set", + json!({"tool": "zoomIn"}), + app.session.tool == Tool::ZoomIn, + ); }); col(ui, |ui| { let fp = app.session.map().view.first_person; @@ -421,16 +430,10 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { } } }); - small( - ui, - app, - "search", - "Find Place", - "Find a place or an address by name (OpenStreetMap).", - "ui.dialog", - json!({"name": "findPlace"}), - false, - ); + small_menu(ui, app, "search", "Locate", "Find a place by coordinates, name, or address.", |ui, app| { + menu_item(ui, app, "goto_xy", "By Coordinates…", "ui.dialog", json!({"name": "goToXY"})); + menu_item(ui, app, "search", "By Place or Address…", "ui.dialog", json!({"name": "findPlace"})); + }); }); let on = app.ui.right == Some(crate::RightPane::Directions); big( @@ -447,33 +450,55 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { }); group(ui, "Add", |ui| { big_form_menu(ui, app, "basemap", "Basemap", "Change the map's backdrop.", basemap_gallery); - big_menu(ui, app, "add_data", "Add\nData", "Add GeoJSON, CSV or Shapefile data to the map.", |ui, app| { - menu_item(ui, app, "", "Data…", "ui.addData", Value::Null); - menu_item(ui, app, "", "Delimited Text…", "ui.addDelimited", Value::Null); - menu_item(ui, app, "", "Geocode Addresses…", "ui.dialog", json!({"name": "geocode"})); - menu_item(ui, app, "", "From URL…", "ui.addUrl", Value::Null); - menu_item(ui, app, "", "From Service…", "ui.addService", Value::Null); - menu_item(ui, app, "", "Zarr…", "ui.dialog", json!({"name": "zarr"})); - // Databases are read through DuckDB, a program the browser can't run. - #[cfg(not(target_arch = "wasm32"))] - menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); - menu_item(ui, app, "", "S3…", "ui.dialog", json!({"name": "s3"})); - // Saved connections live in the Data Sources pane, not here: one entry opens the pane - // (never closes it), and New Connection starts the form for a kind. - menu_item(ui, app, "", "Connections…", "ui.openSources", Value::Null); - submenu_row(ui, "", tl!("New Connection"), |ui| { - for (kind, label) in crate::connectionui::KINDS { - // Database drivers aren't built for the browser. - if cfg!(target_arch = "wasm32") && rustgis_engine::connections::DATABASE_KINDS.contains(&kind) { - continue; + big_split_menu( + ui, + app, + "add_data", + "Add\nData", + "Add a data file to the map.", + "ui.addData", + Value::Null, + false, + "Add Data options", + "Add data from another source or use a data-adding option.", + true, + |ui, app| { + menu_item(ui, app, "", "Delimited Text…", "ui.addDelimited", Value::Null); + menu_item(ui, app, "", "Geocode Addresses…", "ui.dialog", json!({"name": "geocode"})); + menu_item(ui, app, "", "From URL…", "ui.addUrl", Value::Null); + menu_item(ui, app, "", "From Service…", "ui.addService", Value::Null); + menu_item(ui, app, "", "Zarr…", "ui.dialog", json!({"name": "zarr"})); + // Databases are read through DuckDB, a program the browser can't run. + #[cfg(not(target_arch = "wasm32"))] + menu_item(ui, app, "", "Database…", "ui.dialog", json!({"name": "addDatabase"})); + menu_item(ui, app, "", "S3…", "ui.dialog", json!({"name": "s3"})); + submenu_row(ui, "", tl!("New Connection"), |ui| { + for (kind, label) in crate::connectionui::KINDS { + // Database drivers aren't built for the browser. + if cfg!(target_arch = "wasm32") && rustgis_engine::connections::DATABASE_KINDS.contains(&kind) { + continue; + } + menu_item(ui, app, "", label, "ui.connection", json!({"kind": kind})); } - menu_item(ui, app, "", label, "ui.connection", json!({"kind": kind})); - } - }); - menu_item(ui, app, "graphics_layer", "Graphics Layer", "graphics.newLayer", Value::Null); - ui.separator(); - sample_data_gallery(ui, app); - }); + }); + menu_item(ui, app, "graphics_layer", "Graphics Layer", "graphics.newLayer", Value::Null); + #[cfg(not(target_arch = "wasm32"))] + menu_item(ui, app, "add_folder", "Add Folder…", "ui.addFolder", Value::Null); + ui.separator(); + sample_data_gallery(ui, app); + }, + ); + big( + ui, + app, + "web_map", + "Data\nSources", + "Show or hide the Data Sources pane.", + "ui.pane.sources", + Value::Null, + app.ui.left == Some(crate::LeftPane::Sources), + false, + ); big_menu(ui, app, "web_data", "Web\nData", "Add data from public web services and catalogs.", web_data_menu); }); group(ui, "Selection", |ui| { @@ -559,29 +584,16 @@ fn home_tab(app: &mut RustGisApp, ui: &mut Ui) { } } }); - col(ui, |ui| { - small(ui, app, "locate", "Locate", "Find a place by coordinates.", "ui.dialog", json!({"name": "goToXY"}), false); - small( - ui, - app, - "zoom_box", - "Zoom Box", - "Drag a box to zoom into.", - "tool.set", - json!({"tool": "zoomIn"}), - app.session.tool == Tool::ZoomIn, - ); - small( - ui, - app, - "chart", - "Infographics", - "Counts, totals and top categories of what's in view.", - "ui.pane.infographics", - Value::Null, - app.ui.right == Some(RightPane::Infographics), - ); - }); + small( + ui, + app, + "chart", + "Infographics", + "Counts, totals and top categories of what's in view.", + "ui.pane.infographics", + Value::Null, + app.ui.right == Some(RightPane::Infographics), + ); }); } @@ -1060,8 +1072,6 @@ fn publication_label(id: &str) -> Option<&'static str> { fn project_group(app: &mut RustGisApp, ui: &mut Ui) { group(ui, "Project", |ui| { big(ui, app, "new_map", "New\nMap", "Add a new map to the project.", "map.new", Value::Null, false, false); - big(ui, app, "toolbox", "Toolbox", "Browse the geoprocessing toolbox.", "ui.pane.geoprocessing", Value::Null, false, false); - big(ui, app, "open", "Import\nProject", "Open a RustGIS project.", "ui.open", Value::Null, false, false); big_menu(ui, app, "new_layout", "New\nLayout", "Add a page layout for printing and exporting the map.", |ui, app| { for (id, label, w, h) in rustgis_engine::map::layout::PAGES { let text = format!("{} ({w:.0} × {h:.0} mm)", tl!(*label)); @@ -1100,27 +1110,6 @@ fn project_group(app: &mut RustGisApp, ui: &mut Ui) { } menu_item(ui, app, "open", "Import Layout…", "ui.importLayout", Value::Null); }); - col(ui, |ui| { - small(ui, app, "catalog", "Browser", "Browse the project's maps, tables and folders.", "ui.pane.catalog", Value::Null, false); - small(ui, app, "add_data", "Add Data", "Add vector data.", "ui.addData", Value::Null, false); - small(ui, app, "add_folder", "Add Folder", "Connect a folder to the Browser.", "ui.addFolder", Value::Null, false); - }); - col(ui, |ui| { - small( - ui, - app, - "open", - "Import", - "Add the maps of a map file, map package, project or project package.", - "ui.openItem", - json!({"kind": "map"}), - false, - ); - small_menu(ui, app, "save_as", "Templates", "Project templates: start projects from a saved setup.", |ui, app| { - menu_item(ui, app, "save_as", "New Custom Template…", "ui.saveItem", json!({"kind": "template"})); - menu_item(ui, app, "open", "Open Custom Template…", "ui.openItem", json!({"kind": "template"})); - }); - }); }); } diff --git a/crates/ui-egui/src/webdataui.rs b/crates/ui-egui/src/webdataui.rs index c47ed4711..a5e64b13b 100644 --- a/crates/ui-egui/src/webdataui.rs +++ b/crates/ui-egui/src/webdataui.rs @@ -1,4 +1,4 @@ -//! Home › Web Data panels for single services, laid out after GeoLibre's: Fields of the World (a +//! Home › Add › Web Data panels for single services, laid out after GeoLibre's: Fields of the World (a //! year and a score threshold), NASA Earthdata (GIBS layers by category, with a date), EnviroAtlas //! (its folders, services and their layers as a tree, with search), FEMA NFHL (the flood hazard //! layers, filtered) and The National Map (its services by category, with search). Each item is a diff --git a/crates/ui-egui/src/workspacepanes.rs b/crates/ui-egui/src/workspacepanes.rs index 7f2c6554c..e6a3ffbfb 100644 --- a/crates/ui-egui/src/workspacepanes.rs +++ b/crates/ui-egui/src/workspacepanes.rs @@ -657,7 +657,7 @@ pub fn time_series(app: &mut RustGisApp, ui: &mut Ui, t: &Tokens) { .map(|l| l.name.clone()) .collect(); if layers.is_empty() { - ui.label(egui::RichText::new(tl!("Add a Zarr variable first (Add Data › Zarr…).")).weak()); + ui.label(egui::RichText::new(tl!("Add a Zarr variable first (Home › Add › Add Data › Zarr…).")).weak()); return; } if !layers.contains(&app.ws.series_layer) { diff --git a/crates/ui-egui/src/zarrui.rs b/crates/ui-egui/src/zarrui.rs index b6a8da3d7..a6b2851ab 100644 --- a/crates/ui-egui/src/zarrui.rs +++ b/crates/ui-egui/src/zarrui.rs @@ -1,4 +1,4 @@ -//! Add Data › Zarr…: a store's variables, a slice picked by its other dimensions (a slider for +//! Home › Add › Add Data › Zarr…: a store's variables, a slice picked by its other dimensions (a slider for //! time), added as a raster layer. A store by web address is read off the frame through the tile //! cache's downloads (a thread natively, fetch on the web), a few files at a time; a folder (the //! desktop) through the commands. diff --git a/docs/comparison/index.md b/docs/comparison/index.md index c2455ffe9..543647bf4 100644 --- a/docs/comparison/index.md +++ b/docs/comparison/index.md @@ -67,7 +67,7 @@ data and AI providers, send requests over the network. See | **Cloud-native data** | GeoParquet, FlatGeobuf, PMTiles and cloud-optimised GeoTIFF read in place, large files and web addresses alike (by address, over HTTP byte ranges: a COG reads only the tiles the view needs, a FlatGeobuf only its index and the features in view, a GeoParquet its footer and the row groups in view, desktop and web), Zarr 2 and 3 stores (the chunks of the step picked, desktop and web), from `s3://` buckets too (private ones signed with a connection's keys, S3-compatible services by endpoint) | GeoParquet, FlatGeobuf, PMTiles, COG, Zarr, streamed over HTTP | GeoParquet, FlatGeobuf, PMTiles, COG, Zarr | GeoParquet, COG, Zarr | Hosted layers | | **Web maps and tiles** | XYZ/TMS/WMS raster tiles, WMTS, OGC API Tiles, Mapbox Vector Tiles, MapLibre styles | XYZ, WMS/WFS/WMTS, OGC API, vector tiles, MapLibre styles | Full OGC support, vector tiles | Full OGC support | Yes | | **Coordinate systems** | Rasters in any EPSG code, reprojected on the fly; the map in presets or projected EPSG and ESRI codes | Reprojects on import, EPSG catalog | Full PROJ support | Full support | Web Mercator focus | -| **Geocoding** | Find Place and address tables through Nominatim or Photon (OSM's public servers or your own) | Geocoding search and batch geocoding | Through plugins | ArcGIS World Geocoder (credits) | Yes | +| **Geocoding** | Place/address search and address tables through Nominatim or Photon (OSM's public servers or your own) | Geocoding search and batch geocoding | Through plugins | ArcGIS World Geocoder (credits) | Yes | | **Open-data portals** | OpenStreetMap by area (Overpass), administrative boundaries (geoBoundaries), and datasets searched by text on data.gov and CKAN sites, Socrata and ArcGIS Hub | ArcGIS Hub, Socrata, CKAN, US portals, geoBoundaries, Overpass, Natural Earth | Through plugins (QuickOSM) | Living Atlas | Living Atlas | | **STAC, databases, Esri services** | STAC search (Earth Search, Planetary Computer or any STAC API, and `?stac=` links), footprints, assets and time-series cubes; Overture Maps, Source Cooperative and NASA Earthdata discovery; PostgreSQL/PostGIS, MySQL/MariaDB and SQL Server/Azure SQL read live (desktop), and DuckDB databases through DuckDB (desktop); WMS, WMTS, WFS, WCS, OGC API Features and Tiles, and ArcGIS REST services; CSW catalogue search; Planetary Computer assets signed; Data Sources with favourites, recent sources, grouped named connections and a public-service library | STAC browser, PostGIS, DuckDB, Iceberg, ArcGIS services, File Geodatabase (desktop) | STAC, PostGIS, SpatiaLite, Oracle, SQL Server, ArcGIS services | Enterprise geodatabases, Esri services | Hosted only | | **3D** | A 3D globe; tilted views draped over terrain from a DEM layer or elevation tiles; 2.5D extrusion with lighting and shadows; glTF scene export; LiDAR (LAS/LAZ/COPC) as point layers; geotagged photos as points with thumbnails | Cesium globe, 3D Tiles, I3S, LiDAR, Gaussian splats | Point clouds, 3D map view | **Deepest 3D**: scenes, I3S, mesh, LAS | Scene Viewer | diff --git a/docs/comparison/qgis.md b/docs/comparison/qgis.md index 794e76e46..f483558c3 100644 --- a/docs/comparison/qgis.md +++ b/docs/comparison/qgis.md @@ -163,7 +163,7 @@ Most QGIS ideas have a counterpart under a different name: | In QGIS | In RustGIS | |---|---| | Layers panel | Layers pane (left) | -| Browser panel, Data Source Manager | Browser pane, **Home › Add Data**, the Data Sources pane | +| Browser panel, Data Source Manager | Browser pane, **Home › Add › Data Sources**, the Data Sources pane | | Processing Toolbox | Tools pane (**Analyze › Tools**), or Command Search (++ctrl+k++) | | Graphical Modeler | ModelBuilder: a node canvas of inputs, tools and outputs, which can also build a model from your history | | `qgis_process` | `rustgis-cli run --cmd 'gp.run={…}'` | diff --git a/docs/contributing.md b/docs/contributing.md index d236a2434..4b774b214 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -90,8 +90,9 @@ is reorganised, is an interim exception: record it in the issue that tracks the appears: ribbon, panes and Command Search. Do not give one tool different names, or expose one setting as two differently named toggles. - **Labels say what happens.** Name a control for what it does, not for a similar feature - elsewhere. Go To XY pans to coordinates; “Locate” would suggest it also finds addresses, as - Find Place does. End a label with “…” when it opens a dialog or file picker that asks for input. + elsewhere. A coordinates-only action should say “By Coordinates”; the “Locate” menu covers + both coordinates and place/address search. End a label with “…” when it opens a dialog or + file picker that asks for input. - **Keep a route to every feature.** Removing a ribbon control must not remove access to its action. Name and check the remaining route, including for UI-only actions. diff --git a/docs/getting-started.md b/docs/getting-started.md index 060420798..fbdfe2933 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -45,18 +45,16 @@ sample again. **Settings › Start-up › Start With** chooses what opens next t ## Your own data -**Home › Add Data › Data…** (or drop files on the window) opens GeoJSON, GeoPackage, -GeoParquet, FlatGeobuf, KML/KMZ, GPX, GeoTIFF, ASCII grids and more. Shapefiles work in the -browser too: pick or drop the `.shp` and `.dbf` together, with `.shx` and `.prj` if available, -or use a ZIP containing them. Use **Add Data › Delimited Text…** to choose a CSV's coordinate -columns or WKT geometry and coordinate system. +**Home › Add › Add Data** opens data files directly; its arrow offers other ways to add data, +including delimited text, URLs and services. Shapefiles work in the browser too: pick or drop +the `.shp` and `.dbf` together, with `.shx` and `.prj` if available, or use a ZIP containing +them. **Home › Add › Data Sources** opens saved data connections and recent sources. -**Project › New › Map** starts a blank project. **Home › Web Data** finds public satellite -imagery, Overture Maps, OpenStreetMap, NASA Earthdata, open-data portals and more -(see [Web Data](user-guide/web-data.md)). **Home › Add Data › Connections…** opens saved data -connections; desktop database connections include PostGIS, MySQL/MariaDB and SQL Server/Azure SQL. -See [Data](user-guide/data.md) and [Supported Data Formats](user-guide/data-formats.md) for -format and platform limits. +**Project › New › Map** starts a blank project. **Home › Add › Web Data** finds public satellite imagery, +Overture Maps, OpenStreetMap, NASA Earthdata, open-data portals and more +(see [Web Data](user-guide/web-data.md)). Desktop database connections include PostGIS, +MySQL/MariaDB and SQL Server/Azure SQL. See [Data](user-guide/data.md) and +[Supported Data Formats](user-guide/data-formats.md) for format and platform limits. ## Ask the AI Assistant diff --git a/docs/plugins.md b/docs/plugins.md index f5af7a4a7..d7e6df176 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -391,7 +391,7 @@ Inside a request, `host::call(op, params)` sends one of these to RustGIS: In the web app a command can't wait for a download, so where `layer.addUrl` has to download a file whole it answers `{"pending": true, "url": …}` there: RustGIS downloads the data off the -frame and adds the layer when it lands, as **Home › Add Data › From URL** does. A PMTiles +frame and adds the layer when it lands, as **Home › Add › Add Data › From URL** does. A PMTiles archive, a cloud-optimised GeoTIFF or a FlatGeobuf or GeoParquet read in place is added at once, as elsewhere. diff --git a/docs/privacy.md b/docs/privacy.md index 38677034b..9b3e6a393 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -6,8 +6,8 @@ send searches, coordinates, data or assistant context to those services, as desc ## Online services -- **Find Place** and place search send your query to OpenStreetMap's Nominatim by default. - Find Place and **Geocode Addresses** can also use Komoot's Photon or a server you specify; +- **Locate › By Place or Address** and place search send your query to OpenStreetMap's Nominatim by default. + **By Place or Address** and **Geocode Addresses** can also use Komoot's Photon or a server you specify; geocoding sends the address values you choose from your table or layer. - **Directions** sends the route's start, stops and destination to FOSSGIS's public Valhalla server by default. The Directions pane asks for consent before sending places and remembers diff --git a/docs/user-guide/data-formats.md b/docs/user-guide/data-formats.md index 25e4d43f8..64b8ce184 100644 --- a/docs/user-guide/data-formats.md +++ b/docs/user-guide/data-formats.md @@ -15,8 +15,8 @@ not every EPSG code; azimuthal equidistant systems such as Equi7 need the deskto | Format | Extensions or source | How to add it | Notes | | --- | --- | --- | --- | -| GeoJSON | `.geojson`, `.json` | Home › Add Data, or drop the file | Features with typed attributes; a `GeometryCollection` keeps its first readable member. Desktop FeatureCollections larger than 32 MiB use an in-place reader; layers with more than 100,000 features stream a view at a time. Other GeoJSON files load whole, up to 256 MiB. | -| Delimited text | `.csv`, `.tsv`, `.txt` | Add Data, or Add Data › Delimited Text… to choose the columns from the file's header | Comma, semicolon, tab or pipe separated (found from the header, or `"delimiter"`); `info.fileColumns` lists the columns and the ones found. Points from x/y columns (named like longitude/latitude or easting/northing, or chosen with `"x"` and `"y"`), or any geometry from a WKT/EWKT column (named like `wkt` or `geometry`, or `"wkt"`). Coordinates in another system are converted from `"crs"` (`EPSG:2263`, or WKT) or an EWKT `SRID=`; without either, coordinates outside longitude/latitude are pointed out. A file without coordinates opens as a standalone table. | +| GeoJSON | `.geojson`, `.json` | Home › Add › Add Data, or drop the file | Features with typed attributes; a `GeometryCollection` keeps its first readable member. Desktop FeatureCollections larger than 32 MiB use an in-place reader; layers with more than 100,000 features stream a view at a time. Other GeoJSON files load whole, up to 256 MiB. | +| Delimited text | `.csv`, `.tsv`, `.txt` | Add Data, or Home › Add › Add Data › Delimited Text… to choose the columns from the file's header | Comma, semicolon, tab or pipe separated (found from the header, or `"delimiter"`); `info.fileColumns` lists the columns and the ones found. Points from x/y columns (named like longitude/latitude or easting/northing, or chosen with `"x"` and `"y"`), or any geometry from a WKT/EWKT column (named like `wkt` or `geometry`, or `"wkt"`). Coordinates in another system are converted from `"crs"` (`EPSG:2263`, or WKT) or an EWKT `SRID=`; without either, coordinates outside longitude/latitude are pointed out. A file without coordinates opens as a standalone table. | | Shapefile | `.shp` with `.dbf` (and `.shx`), or zipped (`.zip`) | Add Data or drop the file; in the web app, pick or drop the `.shp` and `.dbf` together, with the `.shx` and `.prj` if there are (they open as one layer), or the zip | A zip may hold several Shapefiles, in folders, and other vector files (GeoJSON, CSV, GeoParquet, FlatGeobuf); each becomes a layer, at most 32 for the whole archive (Shapefiles first, so other files are left out when Shapefiles fill it). `.txt` files and macOS `__MACOSX` entries are passed over, and a member that can't be read is skipped and named in a notice. Converted to longitude/latitude from the coordinate system in its `.prj` (EPSG, Esri or OGC WKT, or WKT2 as PROJ and QGIS write it; Web Mercator, UTM, State Plane and other common projections); without a `.prj` it must be in longitude/latitude. On the desktop, one with more than 100,000 shapes (counted from its `.shx`) is read in place, a view at a time. | | KML / KMZ | `.kml`, `.kmz` | Add Data, drop the file, or upload it in the web app | A layer for each kind of geometry (points, lines, polygons), with each placemark's name, description, folder path and extended data. Styles are kept: the layer is drawn by unique values of its `style` field, with each KML style's line and fill colours, widths and icon colours (up to 64 styles). Multi-geometries and `gx:Track`s are read. Ground overlays (pictures on a `LatLonBox`, turned by its `rotation`) are raster layers kept in the project: the picture inside a KMZ, or a file beside a KML (not a web address, nor outside its folder); a `gx:LatLonQuad` isn't drawn. | | GPX | `.gpx` | Add Data, drop the file, or upload it in the web app | Waypoints, routes, tracks (one line per track, its segments as parts) and track points (with elevation and time) as separate layers. | @@ -68,7 +68,7 @@ GDAL](data.md#formats-through-gdal). ## Files on the web -**Home › Add Data › From URL…** (`layer.addUrl` with `"url"`) downloads a file by its `https://` +**Home › Add › Add Data › From URL…** (`layer.addUrl` with `"url"`) downloads a file by its `https://` (or `http://` or `s3://`, see [S3 buckets](data.md#s3-buckets)) address and adds it like an uploaded file. Native formats include GeoJSON, delimited text, GeoParquet, FlatGeobuf, GeoPackage, zipped Shapefiles, KML/KMZ, GPX, spreadsheets, GeoRSS, GML, DXF, TopoJSON, LiDAR and rasters. @@ -132,7 +132,7 @@ FlatGeobuf whose header hasn't been fetched yet, use the From URL… dialog or a ## Map and feature services -**Home › Add Data › From Service…** connects to a service by its address, lists what it offers from +**Home › Add › Add Data › From Service…** connects to a service by its address, lists what it offers from its capabilities. For OGC API feature collections, enter a group name (initially **My OGC Layer**) and choose **Import All**. The group expands into ordinary point, line and polygon layers with the same legends, styling and attribute tables as file imports. Other offerings have individual @@ -271,7 +271,7 @@ server itself: see [PostgreSQL and PostGIS](data.md#postgresql-and-postgis). | Source | How to add it | Notes | | --- | --- | --- | -| PostGIS table or view | Add Data › Database… (`layer.addDatabase`) with a libpq connection string or `postgresql://` address | Large tables read a view at a time by their spatial index, others loaded whole. Converted to longitude/latitude by the server; `"crs"` for geometry that doesn't say. A column of mixed types becomes a layer per type. The password is kept for the session only. | +| PostGIS table or view | Home › Add › Add Data › Database… (`layer.addDatabase`) with a libpq connection string or `postgresql://` address | Large tables read a view at a time by their spatial index, others loaded whole. Converted to longitude/latitude by the server; `"crs"` for geometry that doesn't say. A column of mixed types becomes a layer per type. The password is kept for the session only. | ## MySQL and MariaDB @@ -280,7 +280,7 @@ database live, talking to the server itself: see [MySQL and MariaDB](data.md#mys | Source | How to add it | Notes | | --- | --- | --- | -| MySQL or MariaDB table or view | Add Data › Database… (`layer.addDatabase`) with a `mysql://user@host:3306/database` or `mariadb://…` address | Large tables read a view at a time by their spatial index, others loaded whole. Converted to longitude/latitude by RustGIS from the column's SRID; `"crs"` for an SRID of 0. A column of mixed types becomes a layer per type. `ssl-mode` sets the encryption. The password is kept for the session only. | +| MySQL or MariaDB table or view | Home › Add › Add Data › Database… (`layer.addDatabase`) with a `mysql://user@host:3306/database` or `mariadb://…` address | Large tables read a view at a time by their spatial index, others loaded whole. Converted to longitude/latitude by RustGIS from the column's SRID; `"crs"` for an SRID of 0. A column of mixed types becomes a layer per type. `ssl-mode` sets the encryption. The password is kept for the session only. | ## SQL Server and Azure SQL @@ -289,7 +289,7 @@ external client: see [SQL Server and Azure SQL](data.md#sql-server-and-azure-sql | Source | How to add it | Notes | | --- | --- | --- | -| SQL Server table or view | Add Data › Database… (`layer.addDatabase`) with an `mssql://user@host:1433/database` address | Read only; large tables read a view at a time. Geometry and geography, including curves, are converted to longitude/latitude. `"crs"` supplies SRID 0; mixed types become separate layers. TLS and certificate checking are on by default. SQL Server logins on every desktop platform; Windows logins on Windows only. Passwords are kept for the session, never in projects. | +| SQL Server table or view | Home › Add › Add Data › Database… (`layer.addDatabase`) with an `mssql://user@host:1433/database` address | Read only; large tables read a view at a time. Geometry and geography, including curves, are converted to longitude/latitude. `"crs"` supplies SRID 0; mixed types become separate layers. TLS and certificate checking are on by default. SQL Server logins on every desktop platform; Windows logins on Windows only. Passwords are kept for the session, never in projects. | ## SQL queries and DuckDB databases @@ -311,7 +311,7 @@ databases](data.md#sql-queries-and-duckdb-databases). | JPEG 2000 | `.jp2`, `.j2k` | Add Data, byte upload (`layer.addDataBytes`), or From URL… (desktop, with [GDAL](https://gdal.org) installed) | Converted through GDAL. Uploads and downloads keep the converted GeoTIFF in the project, so reopening it needs neither GDAL nor the original file or address; their output and workspace limits are described under [Files on the web](#files-on-the-web). The web app rejects JPEG 2000 uploads and downloads with an explicit GDAL limitation: use GeoTIFF instead. | | Scans with no coordinates | an untagged `.tif` or `.png`; any `.tif`, `.png` or `.jpg`/`.jpeg` with `"as": "scan"` | Add Data, or upload (`layer.addData`, `layer.addDataBytes`) | Grey or RGB(A), 8- or 16-bit pixels. An untagged TIFF or PNG opens as a scan by default; a JPEG needs `"as": "scan"` (otherwise it is a photo). This also discards a GeoTIFF's coordinates. The scan initially covers the current view and is marked not georeferenced (`georeferenced: false`) until **Georeference** places it. Export and tools needing ground coordinates refuse it until then. PNG/JPEG decoding is bounded and may reduce the image; large TIFFs can be read at reduced resolution. | | ASCII grid | `.asc` (with an optional `.prj` on desktop) | Add Data, or upload | A single numeric band. Desktop files use the neighbouring `.prj`; uploaded and downloaded grids have no sidecar and must have coordinates that fit WGS 84 longitude/latitude. | -| Zarr | a store's folder (`.zarr`, desktop) or web address (desktop and browser), format 2 (consolidated) or 3 | Add Data › Zarr… (`layer.addZarr`), Add Data on a local folder, or a `?zarr=` link | A variable's latitude/longitude (or y/x) map at the steps picked of its other dimensions (time, level), read chunk by chunk in pure Rust (blosc, zlib, gzip, zstd, LZ4; sharded arrays too). See [Zarr stores](data.md#zarr-stores). | +| Zarr | a store's folder (`.zarr`, desktop) or web address (desktop and browser), format 2 (consolidated) or 3 | Home › Add › Add Data › Zarr… (`layer.addZarr`), Add Data on a local folder, or a `?zarr=` link | A variable's latitude/longitude (or y/x) map at the steps picked of its other dimensions (time, level), read chunk by chunk in pure Rust (blosc, zlib, gzip, zstd, LZ4; sharded arrays too). See [Zarr stores](data.md#zarr-stores). | | Rasters GDAL reads | `.img`, `.hgt`, `.dem`, `.dt0`–`.dt2`, `.nc`, `.nc4`, `.hdf`, `.hdf5`, `.h5`, `.he5`, `.bil`, `.bsq`, `.bip`, `.envi`, `.grd`, `.rst`, `.sdat`, `.kea`, `.ers`, `.ntf`, `.nitf`, `.ecw`, `.sid`, `.gsb`, `.bt`, `.rsw` | Add Data (desktop, with [GDAL](https://gdal.org): `gdal_translate`, `gdalinfo` and `gdalwarp`; the installed build must include the driver) | Converted once to a cached cloud-optimised GeoTIFF. NetCDF/HDF variables are subdatasets: the first is added, the others named in a notice; `"layer"` picks one. Without a declared coordinate system, longitude/latitude-like coordinates use WGS 84 (0–360° longitudes are moved to −180–180°); otherwise give `"crs"`. JPEG 2000 is listed separately above. | ## Tiles and web services diff --git a/docs/user-guide/data.md b/docs/user-guide/data.md index ac486409f..bde7088af 100644 --- a/docs/user-guide/data.md +++ b/docs/user-guide/data.md @@ -31,11 +31,16 @@ Formats](data-formats.md). | Web tiles: XYZ/TMS/WMS rasters, Mapbox Vector Tiles, MapLibre styles, PMTiles and MBTiles archives | Yes | Yes (Analyze › Create Hosted Imagery) | | RustGIS project (`.rgis`; older `.rgisx` files open too) | Yes | Yes | -Add data with **Home › Add Data** (**From URL…** adds a file by its web address, reading a -cloud-optimised GeoTIFF, GeoParquet or FlatGeobuf in place, a view at a time, and downloading -others; see [Files on the web](data-formats.md#files-on-the-web)), by dropping files on the window, or by -passing them on the command line (`rustgis places.geojson`). **Home › Add Data › Delimited -Text…** asks for a CSV or a spreadsheet first, then lists its columns to pick the x and y (or WKT) +Add data with **Home › Add › Add Data**: its main button opens the file picker, and its menu offers +other ways to add data, including **Delimited Text…**, **From URL…** and **From Service…**. Files +can also be dropped on the window or passed on the command line (`rustgis places.geojson`). The +file picker opens formats listed on [Supported Data Formats](data-formats.md), including +cloud-optimised GeoTIFF, GeoParquet and FlatGeobuf (read in place, a view at a time; see [Files +on the web](data-formats.md#files-on-the-web)). **Home › Add › Data Sources** opens the pane with +saved connections, favourites, recent data and the public-source library. +On the desktop, **Home › Add › Add Data › Add Folder…** connects a folder to the Browser; the web +app does not offer folder connections. +**Home › Add › Add Data › Delimited Text…** asks for a CSV or a spreadsheet first, then lists its columns to pick the x and y (or WKT) ones from, with the coordinate system (such as `EPSG:2263`) and the separator (changing it reads the columns again); `info.fileColumns` lists them for scripts. Add Data finds them itself (columns named like longitude/latitude, easting/northing or geometry, and EWKT's `SRID=`) and @@ -48,7 +53,7 @@ the table's name beside it (`input_name`). The **XY Table To Point** tool ([Geoprocessing](analysis.md), and a table's right-click menu) makes points of a table already in the project, from two fields of coordinates and their coordinate system. In a text field a single comma is the decimal separator (`1,234` is 1.234); thousands separators are not read. -**Home › Add Data › Graphics Layer** adds a layer for [drawn graphics](cartography.md#graphics), +**Home › Add › Add Data › Graphics Layer** adds a layer for [drawn graphics](cartography.md#graphics), and **Data › Layer Templates** adds empty layers with ready-made schemas to draw into. Vector geometry is stored as WGS 84 longitude/latitude. Data in another supported coordinate @@ -139,12 +144,12 @@ result mixing geometry types becomes a layer per type, and rows without a geomet `"database"` opens a DuckDB database (read only) so the query names its tables plainly; the database is trusted like the query, since its views run as part of it. -**Home › Add Data** opens a DuckDB database file (`.duckdb`, `.ddb`): every table with a geometry +**Home › Add › Add Data** opens a DuckDB database file (`.duckdb`, `.ddb`): every table with a geometry column becomes a layer (`"layer"` picks one). ### PostgreSQL and PostGIS -**Home › Add Data › Database…** (`layer.addDatabase`, desktop) connects to a PostgreSQL database with +**Home › Add › Add Data › Database…** (`layer.addDatabase`, desktop) connects to a PostgreSQL database with PostGIS and reads its tables live. The dialog takes a saved connection or a typed one, a password, a table (**List Tables**, or all tables with geometry) and a coordinate system for geometry that doesn't say its own. RustGIS talks to the server itself (nothing else to install; @@ -177,7 +182,7 @@ opt-in to the operating system's password store; see [Named connections](#named- for reconnect, selection and forgetting rules. Opening a project can use a remembered password only for a unique, exact saved-address match, or an explicit session selection. Otherwise a waiting layer offers **Log In…**, or a chooser when several remembered connections match. -Without a matching connection, connect again with **Add Data › Database…**. +Without a matching connection, connect again with **Home › Add › Add Data › Database…**. `PGPASSWORD` or libpq's password file (`~/.pgpass`, or `%APPDATA%\postgresql\pgpass.conf`) can serve when you explicitly add a table without a password; opening a project never newly authorizes these ambient credentials. A small table's features @@ -207,7 +212,7 @@ plugins need both the files and network permissions. ## MySQL and MariaDB -**Home › Add Data › Database…** (`layer.addDatabase`) also connects to a MySQL (8 or later) or +**Home › Add › Add Data › Database…** (`layer.addDatabase`) also connects to a MySQL (8 or later) or MariaDB (10.2 or later) database and reads its spatial columns live, as it does PostGIS tables: RustGIS talks to the server itself, with nothing else to install. The connection is an address, `mysql://user@host:3306/database` (or `mariadb://…`); what it leaves out is, as with the @@ -247,7 +252,7 @@ SQL queries (`layer.addSql`, the SQL pane) don't run on MySQL or MariaDB databas ## SQL Server and Azure SQL -**Home › Add Data › Database…** (`layer.addDatabase`) reads the `geometry` and `geography` +**Home › Add › Add Data › Database…** (`layer.addDatabase`) reads the `geometry` and `geography` columns of SQL Server 2012 or later and Azure SQL tables and views directly, on the desktop. Use `mssql://user@host:1433/database`; an empty host means `localhost`, and omitting the database uses the login's default database. **List Tables** (`info.databaseTables`) lists spatial columns. @@ -294,18 +299,18 @@ remembered reconnects and explicit selection. Secret-named query values such as `access_token` are removed from saved connection addresses. `SQLCMDPASSWORD` serves only when explicitly adding a database, not when opening a project. Windows integrated sign-in still requires that same address to have been added this session: opening a project does not -authorize Windows identity. Reconnect with **Add Data › Database…** when needed. SQL queries +authorize Windows identity. Reconnect with **Home › Add › Add Data › Database…** when needed. SQL queries (`layer.addSql`, the SQL pane) do not run on SQL Server connections. ## Web Data -**Home › Web Data** gathers public web services and catalogs: STAC imagery catalogs, Overture +**Home › Add › Web Data** gathers public web services and catalogs: STAC imagery catalogs, Overture Maps, OpenStreetMap, NASA Earthdata, open-data portals, satellite embeddings and more. They have a page of their own: [Web Data](web-data.md). ## Geocoding addresses -**Home › Add Data › Geocode Addresses…** (`layer.geocode`) turns a standalone table's or a +**Home › Add › Add Data › Geocode Addresses…** (`layer.geocode`) turns a standalone table's or a layer's addresses into points: a CSV of addresses without coordinates is added as a standalone table, then each row's address fields (`"address"`: a field, or several such as street, city and country, joined with commas) are looked up, each distinct address once. The result is a new point layer holding every field @@ -328,12 +333,12 @@ it geocodes, and plugins need the network permission. ## S3 buckets -An `s3://bucket/key` address works wherever data is added by its address: **Add Data › From -URL…** and `layer.addUrl` (a cloud-optimised GeoTIFF is read in place by range, other files are +An `s3://bucket/key` address works wherever data is added by its address: **Home › Add › Add Data › +From URL…** and `layer.addUrl` (a cloud-optimised GeoTIFF is read in place by range, other files are downloaded), PMTiles archives, `?data=` links, and STAC assets whose address is an `s3://` one. A public bucket needs nothing more: it is read anonymously from AWS. -**Home › Add Data › S3…** (`s3.connect`) connects to a private bucket, or to an S3-compatible +**Home › Add › Add Data › S3…** (`s3.connect`) connects to a private bucket, or to an S3-compatible service (MinIO, Cloudflare R2, Wasabi, source.coop…) by its endpoint, and browses a bucket a folder at a time (`s3.list`), adding an object with **Add**. A connection names its endpoint (AWS when empty), its region and the buckets it serves: the ones it is given (`"buckets"`) and the ones @@ -409,8 +414,8 @@ before it connects or lists; plugins can't connect, and need the network permiss ## Data Sources -The **Data Sources** pane (**View › Windows › Data Sources**, `ui.pane.sources`; **Home › Add -Data › Connections…** also opens it) gathers the places data comes from: +The **Data Sources** pane (**View › Windows › Data Sources**, `ui.pane.sources`; **Home › Add › +Data Sources** also opens it) gathers the places data comes from: - **Favourites**: any source, right-clicked and added. - **Recent**: the last 20 sources added (files, web addresses, tile layers, services, database @@ -449,7 +454,7 @@ database is kept without its password. The commands `sources.list`, `sources.sav ### Named connections In the pane, **New Connection ▾** and **Edit…** let you set the name, group and address; -**Home › Add Data › New Connection** opens the same form for a chosen kind. Leave a new +**Home › Add › Add Data › New Connection** opens the same form for a chosen kind. Leave a new connection's name blank to derive it from the address. **Test** checks the form without saving it: web-service, catalogue, STAC and S3 requests run in the background on both desktop and web; database tests run synchronously on desktop. The web app doesn't offer database kinds for new @@ -768,7 +773,7 @@ path relative to the project and reopen it; rasters added in the browser are sto ### Zarr stores -**Home › Add Data › Zarr…** (`layer.addZarr`) adds a variable of a [Zarr](https://zarr.dev) store +**Home › Add › Add Data › Zarr…** (`layer.addZarr`) adds a variable of a [Zarr](https://zarr.dev) store (format 2 or 3), the multidimensional arrays climate and Earth-observation data are kept in: give the store's web address, or (on the desktop) its folder, and **Connect** lists its variables (`raster.zarrInfo`). A variable's map is its latitude/longitude (or y/x) dimensions; its other @@ -849,13 +854,13 @@ RustGIS saves pieces of a project as files you can share and reuse: | Item | Extension | Made with | Opened with | | --- | --- | --- | --- | -| Map file (a map with its data) | `.rgismap` | Publish › Save As › Map File | Home › Import | +| Map file (a map with its data) | `.rgismap` | Publish › Save As › Map File | Command Search (`map.import`), or drop it on the window | | Layer file (a layer with its symbology) | `.rgislyr` | Publish › Save As › Layer File | Add Data | | Styles (saved symbology) | `.rgisstyle` | Style › Styles › New Style, then Publish › Save As › Style Items | Style › Styles › Add Style File | | [Color ramps](cartography.md#custom-color-ramps) | `.rgisramp` (also reads QGIS `.xml`/`.qml`, GDAL color `.txt` and GMT `.cpt`) | Publish › Save As › Color Ramps, or by hand | Style › Styles › Add Color Ramp File | -| Project template (the setup without features) | `.rgistemplate` | Home › Templates, or Publish › Save As | Home › Templates › Open Custom Template | +| Project template (the setup without features) | `.rgistemplate` | Publish › Save As, or Command Search (`project.saveTemplate`) | Command Search (`project.fromTemplate`) | | Project package (the project, with linked raster files embedded while they fit) | `.rgispkg` | Publish › Package › Project | Project › Open, or drop it on the window | -| Map package / mobile map package | `.rgismpk` | Publish › Package › Map or Mobile Map | Home › Import | +| Map package / mobile map package | `.rgismpk` | Publish › Package › Map or Mobile Map | Open the package as a project, or drop it on the window | | Results package (geoprocessing history and outputs) | `.rgisgpk` | Publish › Package › Results | any zip tool | | Task (guided command steps) | `.rgistask` | Analyze › Task, Publish › Save As › Task Item | Analyze › Task › Import Task | | Bookmarks (as longitude, latitude and scale) | `.rgisbookmarks` | Home › Navigate › Bookmarks › Export Bookmarks | Home › Navigate › Bookmarks › Import Bookmarks | diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index e9407e912..c253029eb 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -108,7 +108,7 @@ ribbon, arrows at its ends scroll it. | Tab | Groups | | --- | --- | -| **Home** | Project (New Map, Toolbox, Import Project, New Layout, Browser, Add Data, Add Folder, Import, Templates), Navigate (Pan, Bookmarks, Full Extent, Zoom In/Out, Previous/Next, Go To XY, Planar/First-Person, Identify, Find Place, Directions), Add (Basemap, Add Data, [Web Data](web-data.md)), Selection (Select, Select By Attributes, Select By Location, Clear, Zoom To, Attributes), Inspect (Measure, Locate, Zoom Box, Infographics) | +| **Home** | Project (New Map, New Layout), Navigate (Pan, Bookmarks, Full Extent, Zoom In/Out, Previous/Next, Zoom Box, Planar/First-Person, Identify, Locate [By Coordinates, By Place or Address], Directions), Add (Basemap, Add Data, Data Sources, [Web Data](web-data.md)), Selection (Select, Select By Attributes, Select By Location, Clear, Zoom To, Attributes), Inspect (Measure, Infographics) | | **Data** | Layer Templates, Data Source (Repair Source); for a feature layer: Definition Query, Table, Selection, Data Design, Archiving, Relationship and Export; for a raster layer: Information and Export; then Offline and Feature Cache | | **Style** | For the layer selected in Layers. A feature layer: Visibility Range, Effects, Drawing, Drawing Effects, Extrusion for polygons (with Faces and Light and Shadow once on), Layer, Labels, Label Range, Label Placement, Label Class and Text Symbol. A raster layer: Visibility Range, Effects, Compare, Raster Effects, Rendering, Enhancement and Layer. Both: Styles | | **Edit** | Manage Edits (Edit, Save, Undo, Redo, Status, Settings, Templates, Topology), Snapping, Features (Create, Delete, Edit Vertices, Modify), Tools, Elevation, Corrections, Graphics, Graphics and Text, Clipboard | diff --git a/docs/user-guide/maps.md b/docs/user-guide/maps.md index 455ffcdf0..e6b537535 100644 --- a/docs/user-guide/maps.md +++ b/docs/user-guide/maps.md @@ -8,17 +8,17 @@ For selecting, grouping, reordering and copying layers between maps, see ## Navigating -Drag to pan and scroll to zoom; ++home++ (or **Full Extent**) shows all the data. **Home › -Navigate** has fixed zoom steps, **Previous** and **Next** extents, **Bookmarks** (the World Atlas -has World, Europe, Africa, Americas and East Asia) and **Go To XY**, which pans to a longitude and -latitude. **New Bookmark…** saves the current view under a name, with an optional description of -the place. The Bookmarks menu removes a bookmark with the button beside it, and exports or imports -them as a `.rgisbookmarks` file. Imported bookmarks show the same place on a map in another -projection; places that projection can't show are skipped. Type a scale in the status bar to jump -to it: the map zooms from the whole world in past 2:1, close enough for site plans at 1:20 and -finds drawings at 1:1 (type `2:1` for twice life size). Beside it are the zoom level, bearing -and pitch; click them to face north and look straight down again. More navigation tools are -under [View effects](#view-effects) below. +Drag to pan and scroll to zoom; ++home++ (or **Full Extent**) shows all the data. **Home › Navigate** +has fixed zoom steps, **Previous** and **Next** extents, **Bookmarks** (the World Atlas has World, +Europe, Africa, Americas and East Asia), and **Locate**: **By Coordinates…** pans to a longitude +and latitude; **By Place or Address…** finds a place or address with a geocoder. **New Bookmark…** +saves the current view under a name, with an optional description of the place. The Bookmarks menu +removes a bookmark with the button beside it, and exports or imports them as a `.rgisbookmarks` +file. Imported bookmarks show the same place on a map in another projection; places that projection +can't show are skipped. Type a scale in the status bar to jump to it: the map zooms from the whole +world in past 2:1, close enough for site plans at 1:20 and finds drawings at 1:1 (type `2:1` for twice +life size). Beside it are the zoom level, bearing and pitch; click them to face north and look +straight down again. More navigation tools are under [View effects](#view-effects) below. **Measurement units.** In **Home › Inspect › Measure**, choose **Metric** or **Imperial**. The choice is remembered and updates existing distance and area measurements, the live @@ -57,7 +57,7 @@ height, with the labels of extruded features on their roofs. While a tilted map picture drawn last is laid over the ground and the extrusions and markers are moved with it, and the map is drawn again once it settles. `map.setBearing` sets the bearing (`{"bearing": 90}` puts east at the top; `{"by": 15}` turns it further), `map.setPitch` the pitch (`{"pitch": 60}`, -or `{"by": 10}`), `map.resetNorth` faces north and looks straight down, and **Go To XY** takes a +or `{"by": 10}`), `map.resetNorth` faces north and looks straight down, and `map.goToXY` takes a `bearing` and a `pitch` too. Page layout map frames take their own bearing and pitch (see [Layouts](layouts.md)), and the globe turns and tilts the same way: tilted, its camera swings back about the centre of view, so the horizon comes into sight past the curve of the Earth. @@ -100,10 +100,11 @@ Double-click a name to rename it, and right-click it to rename, describe, update down, or delete it. Bookmark edits run `bookmark.*` commands and undo with ++ctrl+z++; **Zoom To** and **Pan To** change the view without adding an undo step. -**Find Place** (`map.findPlace`) looks up a place or an address by name with OpenStreetMap's -[Nominatim](https://nominatim.org) (or a [Photon](https://photon.komoot.io) server, or a -geocoder of your own given by its address) and zooms to the one picked. The search goes to that -server over the network; see [Geocoding addresses](data.md#geocoding-addresses) for whole tables. +**Home › Navigate › Locate › By Place or Address…** (`map.findPlace`) looks up a place or an +address by name with OpenStreetMap's [Nominatim](https://nominatim.org) (or a +[Photon](https://photon.komoot.io) server, or a geocoder of your own given by its address) and +zooms to the one picked. The search goes to that server over the network; see +[Geocoding addresses](data.md#geocoding-addresses) for whole tables. The **search box** at the foot of the left pane searches the map as you type: a latitude and longitude (`40.7, -74.0`, `40.7N 74W` or `40°42'46"N 74°0'21"W`; latitude first unless the diff --git a/docs/user-guide/web-data.md b/docs/user-guide/web-data.md index 301587c5d..49704861a 100644 --- a/docs/user-guide/web-data.md +++ b/docs/user-guide/web-data.md @@ -1,6 +1,6 @@ # Web Data -**Home › Web Data** gathers public web services and catalogs: the ones that cover the whole world +**Home › Add › Web Data** gathers public web services and catalogs: the ones that cover the whole world in one alphabetical list, and those that cover one country in a submenu named after it. Each opens in the Web Data pane on the right, beside the map it searches. Unless a section says otherwise, the panes work in the web app too, provided the servers allow browser requests @@ -42,7 +42,7 @@ Data added by its address, from a web server or an S3 bucket, is described in ## STAC catalogs -**Home › Web Data › STAC Search…** searches a [STAC](https://stacspec.org) API for imagery and +**Home › Add › Web Data › STAC Search…** searches a [STAC](https://stacspec.org) API for imagery and other assets: by collection, dates, the most cloud cover and (by default) where the map is. The default API is Element 84's [Earth Search](https://earth-search.aws.element84.com/v1); Microsoft's [Planetary Computer](https://planetarycomputer.microsoft.com/api/stac/v1) is offered @@ -210,7 +210,7 @@ start from), `"phase"` and `"search": true` (find items at once). ### Planet Open Data -**Home › Web Data › Planet Open Data…** browses Planet's [disaster data +**Home › Add › Web Data › Planet Open Data…** browses Planet's [disaster data releases](https://source.coop/planet/disasterdata) on Source Cooperative: imagery of earthquakes, floods, fires and storms from PlanetScope, SkySat and Pelican, before and after the event, licensed CC BY-NC 4.0. Pick an event (and, below it, its pre-event or post-event imagery or one collection), @@ -218,7 +218,7 @@ then **Find Items**; **Add** adds a scene's `visual` image. ### Portolan -**Home › Web Data › Portolan…** opens the [Portolan +**Home › Add › Web Data › Portolan…** opens the [Portolan registry](https://github.com/portolan-sdi/portolan-registry), a catalog of the open data catalogs published with Portolan (cities, regions and agencies, most in Europe). Pick a publisher's catalog, then a theme or collection, then **Find Items**: each collection's files (GeoParquet, PMTiles and @@ -231,7 +231,7 @@ others) are listed, and **Add** adds the one chosen (`visual` first, often a PMT
The Sentinel-2 Explorer: the clearest recent scenes where the map is, with their footprints.
-**Home › Web Data › Sentinel-2 Explorer…** searches Earth Search's Sentinel-2 Collection 1 Level-2A +**Home › Add › Web Data › Sentinel-2 Explorer…** searches Earth Search's Sentinel-2 Collection 1 Level-2A scenes (`sentinel-2-c1-l2a`) where the map is, over the last 90 days and with at most 20 % cloud cover, clearest first (sorted by cloud cover). **Add** adds the true colour image (`visual`); the asset list beside each scene offers its bands (`red`, `nir`, `swir16`, `scl`…), each a @@ -240,14 +240,14 @@ cloud-optimised GeoTIFF read in place. `stac.search` sorts with `"sortby"` ### Vantor -**Home › Web Data › Vantor…** browses [Vantor](https://vantor.com)'s open data (formerly Maxar's +**Home › Add › Web Data › Vantor…** browses [Vantor](https://vantor.com)'s open data (formerly Maxar's Open Data program): very high resolution imagery of disasters, one collection per event. Pick an event and a phase (before or after the event), then **Find Items**; **Add** adds a scene's `visual` cloud-optimised GeoTIFF. ### USGS LiDAR -**Home › Web Data › United States › USGS LiDAR…** searches the USGS 3D Elevation Program's LiDAR +**Home › Add › Web Data › United States › USGS LiDAR…** searches the USGS 3D Elevation Program's LiDAR point clouds on the Planetary Computer (`3dep-lidar-copc`) where the map is: zoom to a town first. Each tile's `data` asset is a COPC file, signed with the Planetary Computer's token and added as a LiDAR point cloud (an evenly spread sample of its points, see [Formats](data.md#formats)). A tile is @@ -255,7 +255,7 @@ downloaded whole, often tens of megabytes, so adding one takes a while. ## Overture Maps -**Home › Web Data › Overture Maps…** adds [Overture Maps](https://overturemaps.org)' open data: +**Home › Add › Web Data › Overture Maps…** adds [Overture Maps](https://overturemaps.org)' open data: buildings, places, transportation (segments and connectors), administrative divisions, base layers (land use, land, land cover, water, infrastructure, bathymetry) and addresses, from the latest release (or one named with `"release"`). Two ways: @@ -281,7 +281,7 @@ places); the layer says which. ## Source Cooperative -**Home › Web Data › Source Cooperative…** lists [Source Cooperative](https://source.coop)'s +**Home › Add › Web Data › Source Cooperative…** lists [Source Cooperative](https://source.coop)'s featured products (or an account's), browses a product's folders and adds a file by its address on the data proxy (`https://data.source.coop///…`): GeoParquet, FlatGeobuf, cloud-optimised GeoTIFFs and PMTiles are read in place, other files downloaded. @@ -292,7 +292,7 @@ plugins need the network permission. ## Natural Earth -**Home › Web Data › Natural Earth…** is the Source Cooperative browser locked to +**Home › Add › Web Data › Natural Earth…** is the Source Cooperative browser locked to [Natural Earth](https://www.naturalearthdata.com)'s public domain map data as Source Cooperative holds it (`opengeos/natural-earth`): countries, states, coastlines, land, ocean, rivers, lakes, glaciers, urban areas, airports, railways and more. It opens on the `pmtiles/` folder, whose @@ -303,7 +303,7 @@ archives are added as tile layers read in place; the `geoparquet/`, `geojson/`, ## Fields of the World -**Home › Web Data › Fields of the World…** adds the agricultural field boundaries the +**Home › Add › Web Data › Fields of the World…** adds the agricultural field boundaries the [Fields of the World](https://fieldsofthe.world) model found in Sentinel-2 imagery, worldwide, one year at a time (2017 to 2025; 2nd Edition, CC BY 4.0). Each year is a PMTiles archive on Source Cooperative read in place. Fields are drawn from zoom 9, filled and outlined by the model's score @@ -322,7 +322,7 @@ The look is the tile layer's own style layers (`map.addTiles` takes MapLibre sty ## OpenStreetMap and administrative boundaries -**Home › Web Data › OpenStreetMap…** (`osm.download`) downloads OpenStreetMap data where the map +**Home › Add › Web Data › OpenStreetMap…** (`osm.download`) downloads OpenStreetMap data where the map is from an [Overpass](https://overpass-api.de) server: a preset (buildings, roads, railways, water, land use, parks, points of interest or administrative boundaries) or tag filters such as `amenity=cafe, shop`. The answer becomes up to three layers, points, lines and polygons: closed @@ -333,7 +333,7 @@ degrees (about 50 × 50 km), and an answer up to 50,000 elements: the public ser a larger area is better read from an extract (`.osm.pbf`). `"url"` names another Overpass server when the public one is busy. -**Home › Web Data › Boundaries…** (`boundaries.add`) adds a country's administrative boundaries from +**Home › Add › Web Data › Boundaries…** (`boundaries.add`) adds a country's administrative boundaries from [geoBoundaries](https://www.geoboundaries.org) (CC BY 4.0): the country itself (`ADM0`), its states or provinces (`ADM1`) and finer divisions down to `ADM5` where they exist, simplified for the map. `boundaries.countries` lists the countries. @@ -353,7 +353,7 @@ NASA's data comes two ways: **NASA Earthdata…** adds imagery that NASA serves ### NASA Earthdata -**Home › Web Data › NASA Earthdata…** lists the layers of NASA's +**Home › Add › Web Data › NASA Earthdata…** lists the layers of NASA's [Global Imagery Browse Services](https://www.earthdata.nasa.gov/engage/open-data-services-software/earthdata-developer-portal/gibs-api) (GIBS) in Web Mercator: more than a thousand layers of satellite imagery and science data (MODIS, VIIRS, Landsat, sea surface temperature, aerosols, fires…), grouped by mission or instrument (the @@ -376,7 +376,7 @@ rustgis-cli run --cmd 'service.add={"url":"https://wmts.geo.admin.ch/EPSG/3857/1 ### NASA Earthaccess -**Home › Web Data › NASA Earthaccess…** searches [NASA Earthdata](https://www.earthdata.nasa.gov) +**Home › Add › Web Data › NASA Earthaccess…** searches [NASA Earthdata](https://www.earthdata.nasa.gov) as GeoLibre's Earthaccess panel does, through NASA's Common Metadata Repository (CMR): 1. **Dataset**: pick one of the **Popular datasets** (ICESat-2 ATL03/ATL06/ATL08, GEDI L2A/L2B/L4A, @@ -413,7 +413,7 @@ rustgis-cli run --sample --cmd 'earthdata.login={"token":"…"}' --cmd 'layer.ad ## Open-data portals -**Home › Web Data › Search Open Data…** (`portal.search`) finds datasets by text in an open-data +**Home › Add › Web Data › Search Open Data…** (`portal.search`) finds datasets by text in an open-data portal and lists, for each, the resources RustGIS can add, the best format first: - **data.gov and CKAN catalogues**: [catalog.data.gov](https://catalog.data.gov) by default, @@ -445,7 +445,7 @@ and plugins need the network permission. A dataset's licence is the portal's: it ### Web services -Several **Home › Web Data** entries open the same search on one service, with its own choices +Several **Home › Add › Web Data** entries open the same search on one service, with its own choices (`portal.search` with the `"portal"` in brackets): | Entry | What it finds | Added as | @@ -484,7 +484,7 @@ rustgis-cli run --sample --cmd 'portal.add={"url":"https://data.geopf.fr/telecha ## United States -**Home › Web Data › United States** holds services covering the United States: the +**Home › Add › Web Data › United States** holds services covering the United States: the portals in the [Web services](#web-services) table (Tennessee GIS, US Federal, State and Local GIS, USGS 3DEP DEM), [USGS LiDAR](#usgs-lidar), and the panels below. @@ -519,7 +519,7 @@ rustgis-cli run --cmd 'nationalMap.add={"service":"basemap/USGSTopo"}' --cmd 'na ### USGS NLDI -**Home › Web Data › United States › USGS NLDI…** uses the USGS +**Home › Add › Web Data › United States › USGS NLDI…** uses the USGS [Network-Linked Data Index](https://api.water.usgs.gov/nldi) on the NHDPlus river network, as GeoLibre's NLDI panel does. Each result is a layer: @@ -544,7 +544,7 @@ rustgis-cli run --sample --cmd 'nldi.flowline={"lon":-89.36083,"lat":43.08944}' ## Satellite embeddings -**Home › Web Data › Satellite Embeddings…** shows pre-computed satellite embeddings: vectors a +**Home › Add › Web Data › Satellite Embeddings…** shows pre-computed satellite embeddings: vectors a geospatial foundation model made for every pixel or image patch, so that places alike have values alike. Pick a dataset (its provider, model, resolution, years, coverage, licence and links are shown), a year, and **Search where the map is**; the results are cards with **Zoom** and **Add**, diff --git a/docs/user-guide/workspace.md b/docs/user-guide/workspace.md index 10564aa09..9f877e0bc 100644 --- a/docs/user-guide/workspace.md +++ b/docs/user-guide/workspace.md @@ -152,12 +152,11 @@ duplication and nudging have their own [layout shortcuts](layouts.md#layouts). - **Pane Sets** (View › Workspace) save the left pane, open right-hand pane tabs, active pane and ribbon tab and bring them back. The **Workflow Manager** tracks jobs (new, in progress, on hold, done) with the area each covers. -- **Connections** (Home › Add Data › **Connections…**) opens the Data Sources pane on its saved - connections; it only ever opens the pane, so it is safe to choose when the pane is already open - (**View › Windows › Data Sources** still shows or hides it). **Add Data › New Connection** lists - each kind of connection (PostgreSQL/PostGIS, MySQL/MariaDB and SQL Server are left out in the web - app) and opens its form. Saved connections are listed in the pane, not in the menu. Command - Search finds **Connections…** by name. +- **Data Sources** (Home › Add › **Data Sources**) opens the Data Sources pane; **View › Windows › + Data Sources** still shows or hides it. **Home › Add › Add Data › New Connection** lists each + kind of connection (PostgreSQL/PostGIS, MySQL/MariaDB and SQL Server are left out in the web + app) and opens its form. Saved connections are listed in the pane, not in the Add Data menu. + Command Search finds the pane as **Data Sources**. - **Tasks** (Analyze › Tasks, or View › Tasks) steps through a task's instructions: read the current step and do it yourself, with **Show Me** to find where, or click **Run** to have it done. - **Show Me** points at the step's command: @@ -215,8 +214,8 @@ duplication and nudging have their own [layout shortcuts](layouts.md#layouts). cloud-optimised GeoTIFF a file per date with `layer.addUrl`, giving its `start` and `end`. For example NASA GIBS daily MODIS imagery: `https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/MODIS_Terra_CorrectedReflectance_TrueColor/default/{date:YYYY-MM-DD}/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg`. - - A Zarr variable added with **Follow the time slider** (Add Data › Zarr…) shows the step the - slider is at; see [Zarr stores](data.md#zarr-stores). + - A Zarr variable added with **Follow the time slider** (Home › Add › Add Data › Zarr…) shows + the step the slider is at; see [Zarr stores](data.md#zarr-stores). - From scripts: `time.bind` (`"layer"`, `"field"`, `"start"`, `"end"`, `"unit"`, `"interval"`, `"show"`), `time.set` (the window: `"start"`, `"end"`, `"unit"`, `"interval"`, `"show"`, `"speed"`, `"loop"`, `"format"`…), `time.goTo` (`"date"`, or `"step"` from 0), `time.step` (`"by"`, negative goes