diff --git a/Cargo.lock b/Cargo.lock index f4ec22f..6f7f1a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -464,6 +481,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -593,8 +616,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -604,8 +629,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -737,6 +784,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -745,6 +793,23 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", ] [[package]] @@ -753,13 +818,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -875,6 +948,12 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "itoa" version = "1.0.18" @@ -984,6 +1063,12 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.2.0" @@ -1088,7 +1173,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.7", "smallvec", "zeroize", ] @@ -1170,7 +1255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1261,6 +1346,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.19", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.19", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -1290,7 +1431,18 @@ checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1300,7 +1452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1312,6 +1464,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1347,6 +1514,47 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + [[package]] name = "ring" version = "0.17.14" @@ -1382,7 +1590,7 @@ dependencies = [ [[package]] name = "rs_maps" -version = "0.0.6" +version = "0.1.0" dependencies = [ "anyhow", "argon2", @@ -1392,7 +1600,8 @@ dependencies = [ "lettre", "mime_guess", "password-hash", - "rand", + "rand 0.8.7", + "reqwest", "rust-embed", "serde", "serde_json", @@ -1423,7 +1632,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1465,6 +1674,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustls" version = "0.23.43" @@ -1486,6 +1701,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ + "web-time", "zeroize", ] @@ -1658,7 +1874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1823,7 +2039,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.7", "rsa", "serde", "sha1", @@ -1863,7 +2079,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.7", "serde", "serde_json", "sha2 0.10.9", @@ -1953,6 +2169,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2118,6 +2337,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -2158,12 +2391,15 @@ checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", + "futures-util", "http", "http-body", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", + "url", ] [[package]] @@ -2208,7 +2444,7 @@ dependencies = [ "futures", "http", "parking_lot", - "rand", + "rand 0.8.7", "serde", "serde_json", "thiserror 2.0.19", @@ -2305,6 +2541,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.1" @@ -2408,6 +2650,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2433,6 +2684,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.126" @@ -2465,6 +2726,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/Cargo.toml b/Cargo.toml index e15146c..85c16d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # Cargo.toml [package] name = "rs_maps" -version = "0.0.6" +version = "0.1.0" edition = "2021" [dependencies] @@ -23,6 +23,13 @@ tower-sessions-sqlx-store = { version = "0.15", features = ["postgres"] } argon2 = "0.5" password-hash = { version = "0.5", features = ["std"] } rand = "0.8" +# HTTP client for proxying route requests to brouter.de. rustls-tls-manual-roots +# is avoided; ring + webpki roots keeps one crypto provider across the binary, +# matching the sqlx and lettre feature choices above. +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls-webpki-roots", "json", "charset", "http2", +] } + sha2 = "0.10" subtle = "2" hex = "0.4" diff --git a/frontend/app.js b/frontend/app.js index f0f9ec4..1a008ed 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -50,6 +50,7 @@ const state = { draftLatLng: null, colour: DEFAULT_COLOUR, searchPin: null, // transient pin for the current search hit + draw: { active: false, points: [], markers: [], line: null }, }; /* ───────────────────────────── api ───────────────────────────── */ @@ -229,7 +230,10 @@ function initMap() { state.map = L.map('map', { zoomControl: true }).setView([54.5, -3.0], 6); L.tileLayer(tileUrl, { maxZoom: 19, attribution: TILE_ATTRIB }).addTo(state.map); - state.map.on('click', (e) => openMarkerSheet(null, e.latlng)); + state.map.on('click', (e) => { + if (state.draw.active) { addWaypoint(e.latlng); return; } + openMarkerSheet(null, e.latlng); + }); state.map.on('move zoom', updateReadout); updateReadout(); } @@ -455,6 +459,139 @@ $('#marker-delete').addEventListener('click', async () => { }); + +/* ─────────────────────── route drawing (snapped) ─────────────────────── */ + +/* Waypoints are snapped server-side by brouter.de via POST /api/routes/calculate. + Nothing is snapped locally: the straight guide line below is only a preview of + the order of points, not the route that gets saved. */ + +const DRAW_COLOUR = '#4FA3D1'; + +function drawReset() { + state.draw.markers.forEach((m) => state.map.removeLayer(m)); + if (state.draw.line) state.map.removeLayer(state.draw.line); + state.draw = { active: false, points: [], markers: [], line: null }; +} + +function drawSetActive(on) { + if (!on) { drawReset(); } + state.draw.active = on; + + $('#draw-panel').hidden = !on; + $('#draw-start').textContent = on ? 'Drawing…' : 'New route'; + $('#draw-start').disabled = on; + // Crosshair makes the mode obvious; without it the map looks unchanged. + state.map.getContainer().style.cursor = on ? 'crosshair' : ''; + if (on) drawUpdate(); +} + +/* Straight-line distance through the waypoints. Not the routed distance — the + real figure is only known once brouter has answered, so this is labelled as + a direct measurement rather than presented as the route length. */ +function directDistance(points) { + let metres = 0; + for (let i = 1; i < points.length; i += 1) { + metres += state.map.distance(points[i - 1], points[i]); + } + return metres; +} + +function fmtDistance(metres) { + if (!metres) return '—'; + return metres < 1000 + ? `${Math.round(metres)} m direct` + : `${(metres / 1000).toFixed(1)} km direct`; +} + +function drawUpdate() { + const points = state.draw.points; + $('#draw-n').textContent = points.length; + $('#draw-dist').textContent = fmtDistance(directDistance(points)); + $('#draw-save').disabled = points.length < 2; + $('#draw-undo').disabled = points.length === 0; + + const latlngs = points.map((p) => [p.lat, p.lng]); + if (state.draw.line) { + state.draw.line.setLatLngs(latlngs); + } else { + state.draw.line = L.polyline(latlngs, { + color: DRAW_COLOUR, weight: 2, dashArray: '4 5', interactive: false, + }).addTo(state.map); + } +} + +function addWaypoint(latlng) { + state.draw.points.push(latlng); + + const index = state.draw.points.length - 1; + const marker = L.marker(latlng, { + draggable: true, + icon: L.divIcon({ + className: 'wp-icon', + html: `
0 waypoints · —
+Click the map to add points. Drag any point to move it.
+No routes on the server yet.
diff --git a/frontend/style.css b/frontend/style.css index 9049358..a5e7df0 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -540,3 +540,32 @@ button.link:hover { color: var(--paper); background: none; } border-top: 1px solid var(--hair); font-size: 12px; color: var(--paper-dim); } + +/* ───────────────────────── route drawing ───────────────────────── */ + +#draw-panel { margin-top: 8px; display: flex; flex-direction: column; gap: 8px; } +#draw-panel label { display: block; } + +.draw-count { + margin: 0; + font-family: var(--mono); font-size: 11px; + letter-spacing: .06em; color: var(--paper-dim); +} + +.hint { font-size: 11.5px; color: var(--paper-dim); } + +/* Numbered waypoint handles. Distinct from place pins and from the amber search + pin so the three never read as the same kind of thing. */ +.wp-icon { background: none; border: none; } +.wp { + width: 22px; height: 22px; + display: flex; align-items: center; justify-content: center; + border-radius: 50%; + background: #4FA3D1; + color: #08131b; + font-family: var(--mono); font-size: 11px; font-weight: 600; + border: 2px solid rgba(255,255,255,.85); + box-shadow: 0 1px 4px rgba(0,0,0,.45); + cursor: grab; +} +.wp:active { cursor: grabbing; } diff --git a/src/error.rs b/src/error.rs index cc2b0a8..0140f99 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,12 @@ pub enum AppError { #[error("payload too large")] TooLarge, + /// An upstream service we proxy to (currently brouter.de) failed or was + /// unreachable. Distinct from Internal so the user is told it's not their + /// request that's at fault. + #[error("{0}")] + BadGateway(String), + #[error(transparent)] Database(#[from] sqlx::Error), @@ -42,6 +48,7 @@ impl IntoResponse for AppError { AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m.clone()), AppError::Conflict(m) => (StatusCode::CONFLICT, m.clone()), AppError::TooLarge => (StatusCode::PAYLOAD_TOO_LARGE, self.to_string()), + AppError::BadGateway(m) => (StatusCode::BAD_GATEWAY, m.clone()), other => { // Internal detail stays in the log, never in the response body. tracing::error!(error = %other, "internal error"); diff --git a/src/gpx.rs b/src/gpx.rs index 851f849..4a18741 100644 --- a/src/gpx.rs +++ b/src/gpx.rs @@ -34,7 +34,7 @@ pub struct GpxFile { // ---------------------------------------------------------------- name safety /// Accepts only a plain filename: no separators, no traversal, must be a .gpx. -fn validate_filename(name: &str) -> AppResult<()> { +pub fn validate_filename(name: &str) -> AppResult<()> { let bad = |m: &str| Err(AppError::BadRequest(m.to_string())); if name.is_empty() || name.len() > 120 { @@ -81,7 +81,7 @@ fn split_name(name: &str) -> (String, String) { /// Reserves a free filename atomically. `create_new` fails if the path exists, /// so there's no check-then-write race: track.gpx → track-1.gpx → track-2.gpx. -fn reserve(dir: &StdPath, name: &str) -> AppResult<(std::fs::File, String)> { +pub fn reserve(dir: &StdPath, name: &str) -> AppResult<(std::fs::File, String)> { let (stem, ext) = split_name(name); for n in 0..MAX_COLLISION_ATTEMPTS { diff --git a/src/main.rs b/src/main.rs index c1bb3cd..b676950 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod gpx; mod mail; mod markers; mod ratelimit; +mod routes; mod web; use std::net::SocketAddr; @@ -98,10 +99,16 @@ async fn main() -> Result<()> { .merge(auth_routes) .route("/config", get(web::client_config)) .nest("/markers", markers::routes()) - .nest("/gpx", gpx::routes()); + .nest("/gpx", gpx::routes()) + .nest("/routes", routes::routes()); let app = Router::new() .nest("/api", api) + // Explicit root route. Inside a nest, "/maps/" reduces to "/" once the + // prefix is stripped, and that empty path is the one case the fallback + // below does not catch — which happens to be the exact URL a browser + // requests. "/maps" and "/maps/anything" work via the fallback. + .route("/", get(web::serve_asset)) .fallback(web::serve_asset) .layer(axum::middleware::from_fn_with_state( state.clone(), diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..140f00d --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,225 @@ +// src/routes.rs +// +// Snapped route creation. Waypoints in, a GPX file in GPX_DIR out. +// +// The calculation itself is delegated to the public BRouter instance at +// brouter.de. Self-hosting BRouter was considered and rejected: it needs a Java +// process plus ~1GB of pre-generated .rd5 segment files for Britain alone, which +// is poor value for a feature used a handful of times a year. +// +// The call is proxied rather than made from the browser so that calculating and +// saving are one action: brouter returns GPX bytes and we write them straight +// into the shared folder, instead of round-tripping them back through +// /api/gpx/upload. + +use std::time::Duration; + +use axum::extract::State; +use axum::routing::post; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; + +use crate::auth::CurrentUser; +use crate::error::{AppError, AppResult}; +use crate::gpx; +use crate::AppState; + +const BROUTER_URL: &str = "https://brouter.de/brouter"; + +/// Hiking only, per the build brief. BRouter's own profile name. +const PROFILE: &str = "hiking-beta"; + +/// Two is a line; beyond this the URL gets unwieldy and the request slow. +const MAX_WAYPOINTS: usize = 50; + +/// brouter.de is donated infrastructure and can be slow under load. Long enough +/// to be useful, short enough that a hung request doesn't tie up a connection. +const TIMEOUT: Duration = Duration::from_secs(30); + +/// A generous ceiling for a returned track — a long multi-day route with +/// elevation runs to a few MB at most. +const MAX_RESPONSE_BYTES: usize = 20 * 1024 * 1024; + +#[derive(Debug, Deserialize)] +pub struct Waypoint { + pub lat: f64, + pub lon: f64, +} + +#[derive(Debug, Deserialize)] +pub struct CalculateRequest { + pub waypoints: Vec