diff --git a/Cargo.lock b/Cargo.lock index e4bf79c057..31ceb563e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,6 +1248,7 @@ dependencies = [ "downcast-rs", "enumset", "futures-channel", + "geo", "geojson", "geom", "getrandom", diff --git a/game/Cargo.toml b/game/Cargo.toml index 45eaa2a865..9ca85222bd 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -27,6 +27,7 @@ csv = "1.1.4" downcast-rs = "1.2.0" enumset = "1.0.3" futures-channel = { version = "0.3.12"} +geo = "0.18" geojson = { version = "0.22.0", features = ["geo-types"] } geom = { path = "../geom" } getrandom = { version = "0.2.3", optional = true } diff --git a/game/src/ltn/browse.rs b/game/src/ltn/browse.rs index 7bdc952461..d565e19475 100644 --- a/game/src/ltn/browse.rs +++ b/game/src/ltn/browse.rs @@ -1,8 +1,10 @@ use std::collections::HashSet; +use anyhow::Result; + use abstutil::Timer; -use geom::Distance; -use map_gui::tools::{CityPicker, DrawRoadLabels, Navigator, URLManager}; +use geom::{Distance, PolyLine, Pt2D}; +use map_gui::tools::{CityPicker, DrawRoadLabels, Navigator, PopupMsg, URLManager}; use widgetry::mapspace::{ToggleZoomed, World, WorldOutcome}; use widgetry::{ lctrl, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Outcome, Panel, State, TextExt, @@ -42,6 +44,10 @@ impl BrowseNeighborhoods { .align_right(), ]), Toggle::checkbox(ctx, "highlight boundary roads", Key::H, true), + ctx.style() + .btn_outline + .text("Export to GeoJSON") + .build_def(ctx), ])) .aligned(HorizontalAlignment::Left, VerticalAlignment::Top) .build(ctx); @@ -77,6 +83,18 @@ impl State for BrowseNeighborhoods { "search" => { return Transition::Push(Navigator::new_state(ctx, app)); } + "Export to GeoJSON" => { + return Transition::Push(match export_geojson(app) { + Ok(path) => PopupMsg::new_state( + ctx, + "LTNs exported", + vec![format!("Data exported to {}", path)], + ), + Err(err) => { + PopupMsg::new_state(ctx, "Export failed", vec![err.to_string()]) + } + }); + } _ => unreachable!(), } } @@ -165,3 +183,100 @@ fn draw_boundary_roads(ctx: &EventCtx, app: &App) -> ToggleZoomed { } batch.build(ctx) } + +fn export_geojson(app: &App) -> Result { + if cfg!(target_arch = "wasm32") { + bail!("Export only supported in the installed version"); + } + + use geo::algorithm::map_coords::MapCoordsInplace; + use geojson::{Feature, FeatureCollection, GeoJson, Geometry, Value}; + use std::io::Write; + + let map = &app.primary.map; + let mut features = Vec::new(); + + // All neighborhood boundaries + for (_, (block, color)) in &app.session.partitioning.neighborhoods { + let mut feature = Feature { + bbox: None, + geometry: Some(block.polygon.to_geojson(None)), + id: None, + properties: None, + foreign_members: None, + }; + feature.set_property("type", "neighborhood"); + feature.set_property("fill", color.as_hex()); + features.push(feature); + } + + // TODO Cells per neighborhood -- contouring the gridded version is hard! + + // All modal filters + for (r, dist) in &app.session.modal_filters.roads { + let road = map.get_r(*r); + if let Ok((pt, angle)) = road.center_pts.dist_along(*dist) { + let road_width = road.get_width(); + let pl = PolyLine::must_new(vec![ + pt.project_away(0.8 * road_width, angle.rotate_degs(90.0)), + pt.project_away(0.8 * road_width, angle.rotate_degs(-90.0)), + ]); + let mut feature = Feature { + bbox: None, + geometry: Some(pl.to_geojson(None)), + id: None, + properties: None, + foreign_members: None, + }; + feature.set_property("type", "road filter"); + feature.set_property("stroke", "red"); + features.push(feature); + } + } + for (_, filter) in &app.session.modal_filters.intersections { + let pl = filter.geometry(map).to_polyline(); + let mut feature = Feature { + bbox: None, + geometry: Some(pl.to_geojson(None)), + id: None, + properties: None, + foreign_members: None, + }; + feature.set_property("type", "diagonal filter"); + feature.set_property("stroke", "red"); + features.push(feature); + } + + // Transform to WGS84 + let gps_bounds = map.get_gps_bounds(); + for feature in &mut features { + // geojson to geo + // This could be a Polygon, MultiPolygon, LineString + let mut geom: geo::Geometry = feature.geometry.take().unwrap().value.try_into()?; + + geom.map_coords_inplace(|c| { + let gps = Pt2D::new(c.0, c.1).to_gps(gps_bounds); + (gps.x(), gps.y()) + }); + + // geo to geojson + feature.geometry = Some(Geometry { + bbox: None, + value: Value::from(&geom), + foreign_members: None, + }); + } + + let gj = GeoJson::FeatureCollection(FeatureCollection { + features, + bbox: None, + foreign_members: None, + }); + + // Don't use abstio::write_json; it writes to local storage in web, where we want to eventually + // make the browser download something + let path = format!("ltn_{}.geojson", map.get_name().map); + let mut file = std::fs::File::create(&path)?; + write!(file, "{}", serde_json::to_string_pretty(&gj)?)?; + Ok(path) +} diff --git a/game/src/ltn/filters.rs b/game/src/ltn/filters.rs index 0d8824609d..ca95bb1fb6 100644 --- a/game/src/ltn/filters.rs +++ b/game/src/ltn/filters.rs @@ -186,7 +186,7 @@ impl DiagonalFilter { } /// Physically where is the filter placed? - fn geometry(&self, map: &Map) -> Line { + pub fn geometry(&self, map: &Map) -> Line { let r1 = map.get_r(self.r1); let r2 = map.get_r(self.r2);