From 226f95939b4194407727f6479e514d7d872c7a15 Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Thu, 4 Jun 2026 17:27:10 +0200 Subject: [PATCH 1/3] `recentre` script for Galiano Ensures that the centre of the georeference matches the centre of the raster. --- scripts/galiano.bash | 50 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/scripts/galiano.bash b/scripts/galiano.bash index 0cada24..ef8bc4a 100644 --- a/scripts/galiano.bash +++ b/scripts/galiano.bash @@ -1,3 +1,5 @@ +# shellcheck disable=SC2155 + # CRS: NAD83(CSRS) / UTM zone 10N (already metric and equally spaced pixels) # Center: 467048.260, 5420616.975 (-123.44996829267, 48.9376084029813) # Pixels: 87616x78341 (target for factor of 48 87648x87648) @@ -10,7 +12,6 @@ # compute dsm-1.0.tiff \ # --thread-count 160 \ # --database-per-thread \ -# --backend cpu \ # --process total-surfaces,viewsheds \ # --observer-height 0.0 \ # --aoi-point -123.57209623620894,49.02110568820024 \ @@ -18,6 +19,9 @@ # --aoi-point -123.34211991205905,48.83884334550575 \ # --aoi-point -123.61305988176157,48.99841636016359 \ # --aoi-point -123.57209623620894,49.0211056882002 \ +# --tvs-source-path /mnt/disks/viewshed/total_surfaces.tiff \ +# --only-save-biggest-viewsheds 16 \ +# --angle-subdivisions 10 \ # --output-dir /mnt/disks/viewshed \ # --viewsheds-db-path /mnt/disks/viewshed/dbs @@ -33,8 +37,7 @@ function galiano_prepare { local resolution=$3 read -r xcentre ycentre <<<"$( - gdalinfo \ - -json "$input" | jq -r '.cornerCoordinates.center | join(" ")' + gdalinfo -json "$input" | jq -r '.cornerCoordinates.center | join(" ")' )" source_crs=$(gdalsrsinfo -o wkt "$input") @@ -58,6 +61,15 @@ function galiano_prepare { pad_to_factor "$output" + read -r xcentre ycentre <<<"$( + gdalinfo -json "$output" | jq -r '.cornerCoordinates.center | join(" ")' + )" + + if (($(echo "scale=3; $xcentre != 0 || $ycentre != 0" | bc -l))); then + echo "Recentering from $xcentre,$ycentre to 0,0..." + recentre "$output" + fi + } function _galiano_extent_padded { @@ -99,3 +111,35 @@ function pad_to_factor { mv "$temp" "$input" } + + +# Ensure that the centre of the georeference matches the centre of the raster. +function recentre() { + local input_file="$1" + + local temp=$(dirname "$input")/tmp.tiff + + local info=$(gdalinfo -json "$input_file") + + local lon_0=$(echo "$info" | jq '.wgs84Extent.coordinates[0] | (.[0][0] + .[2][0]) / 2') + local lat_0=$(echo "$info" | jq '.wgs84Extent.coordinates[0] | (.[0][1] + .[2][1]) / 2') + + local width=$(echo "$info" | jq -r '.size[0]') + local height=$(echo "$info" | jq -r '.size[1]') + local resolution_x=$(echo "$info" | jq -r '.geoTransform[1]') + local resolution_y=$(echo "$info" | jq -r '.geoTransform[5] | abs') + + local x_min=$(python3 -c "print(-($width * $resolution_x) / 2.0)") + local x_max=$(python3 -c "print(($width * $resolution_x) / 2.0)") + local y_min=$(python3 -c "print(-($height * $resolution_y) / 2.0)") + local y_max=$(python3 -c "print(($height * $resolution_y) / 2.0)") + + gdalwarp \ + -t_srs "+proj=aeqd +lat_0=$lat_0 +lon_0=$lon_0 +datum=WGS84 +units=m" \ + -te "$x_min" "$y_min" "$x_max" "$y_max" \ + -r bilinear \ + -dstnodata 0 \ + "$input_file" "$temp" + + mv "$temp" "$input_file" +} From eea630d8b591b17cc02df2cc6c9ce3e6a1181f75 Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Thu, 4 Jun 2026 17:35:27 +0200 Subject: [PATCH 2/3] Render viewsheds with higher angular resolutions And also render viewsheds by neighbourhood ID, therefore support compute runs where only the largest viewshed amongst its neighbours is saved. --- .gitignore | 1 + Cargo.lock | 182 ++++++++++++++++-- crates/server/fixtures/shards/sample_16x16.db | Bin 57344 -> 40960 bytes crates/server/src/get_viewshed.rs | 65 ++++++- crates/server/src/test.rs | 60 +----- crates/shared/src/metadata.rs | 7 +- crates/shared/src/utils.rs | 44 ++++- crates/tasks/Cargo.toml | 2 +- website/src/ViewshedWorker.ts | 56 ++++-- website/src/lib/Viewshed.ts | 30 +-- 10 files changed, 330 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index 484cdfa..017715d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ *.pmtiles .env /state +rustc-ice-* diff --git a/Cargo.lock b/Cargo.lock index f351d4b..7824465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,18 @@ dependencies = [ "password-hash", ] +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.7", + "stable_deref_trait", +] + [[package]] name = "async-compression" version = "0.4.41" @@ -314,6 +326,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -449,9 +470,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", @@ -469,11 +490,11 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -855,6 +876,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1383,9 +1410,9 @@ dependencies = [ [[package]] name = "gdal" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e721cea67b420fd4b5cb15ba8145f2f1d3a6931a27fdbfadb46cff02015e1cde" +checksum = "e26906e99e54000acd7f6328f85d161e6280390b9b596e06dbed0637a6e29717" dependencies = [ "bitflags", "chrono", @@ -1397,15 +1424,33 @@ dependencies = [ [[package]] name = "gdal-sys" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "febef67dc08a956a9ecb04de2b40dbd15ad56be49421aad9ae0cdcbe9a24166c" +checksum = "cceef1cc08a1f031c5717cb645bb361a3114470cc142cc96bc5e62b79695632e" dependencies = [ "bindgen", "pkg-config", "semver", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1442,20 +1487,24 @@ dependencies = [ "log", "num-traits", "robust", - "rstar", + "rstar 0.12.2", "spade", ] [[package]] name = "geo-types" -version = "0.7.17" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a4dcd69d35b2c87a7c83bce9af69fd65c9d68d3833a0ded568983928f3fc99" +checksum = "94776032c45f950d30a13af6113c2ad5625316c9abfbccee4dd5a6695f8fe0f5" dependencies = [ "approx", "num-traits", "rayon", - "rstar", + "rstar 0.10.0", + "rstar 0.11.0", + "rstar 0.12.2", + "rstar 0.8.4", + "rstar 0.9.3", "serde", ] @@ -1558,6 +1607,24 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b42eb4efef1f96510ae1a33b2682562a677d504641e9903a77bf5c666b9013e" +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1630,13 +1697,38 @@ dependencies = [ "uuid", ] +[[package]] +name = "heapless" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" +dependencies = [ + "as-slice", + "generic-array 0.14.7", + "hash32 0.1.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version", + "spin", + "stable_deref_trait", +] + [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "hash32", + "hash32 0.3.1", "stable_deref_trait", ] @@ -2532,6 +2624,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "pdqselect" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2921,14 +3019,64 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstar" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45c0e8804d37e4d97e55c6f258bc9ad9c5ee7b07437009dd152d764949a27c" +dependencies = [ + "heapless 0.6.1", + "num-traits", + "pdqselect", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40f1bfe5acdab44bc63e6699c28b74f75ec43afb59f3eda01e145aff86a25fa" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f39465655a1e3d8ae79c6d9e007f4953bfc5d55297602df9dc38f9ae9f1359a" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + +[[package]] +name = "rstar" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" +dependencies = [ + "heapless 0.7.17", + "num-traits", + "serde", + "smallvec", +] + [[package]] name = "rstar" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" dependencies = [ - "heapless", + "heapless 0.8.0", "num-traits", + "serde", "smallvec", ] @@ -3289,7 +3437,7 @@ dependencies = [ "color-eyre", "geo", "proj4rs", - "rstar", + "rstar 0.12.2", "serde", "serde_json", ] @@ -3696,7 +3844,7 @@ dependencies = [ "geojson", "h3o", "proj4rs", - "rstar", + "rstar 0.12.2", "serde", "serde_json", "shared", diff --git a/crates/server/fixtures/shards/sample_16x16.db b/crates/server/fixtures/shards/sample_16x16.db index b2c7a54fe62a3fcd6d53a1d36ad22e03b6b9e589..af26bd11d6a7d5b13d6ae89388c5a172011eaa82 100644 GIT binary patch literal 40960 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 XmF{0fe}((T@Rz%94F4AQ!~8!13)|13 literal 57344 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 diff --git a/crates/server/src/get_viewshed.rs b/crates/server/src/get_viewshed.rs index 1317a04..1154018 100644 --- a/crates/server/src/get_viewshed.rs +++ b/crates/server/src/get_viewshed.rs @@ -28,18 +28,51 @@ pub async fn get_viewshed( .into_response(); }; + let neighbourhood_id = get_neighbourhood_id( + dem_id, + state.metadata.width.into(), + state.metadata.neighbourhood_size.into(), + ); + let start = tokio::time::Instant::now(); - let mut tasks = query_shards(state.shards, dem_id); + let mut tasks = query_shards(state.shards, neighbourhood_id); let mut payload = Vec::with_capacity(1024 * 16); + payload.extend_from_slice(&state.metadata.angle_subdivisions.to_be_bytes()); + let mut is_dem_id_of_biggest_added = false; while let Some(task_result) = tasks.next().await { let fetch_result: Result<()> = async { let rows = task_result??; for row in rows { let angle_id: u16 = row.try_get(0)?; - let bytes: Vec = row.try_get(1)?; + let dem_id_of_biggest_viewshed: i64 = row.try_get(1)?; + let bytes: Vec = row.try_get(2)?; let bytes_length = u16::try_from(bytes.len())?; + // TODO: This should all be done via another table. It'll save a fair bit of storage + // space too. + if !is_dem_id_of_biggest_added { + let lonlat_of_biggest_viewshed = shared::utils::dem_id_to_lonlat( + &state.metadata, + dem_id_of_biggest_viewshed, + )?; + tracing::debug!("Lon/lat of biggest viewshed: {lonlat_of_biggest_viewshed:?}"); + #[expect( + clippy::cast_possible_truncation, + clippy::as_conversions, + reason = "Just lonlat coordinates" + )] + { + payload.extend_from_slice( + &(lonlat_of_biggest_viewshed.0.x as f32).to_be_bytes(), + ); + payload.extend_from_slice( + &(lonlat_of_biggest_viewshed.0.y as f32).to_be_bytes(), + ); + }; + is_dem_id_of_biggest_added = true; + } + payload.extend_from_slice(&angle_id.to_be_bytes()); payload.extend_from_slice(&bytes_length.to_be_bytes()); payload.extend_from_slice(&bytes); @@ -77,10 +110,22 @@ pub async fn get_viewshed( .into_response() } +/// Find which neighbourhood a given index is in. +pub const fn get_neighbourhood_id(index: i64, global_width: i64, neighbourhood_size: i64) -> i64 { + let global_x = index.rem_euclid(global_width); + let global_y = index.div_euclid(global_width); + + let neighbourhood_width = neighbourhood_size.isqrt(); + let neighbourhoods_per_row = global_width.div_euclid(neighbourhood_width); + let neighbourhood_x = global_x.div_euclid(neighbourhood_width); + let neighbourhood_y = global_y.div_euclid(neighbourhood_width); + (neighbourhood_y * neighbourhoods_per_row) + neighbourhood_x +} + /// Query all shards in parallel. fn query_shards( shards: Vec, - dem_id: i64, + neighbourhood_id: i64, ) -> futures_util::stream::FuturesUnordered< tokio::task::JoinHandle>>, > { @@ -88,10 +133,16 @@ fn query_shards( for shard in shards { tasks.push(tokio::spawn(async move { - sqlx::query("SELECT angle_id, visible_segments FROM polar_segments WHERE dem_id = ?") - .bind(dem_id) - .fetch_all(&*shard) - .await + sqlx::query( + " + SELECT angle_id, dem_id, visible_segments + FROM polar_segments + WHERE neighbourhood_id = ? + ", + ) + .bind(neighbourhood_id) + .fetch_all(&*shard) + .await })); } diff --git a/crates/server/src/test.rs b/crates/server/src/test.rs index dcf20f5..323e5d1 100644 --- a/crates/server/src/test.rs +++ b/crates/server/src/test.rs @@ -1,49 +1,11 @@ #[cfg(test)] mod tests { - #![expect(clippy::indexing_slicing, reason = "These are just tests")] - use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode}, }; use tower::ServiceExt as _; // for `oneshot` - use std::convert::TryInto as _; - use std::io::Read as _; - - fn read_u16_be(cursor: &mut std::io::Cursor<&[u8]>) -> u16 { - let mut buffer = [0u8; 2]; - cursor.read_exact(&mut buffer).unwrap(); - u16::from_be_bytes(buffer) - } - - #[expect( - clippy::as_conversions, - clippy::cast_possible_truncation, - clippy::integer_division, - clippy::integer_division_remainder_used, - reason = "These are just for tests" - )] - fn parse_payload_cursor(data: &[u8]) -> Vec<(u16, Vec)> { - let mut cursor = std::io::Cursor::new(data); - let mut out = Vec::new(); - - while (cursor.position() as usize) < data.len() { - let angle_id = read_u16_be(&mut cursor); - - let segments_length = read_u16_be(&mut cursor) as usize; - let mut values = Vec::with_capacity(segments_length / 2); - let mut buffer = vec![0u8; segments_length]; - cursor.read_exact(&mut buffer).unwrap(); - for chunk in buffer.chunks_exact(2) { - values.push(u16::from_be_bytes(chunk.try_into().unwrap())); - } - out.push((angle_id, values)); - } - - out - } - async fn app() -> axum::Router { let config = crate::config::Config { db_dir: "./fixtures/shards".into(), @@ -67,25 +29,5 @@ mod tests { assert_eq!(body, "hello"); } - #[tokio::test] - async fn unpacking_viewshed() { - let response = app() - .await - .oneshot( - Request::get("/viewshed/-3.123,51.4898") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = to_bytes(response.into_body(), 10000).await.unwrap(); - - let segments = parse_payload_cursor(&body); - - assert_eq!(segments[0], (0, vec![0, 4])); - assert_eq!(segments[300], (300, vec![0, 2, 3, 1])); - } + // TODO: Test parsing GeoJSON viewshed } diff --git a/crates/shared/src/metadata.rs b/crates/shared/src/metadata.rs index 3713ef1..ef66906 100644 --- a/crates/shared/src/metadata.rs +++ b/crates/shared/src/metadata.rs @@ -13,9 +13,12 @@ pub struct MetaData { /// to instantiate the `DEM` struct and therefore reconstruct the bands of sight used to create /// the ring data. pub max_line_of_sight: u32, - /// The number of items reserved to place ring DEM IDs in. - pub reserved_ring_size: usize, /// The lat/lon coordinates for the centre of the 2D DEM grid. Used for accurately converting /// between degree and metric coordinate systems. pub centre: crate::projector::LonLatCoord, + /// The size of the region (in raster points) within which we will find the viewsheds with the + /// largest surface area. Used for reducing the final size of viewshed data saved to disk. + pub neighbourhood_size: u32, + /// The number of angle subdivisions used. + pub angle_subdivisions: u32, } diff --git a/crates/shared/src/utils.rs b/crates/shared/src/utils.rs index 13201f7..0dd6e15 100644 --- a/crates/shared/src/utils.rs +++ b/crates/shared/src/utils.rs @@ -3,7 +3,7 @@ use color_eyre::Result; #[inline] -/// Convert a lon/lat to a DEM coordinate. +/// Convert a lon/lat to a DEM ID. /// /// # Errors /// @@ -33,3 +33,45 @@ pub fn lonlat_to_dem_id( let dem_id = (y * i64::from(metadata.width)) + x; Ok(dem_id) } + +#[inline] +/// Convert a DEM ID to a lon/lat. +/// +/// # Errors +/// +/// If projection errors. +pub fn dem_id_to_lonlat( + metadata: &crate::metadata::MetaData, + dem_id: i64, +) -> Result { + let width_f64 = f64::from(metadata.width + 1); + let scale = f64::from(metadata.scale); + let offset = (width_f64 * scale) / 2.0f64; + + #[expect( + clippy::as_conversions, + clippy::cast_precision_loss, + reason = "These are just lon/lat coordinates" + )] + let (x_raster, y_raster) = { + ( + dem_id.rem_euclid(metadata.width.into()) as f64, + dem_id.div_euclid(metadata.width.into()) as f64, + ) + }; + #[expect( + clippy::suboptimal_flops, + reason = "We don't need the perfomance and this reads better" + )] + let coord_metric = geo::coord! { + x: (x_raster * scale) - offset, + y: (-y_raster * scale) + offset + }; + + let coord_lonlat = crate::projector::Convert { + base: metadata.centre, + } + .to_degrees(coord_metric)?; + + Ok(coord_lonlat) +} diff --git a/crates/tasks/Cargo.toml b/crates/tasks/Cargo.toml index 9decd5e..d65cb35 100644 --- a/crates/tasks/Cargo.toml +++ b/crates/tasks/Cargo.toml @@ -14,7 +14,7 @@ bytemuck = { version = "1.23.2", features = ["derive"] } clap.workspace = true color-eyre.workspace = true futures = "0.3.31" -gdal = { version = "0.18.0", features = ["bindgen"] } +gdal = { version = "0.19.0", features = ["bindgen"] } geo.workspace = true geojson = { version = "0.24.2", features = ["geo-types"] } h3o = "0.9.4" diff --git a/website/src/ViewshedWorker.ts b/website/src/ViewshedWorker.ts index bb3dddf..31d5d73 100644 --- a/website/src/ViewshedWorker.ts +++ b/website/src/ViewshedWorker.ts @@ -1,4 +1,4 @@ -import type { LngLat } from 'maplibre-gl'; +import { LngLat } from 'maplibre-gl'; import { type PolarSegments, Viewshed } from './lib/Viewshed'; export type ViewshedWorkerEvent = @@ -10,28 +10,39 @@ self.onmessage = async (event: MessageEvent) => { if (event.data.type === 'getViewshed') { const bytes = await getViewshedData(event.data.coordinate); - const segments = parseViewshedBytes(bytes); + const payload = parseViewshedBytes(bytes); - let scale = 1; // TODO: Get from API? + let demScale = 1; // TODO: Get from API? if (import.meta.env.DEV) { - scale = 10; + demScale = 100; } const id = crypto.randomUUID(); - const viewshed = new Viewshed(id, event.data.coordinate, scale, segments); + const viewshed = new Viewshed( + id, + payload.lonLatOfBiggestViewshed, + demScale, + payload.angleScale, + payload.segments, + ); + const messageDirty = { type: 'setViewshed', viewshed, } as ViewshedWorkerEvent; self.postMessage(messageDirty); - viewshed.unionGeoJSON(); - const messageClean = { - type: 'updateViewshed', - viewshed, - } as ViewshedWorkerEvent; - self.postMessage(messageClean); + // TODO: + // This is reeeeeally slow for higher angular resolutions. + // So we need to construct the viewsheds server side + // + // viewshed.unionGeoJSON(); + // const messageClean = { + // type: "updateViewshed", + // viewshed, + // } as ViewshedWorkerEvent; + // self.postMessage(messageClean); } }; @@ -53,14 +64,27 @@ async function getViewshedData(lngLat: LngLat) { return await response.bytes(); } -function parseViewshedBytes(data: Uint8Array): PolarSegments[] { +function parseViewshedBytes(data: Uint8Array): { + angleScale: number; + lonLatOfBiggestViewshed: LngLat; + segments: PolarSegments[]; +} { const buffer = data.buffer; const view = new DataView(buffer); let offset = 0; - const out: PolarSegments[] = []; + const segments: PolarSegments[] = []; + + const angleScale = view.getUint32(offset); + offset += 4; + + const lonOfBiggestViewshed = view.getFloat32(offset, false); + offset += 4; + const latOfBiggestViewshed = view.getFloat32(offset, false); + offset += 4; + const lngLat = new LngLat(lonOfBiggestViewshed, latOfBiggestViewshed); while (offset < buffer.byteLength) { - const angleID = view.getUint16(offset, false); + const angle = view.getUint16(offset, false) / angleScale; offset += 2; const segmentsLength = view.getUint16(offset, false); @@ -74,8 +98,8 @@ function parseViewshedBytes(data: Uint8Array): PolarSegments[] { offset += 2; } - out.push({ angleID, pairs: pairs }); + segments.push({ angle, pairs: pairs }); } - return out; + return { angleScale, lonLatOfBiggestViewshed: lngLat, segments }; } diff --git a/website/src/lib/Viewshed.ts b/website/src/lib/Viewshed.ts index 216b24a..44aa91a 100644 --- a/website/src/lib/Viewshed.ts +++ b/website/src/lib/Viewshed.ts @@ -10,7 +10,7 @@ import type { LngLat } from 'maplibre-gl'; import proj4 from 'proj4'; import { aeqdProjectionString, rotate, toRadians } from './utils'; -export type PolarSegments = { angleID: number; pairs: number[] }; +export type PolarSegments = { angle: number; pairs: number[] }; export const DEFAULT_OPACITY = 0.5; export class Viewshed { @@ -19,7 +19,8 @@ export class Viewshed { geoJSON: | FeatureCollection | Feature; - scale: number; + demScale: number; + angleScale: number; colour = '#00ff00'; isVisible = true; isLocked = false; @@ -27,12 +28,14 @@ export class Viewshed { constructor( id: string, centre: LngLat, - scale: number, + demScale: number, + angleScale: number, polar_segments: PolarSegments[], ) { this.id = id; this.centre = centre; - this.scale = scale; + this.demScale = demScale; + this.angleScale = angleScale; this.geoJSON = this.generateGeoJSON(polar_segments); } @@ -47,13 +50,13 @@ export class Viewshed { const features = []; for (const polar_segment of polar_segments) { for (let i = 0; i < polar_segment.pairs.length; i += 2) { - const start = polar_segment.pairs[i] * this.scale; - const end = start + polar_segment.pairs[i + 1] * this.scale; + const start = polar_segment.pairs[i] * this.demScale; + const end = start + polar_segment.pairs[i + 1] * this.demScale; const latLonStart = this.polarDistanceToPair( - polar_segment.angleID, + polar_segment.angle, start, ); - const latLonEnd = this.polarDistanceToPair(polar_segment.angleID, end); + const latLonEnd = this.polarDistanceToPair(polar_segment.angle, end); const polygon: Feature = { type: 'Feature', geometry: { @@ -88,15 +91,14 @@ export class Viewshed { this.geoJSON = unioned; } - polarDistanceToPair(angleID: number, distance: number) { - // TODO: Don't assume that the kernel is computing 360 angles. - const angle = angleID - 90; - + polarDistanceToPair(angle: number, distance: number) { + const overlap = 1.1; + const arc = 1 / (this.angleScale * overlap) / 2.0; const θ = toRadians(angle); const dx = distance * Math.cos(θ); const dy = distance * Math.sin(θ); - const rotatedClockwiseAEQD = rotate(dx, dy, -0.6); - const rotatedAntiAEQD = rotate(dx, dy, +0.6); + const rotatedClockwiseAEQD = rotate(dx, dy, -arc); + const rotatedAntiAEQD = rotate(dx, dy, +arc); const aeqd = aeqdProjectionString(this.centre.lng, this.centre.lat); From 2604d03f0093357cd09aef5d6be98ec24ebcc089 Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Wed, 15 Jul 2026 15:51:59 +0700 Subject: [PATCH 3/3] Render viewsheds using WASM --- biome.json | 114 +++--- website/.gitignore | 4 + website/src/App.svelte | 2 +- website/src/Home.svelte | 20 +- website/src/ViewshedWorker.ts | 33 +- website/src/components/LayerToggles.svelte | 4 +- website/src/lib/Viewshed.ts | 141 +++++-- website/src/lib/getLongestLine.ts | 1 - website/src/lib/renderLongestLine.ts | 11 +- website/src/lib/renderViewsheds.ts | 47 ++- website/src/lib/utils.ts | 16 +- .../viewshed_reconstructor.d.ts | 69 ++++ .../viewshed_reconstructor.js | 353 ++++++++++++++++++ .../viewshed_reconstructor_bg.wasm | Bin 0 -> 119578 bytes .../viewshed_reconstructor_bg.wasm.d.ts | 13 + 15 files changed, 700 insertions(+), 128 deletions(-) create mode 100644 website/src/lib/viewshed-reconstructor/viewshed_reconstructor.d.ts create mode 100644 website/src/lib/viewshed-reconstructor/viewshed_reconstructor.js create mode 100644 website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm create mode 100644 website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm.d.ts diff --git a/biome.json b/biome.json index f779607..dbd4918 100644 --- a/biome.json +++ b/biome.json @@ -1,57 +1,61 @@ { - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "formatter": { - "indentStyle": "space" - }, - "javascript": { - "formatter": { - "quoteStyle": "single" - } - }, - "css": { - "parser": { - "cssModules": true - } - }, - "html": { - "formatter": { - "indentScriptAndStyle": true - } - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "overrides": [ - { - "includes": ["**/*.svelte"], - "linter": { - "rules": { - "correctness": { - "noUnusedImports": "off" - }, - "style": { - "useConst": "off" - } - } - } - } - ], - "files": { - "includes": ["**", "!website/public/tiles.json"] - } + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "quoteStyle": "single" + } + }, + "css": { + "parser": { + "cssModules": true + } + }, + "html": { + "formatter": { + "indentScriptAndStyle": true + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "overrides": [ + { + "includes": ["**/*.svelte"], + "linter": { + "rules": { + "correctness": { + "noUnusedImports": "off" + }, + "style": { + "useConst": "off" + } + } + } + } + ], + "files": { + "includes": [ + "**", + "!website/public/tiles.json", + "!website/src/lib/viewshed-reconstructor" + ] + } } diff --git a/website/.gitignore b/website/.gitignore index 7d0991e..dbc0687 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,6 +3,10 @@ public/scratch public/**/*.tiff public/longest_lines_grided.bin +src/lib/viewshed-reconstructor/.gitignore +src/lib/viewshed-reconstructor/README.md +src/lib/viewshed-reconstructor/package.json + # Local .DS_Store *.local diff --git a/website/src/App.svelte b/website/src/App.svelte index b5fa7c5..cbec7b0 100644 --- a/website/src/App.svelte +++ b/website/src/App.svelte @@ -11,7 +11,7 @@
{#snippet children(params)} - + {/snippet} diff --git a/website/src/Home.svelte b/website/src/Home.svelte index fd9dd12..bc824d1 100644 --- a/website/src/Home.svelte +++ b/website/src/Home.svelte @@ -17,7 +17,7 @@ import { transformConstrain } from './lib/mapConstrains.ts'; import { setupLongestLines } from './lib/renderLongestLine.ts'; import { setupViewsheds } from './lib/renderViewsheds.ts'; - import { enablePointer, setVectorVisibility } from './lib/utils.ts'; + import { enablePointer, setVectorMapVisibilities } from './lib/utils.ts'; import { findLongestLineInBoundsFromGrid } from './lib/worldLines.ts'; import map_vector from './map_vector.styles.json'; import Acknowledgements from './modals/Acknowledgements.svelte'; @@ -28,7 +28,7 @@ import Slider from './Slider.svelte'; import { state } from './state.svelte.ts'; - let { longest } = $props(); + let { coordinate } = $props(); const config = getConfig(); function addHeatmapLayer() { @@ -82,18 +82,20 @@ state.map.on('load', async () => { initClickEffect(); - if (longest === '') { - addHeatmapLayer(); - } - if (state.config.project === 'world') { - setupLongestLines(longest); - setVectorVisibility(state, true); + if (coordinate === '') { + // If there's a coordinate then the map is going to move and zoom over some GeoJSON. + // So only render the heatmap _after_ the moving has finished. + addHeatmapLayer(); + } + setupLongestLines(coordinate); + setVectorMapVisibilities(state, true); await updateTopLongestLines(); } if (state.config.project === 'galiano') { - setupViewsheds(); + addHeatmapLayer(); + setupViewsheds(coordinate); } }); diff --git a/website/src/ViewshedWorker.ts b/website/src/ViewshedWorker.ts index 31d5d73..a297463 100644 --- a/website/src/ViewshedWorker.ts +++ b/website/src/ViewshedWorker.ts @@ -1,9 +1,13 @@ import { LngLat } from 'maplibre-gl'; import { type PolarSegments, Viewshed } from './lib/Viewshed'; +import init from './lib/viewshed-reconstructor/viewshed_reconstructor.js'; + +await init(); +self.postMessage({ status: 'ready' }); export type ViewshedWorkerEvent = | { type: 'getViewshed'; coordinate: LngLat } - | { type: 'setViewshed'; viewshed: Viewshed } + | { type: 'renderViewshed'; viewshed: Viewshed } | { type: 'updateViewshed'; viewshed: Viewshed }; self.onmessage = async (event: MessageEvent) => { @@ -28,21 +32,10 @@ self.onmessage = async (event: MessageEvent) => { ); const messageDirty = { - type: 'setViewshed', + type: 'renderViewshed', viewshed, } as ViewshedWorkerEvent; self.postMessage(messageDirty); - - // TODO: - // This is reeeeeally slow for higher angular resolutions. - // So we need to construct the viewsheds server side - // - // viewshed.unionGeoJSON(); - // const messageClean = { - // type: "updateViewshed", - // viewshed, - // } as ViewshedWorkerEvent; - // self.postMessage(messageClean); } }; @@ -58,7 +51,7 @@ async function getViewshedData(lngLat: LngLat) { ); const end = performance.now(); if (import.meta.env.DEV) { - console.log(`Viewshed fetched in ms`, end - start); + console.log(`Viewshed fetched in: ${end - start}ms`); } return await response.bytes(); @@ -90,15 +83,15 @@ function parseViewshedBytes(data: Uint8Array): { const segmentsLength = view.getUint16(offset, false); offset += 2; - const pairs = []; - const numElements = segmentsLength / 2; + const bitpacks = []; + const numElements = segmentsLength / 4; - for (let j = 0; j < numElements; j++) { - pairs.push(view.getUint16(offset, false)); - offset += 2; + for (let i = 0; i < numElements; i++) { + bitpacks.push(view.getUint32(offset, false)); + offset += 4; } - segments.push({ angle, pairs: pairs }); + segments.push({ angle, bitpacks }); } return { angleScale, lonLatOfBiggestViewshed: lngLat, segments }; diff --git a/website/src/components/LayerToggles.svelte b/website/src/components/LayerToggles.svelte index fa8fd18..1447e8f 100644 --- a/website/src/components/LayerToggles.svelte +++ b/website/src/components/LayerToggles.svelte @@ -3,7 +3,7 @@ import heatmap_layer from '../images/heatmap_layer.png'; import mountain_peak from '../images/mountain_peak.png'; import vector_layer from '../images/vector_layer.png'; - import { setVectorVisibility } from '../lib/utils'; + import { setVectorMapVisibilities } from '../lib/utils'; import { state } from '../state.svelte'; import LayerToggle from './LayerToggle.svelte'; @@ -21,7 +21,7 @@ /> setVectorVisibility(state, isToggled)} + callback={(isToggled) => setVectorMapVisibilities(state, isToggled)} isToggled={state.config.project === "world"} /> left.angle - right.angle); + const segmentsByAngle = []; + for (const polarSegment of polarSegments) { + const bitpacks = []; + for (const bitpack of polarSegment.bitpacks) { + bitpacks.push(bitpack); + } + segmentsByAngle.push(bitpacks); + } + + return segmentsByAngle; + } + + buildViewshed(flattenedSegments: number[][]) { + const polygons = reconstruct(flattenedSegments, this.demScale); + + const features = []; + + for (const polygon of polygons) { + const all = []; + + const exteriorCoords = []; + for (let i = 0; i < polygon.exterior.length; i += 2) { + const coordinate = [polygon.exterior[i], polygon.exterior[i + 1]]; + const projected = this.project(coordinate); + exteriorCoords.push(projected); + } + if (exteriorCoords.length > 1) { + exteriorCoords.push(exteriorCoords[0]); + } + all.push(exteriorCoords); + + for (let index = 0; index < polygon.interiors.length; index++) { + const start = polygon.hole_indices[index]; + + const end = + index + 1 < polygon.hole_indices.length + ? polygon.hole_indices[index + 1] + : polygon.interiors.length; + + const holeCoords = []; + for (let i = start; i < end; i += 2) { + const coordinate = [polygon.interiors[i], polygon.interiors[i + 1]]; + const projected = this.project(coordinate); + holeCoords.push(projected); + } + all.push(holeCoords); + } + + const polygonGeoJSON: Feature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: all, + }, + properties: { + color: getRandomColor(), + }, + }; + + features.push(polygonGeoJSON); + + polygon.free(); // Free WASM memory + } + + return { + type: 'FeatureCollection', + features: features, + } as FeatureCollection; + } + + buildViewshedWithoutUnion(polarSegments: PolarSegments[]) { + console.log(polarSegments); const features = []; - for (const polar_segment of polar_segments) { - for (let i = 0; i < polar_segment.pairs.length; i += 2) { - const start = polar_segment.pairs[i] * this.demScale; - const end = start + polar_segment.pairs[i + 1] * this.demScale; - const latLonStart = this.polarDistanceToPair( - polar_segment.angle, - start, - ); - const latLonEnd = this.polarDistanceToPair(polar_segment.angle, end); + for (const polarSegment of polarSegments) { + for (let i = 0; i < polarSegment.bitpacks.length; i++) { + const high16 = polarSegment.bitpacks[i] >>> 16; + const low16 = polarSegment.bitpacks[i] & 0xffff; + const start = high16 * this.demScale; + const end = start + low16 * this.demScale; + const latLonStart = this.polarDistanceToPair(polarSegment.angle, start); + const latLonEnd = this.polarDistanceToPair(polarSegment.angle, end); const polygon: Feature = { type: 'Feature', geometry: { @@ -91,6 +189,11 @@ export class Viewshed { this.geoJSON = unioned; } + project(coordinate: number[]) { + const aeqd = aeqdProjectionString(this.centre.lng, this.centre.lat); + return proj4(aeqd, proj4.WGS84, coordinate); + } + polarDistanceToPair(angle: number, distance: number) { const overlap = 1.1; const arc = 1 / (this.angleScale * overlap) / 2.0; @@ -100,18 +203,8 @@ export class Viewshed { const rotatedClockwiseAEQD = rotate(dx, dy, -arc); const rotatedAntiAEQD = rotate(dx, dy, +arc); - const aeqd = aeqdProjectionString(this.centre.lng, this.centre.lat); - - const rotatedClockwiseLonLat = proj4( - aeqd, - proj4.WGS84, - rotatedClockwiseAEQD, - ); - const rotatedAntiLonLat = proj4( - aeqd, - '+proj=longlat +datum=WGS84 +no_defs', - rotatedAntiAEQD, - ); + const rotatedClockwiseLonLat = this.project(rotatedClockwiseAEQD); + const rotatedAntiLonLat = this.project(rotatedAntiAEQD); return [rotatedClockwiseLonLat, rotatedAntiLonLat]; } diff --git a/website/src/lib/getLongestLine.ts b/website/src/lib/getLongestLine.ts index d0d9c87..b7c9d8c 100644 --- a/website/src/lib/getLongestLine.ts +++ b/website/src/lib/getLongestLine.ts @@ -346,7 +346,6 @@ export function convertRasterXYToLngLat( const x_metric = (x - offset) * scale; const y_metric = (y_flipped - offset) * scale; const [lng, lat] = proj4(projection, proj4.WGS84, [x_metric, y_metric]); - console.log(x, y, x_metric, y_metric, lng, lat); return new LngLat(lng, lat); } diff --git a/website/src/lib/renderLongestLine.ts b/website/src/lib/renderLongestLine.ts index 54dfaa0..b178365 100644 --- a/website/src/lib/renderLongestLine.ts +++ b/website/src/lib/renderLongestLine.ts @@ -9,6 +9,7 @@ import { aeqdProjectionString, computeBBox, disablePointer, + extractCoordFromURL, rotate, toRadians, } from './utils.ts'; @@ -45,19 +46,11 @@ export function setupLongestLines(coordFromURL: string | undefined) { }); if (coordFromURL?.startsWith('longest/')) { - const coord = extractCoordFromURL(coordFromURL); + const coord = extractCoordFromURL(coordFromURL.replace('longest/', '')); render(coord); } } -function extractCoordFromURL(coordFromURL: string) { - const parts = coordFromURL.replace('longest/', '').split('_'); - const lng = parseFloat(parts[0]); - const lat = parseFloat(parts[1]); - const coord = new LngLat(lng, lat); - return coord; -} - export async function render(lngLat: LngLat) { const longestLine = await getLongestLine(lngLat); if (longestLine === undefined) { diff --git a/website/src/lib/renderViewsheds.ts b/website/src/lib/renderViewsheds.ts index 4c4b484..ccde6ea 100644 --- a/website/src/lib/renderViewsheds.ts +++ b/website/src/lib/renderViewsheds.ts @@ -1,13 +1,42 @@ import { state } from '../state.svelte.ts'; import type { ViewshedWorkerEvent } from '../ViewshedWorker.ts'; +import { + endLoadingSpinner, + extractCoordFromURL, + startLoadingSpinner, +} from './utils.ts'; import { DEFAULT_OPACITY, Viewshed } from './Viewshed.ts'; -export function setupViewsheds() { - const worker = new Worker(new URL('../ViewshedWorker.js', import.meta.url)); - worker.onmessage = renderNewViewshed; +function startViewshedWorker(): Promise { + return new Promise((resolve) => { + const worker = new Worker(new URL('../ViewshedWorker.js', import.meta.url)); + worker.onmessage = mainThreadViewshedWorkerCallbacks; + + worker.addEventListener('message', function onReady(event) { + if (event.data.status === 'ready') { + worker.removeEventListener('message', onReady); + resolve(worker); + } + }); + }); +} + +export async function setupViewsheds(coordFromURL: string | undefined) { + const worker = await startViewshedWorker(); renderAllViewsheds(); + if (coordFromURL?.startsWith('viewshed')) { + const coordinate = extractCoordFromURL( + coordFromURL.replace('viewshed/', ''), + ); + startLoadingSpinner(); + worker.postMessage({ + type: 'getViewshed', + coordinate, + } as ViewshedWorkerEvent); + } + state.map?.on('click', async (event) => { if (!state.map) { return; @@ -15,6 +44,7 @@ export function setupViewsheds() { removeUnlockedViewsheds(); + startLoadingSpinner(); worker.postMessage({ type: 'getViewshed', coordinate: event.lngLat, @@ -22,15 +52,18 @@ export function setupViewsheds() { }); } -export function renderNewViewshed(event: MessageEvent) { +export function mainThreadViewshedWorkerCallbacks( + event: MessageEvent, +) { const isViewshedData = - event.data.type === 'setViewshed' || event.data.type === 'updateViewshed'; + event.data.type === 'renderViewshed' || + event.data.type === 'updateViewshed'; if (!isViewshedData) return; const viewshed = event.data.viewshed; Object.setPrototypeOf(viewshed, Viewshed.prototype); // Rehydrate the serialised data. - if (event.data.type === 'setViewshed') { + if (event.data.type === 'renderViewshed') { state.viewsheds.push(viewshed); renderAllViewsheds(); } @@ -38,6 +71,8 @@ export function renderNewViewshed(event: MessageEvent) { if (event.data.type === 'updateViewshed') { updateExistingViewshedData(viewshed); } + + endLoadingSpinner(); } function updateExistingViewshedData(viewshed: Viewshed) { diff --git a/website/src/lib/utils.ts b/website/src/lib/utils.ts index f6e55d0..c63ee0d 100644 --- a/website/src/lib/utils.ts +++ b/website/src/lib/utils.ts @@ -177,7 +177,7 @@ export function getPMTilesSource() { } } -export function setVectorVisibility(state: AppState, isVisible: boolean) { +export function setVectorMapVisibilities(state: AppState, isVisible: boolean) { const layers = state.map?.getStyle().layers || []; for (const layer of layers) { if (layer.id === 'mountain_peaks') continue; @@ -206,3 +206,17 @@ export function getScaleFromCog(cog: GeoTIFFImage) { const scale = Math.abs(resolution[0]); return scale; } + +export function extractCoordFromURL(coordFromURL: string) { + const parts = coordFromURL.split('_'); + const lng = parseFloat(parts[0]); + const lat = parseFloat(parts[1]); + const coord = new LngLat(lng, lat); + return coord; +} + +export function getRandomColor(): string { + return `#${Math.floor(Math.random() * 16777215) + .toString(16) + .padStart(6, '0')}`; +} diff --git a/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.d.ts b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.d.ts new file mode 100644 index 0000000..47fae0a --- /dev/null +++ b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.d.ts @@ -0,0 +1,69 @@ +/* tslint:disable */ +/* eslint-disable */ + +/** + * WASM only supports simple types, so this is a simple representation of a polygon. + */ +export class PlainPolygon { + private constructor(); + free(): void; + [Symbol.dispose](): void; + /** + * Getter for `exterior`. + */ + readonly exterior: Float64Array; + /** + * Getter for `hole_indices`. + */ + readonly hole_indices: Uint32Array; + /** + * Getter for `interiors`. + */ + readonly interiors: Float64Array; +} + +/** + * Reconstruct a viewshed from raw polar segments. + * + * # Panics + * When reconstructing the viewshed fails. + */ +export function reconstruct(js_data: Array, dem_scale: number): Array; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_plainpolygon_free: (a: number, b: number) => void; + readonly plainpolygon_exterior: (a: number) => [number, number]; + readonly plainpolygon_hole_indices: (a: number) => [number, number]; + readonly plainpolygon_interiors: (a: number) => [number, number]; + readonly reconstruct: (a: any, b: number) => any; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.js b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.js new file mode 100644 index 0000000..fc1e6af --- /dev/null +++ b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor.js @@ -0,0 +1,353 @@ +/* @ts-self-types="./viewshed_reconstructor.d.ts" */ + +/** + * WASM only supports simple types, so this is a simple representation of a polygon. + */ +export class PlainPolygon { + static __wrap(ptr) { + const obj = Object.create(PlainPolygon.prototype); + obj.__wbg_ptr = ptr; + PlainPolygonFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + PlainPolygonFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_plainpolygon_free(ptr, 0); + } + /** + * Getter for `exterior`. + * @returns {Float64Array} + */ + get exterior() { + const ret = wasm.plainpolygon_exterior(this.__wbg_ptr); + var v1 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v1; + } + /** + * Getter for `hole_indices`. + * @returns {Uint32Array} + */ + get hole_indices() { + const ret = wasm.plainpolygon_hole_indices(this.__wbg_ptr); + var v1 = getArrayU32FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v1; + } + /** + * Getter for `interiors`. + * @returns {Float64Array} + */ + get interiors() { + const ret = wasm.plainpolygon_interiors(this.__wbg_ptr); + var v1 = getArrayF64FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 8, 8); + return v1; + } +} +if (Symbol.dispose) PlainPolygon.prototype[Symbol.dispose] = PlainPolygon.prototype.free; + +/** + * Reconstruct a viewshed from raw polar segments. + * + * # Panics + * When reconstructing the viewshed fails. + * @param {Array} js_data + * @param {number} dem_scale + * @returns {Array} + */ +export function reconstruct(js_data, dem_scale) { + const ret = wasm.reconstruct(js_data, dem_scale); + return ret; +} +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg___wbindgen_number_get_394265ed1e1b84ee: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_error_a6fa202b58aa1cd3: function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } + }, + __wbg_get_unchecked_6e0ad6d2a41b06f6: function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, + __wbg_length_370319915dc99107: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_new_227d7c05414eb861: function() { + const ret = new Error(); + return ret; + }, + __wbg_new_32b398fb48b6d94a: function() { + const ret = new Array(); + return ret; + }, + __wbg_plainpolygon_new: function(arg0) { + const ret = PlainPolygon.__wrap(arg0); + return ret; + }, + __wbg_push_d2ae3af0c1217ae6: function(arg0, arg1) { + const ret = arg0.push(arg1); + return ret; + }, + __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./viewshed_reconstructor_bg.js": import0, + }; +} + +const PlainPolygonFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_plainpolygon_free(ptr, 1)); + +function getArrayF64FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getFloat64ArrayMemory0().subarray(ptr / 8, ptr / 8 + len); +} + +function getArrayU32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +let cachedFloat64ArrayMemory0 = null; +function getFloat64ArrayMemory0() { + if (cachedFloat64ArrayMemory0 === null || cachedFloat64ArrayMemory0.byteLength === 0) { + cachedFloat64ArrayMemory0 = new Float64Array(wasm.memory.buffer); + } + return cachedFloat64ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint32ArrayMemory0 = null; +function getUint32ArrayMemory0() { + if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) { + cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32ArrayMemory0; +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedFloat64ArrayMemory0 = null; + cachedUint32ArrayMemory0 = null; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('viewshed_reconstructor_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f6bd90d6d4450eb67428a44d6bdaa97ec7138284 GIT binary patch literal 119578 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%0AO~)*DtxP`lL_w=bpiVCWPw4SB+;>?XsaqF(8A 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 F{|9%Qxv2mE literal 0 HcmV?d00001 diff --git a/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm.d.ts b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm.d.ts new file mode 100644 index 0000000..1ff08f6 --- /dev/null +++ b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm.d.ts @@ -0,0 +1,13 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const __wbg_plainpolygon_free: (a: number, b: number) => void; +export const plainpolygon_exterior: (a: number) => [number, number]; +export const plainpolygon_hole_indices: (a: number) => [number, number]; +export const plainpolygon_interiors: (a: number) => [number, number]; +export const reconstruct: (a: any, b: number) => any; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_start: () => void;