diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56d57f7..e8c8d18 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,6 +18,27 @@ concurrency: cancel-in-progress: true jobs: + build-test: + name: Build and Test 🔬 + strategy: + fail-fast: false + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.94.1 + - name: Install Linux system dependencies + run: | + sudo apt-get update -y + sudo apt-get install libgdal-dev gdal-bin + - uses: Swatinem/rust-cache@v2 + - name: Build + run: cargo build --verbose --all + - name: Run tests + run: cargo test + lints: name: "Lints 💅" runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 8fdc42d..f351d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -254,6 +269,18 @@ dependencies = [ "password-hash", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-ssh2-tokio" version = "0.12.0" @@ -420,6 +447,26 @@ dependencies = [ "sha2", ] +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "2.9.4" @@ -466,6 +513,27 @@ dependencies = [ "cipher", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -500,9 +568,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cbc" @@ -525,6 +593,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -572,6 +649,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.47" @@ -654,6 +742,24 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -811,6 +917,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1166,9 +1293,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1176,9 +1303,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" @@ -1204,15 +1331,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1221,15 +1348,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -1239,9 +1366,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1251,7 +1378,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1275,6 +1401,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "febef67dc08a956a9ecb04de2b40dbd15ad56be49421aad9ae0cdcbe9a24166c" dependencies = [ + "bindgen", "pkg-config", "semver", ] @@ -1395,6 +1522,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "group" version = "0.13.0" @@ -2011,6 +2144,16 @@ dependencies = [ "rand 0.9.2", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.15" @@ -2119,6 +2262,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2152,6 +2301,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -2505,6 +2664,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2672,6 +2841,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.10" @@ -2858,6 +3039,12 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2980,9 +3167,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.224" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaeb1e94f53b16384af593c71e20b095e958dab1d26939c1b70645c5cfbcc0b" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -2990,18 +3177,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.224" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f39390fa6346e24defbcdd3d9544ba8a19985d0af74df8501fbfe9a64341ab" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.224" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ff78ab5e8561c9a675bfc1785cb07ae721f0ee53329a595cefd8c04c2ac4e0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3010,15 +3197,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3049,9 +3236,19 @@ name = "server" version = "0.1.0" dependencies = [ "axum", + "bytes", + "clap", + "color-eyre", + "futures-util", + "serde", + "serde_json", + "shared", + "sqlx", "tokio", "tower", "tower-http", + "tracing", + "tracing-subscriber", ] [[package]] @@ -3085,6 +3282,18 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared" +version = "0.1.0" +dependencies = [ + "color-eyre", + "geo", + "proj4rs", + "rstar", + "serde", + "serde_json", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3480,6 +3689,7 @@ dependencies = [ "bytemuck", "clap", "color-eyre", + "csv", "futures", "gdal", "geo", @@ -3489,6 +3699,7 @@ dependencies = [ "rstar", "serde", "serde_json", + "shared", "sqlx", "srtm_reader", "strip-ansi-escapes", @@ -3692,6 +3903,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ + "async-compression", "bitflags", "bytes", "futures-core", @@ -4576,3 +4788,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index bbca4ea..205095a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,24 @@ [workspace] resolver = "2" -members = [ "crates/server", +members = [ + "crates/server", "crates/tasks", + "crates/shared", ] +[workspace.dependencies] +color-eyre = "0.6.5" +clap = { version = "4.5.47", features = ["derive"] } +geo = "0.31.0" +proj4rs = { version = "0.1.9", features = ["aeqd"] } +rstar = "0.12.2" +shared = { path = "crates/shared", version = "0.1.0" } +serde = "1.0.228" +serde_json = "1.0.149" +sqlx = { version = "0.8.6", features = [ "runtime-tokio" ] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", default-features = false, features = ["env-filter", "ansi"] } + [patch.crates-io] # Just waiting for any release after v0.54.6 and for `async-ssh2-tokio` to update from 0.12.0. russh = { git = "https://github.com/Eugeny/russh", rev = "258c6bbb2877fd8d5295e2bd406fac334890083c" } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 2382f63..3cac4b9 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -5,9 +5,19 @@ edition = "2024" [dependencies] axum = "0.8.4" +bytes = "1.11.1" +clap.workspace = true +color-eyre = "0.6.5" +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +shared.workspace = true +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", default-features = false, features = ["env-filter", "ansi"] } tokio = { version = "1.47.1", features = ["rt-multi-thread"] } tower = "0.5.2" -tower-http = { version = "0.6.6", features = ["fs", "trace", "cors"] } +tower-http = { version = "0.6.6", features = ["fs", "trace", "cors", "compression-gzip", "compression-br"] } +futures-util = "0.3.32" [lints] workspace = true diff --git a/crates/server/fixtures/shards/sample_16x16.db b/crates/server/fixtures/shards/sample_16x16.db new file mode 100644 index 0000000..b2c7a54 Binary files /dev/null and b/crates/server/fixtures/shards/sample_16x16.db differ diff --git a/crates/server/src/app.rs b/crates/server/src/app.rs new file mode 100644 index 0000000..d1dae7b --- /dev/null +++ b/crates/server/src/app.rs @@ -0,0 +1,128 @@ +//! The main app code. + +use std::sync::Arc; + +use color_eyre::{Result, eyre::ContextCompat as _}; + +use axum::{Router, routing}; + +use sqlx::ConnectOptions as _; + +/// Shorthand for a single connection to a single shard. Even though it's a `Pool` it's limited to +/// a concurrency of 1. +pub type Shard = Arc>; + +/// All state needed for the app's lifetime. +#[derive(Clone)] +pub struct AppState { + /// A collection of single-concurrency pools, one for each database shard. + pub shards: Vec, + /// Metadata about the underlying DEM that generated the viewsheds. + pub metadata: shared::metadata::MetaData, +} + +/// Entrypoint for starting the server. +pub async fn build(config: crate::config::Config) -> Result { + let pools = crate::app::all_databases(config).await?; + crate::app::router(pools).await +} + +/// Setup all the databases. +pub async fn all_databases(config: crate::config::Config) -> Result> { + #[expect( + clippy::redundant_closure_for_method_calls, + reason = "It's too verbose" + )] + let db_paths: Vec<_> = std::fs::read_dir(&config.db_dir)? + .filter_map(|result| result.ok()) + .map(|file| file.path()) + .filter(|path| path.extension().is_some_and(|ext| ext == "db")) + .collect(); + + let mut pools = Vec::new(); + for path in db_paths { + let pool = one_database(&path).await?; + pools.push(Arc::new(pool)); + } + + tracing::info!("Connected to {} databases.", pools.len()); + + Ok(pools) +} + +/// Setup a DB. +async fn one_database(path: &std::path::PathBuf) -> Result> { + let options = sqlx::sqlite::SqliteConnectOptions::new() + .filename(path) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Off) + .synchronous(sqlx::sqlite::SqliteSynchronous::Off) + .disable_statement_logging() + .read_only(true) + .immutable(true) + .pragma("locking_mode", "EXCLUSIVE") + .pragma("cache_size", "-20000") + .pragma("mmap_size", "1073741824") + .pragma("query_only", "ON"); + + let pool = sqlx::sqlite::SqlitePoolOptions::new() + .max_connections(1) + .connect_with(options) + .await?; + + Ok(pool) +} + +/// Server routes. +async fn router(shards: Vec) -> Result { + let metadata = load_metadata( + shards + .first() + .context("No DBs available to query metadate from.")?, + ) + .await?; + let state = AppState { shards, metadata }; + + let router = Router::new() + .route("/", routing::get(|| async { "hello" })) + .route( + "/viewshed/{coordinate}", + routing::get(crate::get_viewshed::get_viewshed), + ) + .with_state(state) + .layer( + tower_http::compression::CompressionLayer::new() + .gzip(true) + .br(true), + ) + .layer( + tower_http::cors::CorsLayer::new() + .allow_origin([ + "http://localhost:3000".parse()?, + "https://alltheviews.world".parse()?, + "https://map.alltheviews.world".parse()?, + "https://galiano.alltheviews.world".parse()?, + "https://tombh-galiano-viewview.tom-364.workers.dev".parse()?, + ]) + .allow_methods([axum::http::Method::GET]), + ) + .layer(tower_http::trace::TraceLayer::new_for_http().on_response( + |_response: &axum::response::Response, + latency: std::time::Duration, + _span: &tracing::Span| { + tracing::debug!("Request completed in {:?}", latency); + }, + )); + + Ok(router) +} + +/// Load the metadata for the DEM that generated the viewsheds. +async fn load_metadata(shard: &sqlx::Pool) -> Result { + let (json,): (String,) = sqlx::query_as("SELECT json FROM metadata") + .fetch_one(shard) + .await?; + + let metadata: shared::metadata::MetaData = serde_json::from_str(&json)?; + tracing::info!("Loaded metadata: {metadata:?}"); + Ok(metadata) +} diff --git a/crates/server/src/config.rs b/crates/server/src/config.rs new file mode 100644 index 0000000..9f9888c --- /dev/null +++ b/crates/server/src/config.rs @@ -0,0 +1,9 @@ +//! Config for the CLI. + +/// Config arguments. +#[derive(clap::Parser, Debug, Clone, Default)] +pub struct Config { + /// Path to the viewshed database. + #[arg(long, value_name = "Database path")] + pub db_dir: std::path::PathBuf, +} diff --git a/crates/server/src/get_viewshed.rs b/crates/server/src/get_viewshed.rs new file mode 100644 index 0000000..1317a04 --- /dev/null +++ b/crates/server/src/get_viewshed.rs @@ -0,0 +1,99 @@ +//! Get a single viewshed from many database shards. + +use color_eyre::Result; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; + +use futures_util::StreamExt as _; +use sqlx::Row as _; + +/// Return the polar segments for an entire viewshed. +pub async fn get_viewshed( + State(state): State, + Path(coordinate): Path, +) -> impl IntoResponse { + let Ok(lonlat) = shared::projector::LonLatCoord::parse(&coordinate) else { + return (StatusCode::BAD_REQUEST, "Couldn't parse coordinate").into_response(); + }; + + let Ok(dem_id) = shared::utils::lonlat_to_dem_id(&state.metadata, lonlat) else { + return ( + StatusCode::BAD_REQUEST, + "Couldn't get a DEM ID for the provided coordinate", + ) + .into_response(); + }; + + let start = tokio::time::Instant::now(); + + let mut tasks = query_shards(state.shards, dem_id); + let mut payload = Vec::with_capacity(1024 * 16); + 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 bytes_length = u16::try_from(bytes.len())?; + + payload.extend_from_slice(&angle_id.to_be_bytes()); + payload.extend_from_slice(&bytes_length.to_be_bytes()); + payload.extend_from_slice(&bytes); + } + Ok(()) + } + .await; + + if let Err(error) = fetch_result { + tracing::error!("{error:?}"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Couldn't fetch viewshed data from DB", + ) + .into_response(); + } + } + + if payload.is_empty() { + let msg = format!("No viewshed found. Using DEM ID: {dem_id}"); + return (StatusCode::NOT_FOUND, msg).into_response(); + } + + let body = bytes::Bytes::from(payload); + tracing::debug!( + "Viewshed request for DEM ID {dem_id} found {} bytes in {:?}", + body.len(), + start.elapsed() + ); + ( + StatusCode::OK, + [("content-type", "application/octet-stream")], + body, + ) + .into_response() +} + +/// Query all shards in parallel. +fn query_shards( + shards: Vec, + dem_id: i64, +) -> futures_util::stream::FuturesUnordered< + tokio::task::JoinHandle>>, +> { + let tasks = futures_util::stream::FuturesUnordered::new(); + + 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 + })); + } + + tasks +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index d20dc05..f20bbc1 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,16 +1,46 @@ -//! serves the website +//! An HTTP server to return viewshed data. +#![expect( + clippy::big_endian_bytes, + reason = "It's how we pack the viewhsed blobs" +)] + +use clap::Parser as _; +use color_eyre::Result; +use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _}; + +mod app; +mod config; +mod get_viewshed; + +#[cfg(test)] +mod test; + +/// Entrypoint #[tokio::main] -async fn main() { - let serve_index = tower_http::services::ServeDir::new("./assets"); - - let app = axum::Router::new() - .fallback_service(serve_index) - .layer(tower::ServiceBuilder::new().layer(tower_http::trace::TraceLayer::new_for_http())); - - // run our app with hyper, listening globally on port 3000 - #[expect(clippy::unwrap_used, reason="we need to crash the server if it can't start")] - let listener = tokio::net::TcpListener::bind("0.0.0.0:3333").await.unwrap(); - #[expect(clippy::unwrap_used, reason="we need to crash the server if it can't start")] - axum::serve(listener, app).await.unwrap(); +async fn main() -> Result<()> { + setup_logging()?; + let config = crate::config::Config::parse(); + let router = app::build(config).await?; + + let address = "0.0.0.0:3333"; + + tracing::info!("Starting server on: {address}"); + + let listener = tokio::net::TcpListener::bind(address).await?; + axum::serve(listener, router).await?; + + Ok(()) +} + +/// Setup logging. +fn setup_logging() -> Result<()> { + let filters = tracing_subscriber::EnvFilter::builder() + .with_default_directive("server=info".parse()?) + .from_env_lossy(); + let filter_layer = tracing_subscriber::fmt::layer().with_filter(filters); + let tracing_setup = tracing_subscriber::registry().with(filter_layer); + tracing_setup.init(); + + Ok(()) } diff --git a/crates/server/src/test.rs b/crates/server/src/test.rs new file mode 100644 index 0000000..dcf20f5 --- /dev/null +++ b/crates/server/src/test.rs @@ -0,0 +1,91 @@ +#[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(), + }; + + crate::app::build(config).await.unwrap() + } + + #[tokio::test] + async fn hello() { + let response = app() + .await + .oneshot(Request::get("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), 100).await.unwrap(); + + 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])); + } +} diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml new file mode 100644 index 0000000..408ce69 --- /dev/null +++ b/crates/shared/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "shared" +version = "0.1.0" +edition = "2024" + +[dependencies] +color-eyre.workspace = true +geo.workspace = true +proj4rs.workspace = true +rstar.workspace = true +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs new file mode 100644 index 0000000..5daf4c0 --- /dev/null +++ b/crates/shared/src/lib.rs @@ -0,0 +1,11 @@ +//! Code we want to share amongst internal crates. + +#![allow( + clippy::exhaustive_structs, + reason = "This lib is mostly for our internal use" +)] + +pub mod metadata; +pub mod projector; +pub mod utils; + diff --git a/crates/shared/src/metadata.rs b/crates/shared/src/metadata.rs new file mode 100644 index 0000000..3713ef1 --- /dev/null +++ b/crates/shared/src/metadata.rs @@ -0,0 +1,21 @@ +//! Struct for storing essential data about the underlying DEM for which viewsheds are created. + +// TODO: Make a public lib in the CacheTVS repo. +/// Metadata about the viewshed data. +#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)] +pub struct MetaData { + /// The width of the 2D grid of elevation data. The algorithm requires that the grid be square, + /// so there is no need for a height field. + pub width: u32, + /// The diameter in meters each point of the data covers. + pub scale: f32, + /// The maximum line of sight (in meters) that was used to calculate the ring data. It is needed + /// 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, +} diff --git a/crates/tasks/src/projector.rs b/crates/shared/src/projector.rs similarity index 82% rename from crates/tasks/src/projector.rs rename to crates/shared/src/projector.rs index 6ca30d6..b28d354 100644 --- a/crates/tasks/src/projector.rs +++ b/crates/shared/src/projector.rs @@ -1,19 +1,37 @@ //! Project coordinates between different systems. -use color_eyre::Result; +use color_eyre::{Result, eyre::ContextCompat as _}; /// The radius of the planet in kilometers. pub const EARTH_RADIUS: f32 = 6371.0; -/// A latitude/longtitude coordinate. +/// A longtitude/latitude coordinate. #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Default)] pub struct LonLatCoord(pub geo::Coord); -#[expect(clippy::unreachable, reason = "This is just a 2D coordinate.")] +impl LonLatCoord { + /// Parse coordinates from a string. + /// + /// # Errors + /// On parsing errors. + #[inline] + pub fn parse(coordinates: &str) -> Result { + let mut parts = coordinates.split(','); + let lon_str = parts.next().context("missing longtitude")?.trim(); + let lat_str = parts.next().context("missing latitude")?.trim(); + + Ok(Self(geo::Coord { + x: lon_str.parse()?, + y: lat_str.parse()?, + })) + } +} + impl rstar::Point for LonLatCoord { type Scalar = f64; const DIMENSIONS: usize = 2; + #[inline] fn generate(mut generator: impl FnMut(usize) -> Self::Scalar) -> Self { Self(geo::coord! { x: generator(0), @@ -21,18 +39,22 @@ impl rstar::Point for LonLatCoord { }) } + #[inline] fn nth(&self, index: usize) -> Self::Scalar { match index { 0 => self.0.x, 1 => self.0.y, + #[expect(clippy::unreachable, reason = "This is just a 2D coordinate.")] _ => unreachable!(), } } + #[inline] fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar { match index { 0 => &mut self.0.x, 1 => &mut self.0.y, + #[expect(clippy::unreachable, reason = "This is just a 2D coordinate.")] _ => unreachable!(), } } @@ -40,7 +62,7 @@ impl rstar::Point for LonLatCoord { /// Convert between different coordinate system. pub struct Convert { - /// The lat/lon base coordinates for the AEQD mercator projected coordinates. + /// The lon/lat base coordinates for the AEQD mercator projected coordinates. pub base: LonLatCoord, } @@ -60,7 +82,11 @@ impl Convert { Ok(proj4rs::Proj::from_proj_string(&string)?) } + #[inline] /// Convert from degrees to the AEQD metric projection. + /// + /// # Errors + /// When projection conversion fails. pub fn to_meters(&self, source: LonLatCoord) -> Result { let mut converted = (source.0.x.to_radians(), source.0.y.to_radians(), 0.0f64); proj4rs::transform::transform( @@ -72,7 +98,11 @@ impl Convert { Ok(geo::coord! { x: converted.0, y: converted.1 }) } + #[inline] /// Convert from the AEQD metric projection to degrees. + /// + /// # Errors + /// When projection conversion fails. pub fn to_degrees(&self, source: geo::Coord) -> Result { let mut converted = (source.x, source.y, 0.0f64); proj4rs::transform::transform( @@ -86,6 +116,8 @@ impl Convert { )) } + #[inline] + #[must_use] /// Calculate the width of a degree in meters at the given latitude. pub fn meters_per_degree(latitude: f64) -> f32 { #[expect( diff --git a/crates/shared/src/utils.rs b/crates/shared/src/utils.rs new file mode 100644 index 0000000..13201f7 --- /dev/null +++ b/crates/shared/src/utils.rs @@ -0,0 +1,35 @@ +//! Helper code. + +use color_eyre::Result; + +#[inline] +/// Convert a lon/lat to a DEM coordinate. +/// +/// # Errors +/// +/// If projection errors. +pub fn lonlat_to_dem_id( + metadata: &crate::metadata::MetaData, + latlon: crate::projector::LonLatCoord, +) -> Result { + let width_f64 = f64::from(metadata.width + 1); + let scale = f64::from(metadata.scale); + let coord_metric = crate::projector::Convert { + base: metadata.centre, + } + .to_meters(latlon)?; + let offset = (width_f64 * scale) / 2.0f64; + #[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "The coordinates should always fit in `u64`" + )] + let (x, y) = { + ( + ((coord_metric.x + offset) / scale) as i64, + ((-coord_metric.y + offset) / scale) as i64, + ) + }; + let dem_id = (y * i64::from(metadata.width)) + x; + Ok(dem_id) +} diff --git a/crates/tasks/Cargo.toml b/crates/tasks/Cargo.toml index 6023dd9..9decd5e 100644 --- a/crates/tasks/Cargo.toml +++ b/crates/tasks/Cargo.toml @@ -11,24 +11,26 @@ async-trait = "0.1.89" async-ssh2-tokio = "0.12.0" axum = { version = "0.8", features = ["json", "tokio", "query"] } bytemuck = { version = "1.23.2", features = ["derive"] } -clap = { version = "4.5.47", features = ["derive"] } -color-eyre = "0.6.5" +clap.workspace = true +color-eyre.workspace = true futures = "0.3.31" -gdal = "0.18.0" -geo = "0.31.0" +gdal = { version = "0.18.0", features = ["bindgen"] } +geo.workspace = true geojson = { version = "0.24.2", features = ["geo-types"] } h3o = "0.9.4" -proj4rs = { version = "0.1.9", features = ["aeqd"] } -rstar = "0.12.2" -serde = "1.0.219" -serde_json = "1.0.143" +proj4rs.workspace = true +rstar.workspace = true +shared.workspace = true +serde.workspace = true +serde_json.workspace = true srtm_reader = "0.5.1" -sqlx = { version = "0.8", features = [ "runtime-tokio" ] } +sqlx.workspace = true tokio = { version = "1.48.0", features = ["full", "tracing"] } -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.20", default-features = false, features = ["env-filter", "ansi"] } +tracing.workspace = true +tracing-subscriber.workspace = true tower-http = { version = "0.6.7", features = ["fs", "normalize-path"] } strip-ansi-escapes = "0.2.1" +csv = "1.4.0" [lints] workspace = true diff --git a/crates/tasks/src/atlas/longest_lines/overview.rs b/crates/tasks/src/atlas/longest_lines/overview.rs index e61c8c3..f1c4b1b 100644 --- a/crates/tasks/src/atlas/longest_lines/overview.rs +++ b/crates/tasks/src/atlas/longest_lines/overview.rs @@ -13,7 +13,7 @@ use color_eyre::{Result, eyre::ContextCompat as _}; #[derive(Debug, Clone, Copy)] struct LongestLine { /// The coordinates of the line. - lonlat: crate::projector::LonLatCoord, + lonlat: shared::projector::LonLatCoord, /// The raster coordinates of the line in its containing COG. Used mostly for debugging /// because different COG can contain the same line. coord: geo::Coord, @@ -31,7 +31,7 @@ type StateHash = Arc>; /// Representation of a longest lines COG file. struct Tile { /// The AEQD to lon/lat coordinate converter - projector: crate::projector::Convert, + projector: shared::projector::Convert, /// The raw point data for the tile buffer: gdal::raster::Buffer, /// The width of the tile in points @@ -53,7 +53,7 @@ impl Tile { )] let offset = width as f64 / 2.0f64; let tile = Self { - projector: crate::projector::Convert { base: centre }, + projector: shared::projector::Convert { base: centre }, buffer, width, offset, @@ -65,7 +65,7 @@ impl Tile { } /// Get the lon/lat coordinates representing the centre of the longest lines COG. - fn parse_centre_coords(path: &std::path::Path) -> Result { + fn parse_centre_coords(path: &std::path::Path) -> Result { let filestem = path .file_stem() .context("Couldn't get file stem of longest lines tile")? @@ -85,7 +85,7 @@ impl Tile { .get(1) .context("Latitude not present in path parts")? .parse()?; - let coord = crate::projector::LonLatCoord(geo::coord! {x: lon, y: lat}); + let coord = shared::projector::LonLatCoord(geo::coord! {x: lon, y: lat}); Ok(coord) } @@ -148,7 +148,7 @@ impl Tile { } /// Convert the index of a single point in a tile to its lon/lat coordinate. - fn coord_to_lonlat(&self, point_coord: geo::Coord) -> Result { + fn coord_to_lonlat(&self, point_coord: geo::Coord) -> Result { #[expect( clippy::cast_precision_loss, clippy::as_conversions, @@ -255,7 +255,7 @@ async fn update_state(world: &StateHash, local: HashMap) -> Result<()> { } let mut longest = LongestLine { - lonlat: crate::projector::LonLatCoord(geo::Coord::zero()), + lonlat: shared::projector::LonLatCoord(geo::Coord::zero()), coord: geo::Coord::zero(), packed: crate::atlas::longest_lines::packed::LineOfSight(0.0), }; diff --git a/crates/tasks/src/atlas/run.rs b/crates/tasks/src/atlas/run.rs index fc3f684..94620e6 100644 --- a/crates/tasks/src/atlas/run.rs +++ b/crates/tasks/src/atlas/run.rs @@ -57,7 +57,7 @@ impl Atlas { .next() .context("No width in tile.csv line")? .parse::()?; - let centre = crate::projector::LonLatCoord(geo::Coord { x: lon, y: lat }); + let centre = shared::projector::LonLatCoord(geo::Coord { x: lon, y: lat }); tiles.push(crate::tile::Tile { centre, width }); } @@ -95,7 +95,7 @@ impl Atlas { let mut tile_store = super::db::atlas_worker_store().await?; tracing::debug!("Adding tile jobs to worker..."); - let start_from = crate::projector::LonLatCoord(config.centre.into()); + let start_from = shared::projector::LonLatCoord(config.centre.into()); let amount_of_tiles_to_add = config.amount.unwrap_or_else(|| atlas.tiles.size()); let mut count = 0; for master_tile in atlas @@ -113,21 +113,15 @@ impl Atlas { tile: master_tile.data, }; - // map the priority of the tile width (10,000->900,000) to an integer 1-9 // and then invert the priority so that the smallest go first let priority = -((tile_args.tile.width / 100_000.0f32) as i32); - let ctx = SqlContext::new() - .with_priority(priority); + let ctx = SqlContext::new().with_priority(priority); - let task = Task::builder(tile_args) - .with_ctx(ctx) - .build(); + let task = Task::builder(tile_args).with_ctx(ctx).build(); - tile_store - .push_task(task) - .await?; + tile_store.push_task(task).await?; count += 1; if count >= amount_of_tiles_to_add { diff --git a/crates/tasks/src/config.rs b/crates/tasks/src/config.rs index de28e71..5279190 100644 --- a/crates/tasks/src/config.rs +++ b/crates/tasks/src/config.rs @@ -5,7 +5,7 @@ use color_eyre::Result; /// The marker to indicate that this is a local, non-production run. pub const RUN_ID_LOCAL: &str = "local"; -/// `Config` +/// `Config`. #[derive(clap::Parser, Debug)] #[clap(author, version)] #[command(name = "vv-tasks")] @@ -35,9 +35,9 @@ pub enum Commands { /// `atlas` subcommands. #[derive(clap::Subcommand, Debug)] pub enum AtlasCommands { - /// Create a new machine + /// Create a new machine. NewMachine(NewMachine), - /// Run + /// Run. Run(Atlas), /// Run and manage all the tasks for processing the entire planet. Worker(Worker), @@ -54,7 +54,7 @@ pub enum AtlasCommands { /// `cargo run packer` arguments. #[derive(clap::Parser, Debug, Clone)] pub struct Packer { - /// Just run for one step + /// Just run for one step. #[arg( long, allow_hyphen_values(true), @@ -113,7 +113,7 @@ pub struct StitchAll { #[arg(long, value_name = "Path to master tiles list")] pub master: std::path::PathBuf, - /// Number of CPUS to use, + /// Number of CPUS to use. #[arg(long, value_name = "Number of cpus", default_value_t = number_of_cpus_on_machine())] pub num_cpus: usize, } @@ -134,7 +134,7 @@ pub struct Atlas { #[arg(long, value_name = "Path to master tiles list")] pub master: std::path::PathBuf, - /// The lon/lat coord from which to start processing + /// The lon/lat coord from which to start processing. #[arg( long, allow_hyphen_values(true), @@ -152,7 +152,7 @@ pub struct Atlas { #[arg(long, value_name = "Amount of tiles to process")] pub amount: Option, - /// Maximum width of tile to process + /// Maximum width of tile to process. #[arg(long, value_name = "Max width of tile")] pub max_tile_width: Option, @@ -198,7 +198,7 @@ pub enum Backend { Vulkan, /// Vulkan shader but run on the CPU. VulkanCPU, - /// Optimised cache-efficient CPU kernel + /// Optimised cache-efficient CPU kernel. CPU, } @@ -231,7 +231,7 @@ pub enum ComputeProvider { DigitalOcean, /// Run on Vultr compute. Requires an already authed `vultr-ctl`. Vultr, - /// Run on Google Cloud. Requires an already authed and installed `gcloud` + /// Run on Google Cloud. Requires an already authed and installed `gcloud`. GoogleCloud, } @@ -287,4 +287,3 @@ pub fn number_of_cpus_on_machine() -> usize { .filter(|cpu| *cpu > 0) .unwrap_or(1) } - diff --git a/crates/tasks/src/main.rs b/crates/tasks/src/main.rs index a48181e..7076000 100644 --- a/crates/tasks/src/main.rs +++ b/crates/tasks/src/main.rs @@ -1,4 +1,4 @@ -//! Entrypoint +//! Entrypoint. #![expect( clippy::panic_in_result_fn, reason = "This is just code for short tasks, so panicking is better" @@ -45,7 +45,6 @@ mod atlas { mod config; mod max_subtile; mod packer; -mod projector; mod stitch; mod tile; @@ -53,8 +52,6 @@ use clap::Parser as _; use color_eyre::Result; use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _}; -use crate::projector::LonLatCoord; - #[tokio::main] async fn main() -> Result<()> { let broadcaster = apalis_board_api::sse::TracingBroadcaster::create(); @@ -72,10 +69,12 @@ async fn main() -> Result<()> { config::Commands::Packer(packer_config) => { let mut packer = packer::Packer::new(packer_config.clone())?; match packer_config.one { - Some(coordinate) => packer.run_one(LonLatCoord(geo::coord! { - x: coordinate.0, - y: coordinate.1 - }))?, + Some(coordinate) => { + packer.run_one(shared::projector::LonLatCoord(geo::coord! { + x: coordinate.0, + y: coordinate.1 + }))?; + } None => packer.run_all()?, } } diff --git a/crates/tasks/src/max_subtile.rs b/crates/tasks/src/max_subtile.rs index 29a6107..ff64465 100644 --- a/crates/tasks/src/max_subtile.rs +++ b/crates/tasks/src/max_subtile.rs @@ -21,14 +21,15 @@ pub const ARCSEC_PER_DEG: f32 = 3600.0; #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, bytemuck::Zeroable, bytemuck::Pod)] pub struct MaxSubTile { - /// Longtitude + /// Longtitude. pub lon: f32, - /// Latitude + /// Latitude. pub lat: f32, /// The maximum height within the subtile region. pub max_height: i32, } +/// Break the world down into smaller manageable tiles. pub struct Subtiler { /// Keep track of _all_ the subtiles on the planet. subtiles: Vec, diff --git a/crates/tasks/src/packer.rs b/crates/tasks/src/packer.rs index 11e8ac8..270ac1f 100644 --- a/crates/tasks/src/packer.rs +++ b/crates/tasks/src/packer.rs @@ -11,8 +11,7 @@ use std::{collections::VecDeque, panic}; use color_eyre::{Result, eyre::ContextCompat as _}; use geo::{Area as _, BooleanOps as _, Contains as _, Intersects as _, Within as _}; use rstar::PointDistance as _; - -use crate::projector::LonLatCoord; +use shared::projector::LonLatCoord; /// Where to save the final packed tiles output. const TILES_OUTPUT: &str = "output/tiles.csv"; @@ -38,7 +37,7 @@ const WINDOW_STEP: f64 = WINDOW_RADIUS / 2.0f64; /// A single point of elevation that represents the highest elevation within the resolution range of /// the point. The resolution is defined by another process, `max_subtile.rs`. -type PointRstar = rstar::primitives::GeomWithData; +type PointRstar = rstar::primitives::GeomWithData; /// How we store tiles for fast lookups. pub type TileRstar = rstar::primitives::GeomWithData; @@ -53,7 +52,7 @@ pub struct Packer { /// 3. AEQD anchored to the cnetre of a given tile. This is necessary for things like /// constructing the tile's actual corners. The simple act of adding a distance to a point /// must be done in as local as possible projection. - projector: crate::projector::Convert, + projector: shared::projector::Convert, /// An unchanging canonical reference of all the world's maximum elevation points. canonical: rstar::RTree, /// A stack of points, each of which must at some point be proven to fall within a tile. @@ -76,8 +75,8 @@ impl Packer { stack: VecDeque::new(), // tiles: rstar::RTree::new(), tiles: rstar::RTree::new(), - projector: crate::projector::Convert { - base: crate::projector::LonLatCoord(geo::Coord::zero()), + projector: shared::projector::Convert { + base: shared::projector::LonLatCoord(geo::Coord::zero()), }, }) } @@ -145,13 +144,13 @@ impl Packer { /// Calculate the number of degrees to go Eastward to reach the next window. fn calculate_longtitude_step(latitude: f64) -> f64 { - let degrees_per_meter = 1.0 / crate::projector::Convert::meters_per_degree(latitude); + let degrees_per_meter = 1.0 / shared::projector::Convert::meters_per_degree(latitude); f64::from(degrees_per_meter) * WINDOW_STEP } /// Do a sort of "carriage return" to the next latitude South. fn start_new_latitude(current: LonLatCoord) -> Result> { - let projector = crate::projector::Convert { base: current }; + let projector = shared::projector::Convert { base: current }; let down = projector.to_degrees(geo::coord! { x: 0.0f64, y: -WINDOW_STEP, @@ -180,7 +179,7 @@ impl Packer { /// Build a view onto a subset of the total data. fn build_window(&mut self, centre: LonLatCoord) -> Result<()> { self.stack = VecDeque::new(); - self.projector = crate::projector::Convert { base: centre }; + self.projector = shared::projector::Convert { base: centre }; tracing::debug!("Ordering around coordinate: {centre:?}"); for canonical_point in self.canonical.nearest_neighbor_iter(¢re) { @@ -461,7 +460,7 @@ impl Packer { let mut nearest_tile = *nearest_tile_reference; let old_width = nearest_tile.data.width; - let tile_projecter = crate::projector::Convert { + let tile_projecter = shared::projector::Convert { base: nearest_tile.data.centre, }; @@ -622,7 +621,7 @@ impl Packer { )] let elevation = elevation_i32 as f32; - (2.0 * crate::projector::EARTH_RADIUS * 1000.0) + (2.0 * shared::projector::EARTH_RADIUS * 1000.0) .mul_add(elevation, elevation.powi(2)) .sqrt() * 2.0 @@ -910,7 +909,7 @@ impl Packer { let magic = 1.5; let latitude = tile.data.centre.0.y; let extension = - (crate::projector::Convert::meters_per_degree(latitude) / subtile_resolution) * magic; + (shared::projector::Convert::meters_per_degree(latitude) / subtile_resolution) * magic; let new_width = tile.data.width + extension; self.set_tile_width(tile.data.centre, new_width)?; self.ensure_tile_is_big_enough(tile)?; diff --git a/crates/tasks/src/tile.rs b/crates/tasks/src/tile.rs index 7323a92..b4606ed 100644 --- a/crates/tasks/src/tile.rs +++ b/crates/tasks/src/tile.rs @@ -4,13 +4,13 @@ use color_eyre::{Result, eyre::ContextCompat as _}; use geo::{Area as _, BoundingRect as _, Buffer as _}; use rstar::PointDistance as _; -use crate::projector::LonLatCoord; +use shared::projector::LonLatCoord; /// The tile data itself. #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Tile { /// The centre of the tile. - pub centre: crate::projector::LonLatCoord, + pub centre: shared::projector::LonLatCoord, /// The width of the tile. Therefore, not the distance from the centre to an edge. Width is /// better than radius because it defines the minimum line of sight distance we are interested /// in. @@ -24,8 +24,8 @@ impl Tile { } /// The centre coordinate reprojected to the given metric projection. - pub fn centre_metric(&self, anchor: crate::projector::LonLatCoord) -> Result { - let projecter = crate::projector::Convert { base: anchor }; + pub fn centre_metric(&self, anchor: shared::projector::LonLatCoord) -> Result { + let projecter = shared::projector::Convert { base: anchor }; projecter.to_meters(self.centre) } @@ -46,7 +46,7 @@ impl Tile { /// Make a polygon representing the tile in metric coordinates. pub fn to_polygon_metric( self, - anchor: crate::projector::LonLatCoord, + anchor: shared::projector::LonLatCoord, ) -> Result { let centre = self.centre_metric(anchor)?; let circle = geo::Point::new(centre.x, centre.y).buffer(self.radius()); @@ -82,7 +82,7 @@ impl Tile { /// Calculate the distance in meters of the tile from the given point. pub fn distance_from(&self, point_lonlat: LonLatCoord) -> Result { - let projector = crate::projector::Convert { base: point_lonlat }; + let projector = shared::projector::Convert { base: point_lonlat }; let point = projector.to_meters(self.centre)?; Ok(self.centre_metric(point_lonlat)?.distance_2(&point).sqrt()) @@ -99,7 +99,7 @@ impl Tile { let center_lat = origin.y().to_radians(); let bearing_rad = bearing.to_radians(); - let rad = meters / f64::from(crate::projector::EARTH_RADIUS * 1000.0); + let rad = meters / f64::from(shared::projector::EARTH_RADIUS * 1000.0); let lat = { center_lat.sin() * rad.cos() + center_lat.cos() * rad.sin() * bearing_rad.cos() } diff --git a/ctl.sh b/ctl.sh index edf26cc..064c7b9 100755 --- a/ctl.sh +++ b/ctl.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + export PROJECT_ROOT PROJECT_ROOT=$(dirname "$(readlink -f "$0")") diff --git a/scripts/cloud_init_ubuntu22.bash b/scripts/cloud_init_ubuntu22.bash index 155ba73..bd1d0d3 100644 --- a/scripts/cloud_init_ubuntu22.bash +++ b/scripts/cloud_init_ubuntu22.bash @@ -11,9 +11,9 @@ function cloud_init_ubuntu22 { sudo apt update sudo apt install --yes \ libvulkan1 mesa-vulkan-drivers vulkan-tools \ - build-essential pkg-config \ + build-essential pkg-config clang \ libgdal-dev gdal-bin python3-gdal rsync htop \ - jq rclone tmux sqlite3 parallel bc + jq rclone tmux sqlite3 parallel bc pigz curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y echo 'source ~/.cargo/env' >> ~/.bashrc curl -LsSf https://astral.sh/uv/install.sh | sh diff --git a/scripts/common.bash b/scripts/common.bash index 0cb7654..08bdd79 100644 --- a/scripts/common.bash +++ b/scripts/common.bash @@ -63,3 +63,16 @@ function get_tiff_latitude { local input=$1 gdalinfo -json "$input" | jq '.geoTransform[3]' } + +function get_extent { + local input=$1 + + gdalinfo \ + -json "$input" | + jq -r ' + .cornerCoordinates.lowerLeft[0], + .cornerCoordinates.lowerLeft[1], + .cornerCoordinates.upperRight[0], + .cornerCoordinates.upperRight[1]' | + xargs +} diff --git a/scripts/galiano.bash b/scripts/galiano.bash new file mode 100644 index 0000000..0cada24 --- /dev/null +++ b/scripts/galiano.bash @@ -0,0 +1,101 @@ +# 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) +# Resolution: 0.25m (25cm) +# Extent: 456096.26 5410824.35 478000.26 5430409.6 + +# How to compute: +# +# RUSTFLAGS='-Ctarget-cpu=native' cargo run --release --features=ring_data \ +# 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 \ +# --aoi-point -123.29132955324245,48.87887000504236 \ +# --aoi-point -123.34211991205905,48.83884334550575 \ +# --aoi-point -123.61305988176157,48.99841636016359 \ +# --aoi-point -123.57209623620894,49.0211056882002 \ +# --output-dir /mnt/disks/viewshed \ +# --viewsheds-db-path /mnt/disks/viewshed/dbs + +# Convert the Galiano DSM to: +# * a square +# * a sauare with a width of factor 48 +# * the given resolution +# +# Usage: `./ctl.sh galiano_prepare scratch/Galiano/dsm.tif scratch/Galiano/dsm-1.0.tiff 1.0` +function galiano_prepare { + local input=$1 + local output=$2 + local resolution=$3 + + read -r xcentre ycentre <<<"$( + gdalinfo \ + -json "$input" | jq -r '.cornerCoordinates.center | join(" ")' + )" + + source_crs=$(gdalsrsinfo -o wkt "$input") + read -r lon_centre lat_centre _ <<<"$( + echo "$xcentre" "$ycentre" | gdaltransform -s_srs "$source_crs" -t_srs EPSG:4326 + )" + + read -ra extent < <(_galiano_extent_padded) + + gdalwarp \ + -overwrite \ + -t_srs "+proj=aeqd +lat_0=$lat_centre +lon_0=$lon_centre +datum=WGS84 +units=m" \ + -te "${extent[0]}" "${extent[1]}" "${extent[2]}" "${extent[3]}" \ + -tr "$resolution" "$resolution" \ + -r bilinear \ + -dstnodata 0 \ + -co COMPRESS=ZSTD \ + -co ZSTD_LEVEL=9 \ + "$input" \ + "$output" + + pad_to_factor "$output" + +} + +function _galiano_extent_padded { + local original_scale=0.25 + local auxiliary=3 + local square_to_circle=1.414 # Make the circle _outside_ the DEM + width=$(gdalinfo -json "$input" | jq '.size[0]') + width_all=$(echo "$width * $square_to_circle * $auxiliary" | bc) + extend_meters=$( + echo "($width_all * $original_scale) / 2" | bc + ) + + xmin=-$extend_meters + ymin=-$extend_meters + xmax=$extend_meters + ymax=$extend_meters + + echo "$xmin $ymin $xmax $ymax" +} + +function next_factor { + local width=$1 + local factor=48 + echo "((($width + $factor - 1) / $factor) * $factor)" | bc +} + +function pad_to_factor { + local input=$1 + temp=$(dirname "$input")/tmp.tiff + + width=$(gdalinfo -json "$input" | jq '.size[0]') + next_by_factor=$(next_factor "$width") + gdal_translate \ + -srcwin 0 0 "$next_by_factor" "$next_by_factor" \ + -a_nodata 0 \ + -co COMPRESS=ZSTD \ + -co ZSTD_LEVEL=9 \ + "$input" "$temp" + + mv "$temp" "$input" +} diff --git a/scripts/make_pmtiles.bash b/scripts/make_pmtiles.bash index 1a73461..147f600 100644 --- a/scripts/make_pmtiles.bash +++ b/scripts/make_pmtiles.bash @@ -9,6 +9,8 @@ function make_pmtiles { # Output `.pmtile` tile. local output=$2 + local resolution=${3} + ensure_tiles_env if [[ $version == "latest" ]]; then @@ -26,21 +28,35 @@ function make_pmtiles { exit 1 fi - prepare_all_tiffs work/raw "$archive" + prepare_all_tiffs work/raw "$archive" "$resolution" # Collate all the heatmap GeoTiffs into a single virtual file. gdalbuildvrt "$world_vrt" "$archive"/*.tiff + max_zoom="$(scale_to_max_zoom "$resolution")" + # Create the global `.pmtile` - uv run scripts/to_pmtiles.py "$world_vrt" "$output" \ + UV_PYTHON_DOWNLOADS=automatic \ + uv run --python 3.12 \ + "$PROJECT_ROOT/scripts/to_pmtiles.py" "$world_vrt" "$output" \ --min_zoom 0 \ - --max_zoom 11 + --max_zoom "$max_zoom" if [[ $version != "local" ]]; then rclone_put "$output" viewview/runs/"$version"/pmtiles/ fi } +function scale_to_max_zoom { + local scale=$1 + EARTH_CIRCUMFERENCE=40075016.686 + echo " + scale=10; \ + z = l($EARTH_CIRCUMFERENCE / (256 * $scale)) / l(2);\ + scale=0; if (z>scale(z)) z/1 + 1 else z/1 + " | bc -l +} + # Interpolate the TVS heatmap's data to EPSG:3857. # # Along with all the other heatmap GeoTiff's, it will be used to create the single global @@ -57,12 +73,13 @@ function process_raw_tvs_tiff { local input=$1 local output=$2 + local resolution=${3} # These are in an array because I've been exploring splitting tiles that cross the antimeridian. # Having an array makes it easier to diverge the processing into west and east branches. local warp_args=( "-overwrite" - "-tr" "100" "100" + "-tr" "$resolution" "$resolution" "-t_srs" "EPSG:3857" "-dstnodata" "0" "-srcnodata" "0" @@ -93,22 +110,24 @@ function prepare_tiff { local source=$1 local destination=$2 + local resolution=${3} filename=$(basename "$source") latitude=$(echo "$filename" | sed -E 's#.*_([0-9.-]+)\.tiff#\1#') + is_above_antaricta=$(echo "$latitude > -80" | bc -l) - if (($(echo "$latitude > -80" | bc -l))); then - process_raw_tvs_tiff "$source" "$destination/$filename" + if [[ "$is_above_antaricta" -eq 1 || "$filename" == "total_surfaces.tiff" ]]; then + process_raw_tvs_tiff "$source" "$destination/$filename" "$resolution" create_overviews_for_tiff "$destination/$filename" else - echo "Not creating preparing heatmap tiff for Antartic tile: $input" + echo "Not preparing heatmap tiff for Antartic tile: $input" fi - } function prepare_all_tiffs { local source_directory=$1 local destination_directory=$2 + local resolution=${3} export -f prepare_tiff export -f process_raw_tvs_tiff @@ -117,5 +136,5 @@ function prepare_all_tiffs { mkdir -p "$destination_directory" find "$source_directory" -name "*.tiff" | - parallel -j +0 --halt now,fail=1 prepare_tiff {} "$destination_directory" + parallel -j +0 --halt now,fail=1 prepare_tiff {} "$destination_directory" "$resolution" } diff --git a/scripts/make_tiff_sample.py b/scripts/make_tiff_sample.py new file mode 100644 index 0000000..422bb6f --- /dev/null +++ b/scripts/make_tiff_sample.py @@ -0,0 +1,56 @@ +# /// script +# dependencies = [ +# "rasterio", +# "numpy", +# ] +# /// + +# Helper script to make small sample GeoTiffs for testing etc. + +import numpy as np +import rasterio +from rasterio.transform import from_origin + +# fmt: off +x = 15 +dem = [ + 0,0,0,0, 0,0,0,0, 0,0,0,1, + 0,1,1,1, 1,1,1,1, 0,1,0,0, + 0,1,3,3, 2,2,2,3, 4,3,0,0, + 0,1,3,4, 3,3,3,3, 3,2,1,0, + + 0,1,3,4, 5,5,5,4, 3,2,1,0, + 0,1,3,4, 4,0,6,5, 4,2,1,0, + 0,1,1,2, 5,9,x,7, 5,2,1,0, + 0,1,1,4, 4,4,4,6, 3,2,1,0, + + 0,0,3,4, 4,4,5,4, 0,0,0,0, + 0,3,3,4, 3,2,3,3, 0,0,0,0, + 0,0,3,4, 2,2,1,2, 0,0,0,0, + 3,0,2,3, 1,0,1,0, 0,0,0,0, +] +# fmt: on + +data = np.array(dem, dtype="uint16").reshape(12, 12) + +transform = from_origin(0, 12, 1, 1) + +with rasterio.open( + "sample_16x16.tiff", + "w", + driver="GTiff", + height=12, + width=12, + count=1, + dtype=data.dtype, + crs="EPSG:4326", + transform=transform, +) as dst: + dst.write(data, 1) + +print("Created sample_16x16.tiff successfully.") + +# gdal_edit.py \ +# -a_srs "+proj=aeqd +lat_0=51.4898 +lon_0=-3.123 +units=m" \ +# -a_ullr -6 6 6 -6 \ +# sample_16x16.tiff diff --git a/scripts/mass_edit_map_json.py b/scripts/mass_edit_map_json.py new file mode 100644 index 0000000..197701c --- /dev/null +++ b/scripts/mass_edit_map_json.py @@ -0,0 +1,14 @@ +import json + +file = "website/src/map_vector.styles.json" + +with open(file, "r") as f: + data = json.load(f) + +for layer in data.get("layers", []): + if "layout" not in layer: + layer["layout"] = {} + layer["layout"]["visibility"] = "none" + +with open(file, "w") as f: + json.dump(data, f, indent=2) diff --git a/scripts/s3.bash b/scripts/s3.bash index 51bf6e0..fb9dffc 100644 --- a/scripts/s3.bash +++ b/scripts/s3.bash @@ -6,6 +6,7 @@ function s3 { --host-bucket="%(bucket)s.$R2_HOST" \ --access_key="$VIEWVIEW_S3_ACCESS_KEY" \ --secret_key="$VIEWVIEW_S3_SECRET" \ + --human \ "$@" } @@ -40,19 +41,33 @@ function rclone_put { local source=$1 local destination=$2 - rclone copy "$source" :s3:"$destination" \ - --s3-endpoint=https://$R2_HOST \ - --s3-provider=Cloudflare \ - --s3-access-key-id="$VIEWVIEW_S3_ACCESS_KEY" \ - --s3-secret-access-key="$VIEWVIEW_S3_SECRET" \ - --s3-no-check-bucket \ - --progress \ - --transfers=32 \ - --checkers=32 \ - --multi-thread-streams=16 \ - --multi-thread-cutoff=0 \ - --s3-chunk-size=64M \ + local args=( + "--s3-endpoint=https://$R2_HOST" + --s3-provider=Cloudflare + "--s3-access-key-id=$VIEWVIEW_S3_ACCESS_KEY" + "--s3-secret-access-key=$VIEWVIEW_S3_SECRET" + --s3-no-check-bucket + --progress + --transfers=32 + --checkers=32 + --multi-thread-streams=16 + --multi-thread-cutoff=0 + --s3-chunk-size=64M --s3-upload-concurrency=16 + ) + + if [[ $source == "-" ]]; then + rclone rcat :s3:"$destination" "${args[@]}" + else + rclone copy "$source" :s3:"$destination" "${args[@]}" + fi +} + +function rclone_put_zip_stream { + local source=$1 + local destination=$2 + + cat "$source" | pigz | rclone_put - "$destination" } function get_srtm_folder { diff --git a/website/package.json b/website/package.json index 97c94d0..c7c67b7 100644 --- a/website/package.json +++ b/website/package.json @@ -15,6 +15,7 @@ "dependencies": { "@lucide/svelte": "^0.554.0", "@mapbox/search-js-web": "^1.5.1", + "@turf/union": "^7.3.4", "accessible-nprogress": "^2.1.2", "geotiff": "^2.1.4-beta.1", "maplibre-gl": "^5.11.0", @@ -24,9 +25,10 @@ }, "devDependencies": { "@biomejs/biome": "2.3.4", - "@rsbuild/core": "^1.6.0", - "@rsbuild/plugin-sass": "^1.4.0", - "@rsbuild/plugin-svelte": "^1.0.11", + "@rsbuild/core": "^1.7.5", + "@rsbuild/plugin-sass": "^1.5.1", + "@rsbuild/plugin-svelte": "^1.1.1", + "@types/geojson": "^7946.0.16", "pmtiles": "^4.3.0", "prettier": "^3.6.2", "sass": "^1.97.3", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 29696fe..29e02b6 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@mapbox/search-js-web': specifier: ^1.5.1 version: 1.5.1(mapbox-gl@3.18.0) + '@turf/union': + specifier: ^7.3.4 + version: 7.3.4 accessible-nprogress: specifier: ^2.1.2 version: 2.1.2 @@ -37,14 +40,17 @@ importers: specifier: 2.3.4 version: 2.3.4 '@rsbuild/core': - specifier: ^1.6.0 - version: 1.6.7 + specifier: ^1.7.5 + version: 1.7.5 '@rsbuild/plugin-sass': - specifier: ^1.4.0 - version: 1.4.0(@rsbuild/core@1.6.7) + specifier: ^1.5.1 + version: 1.5.1(@rsbuild/core@1.7.5) '@rsbuild/plugin-svelte': - specifier: ^1.0.11 - version: 1.0.11(@rsbuild/core@1.6.7)(postcss@8.5.6)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3) + specifier: ^1.1.1 + version: 1.1.1(@rsbuild/core@1.7.5)(postcss@8.5.9)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3) + '@types/geojson': + specifier: ^7946.0.16 + version: 7946.0.16 pmtiles: specifier: ^4.3.0 version: 4.3.0 @@ -56,7 +62,7 @@ importers: version: 1.97.3 svelte-check: specifier: ^4.3.3 - version: 4.3.4(svelte@5.43.14)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.4)(svelte@5.43.14)(typescript@5.9.3) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -116,17 +122,17 @@ packages: cpu: [x64] os: [win32] - '@bufbuild/protobuf@2.10.1': - resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==} + '@bufbuild/protobuf@2.11.0': + resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==} - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@floating-ui/core@0.7.3': resolution: {integrity: sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==} @@ -186,6 +192,9 @@ packages: '@mapbox/tiny-sdf@2.0.7': resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} + '@mapbox/tiny-sdf@2.1.0': + resolution: {integrity: sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==} + '@mapbox/unitbezier@0.0.1': resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} @@ -206,23 +215,23 @@ packages: '@maplibre/vt-pbf@4.0.3': resolution: {integrity: sha512-YsW99BwnT+ukJRkseBcLuZHfITB4puJoxnqPVjo72rhW/TaawVYsgQHcqWLzTxqknttYoDpgyERzWSa/XrETdA==} - '@module-federation/error-codes@0.21.4': - resolution: {integrity: sha512-ClpL5MereWNXh+EgDjz7w4RrC1JlisQTvXDa1gLxpviHafzNDfdViVmuhi9xXVuj+EYo8KU70Y999KHhk9424Q==} + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} - '@module-federation/runtime-core@0.21.4': - resolution: {integrity: sha512-SGpmoOLGNxZofpTOk6Lxb2ewaoz5wMi93AFYuuJB04HTVcngEK+baNeUZ2D/xewrqNIJoMY6f5maUjVfIIBPUA==} + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} - '@module-federation/runtime-tools@0.21.4': - resolution: {integrity: sha512-RzFKaL0DIjSmkn76KZRfzfB6dD07cvID84950jlNQgdyoQFUGkqD80L6rIpVCJTY/R7LzR3aQjHnoqmq4JPo3w==} + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} - '@module-federation/runtime@0.21.4': - resolution: {integrity: sha512-wgvGqryurVEvkicufJmTG0ZehynCeNLklv8kIk5BLIsWYSddZAE+xe4xov1kgH5fIJQAoQNkRauFFjVNlHoAkA==} + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} - '@module-federation/sdk@0.21.4': - resolution: {integrity: sha512-tzvhOh/oAfX++6zCDDxuvioHY4Jurf8vcfoCbKFxusjmyKr32GPbwFDazUP+OPhYCc3dvaa9oWU6X/qpUBLfJw==} + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} - '@module-federation/webpack-bundler-runtime@0.21.4': - resolution: {integrity: sha512-dusmR3uPnQh9u9ChQo3M+GLOuGFthfvnh7WitF/a1eoeTfRmXqnMFsXtZCUK+f/uXf+64874Zj/bhAgbBcVHZA==} + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} @@ -233,154 +242,242 @@ packages: cpu: [arm64] os: [android] + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + '@parcel/watcher-darwin-arm64@2.5.1': resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [darwin] + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + '@parcel/watcher-darwin-x64@2.5.1': resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [darwin] + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + '@parcel/watcher-freebsd-x64@2.5.1': resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [freebsd] + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + '@parcel/watcher-linux-arm-glibc@2.5.1': resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [win32] + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + '@parcel/watcher-win32-ia32@2.5.1': resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} engines: {node: '>= 10.0.0'} cpu: [ia32] os: [win32] + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + '@parcel/watcher-win32-x64@2.5.1': resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [win32] + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + '@parcel/watcher@2.5.1': resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} - '@rsbuild/core@1.6.7': - resolution: {integrity: sha512-V0INbMrT/LwyhzKmvpupe2oSvPFWaivz7sdriFRp381BJvD0d2pYcq9iRW91bxgMRX78MgTzFYAu868hMAzoSw==} + '@rsbuild/core@1.7.5': + resolution: {integrity: sha512-i37urpoV4y9NSsGiUOuLdoI42KJ5h4gAZ8EG8Ilmsond3bxoAoOCu7YvC+1pJ7p+r16suVPW8cki891ZKHOoXQ==} engines: {node: '>=18.12.0'} hasBin: true - '@rsbuild/plugin-sass@1.4.0': - resolution: {integrity: sha512-2XcrXUwJftWsWURsvGKKXc7A4yEAUdVkw+K95Qrfmy5E5IfARik0Ko3TFB1828wdvhZ1OFLvDQEzONCIyQV1yg==} + '@rsbuild/plugin-sass@1.5.1': + resolution: {integrity: sha512-rwou4KvDerqvAI0hstjlwF9jgzxPaSOr0PkwtEPjTnQcR5mK6AXZICpUBJbHGgjlAM0aTegKf5IYl/ENvaAQrQ==} peerDependencies: - '@rsbuild/core': 1.x + '@rsbuild/core': ^1.3.0 || ^2.0.0-0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true - '@rsbuild/plugin-svelte@1.0.11': - resolution: {integrity: sha512-ThmBSTMILWDB+i/P61hgw76UnmFnv7WYYZMjcwqxUXEKM3zobL5jdtEj56wN8yn/xadStM0INjNjHWTf5E7kIQ==} + '@rsbuild/plugin-svelte@1.1.1': + resolution: {integrity: sha512-maRgnSH/8nAJKl8yhQyf8W07kzz/tOCK8zZLpOIL7dP1j3Pxir7Uiy98ikPq3fWhPIBBtIzpmY8FUaR44t2CJA==} peerDependencies: - '@rsbuild/core': 1.x + '@rsbuild/core': ^1.4.0 || ^2.0.0-0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true - '@rspack/binding-darwin-arm64@1.6.4': - resolution: {integrity: sha512-qD2C5xwdY2qKEXTZiPJQx1L1kELapOc0AaZDqcAyzXs30d1qTKpx6PdyW3HN+gueKovyWZwMMYfz6RxcMCnaDQ==} + '@rspack/binding-darwin-arm64@1.7.11': + resolution: {integrity: sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==} cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-x64@1.6.4': - resolution: {integrity: sha512-IHceyLDxeubqIrGz4gUqJavnygTij4vtDDE2Fkgobz7hkTJwGtD5mxBKbVNRqGvhrasVw0h9rEjR7tdbDSiUhQ==} + '@rspack/binding-darwin-x64@1.7.11': + resolution: {integrity: sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==} cpu: [x64] os: [darwin] - '@rspack/binding-linux-arm64-gnu@1.6.4': - resolution: {integrity: sha512-Ldpoz2wWnBaL2+XKLIOyCZMkAkd4pk/L24EVgma3SpRtwgenLEr10bQupvwGAK5OLkjayslOTZmRiAv0FH5o/w==} + '@rspack/binding-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-arm64-musl@1.6.4': - resolution: {integrity: sha512-3fLMSDK5yMjKmx7iFbYG3P3A0xNdtmNu09v5P6hzq65tkJ3dflIt3p8DvtOTURtuSgQZV2A1LDd9hpIXdnigqA==} + '@rspack/binding-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-x64-gnu@1.6.4': - resolution: {integrity: sha512-5YzXUKLnaiqND05CDgkKE0WNRtC1ulkVncYs78xPikonzZmgVXa8eRaTPOZC6ZjpLR0eTsg+MSesLUsPUu27hA==} + '@rspack/binding-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==} cpu: [x64] os: [linux] - '@rspack/binding-linux-x64-musl@1.6.4': - resolution: {integrity: sha512-KcSFla8a9bXG1mmV5oQ1R5h/dSXfd41/qHOsNuLqho2UCX8CVh4dezUA153dj7p1S4yOhTy6VZZi6C1szweE9A==} + '@rspack/binding-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==} cpu: [x64] os: [linux] - '@rspack/binding-wasm32-wasi@1.6.4': - resolution: {integrity: sha512-mfFJbDJkRy5I1iW3m0JlWbc0X8pjVd+GRUz5nhbccwEhSQOc27ao3evf7XPU4aaDxud1B3UEqYiRcRmtm1BrjA==} + '@rspack/binding-wasm32-wasi@1.7.11': + resolution: {integrity: sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==} cpu: [wasm32] - '@rspack/binding-win32-arm64-msvc@1.6.4': - resolution: {integrity: sha512-QtIqxsfeTSS1lwfaPGrPFfJ9ir/3aWZv5t3iAgYj/CNUA8MTKWt4vQKcco7NRIGK4ZLMI+dgJBFtvd/lUDMQsw==} + '@rspack/binding-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==} cpu: [arm64] os: [win32] - '@rspack/binding-win32-ia32-msvc@1.6.4': - resolution: {integrity: sha512-HXEWGDllgh0jFwjGhkGcLqb0dzXbc/rA8vQr2JcSdC41p1DTzLgO215jWdKSIvzCzhyPh3VeQkXk76hjFB2cLQ==} + '@rspack/binding-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==} cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@1.6.4': - resolution: {integrity: sha512-MAO5rOnGYoeuT2LPn/P7JVJCi3d78XoXgOq3tkGh6qXhvhkjsBRtYluWCzACXQpXfFHEWYd7uT5yHoZgxiVuoA==} + '@rspack/binding-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==} cpu: [x64] os: [win32] - '@rspack/binding@1.6.4': - resolution: {integrity: sha512-vUxc/zUdsCuyysOvP4CTdIYxsZPb2jIXST5vrLABiTPIaHpXZ0hVdgKif2XPJwJeuCVS6w25xvyPN0mBCU0MvQ==} + '@rspack/binding@1.7.11': + resolution: {integrity: sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==} - '@rspack/core@1.6.4': - resolution: {integrity: sha512-5F1+MQD8rfbFbUHnaiZe4jqOu9pnSb+PliqQvi0lj+uvpMpcS3sJDIs/mz6P1u87lfkfBXChIT4zSLAzeOgMWw==} + '@rspack/core@1.7.11': + resolution: {integrity: sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==} engines: {node: '>=18.12.0'} peerDependencies: '@swc/helpers': '>=0.5.1' @@ -396,8 +493,17 @@ packages: peerDependencies: acorn: ^8.9.0 - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@turf/helpers@7.3.4': + resolution: {integrity: sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==} + + '@turf/meta@7.3.4': + resolution: {integrity: sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw==} + + '@turf/union@7.3.4': + resolution: {integrity: sha512-JJYyPMmGcrTa9sPv2ief2QU9Hb//cEAU1zgKu/OfoCMa9a8Imp5QVm9UTAkhGlc+4qm/N/X16iJ+cvVWaxPjkg==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -439,13 +545,13 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - buffer-builder@0.2.0: - resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} - cheap-ruler@4.0.0: resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} @@ -460,8 +566,8 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} - core-js@3.46.0: - resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} csscolorparser@1.0.3: resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} @@ -475,6 +581,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + earcut@3.0.2: resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} @@ -531,6 +641,9 @@ packages: immutable@5.1.4: resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -629,11 +742,18 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pmtiles@4.3.0: resolution: {integrity: sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + polyclip-ts@0.16.8: + resolution: {integrity: sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==} + + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} potpack@2.1.0: @@ -661,8 +781,8 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - reduce-configs@1.1.1: - resolution: {integrity: sha512-EYtsVGAQarE8daT54cnaY1PIknF2VB78ug6Zre2rs36EsJfC40EG6hmTU2A2P1ZuXnKAt2KI0fzOGHcX7wzdPw==} + reduce-configs@1.1.2: + resolution: {integrity: sha512-AgBP55V8FC7NaqoOP2RCbTpu6LE+YuX3LUZkNAoitcfyS3/PIC8Obg/TJrBzTkJ+lDvZv0TTAeDpLkzjTtYlbw==} resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} @@ -680,122 +800,122 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - sass-embedded-all-unknown@1.93.3: - resolution: {integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==} + sass-embedded-all-unknown@1.99.0: + resolution: {integrity: sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==} cpu: ['!arm', '!arm64', '!riscv64', '!x64'] - sass-embedded-android-arm64@1.93.3: - resolution: {integrity: sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==} + sass-embedded-android-arm64@1.99.0: + resolution: {integrity: sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - sass-embedded-android-arm@1.93.3: - resolution: {integrity: sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==} + sass-embedded-android-arm@1.99.0: + resolution: {integrity: sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ==} engines: {node: '>=14.0.0'} cpu: [arm] os: [android] - sass-embedded-android-riscv64@1.93.3: - resolution: {integrity: sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==} + sass-embedded-android-riscv64@1.99.0: + resolution: {integrity: sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [android] - sass-embedded-android-x64@1.93.3: - resolution: {integrity: sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==} + sass-embedded-android-x64@1.99.0: + resolution: {integrity: sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [android] - sass-embedded-darwin-arm64@1.93.3: - resolution: {integrity: sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==} + sass-embedded-darwin-arm64@1.99.0: + resolution: {integrity: sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - sass-embedded-darwin-x64@1.93.3: - resolution: {integrity: sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==} + sass-embedded-darwin-x64@1.99.0: + resolution: {integrity: sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - sass-embedded-linux-arm64@1.93.3: - resolution: {integrity: sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==} + sass-embedded-linux-arm64@1.99.0: + resolution: {integrity: sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-arm@1.93.3: - resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==} + sass-embedded-linux-arm@1.99.0: + resolution: {integrity: sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-musl-arm64@1.93.3: - resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==} + sass-embedded-linux-musl-arm64@1.99.0: + resolution: {integrity: sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-musl-arm@1.93.3: - resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==} + sass-embedded-linux-musl-arm@1.99.0: + resolution: {integrity: sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-musl-riscv64@1.93.3: - resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==} + sass-embedded-linux-musl-riscv64@1.99.0: + resolution: {integrity: sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-musl-x64@1.93.3: - resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==} + sass-embedded-linux-musl-x64@1.99.0: + resolution: {integrity: sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-linux-riscv64@1.93.3: - resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==} + sass-embedded-linux-riscv64@1.99.0: + resolution: {integrity: sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-x64@1.93.3: - resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==} + sass-embedded-linux-x64@1.99.0: + resolution: {integrity: sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-unknown-all@1.93.3: - resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==} + sass-embedded-unknown-all@1.99.0: + resolution: {integrity: sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg==} os: ['!android', '!darwin', '!linux', '!win32'] - sass-embedded-win32-arm64@1.93.3: - resolution: {integrity: sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==} + sass-embedded-win32-arm64@1.99.0: + resolution: {integrity: sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - sass-embedded-win32-x64@1.93.3: - resolution: {integrity: sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==} + sass-embedded-win32-x64@1.99.0: + resolution: {integrity: sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] - sass-embedded@1.93.3: - resolution: {integrity: sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==} + sass-embedded@1.99.0: + resolution: {integrity: sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg==} engines: {node: '>=16.0.0'} hasBin: true - sass@1.93.3: - resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} engines: {node: '>=14.0.0'} hasBin: true - sass@1.97.3: - resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} engines: {node: '>=14.0.0'} hasBin: true @@ -803,6 +923,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + splaytree-ts@1.0.2: + resolution: {integrity: sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==} + splaytree@0.1.4: resolution: {integrity: sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==} @@ -888,8 +1011,8 @@ packages: resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} engines: {node: '>=16.0.0'} - sync-message-port@1.1.3: - resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} engines: {node: '>=16.0.0'} tabbable@5.3.3: @@ -965,20 +1088,20 @@ snapshots: '@biomejs/cli-win32-x64@2.3.4': optional: true - '@bufbuild/protobuf@2.10.1': {} + '@bufbuild/protobuf@2.11.0': {} - '@emnapi/core@1.7.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true @@ -1041,6 +1164,8 @@ snapshots: '@mapbox/tiny-sdf@2.0.7': {} + '@mapbox/tiny-sdf@2.1.0': {} + '@mapbox/unitbezier@0.0.1': {} '@mapbox/vector-tile@2.0.4': @@ -1075,77 +1200,116 @@ snapshots: pbf: 4.0.1 supercluster: 8.0.1 - '@module-federation/error-codes@0.21.4': {} + '@module-federation/error-codes@0.22.0': {} - '@module-federation/runtime-core@0.21.4': + '@module-federation/runtime-core@0.22.0': dependencies: - '@module-federation/error-codes': 0.21.4 - '@module-federation/sdk': 0.21.4 + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 - '@module-federation/runtime-tools@0.21.4': + '@module-federation/runtime-tools@0.22.0': dependencies: - '@module-federation/runtime': 0.21.4 - '@module-federation/webpack-bundler-runtime': 0.21.4 + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 - '@module-federation/runtime@0.21.4': + '@module-federation/runtime@0.22.0': dependencies: - '@module-federation/error-codes': 0.21.4 - '@module-federation/runtime-core': 0.21.4 - '@module-federation/sdk': 0.21.4 + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 - '@module-federation/sdk@0.21.4': {} + '@module-federation/sdk@0.22.0': {} - '@module-federation/webpack-bundler-runtime@0.21.4': + '@module-federation/webpack-bundler-runtime@0.22.0': dependencies: - '@module-federation/runtime': 0.21.4 - '@module-federation/sdk': 0.21.4 + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 '@napi-rs/wasm-runtime@1.0.7': dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true '@parcel/watcher-android-arm64@2.5.1': optional: true + '@parcel/watcher-android-arm64@2.5.6': + optional: true + '@parcel/watcher-darwin-arm64@2.5.1': optional: true + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + '@parcel/watcher-darwin-x64@2.5.1': optional: true + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + '@parcel/watcher-freebsd-x64@2.5.1': optional: true + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + '@parcel/watcher-linux-arm-glibc@2.5.1': optional: true + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + '@parcel/watcher-linux-arm-musl@2.5.1': optional: true + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + '@parcel/watcher-linux-arm64-glibc@2.5.1': optional: true + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + '@parcel/watcher-linux-arm64-musl@2.5.1': optional: true + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + '@parcel/watcher-linux-x64-glibc@2.5.1': optional: true + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + '@parcel/watcher-linux-x64-musl@2.5.1': optional: true + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + '@parcel/watcher-win32-arm64@2.5.1': optional: true + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + '@parcel/watcher-win32-ia32@2.5.1': optional: true + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + '@parcel/watcher-win32-x64@2.5.1': optional: true + '@parcel/watcher-win32-x64@2.5.6': + optional: true + '@parcel/watcher@2.5.1': dependencies: detect-libc: 1.0.3 @@ -1168,30 +1332,54 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + '@petamoriken/float16@3.9.3': {} - '@rsbuild/core@1.6.7': + '@rsbuild/core@1.7.5': dependencies: - '@rspack/core': 1.6.4(@swc/helpers@0.5.17) + '@rspack/core': 1.7.11(@swc/helpers@0.5.21) '@rspack/lite-tapable': 1.1.0 - '@swc/helpers': 0.5.17 - core-js: 3.46.0 + '@swc/helpers': 0.5.21 + core-js: 3.47.0 jiti: 2.6.1 - '@rsbuild/plugin-sass@1.4.0(@rsbuild/core@1.6.7)': + '@rsbuild/plugin-sass@1.5.1(@rsbuild/core@1.7.5)': dependencies: - '@rsbuild/core': 1.6.7 deepmerge: 4.3.1 loader-utils: 2.0.4 - postcss: 8.5.6 - reduce-configs: 1.1.1 - sass-embedded: 1.93.3 + postcss: 8.5.9 + reduce-configs: 1.1.2 + sass-embedded: 1.99.0 + optionalDependencies: + '@rsbuild/core': 1.7.5 - '@rsbuild/plugin-svelte@1.0.11(@rsbuild/core@1.6.7)(postcss@8.5.6)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3)': + '@rsbuild/plugin-svelte@1.1.1(@rsbuild/core@1.7.5)(postcss@8.5.9)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3)': dependencies: - '@rsbuild/core': 1.6.7 svelte-loader: 3.2.4(svelte@5.43.14) - svelte-preprocess: 6.0.3(postcss@8.5.6)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3) + svelte-preprocess: 6.0.3(postcss@8.5.9)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3) + optionalDependencies: + '@rsbuild/core': 1.7.5 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -1205,58 +1393,58 @@ snapshots: - svelte - typescript - '@rspack/binding-darwin-arm64@1.6.4': + '@rspack/binding-darwin-arm64@1.7.11': optional: true - '@rspack/binding-darwin-x64@1.6.4': + '@rspack/binding-darwin-x64@1.7.11': optional: true - '@rspack/binding-linux-arm64-gnu@1.6.4': + '@rspack/binding-linux-arm64-gnu@1.7.11': optional: true - '@rspack/binding-linux-arm64-musl@1.6.4': + '@rspack/binding-linux-arm64-musl@1.7.11': optional: true - '@rspack/binding-linux-x64-gnu@1.6.4': + '@rspack/binding-linux-x64-gnu@1.7.11': optional: true - '@rspack/binding-linux-x64-musl@1.6.4': + '@rspack/binding-linux-x64-musl@1.7.11': optional: true - '@rspack/binding-wasm32-wasi@1.6.4': + '@rspack/binding-wasm32-wasi@1.7.11': dependencies: '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@rspack/binding-win32-arm64-msvc@1.6.4': + '@rspack/binding-win32-arm64-msvc@1.7.11': optional: true - '@rspack/binding-win32-ia32-msvc@1.6.4': + '@rspack/binding-win32-ia32-msvc@1.7.11': optional: true - '@rspack/binding-win32-x64-msvc@1.6.4': + '@rspack/binding-win32-x64-msvc@1.7.11': optional: true - '@rspack/binding@1.6.4': + '@rspack/binding@1.7.11': optionalDependencies: - '@rspack/binding-darwin-arm64': 1.6.4 - '@rspack/binding-darwin-x64': 1.6.4 - '@rspack/binding-linux-arm64-gnu': 1.6.4 - '@rspack/binding-linux-arm64-musl': 1.6.4 - '@rspack/binding-linux-x64-gnu': 1.6.4 - '@rspack/binding-linux-x64-musl': 1.6.4 - '@rspack/binding-wasm32-wasi': 1.6.4 - '@rspack/binding-win32-arm64-msvc': 1.6.4 - '@rspack/binding-win32-ia32-msvc': 1.6.4 - '@rspack/binding-win32-x64-msvc': 1.6.4 - - '@rspack/core@1.6.4(@swc/helpers@0.5.17)': - dependencies: - '@module-federation/runtime-tools': 0.21.4 - '@rspack/binding': 1.6.4 + '@rspack/binding-darwin-arm64': 1.7.11 + '@rspack/binding-darwin-x64': 1.7.11 + '@rspack/binding-linux-arm64-gnu': 1.7.11 + '@rspack/binding-linux-arm64-musl': 1.7.11 + '@rspack/binding-linux-x64-gnu': 1.7.11 + '@rspack/binding-linux-x64-musl': 1.7.11 + '@rspack/binding-wasm32-wasi': 1.7.11 + '@rspack/binding-win32-arm64-msvc': 1.7.11 + '@rspack/binding-win32-ia32-msvc': 1.7.11 + '@rspack/binding-win32-x64-msvc': 1.7.11 + + '@rspack/core@1.7.11(@swc/helpers@0.5.21)': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.11 '@rspack/lite-tapable': 1.1.0 optionalDependencies: - '@swc/helpers': 0.5.17 + '@swc/helpers': 0.5.21 '@rspack/lite-tapable@1.1.0': {} @@ -1264,8 +1452,27 @@ snapshots: dependencies: acorn: 8.15.0 - '@swc/helpers@0.5.17': + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@turf/helpers@7.3.4': + dependencies: + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/meta@7.3.4': dependencies: + '@turf/helpers': 7.3.4 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/union@7.3.4': + dependencies: + '@turf/helpers': 7.3.4 + '@turf/meta': 7.3.4 + '@types/geojson': 7946.0.16 + polyclip-ts: 0.16.8 tslib: 2.8.1 '@tybys/wasm-util@0.10.1': @@ -1299,13 +1506,13 @@ snapshots: big.js@5.2.2: {} + bignumber.js@9.3.1: {} + braces@3.0.3: dependencies: fill-range: 7.1.1 optional: true - buffer-builder@0.2.0: {} - cheap-ruler@4.0.0: {} chokidar@4.0.3: @@ -1316,7 +1523,7 @@ snapshots: colorjs.io@0.5.2: {} - core-js@3.46.0: {} + core-js@3.47.0: {} csscolorparser@1.0.3: {} @@ -1325,6 +1532,9 @@ snapshots: detect-libc@1.0.3: optional: true + detect-libc@2.1.2: + optional: true + earcut@3.0.2: {} emojis-list@3.0.0: {} @@ -1335,7 +1545,9 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - fdir@6.5.0: {} + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 fflate@0.8.2: {} @@ -1371,6 +1583,8 @@ snapshots: immutable@5.1.4: {} + immutable@5.1.5: {} + is-extglob@2.1.1: optional: true @@ -1413,7 +1627,7 @@ snapshots: '@mapbox/jsonlint-lines-primitives': 2.0.2 '@mapbox/mapbox-gl-supported': 3.0.0 '@mapbox/point-geometry': 1.1.0 - '@mapbox/tiny-sdf': 2.0.7 + '@mapbox/tiny-sdf': 2.1.0 '@mapbox/unitbezier': 0.0.1 '@mapbox/vector-tile': 2.0.4 '@mapbox/whoots-js': 3.1.0 @@ -1503,11 +1717,19 @@ snapshots: picomatch@2.3.1: optional: true + picomatch@4.0.4: + optional: true + pmtiles@4.3.0: dependencies: fflate: 0.8.2 - postcss@8.5.6: + polyclip-ts@0.16.8: + dependencies: + bignumber.js: 9.3.1 + splaytree-ts: 1.0.2 + + postcss@8.5.9: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -1530,7 +1752,7 @@ snapshots: readdirp@4.1.2: {} - reduce-configs@1.1.1: {} + reduce-configs@1.1.2: {} resolve-protobuf-schema@2.1.0: dependencies: @@ -1548,113 +1770,114 @@ snapshots: dependencies: mri: 1.2.0 - sass-embedded-all-unknown@1.93.3: + sass-embedded-all-unknown@1.99.0: dependencies: - sass: 1.93.3 + sass: 1.99.0 optional: true - sass-embedded-android-arm64@1.93.3: + sass-embedded-android-arm64@1.99.0: optional: true - sass-embedded-android-arm@1.93.3: + sass-embedded-android-arm@1.99.0: optional: true - sass-embedded-android-riscv64@1.93.3: + sass-embedded-android-riscv64@1.99.0: optional: true - sass-embedded-android-x64@1.93.3: + sass-embedded-android-x64@1.99.0: optional: true - sass-embedded-darwin-arm64@1.93.3: + sass-embedded-darwin-arm64@1.99.0: optional: true - sass-embedded-darwin-x64@1.93.3: + sass-embedded-darwin-x64@1.99.0: optional: true - sass-embedded-linux-arm64@1.93.3: + sass-embedded-linux-arm64@1.99.0: optional: true - sass-embedded-linux-arm@1.93.3: + sass-embedded-linux-arm@1.99.0: optional: true - sass-embedded-linux-musl-arm64@1.93.3: + sass-embedded-linux-musl-arm64@1.99.0: optional: true - sass-embedded-linux-musl-arm@1.93.3: + sass-embedded-linux-musl-arm@1.99.0: optional: true - sass-embedded-linux-musl-riscv64@1.93.3: + sass-embedded-linux-musl-riscv64@1.99.0: optional: true - sass-embedded-linux-musl-x64@1.93.3: + sass-embedded-linux-musl-x64@1.99.0: optional: true - sass-embedded-linux-riscv64@1.93.3: + sass-embedded-linux-riscv64@1.99.0: optional: true - sass-embedded-linux-x64@1.93.3: + sass-embedded-linux-x64@1.99.0: optional: true - sass-embedded-unknown-all@1.93.3: + sass-embedded-unknown-all@1.99.0: dependencies: - sass: 1.93.3 + sass: 1.99.0 optional: true - sass-embedded-win32-arm64@1.93.3: + sass-embedded-win32-arm64@1.99.0: optional: true - sass-embedded-win32-x64@1.93.3: + sass-embedded-win32-x64@1.99.0: optional: true - sass-embedded@1.93.3: + sass-embedded@1.99.0: dependencies: - '@bufbuild/protobuf': 2.10.1 - buffer-builder: 0.2.0 + '@bufbuild/protobuf': 2.11.0 colorjs.io: 0.5.2 - immutable: 5.1.4 + immutable: 5.1.5 rxjs: 7.8.2 supports-color: 8.1.1 sync-child-process: 1.0.2 varint: 6.0.0 optionalDependencies: - sass-embedded-all-unknown: 1.93.3 - sass-embedded-android-arm: 1.93.3 - sass-embedded-android-arm64: 1.93.3 - sass-embedded-android-riscv64: 1.93.3 - sass-embedded-android-x64: 1.93.3 - sass-embedded-darwin-arm64: 1.93.3 - sass-embedded-darwin-x64: 1.93.3 - sass-embedded-linux-arm: 1.93.3 - sass-embedded-linux-arm64: 1.93.3 - sass-embedded-linux-musl-arm: 1.93.3 - sass-embedded-linux-musl-arm64: 1.93.3 - sass-embedded-linux-musl-riscv64: 1.93.3 - sass-embedded-linux-musl-x64: 1.93.3 - sass-embedded-linux-riscv64: 1.93.3 - sass-embedded-linux-x64: 1.93.3 - sass-embedded-unknown-all: 1.93.3 - sass-embedded-win32-arm64: 1.93.3 - sass-embedded-win32-x64: 1.93.3 - - sass@1.93.3: + sass-embedded-all-unknown: 1.99.0 + sass-embedded-android-arm: 1.99.0 + sass-embedded-android-arm64: 1.99.0 + sass-embedded-android-riscv64: 1.99.0 + sass-embedded-android-x64: 1.99.0 + sass-embedded-darwin-arm64: 1.99.0 + sass-embedded-darwin-x64: 1.99.0 + sass-embedded-linux-arm: 1.99.0 + sass-embedded-linux-arm64: 1.99.0 + sass-embedded-linux-musl-arm: 1.99.0 + sass-embedded-linux-musl-arm64: 1.99.0 + sass-embedded-linux-musl-riscv64: 1.99.0 + sass-embedded-linux-musl-x64: 1.99.0 + sass-embedded-linux-riscv64: 1.99.0 + sass-embedded-linux-x64: 1.99.0 + sass-embedded-unknown-all: 1.99.0 + sass-embedded-win32-arm64: 1.99.0 + sass-embedded-win32-x64: 1.99.0 + + sass@1.97.3: dependencies: chokidar: 4.0.3 immutable: 5.1.4 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.1 - optional: true - sass@1.97.3: + sass@1.99.0: dependencies: chokidar: 4.0.3 - immutable: 5.1.4 + immutable: 5.1.5 source-map-js: 1.2.1 optionalDependencies: - '@parcel/watcher': 2.5.1 + '@parcel/watcher': 2.5.6 + optional: true source-map-js@1.2.1: {} + splaytree-ts@1.0.2: {} + splaytree@0.1.4: {} subtag@0.5.0: {} @@ -1667,11 +1890,11 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.4(svelte@5.43.14)(typescript@5.9.3): + svelte-check@4.3.4(picomatch@4.0.4)(svelte@5.43.14)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 - fdir: 6.5.0 + fdir: 6.5.0(picomatch@4.0.4) picocolors: 1.1.1 sade: 1.8.1 svelte: 5.43.14 @@ -1692,11 +1915,11 @@ snapshots: svelte-dev-helper: 1.1.9 svelte-hmr: 0.14.12(svelte@5.43.14) - svelte-preprocess@6.0.3(postcss@8.5.6)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3): + svelte-preprocess@6.0.3(postcss@8.5.9)(sass@1.97.3)(svelte@5.43.14)(typescript@5.9.3): dependencies: svelte: 5.43.14 optionalDependencies: - postcss: 8.5.6 + postcss: 8.5.9 sass: 1.97.3 typescript: 5.9.3 @@ -1723,9 +1946,9 @@ snapshots: sync-child-process@1.0.2: dependencies: - sync-message-port: 1.1.3 + sync-message-port: 1.2.0 - sync-message-port@1.1.3: {} + sync-message-port@1.2.0: {} tabbable@5.3.3: {} diff --git a/website/rsbuild.config.ts b/website/rsbuild.config.ts index 24fd483..bb2fe98 100644 --- a/website/rsbuild.config.ts +++ b/website/rsbuild.config.ts @@ -3,11 +3,14 @@ import { pluginSass } from '@rsbuild/plugin-sass'; import { pluginSvelte } from '@rsbuild/plugin-svelte'; export default defineConfig({ - plugins: [pluginSvelte(), pluginSass()], - dev: { - // See: https://github.com/sveltejs/svelte-loader?tab=readme-ov-file#hot-reload - hmr: false, - }, + plugins: [ + pluginSvelte({ + svelteLoaderOptions: { + emitCss: true, + }, + }), + pluginSass(), + ], html: { template: './public/index.html', }, diff --git a/website/src/HeatmapLayer.ts b/website/src/HeatmapLayer.ts index 75d6e05..65de4d6 100644 --- a/website/src/HeatmapLayer.ts +++ b/website/src/HeatmapLayer.ts @@ -4,17 +4,17 @@ import { type Map as MapLibre, } from 'maplibre-gl'; import fragment from './fragment.glsl?raw'; -import { state as sharedState } from './state.svelte.ts'; import { getParentTile, + getPMTilesSource, isTileIntersectingBounds, Log, - PMTILES_SERVER, packFloatToU8s, tileKey, -} from './utils'; +} from './lib/utils'; +import { state as sharedState, state } from './state.svelte.ts'; +import type { TileWorkerEvent } from './TileWorker'; import vertex from './vertex.glsl?raw'; -import type { WorkerEvent } from './Worker'; export type TileGL = { key: string; @@ -49,14 +49,10 @@ type HeatmapState = } | undefined; -const config: { tileSize: number } = { +const tilingConfig: { tileSize: number } = { tileSize: 256, }; -// The average surface area visibile from a point far out at sea, where it can only see sea. -// This is used to fill regions for which there is no elevation data. -const AVERAGE_SURFACE_VISIBILITY = 700000.0; - let fillerTile: TileGL; let heatmapState: HeatmapState; @@ -66,18 +62,14 @@ function initialise() { return; } - const params = new URLSearchParams(self.location.search); - let source = params.get('pmtiles'); - if (!source) { - source = PMTILES_SERVER; - } + const source = getPMTilesSource(); heatmapState.worker.postMessage({ type: 'init', source }); heatmapState.worker.onmessage = onWorkerMessage; makeFillerTile(); } -function onWorkerMessage(event: MessageEvent) { +function onWorkerMessage(event: MessageEvent) { if (heatmapState === undefined) { return; } @@ -96,7 +88,7 @@ function onWorkerMessage(event: MessageEvent) { } } -const HeatmapLayer: CustomLayerInterface = { +export const HeatmapLayer: CustomLayerInterface = { id: 'heatmap-tiles', type: 'custom', renderingMode: '2d', @@ -156,7 +148,7 @@ const HeatmapLayer: CustomLayerInterface = { vertexBuffer, uniforms, tileCache: new Map(), - worker: new Worker(new URL('./Worker.js', import.meta.url)), + worker: new Worker(new URL('./TileWorker.js', import.meta.url)), lastGC: Date.now(), }; @@ -322,7 +314,7 @@ const HeatmapLayer: CustomLayerInterface = { ); gl.uniform1f( heatmapState.uniforms.uAverageSurfaceVisibility, - AVERAGE_SURFACE_VISIBILITY, + state.config.heatmap.averageVisibility, ); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); @@ -367,8 +359,8 @@ function makeTile( heatmapState.gl.TEXTURE_2D, 0, heatmapState.gl.RGBA8UI, - config.tileSize, - config.tileSize, + tilingConfig.tileSize, + tilingConfig.tileSize, 0, heatmapState.gl.RGBA_INTEGER, heatmapState.gl.UNSIGNED_BYTE, @@ -384,12 +376,12 @@ function makeTile( } function makeFillerTile() { - const data = new Uint8Array(config.tileSize ** 2 * 4); - data.set(packFloatToU8s(AVERAGE_SURFACE_VISIBILITY)); + const data = new Uint8Array(tilingConfig.tileSize ** 2 * 4); + data.set(packFloatToU8s(state.config.heatmap.averageVisibility)); const tile = makeTile( 'filler', - AVERAGE_SURFACE_VISIBILITY, + state.config.heatmap.averageVisibility, new LngLatBounds(), data, ); @@ -402,5 +394,3 @@ function makeFillerTile() { fillerTile = tile; } - -export { HeatmapLayer }; diff --git a/website/src/Home.svelte b/website/src/Home.svelte index 0e311e8..51c0406 100644 --- a/website/src/Home.svelte +++ b/website/src/Home.svelte @@ -1,103 +1,46 @@ +
+ +
- { - state.isSearchOpen = false; - state.isInfoOpen = true; - }} - onClose={() => { - state.isInfoOpen = false; - }} - > -

All The Views In The World

-

We've calculated all the views on the planet.

-

- Click on any point to show the longest line of sight at that location -

-

- The lines are the theoretical ideals. They rely on perfect weather - conditions and favourable refraction. -

-

- Heatmap colours: the brighter the more and further you can see. The - darker the less you can see. -

- -

- Click the icon below - to see our curated list of the planet's longest lines of sight -

- -

- See alltheviews.world for more details. -

-
+ {#if state.config.project === "galiano"} + + {/if} - -

Top Lines Of Sight

+ {#if state.config.project === "world"} + + {/if} + + {#if state.config.project === "world"} - In current viewport: - {#if state.longestLineInViewport} - { - event.preventDefault(); - if (state.longestLineInViewport !== undefined) { - const url = state.longestLineInViewport?.toURL(); - render(state.longestLineInViewport.coordinate); - navigate(url); - } - }}>{state.longestLineInViewport?.toDistance()} - {:else if state.bruteForceLoadingLine} - loading... - {:else} - - {/if} -
+ {/if} {#if state.longestLine} - -

Current Line Of Sight

-
-
- Distance: {(state.longestLine.distance || 0) / 1000}km -
-
- Bearing: {state.longestLine.angle}° -
-
- From: {lonLatRound(state.longestLine.from)} -
-
- To: {lonLatRound(state.longestLine.to)} -
-
-
+ {/if} @@ -266,72 +152,23 @@ - -

Acknowledgments

-

- This project was made by Tom and - Ryan. Contact us at - hello@alltheviews.world . -

- -

- The raw data comes from NASA's - SRTM - mission. We used the cleaned version by - Jonathon de Ferranti. -

- -

- The vector map data is hosted by Zsolt Ero's openfreemap.org project. -

-
+ {#if state.config.project === "world"} + + {/if}
-
- { - state.map?.setLayoutProperty( - HeatmapLayer.id, - "visibility", - isToggled ? "visible" : "none", - ); - }} - /> - { - const layers = state.map?.getStyle().layers || []; - for (const layer of layers) { - if (layer.id == "mountain_peaks") continue; - if (layer.id == "background") continue; - if (layer.id == "longest-line-fill") continue; - if (state.map?.getLayer(layer.id)) - state.map?.setLayoutProperty( - layer.id, - "visibility", - isToggled ? "visible" : "none", - ); - } - }} - /> - { - state.map?.setLayoutProperty( - "mountain_peaks", - "visibility", - isToggled ? "visible" : "none", - ); - }} - /> -
+
diff --git a/website/src/Layout.svelte b/website/src/Layout.svelte index ae8a17d..66236ff 100644 --- a/website/src/Layout.svelte +++ b/website/src/Layout.svelte @@ -1,27 +1,14 @@ -
- - +{#if state.config.project === "world"} + +{/if}
- - diff --git a/website/src/LinesDump.svelte b/website/src/LinesDump.svelte index cba6f39..2d7d501 100644 --- a/website/src/LinesDump.svelte +++ b/website/src/LinesDump.svelte @@ -1,9 +1,9 @@ + +
+ { + state.map?.setLayoutProperty( + HeatmapLayer.id, + "visibility", + isToggled ? "visible" : "none", + ); + }} + /> + setVectorVisibility(state, isToggled)} + isToggled={state.config.project === "world"} + /> + { + state.map?.setLayoutProperty( + "mountain_peaks", + "visibility", + isToggled ? "visible" : "none", + ); + }} + isToggled={state.config.project === "world"} + /> +
+ + diff --git a/website/src/components/SearchBox.svelte b/website/src/components/SearchBox.svelte index cc82cd8..812b495 100644 --- a/website/src/components/SearchBox.svelte +++ b/website/src/components/SearchBox.svelte @@ -2,8 +2,8 @@ import { Search } from '@lucide/svelte'; import { MapboxSearchBox } from '@mapbox/search-js-web'; import { onMount } from 'svelte'; + import { disablePointer } from '../lib/utils.ts'; import { state } from '../state.svelte.ts'; - import { disablePointer } from '../utils.ts'; onMount(() => { const searchBox = new MapboxSearchBox(); @@ -114,4 +114,3 @@ } } - diff --git a/website/src/config.ts b/website/src/config.ts new file mode 100644 index 0000000..946acc3 --- /dev/null +++ b/website/src/config.ts @@ -0,0 +1,73 @@ +import { LngLat } from 'maplibre-gl'; +import { getPMTilesSource, getSubdomain } from './lib/utils'; + +export type ViewViewProject = 'world' | 'galiano'; + +type Config = { + project: ViewViewProject; + map: { + minZoom: number; + maxZoom: number; + startingZoom: number; + startingCentre: LngLat; + }; + heatmap: { + // The average surface area visibile from a point far out at sea, where it can only see sea. + // This is used to fill regions for which there is no elevation data. + averageVisibility: number; + defaultContrast: number; + defaultIntensity: number; + }; +}; + +export const worldConfig: Config = { + project: 'world', + map: { + minZoom: 1.6, + maxZoom: 16, + startingZoom: 2.0, + startingCentre: new LngLat(-5.0, 25.0), + }, + heatmap: { + averageVisibility: 700000.0, + defaultContrast: 1 - 0.45, + defaultIntensity: 1 - 0.5, + }, +}; + +export const galianoConfig: Config = { + project: 'galiano', + map: { + minZoom: 11, + maxZoom: 20, + startingZoom: 11.5, + startingCentre: new LngLat(-123.445503, 48.934705), + }, + heatmap: { + averageVisibility: 39000000.0, + defaultContrast: 1 - 0.09, + defaultIntensity: 1 - 0.6, + }, +}; + +export function getViewViewProject() { + const galianoSignature = 'galiano'; + if ( + getSubdomain()?.includes(galianoSignature) || + getPMTilesSource()?.includes(galianoSignature) + ) { + console.log('Using Galiano config'); + return 'galiano'; + } + + return 'world'; +} + +export function getConfig() { + switch (getViewViewProject()) { + case 'galiano': + return galianoConfig; + default: + return worldConfig; + } +} diff --git a/website/src/index.ts b/website/src/index.ts index 2b268d1..c09515a 100644 --- a/website/src/index.ts +++ b/website/src/index.ts @@ -1,9 +1,22 @@ import { mount } from 'svelte'; import App from './App.svelte'; import './styles/index.scss'; +import { state } from './state.svelte'; + +const root = document.getElementById('root') as HTMLElement; + +// Because of MaplibreGL we have to manually handle some post PMR setup +root.innerHTML = ''; const app = mount(App, { - target: document.getElementById('root') as HTMLElement, + target: root, }); +if (import.meta.webpackHot) { + import.meta.webpackHot.accept(); + import.meta.webpackHot.dispose(() => { + state.map?.remove(); + }); +} + export default app; diff --git a/website/src/lib/Viewshed.ts b/website/src/lib/Viewshed.ts new file mode 100644 index 0000000..216b24a --- /dev/null +++ b/website/src/lib/Viewshed.ts @@ -0,0 +1,116 @@ +import union from '@turf/union'; +import type { + Feature, + FeatureCollection, + GeoJsonProperties, + MultiPolygon, + Polygon, +} from 'geojson'; +import type { LngLat } from 'maplibre-gl'; +import proj4 from 'proj4'; +import { aeqdProjectionString, rotate, toRadians } from './utils'; + +export type PolarSegments = { angleID: number; pairs: number[] }; +export const DEFAULT_OPACITY = 0.5; + +export class Viewshed { + id: string; + centre: LngLat; + geoJSON: + | FeatureCollection + | Feature; + scale: number; + colour = '#00ff00'; + isVisible = true; + isLocked = false; + + constructor( + id: string, + centre: LngLat, + scale: number, + polar_segments: PolarSegments[], + ) { + this.id = id; + this.centre = centre; + this.scale = scale; + this.geoJSON = this.generateGeoJSON(polar_segments); + } + + getViewshedLayerID() { + return `viewshed-layer-${this.id}`; + } + getViewshedSourceID() { + return `viewshed-source-${this.id}`; + } + + generateGeoJSON(polar_segments: 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.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); + const polygon: Feature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + latLonStart[0], + latLonStart[1], + latLonEnd[1], + latLonEnd[0], + latLonStart[0], + ], + ], + }, + properties: {}, + }; + + features.push(polygon); + } + } + + return { + type: 'FeatureCollection', + features: features, + } as FeatureCollection; + } + + unionGeoJSON() { + if (this.geoJSON === undefined) return; + const unioned = union(this.geoJSON as FeatureCollection); + if (!unioned) return; + this.geoJSON = unioned; + } + + polarDistanceToPair(angleID: number, distance: number) { + // TODO: Don't assume that the kernel is computing 360 angles. + const angle = angleID - 90; + + 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 rotatedClockwiseLonLat = proj4( + aeqd, + proj4.WGS84, + rotatedClockwiseAEQD, + ); + const rotatedAntiLonLat = proj4( + aeqd, + '+proj=longlat +datum=WGS84 +no_defs', + rotatedAntiAEQD, + ); + + return [rotatedClockwiseLonLat, rotatedAntiLonLat]; + } +} diff --git a/website/src/getLongestLine.ts b/website/src/lib/getLongestLine.ts similarity index 90% rename from website/src/getLongestLine.ts rename to website/src/lib/getLongestLine.ts index afa90bb..7fb0983 100644 --- a/website/src/getLongestLine.ts +++ b/website/src/lib/getLongestLine.ts @@ -8,6 +8,7 @@ import { CDN_BUCKET, clamp, endLoadingSpinner, + getElevationAtCoord, Log, startLoadingSpinner, VERSION, @@ -22,6 +23,7 @@ export type LongestLine = { angle: number; from: LngLat; to: LngLat; + googleEarth: string; }; type IndexedTile = { @@ -123,6 +125,33 @@ async function getLongestLineCandidate(cog: GeoTIFFImage, coordinate: LngLat) { return { distance, angle } as LongestLine; } +export async function makeGoogleEarthLink(line: LongestLine) { + const base = 'https://earth.google.com/web/@'; + const latlon = `${line.from.lat},${line.from.lng}`; + + // Add a bit because sometimes we're placed under the terrain. + const extraAltitude = 10; + + const altitude = (await getElevationAtCoord(line.from)) + extraAltitude; + + // We need a little bit of distance so that the `heading` has something to point at. + const distance = 1; + + // Google Earth measures heading from North. TODO: we should too. + const heading = 90 - line.angle; + + // How wide the camera view is? + const fieldOfView = 15; + + // Whether camera is pointing up or down. 90 is just flat looking at the horizon. + const tilt = 90; + + // Looks like "CgRCAggBQgIIAEoNCP___________wEQAA", I think it's just user-specific session stuff? + const data = ``; + + return `${base}${latlon},${altitude}a,${distance}d,${fieldOfView}y,${heading}h,${tilt}t,0r/data=${data}`; +} + async function getPointFromRaster(cog: GeoTIFFImage, x: number, y: number) { // We need to find the longest line in a 5x5 grid to get around precision loss. const around = 2; diff --git a/website/src/lib/mapConstrains.ts b/website/src/lib/mapConstrains.ts new file mode 100644 index 0000000..d7be0f2 --- /dev/null +++ b/website/src/lib/mapConstrains.ts @@ -0,0 +1,66 @@ +import { LngLat } from 'maplibre-gl'; +import type { AppState } from '../state.svelte'; + +export function transformConstrain(state: AppState) { + return (lngLat: LngLat, zoom: number) => { + return transformConstrainInner(lngLat, zoom, state); + }; +} + +// Custom map bounds that allows hiding most of Antartica, whilst still allowing infinite horizontal +// scroll. +// +// For an official fix, follow: https://github.com/maplibre/maplibre-gl-js/issues/6148 +function transformConstrainInner( + lngLat: LngLat, + zoom: number, + state: AppState, +) { + const latitudeToMercatorY = (latitude: number) => { + return ( + 0.5 - + (0.25 * + Math.log( + (1 + Math.sin((latitude * Math.PI) / 180)) / + (1 - Math.sin((latitude * Math.PI) / 180)), + )) / + Math.PI + ); + }; + + const mercatorYToLatitude = (mercatorY: number) => { + return ( + (360 / Math.PI) * Math.atan(Math.exp((0.5 - mercatorY) * 2 * Math.PI)) - + 90 + ); + }; + + const viewportHeight = state.map?.getContainer().clientHeight; + if (viewportHeight === undefined) { + return { + center: state.config.map.startingCentre, + zoom: state.config.map.startingZoom, + }; + } + + const upperLatitudeBound = 85; + const lowerLatitudeBound = -80; + + const worldSize = 512 * 2 ** zoom; + const mercatorYOffset = viewportHeight / 2 / worldSize; + + const maxMercatorY = latitudeToMercatorY(upperLatitudeBound); + const maxLatitude = mercatorYToLatitude(maxMercatorY + mercatorYOffset); + const minMercatorY = latitudeToMercatorY(lowerLatitudeBound); + const minLatitude = mercatorYToLatitude(minMercatorY - mercatorYOffset); + + const latitude = Math.max(minLatitude, Math.min(maxLatitude, lngLat.lat)); + + return { + center: new LngLat(lngLat.lng, latitude), + zoom: Math.max( + state.config.map.minZoom, + Math.min(state.config.map.maxZoom, zoom), + ), + }; +} diff --git a/website/src/renderLongestLine.ts b/website/src/lib/renderLongestLine.ts similarity index 72% rename from website/src/renderLongestLine.ts rename to website/src/lib/renderLongestLine.ts index 3e07d82..a299f5f 100644 --- a/website/src/renderLongestLine.ts +++ b/website/src/lib/renderLongestLine.ts @@ -1,18 +1,19 @@ import { type GeoJSONFeature, type GeoJSONSource, LngLat } from 'maplibre-gl'; import proj4 from 'proj4'; import { navigate } from 'svelte5-router'; -import { getLongestLine } from './getLongestLine.ts'; -import { state } from './state.svelte.ts'; + +import { state } from '../state.svelte.ts'; +import { getLongestLine, makeGoogleEarthLink } from './getLongestLine.ts'; + import { + ANGLE_SHIFT, aeqdProjectionString, computeBBox, disablePointer, + rotate, toRadians, } from './utils.ts'; -// Inherited from the TVS algorithm. It's to counter unfavourable floating point rounding. -const ANGLE_SHIFT = 0.0001; - export function setupLongestLines(coordFromURL: string | undefined) { state.map?.addSource('longest-line', { type: 'geojson', @@ -59,30 +60,31 @@ function extractCoordFromURL(coordFromURL: string) { } export async function render(lngLat: LngLat) { - const longest_line = await getLongestLine(lngLat); - if (longest_line === undefined) { + const longestLine = await getLongestLine(lngLat); + if (longestLine === undefined) { return; } - state.longestLine = longest_line; + state.longestLine = longestLine; if (import.meta.env.DEV) { - console.log(longest_line); + console.log(longestLine); } - longest_line.angle = longest_line.angle + ANGLE_SHIFT; + longestLine.angle = longestLine.angle + ANGLE_SHIFT; - const θ = toRadians(longest_line.angle); - const dx = longest_line.distance * Math.cos(θ); - const dy = longest_line.distance * Math.sin(θ); + const θ = toRadians(longestLine.angle); + const dx = longestLine.distance * Math.cos(θ); + const dy = longestLine.distance * Math.sin(θ); const rotatedClockwiseAEQD = rotate(dx, dy, -0.5); const rotatedAntiAEQD = rotate(dx, dy, +0.5); const aeqd = aeqdProjectionString(lngLat.lng, lngLat.lat); const unrotated = proj4(aeqd, proj4.WGS84, [dx, dy]); - longest_line.from = lngLat; - longest_line.to = new LngLat(unrotated[0], unrotated[1]); - state.longestLine = longest_line; + longestLine.from = lngLat; + longestLine.to = new LngLat(unrotated[0], unrotated[1]); + longestLine.googleEarth = await makeGoogleEarthLink(longestLine); + state.longestLine = longestLine; const rotatedClockwiseLonLat = proj4(aeqd, proj4.WGS84, rotatedClockwiseAEQD); const rotatedAntiLonLat = proj4( @@ -118,14 +120,6 @@ export async function render(lngLat: LngLat) { disablePointer(); } -// Rotate a coordinate around the origin. -function rotate(x: number, y: number, degrees: number) { - const θ = degrees * (Math.PI / 180); - const cos = Math.cos(θ); - const sin = Math.sin(θ); - return [x * cos - y * sin, x * sin + y * cos]; -} - export function longestLineURL(lon: number, lat: number) { return `/longest/${lon}_${lat}${window.location.search}`; } diff --git a/website/src/lib/renderViewsheds.ts b/website/src/lib/renderViewsheds.ts new file mode 100644 index 0000000..4c4b484 --- /dev/null +++ b/website/src/lib/renderViewsheds.ts @@ -0,0 +1,128 @@ +import { state } from '../state.svelte.ts'; +import type { ViewshedWorkerEvent } from '../ViewshedWorker.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; + + renderAllViewsheds(); + + state.map?.on('click', async (event) => { + if (!state.map) { + return; + } + + removeUnlockedViewsheds(); + + worker.postMessage({ + type: 'getViewshed', + coordinate: event.lngLat, + } as ViewshedWorkerEvent); + }); +} + +export function renderNewViewshed(event: MessageEvent) { + const isViewshedData = + event.data.type === 'setViewshed' || 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') { + state.viewsheds.push(viewshed); + renderAllViewsheds(); + } + + if (event.data.type === 'updateViewshed') { + updateExistingViewshedData(viewshed); + } +} + +function updateExistingViewshedData(viewshed: Viewshed) { + const source = state.map?.getSource(viewshed.getViewshedSourceID()); + // @ts-expect-error: For some reason TS thinks this is for a tile layer. + source?.setData(viewshed.geoJSON); +} + +export function renderAllViewsheds() { + for (const viewshed of state.viewsheds) { + const layer = state.map?.getLayer(viewshed.getViewshedLayerID()); + if (!layer) { + addViewshed(viewshed); + } + + state.map?.setLayoutProperty( + viewshed.getViewshedLayerID(), + 'visibility', + viewshed.isVisible ? 'visible' : 'none', + ); + } +} + +export function highlightViewshed(viewshedToHighlight: Viewshed) { + for (const viewshed of state.viewsheds) { + if (viewshed.id === viewshedToHighlight.id) continue; + + state.map?.setPaintProperty( + viewshed.getViewshedLayerID(), + 'fill-opacity', + 0.05, + ); + } +} + +export function desaturateAllViewsheds() { + for (const viewshed of state.viewsheds) { + state.map?.setPaintProperty( + viewshed.getViewshedLayerID(), + 'fill-opacity', + DEFAULT_OPACITY, + ); + } +} + +export function updateViewshedColour(viewshed: Viewshed) { + state.map?.setPaintProperty( + viewshed.getViewshedLayerID(), + 'fill-color', + viewshed.colour, + ); +} + +export function removeUnlockedViewsheds() { + for (const viewshed of state.viewsheds) { + const layer = state.map?.getLayer(viewshed.getViewshedLayerID()); + + if (layer && !viewshed.isLocked) { + removeViewshed(viewshed); + } + } +} + +export function removeViewshed(viewshed: Viewshed) { + state.viewsheds = state.viewsheds.filter((v) => v !== viewshed); + state.map?.removeLayer(viewshed.getViewshedLayerID()); + state.map?.removeSource(viewshed.getViewshedSourceID()); +} + +function addViewshed(viewshed: Viewshed) { + const sourceID = viewshed.getViewshedSourceID(); + + state.map?.addSource(sourceID, { + type: 'geojson', + data: viewshed.geoJSON, + }); + + state.map?.addLayer({ + id: viewshed.getViewshedLayerID(), + type: 'fill', + source: sourceID, + paint: { + 'fill-color': viewshed.colour, + 'fill-outline-color': 'transparent', + 'fill-opacity': DEFAULT_OPACITY, + }, + }); +} diff --git a/website/src/utils.ts b/website/src/lib/utils.ts similarity index 69% rename from website/src/utils.ts rename to website/src/lib/utils.ts index 05cd4c6..0fbdee6 100644 --- a/website/src/utils.ts +++ b/website/src/lib/utils.ts @@ -1,5 +1,6 @@ import nprogress from 'accessible-nprogress'; import { LngLat, LngLatBounds } from 'maplibre-gl'; +import type { AppState } from '../state.svelte'; export const VERSION = 'ryan-fullworld-raw'; export const CDN_BUCKET = 'https://cdn.alltheviews.world'; @@ -10,10 +11,13 @@ export const PMTILES_SERVER = `${MAP_SERVER}/runs/${VERSION}/pmtiles/${WORLD_PMT // This is for busting Cloudflare asset cache. Like for an updated `world.pmtiles`, // longest lines index, etc. -export const CACHE_BUSTER = '?buster=19:26-20/01/2026'; +export const CACHE_BUSTER = '?buster=23:05-19/04/2026'; export const EARTH_RADIUS = 6371_000.0; +// Inherited from the TVS algorithm. It's to counter unfavourable floating point rounding. +export const ANGLE_SHIFT = 0.0001; + export const Log = { // biome-ignore lint/suspicious/noExplicitAny: needed for debugging. debug: (...data: any[]) => { @@ -148,3 +152,53 @@ export function enablePointer() { if (!root) return; root.classList.remove('disable-pointer'); } + +// Rotate a coordinate around the origin. +export function rotate(x: number, y: number, degrees: number) { + const θ = degrees * (Math.PI / 180); + const cos = Math.cos(θ); + const sin = Math.sin(θ); + return [x * cos - y * sin, x * sin + y * cos]; +} + +export function getSubdomain() { + const parts = window.location.hostname.split('.'); + return parts.length > 2 ? parts[0] : null; +} + +export function getPMTilesSource() { + const params = new URLSearchParams(self.location.search); + const source = params.get('pmtiles'); + if (!source) { + if (getSubdomain()?.includes('galiano')) { + return 'https://pmtiles.alltheviews.world/runs/galiano/pmtiles/galiano'; + } + return PMTILES_SERVER; + } else { + return source; + } +} + +export function setVectorVisibility(state: AppState, isVisible: boolean) { + const layers = state.map?.getStyle().layers || []; + for (const layer of layers) { + if (layer.id === 'mountain_peaks') continue; + if (layer.id === 'background') continue; + if (layer.id === 'longest-line-fill') continue; + if (layer.id.includes('viewshed-')) continue; + if (state.map?.getLayer(layer.id)) + state.map?.setLayoutProperty( + layer.id, + 'visibility', + isVisible ? 'visible' : 'none', + ); + } +} + +export async function getElevationAtCoord(coord: LngLat) { + const base = 'https://api.elevationapi.com/api/Elevation'; + const url = `${base}?lat=${coord.lat}&lon=${coord.lng}&dataSet=NASADEM`; + const result = await fetch(url); + const payload = await result.json(); + return payload.geoPoints[0].elevation; +} diff --git a/website/src/worldLines.ts b/website/src/lib/worldLines.ts similarity index 98% rename from website/src/worldLines.ts rename to website/src/lib/worldLines.ts index 4acb384..79b35f7 100644 --- a/website/src/worldLines.ts +++ b/website/src/lib/worldLines.ts @@ -1,5 +1,6 @@ import type { GeoTIFFImage } from 'geotiff'; import { LngLat, type LngLatBounds } from 'maplibre-gl'; +import { state } from '../state.svelte'; import { convertLngLatToRasterXY, convertRasterXYToLngLat, @@ -7,7 +8,6 @@ import { findTilesIntersectingViewport, } from './getLongestLine'; import { longestLineURL } from './renderLongestLine'; -import { state } from './state.svelte'; import { CACHE_BUSTER, CDN_BUCKET, diff --git a/website/src/map_vector.styles.json b/website/src/map_vector.styles.json index 8cff544..75c3c84 100644 --- a/website/src/map_vector.styles.json +++ b/website/src/map_vector.styles.json @@ -16,6 +16,9 @@ "paint": { "background-color": "#151f41", "background-opacity": 1 + }, + "layout": { + "visibility": "visible" } }, { @@ -34,7 +37,8 @@ ], "text-size": 12, "text-font": ["Noto Sans Regular"], - "text-offset": [0, 1.9] + "text-offset": [0, 1.9], + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -55,7 +59,7 @@ ], "layout": { "line-cap": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -82,7 +86,8 @@ ["==", ["get", "intermittent"], 0] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -108,7 +113,8 @@ ["==", ["get", "intermittent"], 1] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -136,7 +142,8 @@ ["==", ["get", "intermittent"], 0] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -163,7 +170,8 @@ ["==", ["get", "intermittent"], 1] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -191,7 +199,8 @@ ["!=", ["get", "intermittent"], 1] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -218,7 +227,8 @@ ["==", ["get", "intermittent"], 1] ], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "#a0c8f0", @@ -243,6 +253,9 @@ "paint": { "fill-color": "hsl(210,67%,85%)", "fill-opacity": 0.7 + }, + "layout": { + "visibility": "none" } }, { @@ -256,7 +269,8 @@ ["match", ["get", "class"], ["service", "track"], true, false] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#cfcdca", @@ -286,7 +300,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(200, 147, 102, 1)", @@ -317,7 +332,8 @@ ["==", ["get", "class"], "minor"] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#cfcdca", @@ -356,7 +372,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -389,7 +406,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -418,7 +436,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -449,7 +468,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -498,6 +518,9 @@ 20, 4 ] + }, + "layout": { + "visibility": "none" } }, { @@ -512,7 +535,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(244, 209, 158, 1)", @@ -542,7 +566,8 @@ ["match", ["get", "class"], ["service", "track"], true, false] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff", @@ -577,7 +602,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff4c6", @@ -607,7 +633,8 @@ ["==", ["get", "class"], "minor"] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff", @@ -637,7 +664,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff4c6", @@ -666,7 +694,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff4c6", @@ -695,7 +724,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#ffdaa6", @@ -736,6 +766,9 @@ 20, 2 ] + }, + "layout": { + "visibility": "none" } }, { @@ -747,7 +780,8 @@ "filter": ["match", ["get", "class"], ["taxiway"], true, false], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(153, 153, 153, 1)", @@ -772,7 +806,8 @@ "filter": ["match", ["get", "class"], ["runway"], true, false], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(153, 153, 153, 1)", @@ -802,6 +837,9 @@ "paint": { "fill-color": "rgba(255, 255, 255, 1)", "fill-opacity": ["interpolate", ["linear"], ["zoom"], 13, 0, 14, 1] + }, + "layout": { + "visibility": "none" } }, { @@ -823,7 +861,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(255, 255, 255, 1)", @@ -858,7 +897,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "rgba(255, 255, 255, 1)", @@ -885,7 +925,7 @@ ["==", ["get", "class"], "pier"] ], "layout": { - "visibility": "visible" + "visibility": "none" }, "paint": { "fill-antialias": true, @@ -911,7 +951,7 @@ "layout": { "line-cap": "round", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#f8f4f0", @@ -941,6 +981,9 @@ "fill-color": "hsla(0,0%,89%,0.56)", "fill-opacity": 0.9, "fill-outline-color": "#cfcdca" + }, + "layout": { + "visibility": "none" } }, { @@ -957,7 +1000,7 @@ "layout": { "line-cap": "round", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -998,7 +1041,7 @@ "layout": { "line-cap": "round", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1038,7 +1081,7 @@ "layout": { "line-cap": "round", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#cfcdca", @@ -1072,7 +1115,7 @@ "layout": { "line-cap": "butt", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1103,7 +1146,7 @@ "layout": { "line-cap": "butt", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1138,7 +1181,7 @@ "layout": { "line-cap": "butt", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1174,7 +1217,7 @@ "layout": { "line-cap": "butt", "line-join": "round", - "visibility": "visible" + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1225,6 +1268,9 @@ 20, 4 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1241,7 +1287,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fc8", @@ -1280,7 +1327,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -1318,7 +1366,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff", @@ -1349,7 +1398,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -1386,7 +1436,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -1423,7 +1474,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -1461,7 +1513,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fc8", @@ -1506,6 +1559,9 @@ 20, 1 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1539,6 +1595,9 @@ 20, 6 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1569,6 +1628,9 @@ 20, 1 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1602,6 +1664,9 @@ 20, 6 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1635,6 +1700,9 @@ 20, 2 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1669,6 +1737,9 @@ 20, 8 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1683,7 +1754,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1721,7 +1793,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1753,7 +1826,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1785,7 +1859,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "hsl(28,76%,67%)", @@ -1816,7 +1891,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#e9ac77", @@ -1854,7 +1930,8 @@ ], "layout": { "line-cap": "butt", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#cfcdca", @@ -1902,6 +1979,9 @@ 20, 18 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1933,6 +2013,9 @@ 20, 4 ] + }, + "layout": { + "visibility": "none" } }, { @@ -1947,7 +2030,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fc8", @@ -1984,7 +2068,8 @@ ["==", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -2022,7 +2107,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fff", @@ -2052,7 +2138,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -2081,7 +2168,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fea", @@ -2110,7 +2198,8 @@ ["!=", ["get", "ramp"], 1] ], "layout": { - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "#fc8", @@ -2150,6 +2239,9 @@ 20, 2 ] + }, + "layout": { + "visibility": "none" } }, { @@ -2176,6 +2268,9 @@ 20, 8 ] + }, + "layout": { + "visibility": "none" } }, { @@ -2186,7 +2281,8 @@ "minzoom": 13, "filter": ["==", ["get", "subclass"], "cable_car"], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "hsl(0,0%,70%)", @@ -2201,7 +2297,8 @@ "minzoom": 13, "filter": ["==", ["get", "subclass"], "cable_car"], "layout": { - "line-cap": "round" + "line-cap": "round", + "visibility": "none" }, "paint": { "line-color": "hsl(0,0%,70%)", @@ -2227,6 +2324,9 @@ "line-color": "hsl(0,0%,70%)", "line-dasharray": [1, 1], "line-width": ["interpolate", ["linear", 1], ["zoom"], 7, 1, 11, 2] + }, + "layout": { + "visibility": "none" } }, { @@ -2243,7 +2343,8 @@ ], "layout": { "line-cap": "round", - "line-join": "round" + "line-join": "round", + "visibility": "none" }, "paint": { "line-color": "hsl(248,7%,66%)", @@ -2265,6 +2366,9 @@ "line-color": "hsl(248,7%,66%)", "line-dasharray": [1, 2], "line-width": ["interpolate", ["linear"], ["zoom"], 3, 1, 5, 1.2, 12, 3] + }, + "layout": { + "visibility": "none" } }, { @@ -2299,7 +2403,8 @@ "icon-rotation-alignment": "map", "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], "symbol-placement": "line", - "symbol-spacing": 75 + "symbol-spacing": 75, + "visibility": "none" }, "paint": { "icon-opacity": 0.5 @@ -2337,7 +2442,8 @@ "icon-rotation-alignment": "map", "icon-size": ["interpolate", ["linear"], ["zoom"], 15, 0.5, 19, 1], "symbol-placement": "line", - "symbol-spacing": 75 + "symbol-spacing": 75, + "visibility": "none" }, "paint": { "icon-opacity": 0.5 @@ -2369,7 +2475,7 @@ "text-letter-spacing": 0.2, "text-max-width": 5, "text-size": 14, - "visibility": "visible" + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2400,7 +2506,7 @@ "text-letter-spacing": 0.2, "text-max-width": 5, "text-size": ["interpolate", ["linear"], ["zoom"], 0, 10, 8, 14], - "visibility": "visible" + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2433,7 +2539,7 @@ "text-letter-spacing": 0.2, "text-max-width": 5, "text-size": 14, - "visibility": "visible" + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2470,7 +2576,8 @@ "text-font": ["Noto Sans Italic"], "text-max-width": 9, "text-offset": [0, 0.6], - "text-size": 12 + "text-size": 12, + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2509,7 +2616,8 @@ "text-font": ["Noto Sans Italic"], "text-max-width": 9, "text-offset": [0, 0.6], - "text-size": 12 + "text-size": 12, + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2548,7 +2656,8 @@ "text-font": ["Noto Sans Italic"], "text-max-width": 9, "text-offset": [0, 0.6], - "text-size": 12 + "text-size": 12, + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2574,7 +2683,8 @@ ], "text-font": ["Noto Sans Regular"], "text-rotation-alignment": "map", - "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13] + "text-size": ["interpolate", ["linear"], ["zoom"], 13, 12, 14, 13], + "visibility": "none" }, "paint": { "text-color": "hsl(30,23%,62%)", @@ -2614,7 +2724,8 @@ "text-field": ["to-string", ["get", "ref"]], "text-font": ["Noto Sans Regular"], "text-rotation-alignment": "viewport", - "text-size": 10 + "text-size": 10, + "visibility": "none" } }, { @@ -2649,7 +2760,8 @@ "text-field": ["to-string", ["get", "ref"]], "text-font": ["Noto Sans Regular"], "text-rotation-alignment": "viewport", - "text-size": 10 + "text-size": 10, + "visibility": "none" } }, { @@ -2674,7 +2786,8 @@ "text-offset": [0, 0.6], "text-optional": true, "text-padding": 2, - "text-size": 12 + "text-size": 12, + "visibility": "none" }, "paint": { "text-color": "#fff", @@ -2707,7 +2820,8 @@ "text-letter-spacing": 0.1, "text-max-width": 9, "text-size": ["interpolate", ["linear"], ["zoom"], 8, 9, 12, 10], - "text-transform": "uppercase" + "text-transform": "uppercase", + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2745,7 +2859,8 @@ 10, 11, 12 - ] + ], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2783,7 +2898,8 @@ 12, 11, 14 - ] + ], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2811,7 +2927,8 @@ "text-letter-spacing": 0.2, "text-max-width": 9, "text-size": ["interpolate", ["linear"], ["zoom"], 5, 10, 8, 14], - "text-transform": "uppercase" + "text-transform": "uppercase", + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2856,7 +2973,8 @@ 13, 11, 18 - ] + ], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2901,7 +3019,8 @@ 14, 11, 20 - ] + ], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2931,7 +3050,8 @@ ], "text-font": ["Noto Sans Bold"], "text-max-width": 6.25, - "text-size": ["interpolate", ["linear"], ["zoom"], 3, 9, 7, 17] + "text-size": ["interpolate", ["linear"], ["zoom"], 3, 9, 7, 17], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2960,7 +3080,8 @@ ], "text-font": ["Noto Sans Bold"], "text-max-width": 6.25, - "text-size": ["interpolate", ["linear"], ["zoom"], 2, 9, 5, 17] + "text-size": ["interpolate", ["linear"], ["zoom"], 2, 9, 5, 17], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", @@ -2989,7 +3110,8 @@ ], "text-font": ["Noto Sans Bold"], "text-max-width": 6.25, - "text-size": ["interpolate", ["linear"], ["zoom"], 1, 9, 4, 17] + "text-size": ["interpolate", ["linear"], ["zoom"], 1, 9, 4, 17], + "visibility": "none" }, "paint": { "text-color": "#ebebeb", diff --git a/website/src/modals/Acknowledgements.svelte b/website/src/modals/Acknowledgements.svelte new file mode 100644 index 0000000..b9c1b90 --- /dev/null +++ b/website/src/modals/Acknowledgements.svelte @@ -0,0 +1,26 @@ + + + +

Acknowledgments

+

+ This project was made by Tom and + Ryan. Contact us at + hello@alltheviews.world . +

+ +

+ The raw data comes from NASA's + SRTM + mission. We used the cleaned version by + Jonathon de Ferranti. +

+ +

+ The vector map data is hosted by Zsolt Ero's openfreemap.org project. +

+
diff --git a/website/src/modals/CurrentLine.svelte b/website/src/modals/CurrentLine.svelte new file mode 100644 index 0000000..a3eac1b --- /dev/null +++ b/website/src/modals/CurrentLine.svelte @@ -0,0 +1,53 @@ + + + + {#if state.longestLine} +

Current Line Of Sight

+
+
+ Distance: {(state.longestLine.distance || 0) / 1000}km +
+
+ Bearing: {state.longestLine.angle}° +
+
+ From: {lonLatRound(state.longestLine.from)} +
+
+ To: {lonLatRound(state.longestLine.to)} +
+
+ + + {/if} +
+ + diff --git a/website/src/modals/TopLines.svelte b/website/src/modals/TopLines.svelte new file mode 100644 index 0000000..efb61bc --- /dev/null +++ b/website/src/modals/TopLines.svelte @@ -0,0 +1,51 @@ + + + +

Top Lines Of Sight

+ + In current viewport: + {#if state.longestLineInViewport} + { + event.preventDefault(); + if (state.longestLineInViewport !== undefined) { + const url = state.longestLineInViewport?.toURL(); + render(state.longestLineInViewport.coordinate); + navigate(url); + } + }}>{state.longestLineInViewport?.toDistance()} + {:else if state.bruteForceLoadingLine} + loading... + {:else} + + {/if} +
+ + diff --git a/website/src/TopLines.svelte b/website/src/modals/TopLinesContent.svelte similarity index 95% rename from website/src/TopLines.svelte rename to website/src/modals/TopLinesContent.svelte index 23db81f..f5fad85 100644 --- a/website/src/TopLines.svelte +++ b/website/src/modals/TopLinesContent.svelte @@ -1,8 +1,8 @@ + + +

Viewsheds

+ {#if state.viewsheds.length == 0} + Click any point to render its viewshed + {/if} + + {#if state.viewsheds.length > 0} +
+ {#each state.viewsheds as viewshed} +
{ + if (viewshed.isVisible) { + highlightViewshed(viewshed); + } + }} + onmouseleave={() => { + desaturateAllViewsheds(); + }} + > + + + + + + + {viewshed.centre.lat.toFixed(5)},{viewshed.centre.lng.toFixed(5)} +
+ {/each} +
+ {/if} +
+ + diff --git a/website/src/modals/Welcome.svelte b/website/src/modals/Welcome.svelte new file mode 100644 index 0000000..e3d18fa --- /dev/null +++ b/website/src/modals/Welcome.svelte @@ -0,0 +1,38 @@ + + + { + state.isSearchOpen = false; + state.isInfoOpen = true; + }} + onClose={() => { + state.isInfoOpen = false; + }} +> +

All The Views In The World

+

We've calculated all the views on the planet.

+

Click on any point to show the longest line of sight at that location

+

+ The lines are the theoretical ideals. They rely on perfect weather + conditions and favourable refraction. +

+

+ Heatmap colours: the brighter the more and further you can see. The darker + the less you can see. +

+ +

+ Click the icon below to + see our curated list of the planet's longest lines of sight +

+ +

+ See alltheviews.world for more details. +

+
diff --git a/website/src/state.svelte.ts b/website/src/state.svelte.ts index 693b017..ae1a22d 100644 --- a/website/src/state.svelte.ts +++ b/website/src/state.svelte.ts @@ -1,36 +1,45 @@ import type { Map as MapLibre } from 'maplibre-gl'; -import type { LongestLine } from './getLongestLine'; -import type { LongestLineH3 } from './worldLines'; +import { getConfig } from './config'; +import type { LongestLine } from './lib/getLongestLine'; +import type { Viewshed } from './lib/Viewshed'; +import type { LongestLineH3 } from './lib/worldLines'; + +const config = getConfig(); export type HeatmapConfig = { contrast: number; intensity: number; }; -let map: MapLibre | undefined; -let worldLongestLines: LongestLineH3[] | undefined; -let longestLine: LongestLine | undefined; -let longestLineInViewport: LongestLineH3 | undefined; -const isFirstInteraction = false; -const bruteForceLoadingLine = false; -const heatmapConfig: HeatmapConfig = { - contrast: 1 - 0.45, - intensity: 1 - 0.5, -}; -const isFlying = false; -// Mobile-only events -const isSearchOpen = false; -const isInfoOpen = true; +export interface AppState { + map: MapLibre | undefined; + config: ReturnType; + worldLongestLines: LongestLineH3[] | undefined; + longestLine: LongestLine | undefined; + longestLineInViewport: LongestLineH3 | undefined; + isFirstInteraction: boolean; + bruteForceLoadingLine: boolean; + heatmapConfig: HeatmapConfig; + viewsheds: Viewshed[]; + isFlying: boolean; + isSearchOpen: boolean; + isInfoOpen: boolean; +} -export const state = $state({ - map, - worldLongestLines, - longestLine, - longestLineInViewport, - isFirstInteraction, - bruteForceLoadingLine, - heatmapConfig, - isFlying, - isSearchOpen, - isInfoOpen, +export const state = $state({ + map: undefined, + config, + worldLongestLines: undefined, + longestLine: undefined, + longestLineInViewport: undefined, + isFirstInteraction: false, + bruteForceLoadingLine: false, + heatmapConfig: { + contrast: config.heatmap.defaultContrast, + intensity: config.heatmap.defaultIntensity, + }, + viewsheds: [], + isFlying: false, + isSearchOpen: false, + isInfoOpen: true, }); diff --git a/website/src/styles/index.scss b/website/src/styles/index.scss index 4c03016..ece1d5e 100644 --- a/website/src/styles/index.scss +++ b/website/src/styles/index.scss @@ -20,6 +20,12 @@ html { pointer-events: none; } +.unclickable_icon { + display: inline-block; + scale: 0.7; + translate: 0px 6px; +} + a { color: color.adjust($primary-colour, $lightness: -15%); &:hover { @@ -61,4 +67,4 @@ a { line-height: 1.2; padding: 2px 4px; } -} \ No newline at end of file +}