map_model/
city.rs

1use serde::{Deserialize, Serialize};
2
3use abstio::{CityName, MapName};
4use abstutil::Timer;
5use geom::{GPSBounds, LonLat, Polygon, Ring};
6
7use crate::{AreaType, Map};
8
9// Hand-tuned. I think this is in units of meters?
10const POLYGON_EPSILON: f64 = 1000.0;
11
12/// A single city (like Seattle) can be broken down into multiple boundary polygons (udistrict,
13/// ballard, downtown, etc). The load map screen uses this struct to display the entire city.
14#[derive(Serialize, Deserialize)]
15pub struct City {
16    pub name: CityName,
17    pub boundary: Polygon,
18    pub areas: Vec<(AreaType, Polygon)>,
19    /// The individual maps
20    pub districts: Vec<(MapName, Polygon)>,
21    // TODO Move nice_map_name from game into here?
22}
23
24impl City {
25    /// If there's a single map covering all the smaller maps, use this.
26    pub fn from_huge_map(huge_map: &Map) -> City {
27        let city_name = huge_map.get_city_name().clone();
28        let mut districts = abstio::list_dir(format!(
29            "importer/config/{}/{}",
30            city_name.country, city_name.city
31        ))
32        .into_iter()
33        .filter(|path| path.ends_with(".geojson"))
34        .map(|path| {
35            let pts = LonLat::read_geojson_polygon(&path).unwrap();
36            (
37                MapName::from_city(&city_name, &abstutil::basename(path)),
38                Ring::must_new(huge_map.get_gps_bounds().convert(&pts)).into_polygon(),
39            )
40        })
41        .collect::<Vec<_>>();
42        // Just a sort of z-ordering hack so that the largest encompassing district isn't first
43        // later in the UI picker.
44        districts.sort_by_key(|(_, poly)| poly.get_bounds().width() as usize);
45
46        City {
47            name: city_name,
48            boundary: huge_map.get_boundary_polygon().clone(),
49            areas: huge_map
50                .all_areas()
51                .iter()
52                .map(|a| (a.area_type, a.polygon.simplify(POLYGON_EPSILON)))
53                .collect(),
54            districts,
55        }
56    }
57
58    /// Generate a city from a bunch of smaller, individual maps. The boundaries of those maps
59    /// may overlap and may have gaps between them.
60    pub fn from_individual_maps(city_name: &CityName, timer: &mut Timer) -> City {
61        let boundary_per_district: Vec<(MapName, Vec<LonLat>)> = abstio::list_dir(format!(
62            "importer/config/{}/{}",
63            city_name.country, city_name.city
64        ))
65        .into_iter()
66        .filter(|path| path.ends_with(".geojson"))
67        .map(|path| {
68            (
69                MapName::from_city(city_name, &abstutil::basename(&path)),
70                LonLat::read_geojson_polygon(&path).unwrap(),
71            )
72        })
73        .collect();
74        // Figure out the total bounds for all the maps
75        let mut gps_bounds = GPSBounds::new();
76        for (_, pts) in &boundary_per_district {
77            for pt in pts {
78                gps_bounds.update(*pt);
79            }
80        }
81        let boundary = gps_bounds.to_bounds().get_rectangle();
82
83        let mut districts = Vec::new();
84        for (name, pts) in boundary_per_district {
85            districts.push((
86                name,
87                Ring::must_new(gps_bounds.convert(&pts)).into_polygon(),
88            ));
89        }
90        // Just a sort of z-ordering hack so that the largest encompassing district isn't first
91        // later in the UI picker.
92        districts.sort_by_key(|(_, poly)| poly.get_bounds().width() as usize);
93
94        // Add areas from every map. It's fine if they partly overlap.
95        let mut areas = Vec::new();
96        for path in abstio::list_dir(abstio::path(format!(
97            "system/{}/{}/maps",
98            city_name.country, city_name.city
99        ))) {
100            let map = Map::load_synchronously(path, timer);
101            for area in map.all_areas() {
102                let pts = map
103                    .gps_bounds
104                    .convert_back(area.polygon.get_outer_ring().points());
105                // TODO Holes in the polygons get lost
106                if let Ok(ring) = Ring::new(gps_bounds.convert(&pts)) {
107                    areas.push((
108                        area.area_type,
109                        ring.into_polygon().simplify(POLYGON_EPSILON),
110                    ));
111                }
112            }
113        }
114
115        City {
116            name: city_name.clone(),
117            boundary,
118            areas,
119            districts,
120        }
121    }
122}