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/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/crates/server/fixtures/shards/sample_16x16.db b/crates/server/fixtures/shards/sample_16x16.db index b2c7a54..af26bd1 100644 Binary files a/crates/server/fixtures/shards/sample_16x16.db and b/crates/server/fixtures/shards/sample_16x16.db differ 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/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" +} 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 bb3dddf..a297463 100644 --- a/website/src/ViewshedWorker.ts +++ b/website/src/ViewshedWorker.ts @@ -1,37 +1,41 @@ -import type { LngLat } from 'maplibre-gl'; +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) => { 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', + type: 'renderViewshed', viewshed, } as ViewshedWorkerEvent; self.postMessage(messageDirty); - - viewshed.unionGeoJSON(); - const messageClean = { - type: 'updateViewshed', - viewshed, - } as ViewshedWorkerEvent; - self.postMessage(messageClean); } }; @@ -47,35 +51,48 @@ 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(); } -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); 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; } - out.push({ angleID, pairs: pairs }); + segments.push({ angle, bitpacks }); } - return out; + 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"} /> | Feature; - scale: number; + demScale: number; + angleScale: number; colour = '#00ff00'; isVisible = true; isLocked = false; @@ -27,12 +34,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); } @@ -43,17 +52,109 @@ export class Viewshed { return `viewshed-source-${this.id}`; } - generateGeoJSON(polar_segments: PolarSegments[]) { + generateGeoJSON(polarSegments: PolarSegments[]) { + const start = performance.now(); + + try { + const flattenedSegments = this.flattenSegmentsArray(polarSegments); + const viewshed = this.buildViewshed(flattenedSegments); + const end = performance.now(); + const duration = end - start; + + console.log(`Viewshed reconstructed in: ${duration}ms`); + return viewshed; + } catch (error) { + console.error( + 'WASM viewshed reconstruction failed, falling back to non-unioned JS.', + error, + ); + return this.buildViewshedWithoutUnion(polarSegments); + } + } + + flattenSegmentsArray(polarSegments: PolarSegments[]): number[][] { + polarSegments.sort((left, right) => 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 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 latLonStart = this.polarDistanceToPair( - polar_segment.angleID, - start, - ); - const latLonEnd = this.polarDistanceToPair(polar_segment.angleID, end); + + 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 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: { @@ -88,28 +189,22 @@ 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; + 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; 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 aeqd = aeqdProjectionString(this.centre.lng, this.centre.lat); + const rotatedClockwiseAEQD = rotate(dx, dy, -arc); + const rotatedAntiAEQD = rotate(dx, dy, +arc); - 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 0000000..f6bd90d Binary files /dev/null and b/website/src/lib/viewshed-reconstructor/viewshed_reconstructor_bg.wasm differ 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;