Skip to main content

euv_chart/geo/
impl.rs

1use super::*;
2
3/// Inherent implementation of [`GeoPoint`].
4impl GeoPoint {
5    /// Returns a point of the globe.
6    ///
7    /// # Arguments
8    ///
9    /// - `f64` - A 64-bit float (`f64`).
10    pub fn new(lng: f64, lat: f64) -> Self {
11        Self { lng, lat }
12    }
13}
14
15/// Default construction for [`GeoBounds`].
16impl Default for GeoBounds {
17    /// Constructs a default [`GeoBounds`] value.
18    fn default() -> Self {
19        Self {
20            min_lng: f64::INFINITY,
21            min_lat: f64::INFINITY,
22            max_lng: f64::NEG_INFINITY,
23            max_lat: f64::NEG_INFINITY,
24        }
25    }
26}
27
28/// Inherent implementation of [`GeoBounds`].
29impl GeoBounds {
30    /// Returns the width of the box, in degrees.
31    ///
32    /// # Returns
33    ///
34    /// - `f64` - A 64-bit float.
35    pub fn width(&self) -> f64 {
36        self.get_max_lng() - self.get_min_lng()
37    }
38
39    /// Returns the height of the box, in degrees.
40    ///
41    /// # Returns
42    ///
43    /// - `f64` - A 64-bit float.
44    pub fn height(&self) -> f64 {
45        self.get_max_lat() - self.get_min_lat()
46    }
47
48    /// Returns `true` when the box holds no point at all.
49    ///
50    /// # Returns
51    ///
52    /// - `bool` - A boolean.
53    pub fn is_empty(&self) -> bool {
54        !(self.get_min_lng() <= self.get_max_lng() && self.get_min_lat() <= self.get_max_lat())
55    }
56
57    /// Extends the box so that it holds the given point.
58    ///
59    /// # Arguments
60    ///
61    /// - `GeoPoint` - A `GeoPoint` parameter.
62    pub fn extend(&mut self, point: GeoPoint) {
63        self.set_min_lng(self.get_min_lng().min(point.lng));
64        self.set_min_lat(self.get_min_lat().min(point.lat));
65        self.set_max_lng(self.get_max_lng().max(point.lng));
66        self.set_max_lat(self.get_max_lat().max(point.lat));
67    }
68}
69
70/// Inherent implementation of [`MapRegion`].
71impl MapRegion {
72    /// Returns the middle of the region, in geographic coordinates.
73    ///
74    /// The middle is the centre of gravity of the outline, which is
75    /// where a label of the region belongs. A region without a usable
76    /// outline has no middle.
77    ///
78    /// # Returns
79    ///
80    /// - `Option<GeoPoint>` - `Some(...)` on success, `None` otherwise.
81    pub fn center(&self) -> Option<GeoPoint> {
82        let polygon: &RegionPolygon = self.get_polygons().first()?;
83        ring_center(&polygon.outer)
84    }
85}
86
87/// Inherent implementation of [`MapData`].
88impl MapData {
89    /// Builds a map out of its regions, and measures the box they cover.
90    ///
91    /// # Arguments
92    ///
93    /// - `&str` - Shared reference to a `str`.
94    /// - `Vec<MapRegion>` - A `Vec<MapRegion>` parameter.
95    pub fn from_regions(name: &str, regions: Vec<MapRegion>) -> Self {
96        let mut bounds: GeoBounds = GeoBounds::default();
97        for region in &regions {
98            for polygon in &region.polygons {
99                for point in &polygon.outer {
100                    bounds.extend(*point);
101                }
102            }
103        }
104        Self {
105            name: name.to_string(),
106            regions,
107            bounds,
108        }
109    }
110
111    /// Reads a GeoJSON document into a map.
112    ///
113    /// The document is the shape GeoJSON has: a collection of features,
114    /// each with a geometry and a name. The name is taken from the
115    /// `name` property of the feature, because that is where GeoJSON
116    /// keeps it.
117    ///
118    /// # Arguments
119    ///
120    /// - `&str` - Shared reference to a `str`.
121    /// - `&DataValue` - Shared reference to a `DataValue`.
122    ///
123    /// # Returns
124    ///
125    /// - `Result<Self, GeoError>` - Result of the operation; an `Err` variant on failure.
126    pub fn from_geojson(name: &str, value: &DataValue) -> Result<Self, GeoError> {
127        parse_geojson(name, value)
128    }
129
130    /// Returns the region with the given name.
131    ///
132    /// # Arguments
133    ///
134    /// - `&str` - Shared reference to a `str`.
135    ///
136    /// # Returns
137    ///
138    /// - `Option<&MapRegion>` - `Some(...)` on success, `None` otherwise.
139    pub fn region(&self, name: &str) -> Option<&MapRegion> {
140        self.get_regions()
141            .iter()
142            .find(|region: &&MapRegion| region.name == name)
143    }
144}
145
146/// Inherent implementation of [`MapRegistry`].
147impl MapRegistry {
148    /// Returns an empty registry.
149    pub fn new() -> Self {
150        Self {
151            maps: BTreeMap::new(),
152        }
153    }
154
155    /// Adds a map to the registry, replacing the one of the same name.
156    ///
157    /// # Arguments
158    ///
159    /// - `MapData` - A `MapData` parameter.
160    ///
161    /// # Returns
162    ///
163    /// - `&mut Self` - A `mut Self` value.
164    pub fn register(&mut self, data: MapData) -> &mut Self {
165        self.get_mut_maps().insert(data.name.clone(), data);
166        self
167    }
168
169    /// Reads a GeoJSON document and adds the map it holds.
170    ///
171    /// # Arguments
172    ///
173    /// - `&str` - Shared reference to a `str`.
174    /// - `&DataValue` - Shared reference to a `DataValue`.
175    ///
176    /// # Returns
177    ///
178    /// - `Result<&mut Self, GeoError>` - Result of the operation; an `Err` variant on failure.
179    pub fn register_geojson(
180        &mut self,
181        name: &str,
182        value: &DataValue,
183    ) -> Result<&mut Self, GeoError> {
184        let data: MapData = MapData::from_geojson(name, value)?;
185        self.get_mut_maps().insert(name.to_string(), data);
186        Ok(self)
187    }
188
189    /// Returns the map with the given name.
190    ///
191    /// # Arguments
192    ///
193    /// - `&str` - Shared reference to a `str`.
194    ///
195    /// # Returns
196    ///
197    /// - `Option<&MapData>` - `Some(...)` on success, `None` otherwise.
198    pub fn get(&self, name: &str) -> Option<&MapData> {
199        self.get_maps().get(name)
200    }
201
202    /// Returns the names of the maps of the registry.
203    ///
204    /// # Returns
205    ///
206    /// - `Vec<String>` - A `Vec<String>` value.
207    pub fn names(&self) -> Vec<String> {
208        self.get_maps().keys().cloned().collect()
209    }
210
211    /// Returns the number of maps in the registry.
212    ///
213    /// # Returns
214    ///
215    /// - `usize` - A non-negative integer.
216    pub fn len(&self) -> usize {
217        self.get_maps().len()
218    }
219
220    /// Returns `true` when the registry holds no map.
221    ///
222    /// # Returns
223    ///
224    /// - `bool` - A boolean.
225    pub fn is_empty(&self) -> bool {
226        self.get_maps().is_empty()
227    }
228
229    /// Removes every map from the registry.
230    pub fn clear(&mut self) {
231        self.get_mut_maps().clear();
232    }
233}
234
235/// Inherent implementation of [`GeoLayout`].
236impl GeoLayout {
237    /// Projects a map into a rectangle of the chart.
238    ///
239    /// The projection is the one ECharts uses: the longitude and the
240    /// latitude are scaled evenly, and the height of the map is
241    /// shortened by the aspect scale so that a map of the mid-latitudes
242    /// does not look stretched sideways. The map is centred in the
243    /// rectangle and takes most of it.
244    ///
245    /// # Arguments
246    ///
247    /// - `&MapData` - Shared reference to a `MapData`.
248    /// - `BBox` - A `BBox` parameter.
249    /// - `f64` - A 64-bit float (`f64`).
250    pub fn compute(data: &MapData, rect: BBox, aspect_scale: f64) -> Self {
251        let bounds: GeoBounds = data.bounds;
252        let geo_width: f64 = bounds.width().max(f64::EPSILON);
253        let geo_height: f64 = bounds.height().max(f64::EPSILON);
254        // The aspect of the map: its width over its height, shortened
255        // by the aspect scale.
256        let aspect: f64 = geo_width / geo_height * aspect_scale.max(f64::EPSILON);
257        let view: BBox = fit_aspect(rect, aspect);
258        // The map is scaled evenly. The direction that binds leaves the
259        // map smaller than the view, and the map is centred in it.
260        let scale: f64 = (view.width() / geo_width).min(view.height() / geo_height);
261        let drawn_width: f64 = geo_width * scale;
262        let drawn_height: f64 = geo_height * scale;
263        let origin_x: f64 = view.min_x + (view.width() - drawn_width) * 0.5;
264        let origin_y: f64 = view.min_y + (view.height() - drawn_height) * 0.5;
265        // The longitude grows to the right and the latitude grows
266        // upwards, while the screen grows downwards: the vertical axis
267        // is flipped.
268        let transform: Transform2D = Transform2D {
269            a: scale,
270            b: 0.0,
271            c: 0.0,
272            d: -scale,
273            e: origin_x - bounds.min_lng * scale,
274            f: origin_y + bounds.max_lat * scale,
275        };
276        let regions: Vec<GeoRegionLayout> = data
277            .regions
278            .iter()
279            .map(|region: &MapRegion| {
280                let path: Path = region_path(region, &transform);
281                let center: Point = region_center(region, &transform);
282                GeoRegionLayout {
283                    name: region.name.clone(),
284                    path,
285                    center,
286                }
287            })
288            .collect();
289        Self {
290            name: data.name.clone(),
291            regions,
292            transform,
293        }
294    }
295
296    /// Returns the region with the given name.
297    ///
298    /// # Arguments
299    ///
300    /// - `&str` - Shared reference to a `str`.
301    ///
302    /// # Returns
303    ///
304    /// - `Option<&GeoRegionLayout>` - `Some(...)` on success, `None` otherwise.
305    pub fn region(&self, name: &str) -> Option<&GeoRegionLayout> {
306        self.get_regions()
307            .iter()
308            .find(|region: &&GeoRegionLayout| region.name == name)
309    }
310}
311
312/// Implements `impl std::fmt::Display for GeoError`.
313impl Display for GeoError {
314    /// Formats the [`GeoError`] via the supplied formatter.
315    ///
316    /// # Arguments
317    ///
318    /// - `&mut std::fmt::Formatter<'_>` - The formatter receiving the formatted output.
319    ///
320    /// # Returns
321    ///
322    /// - `std::fmt::Result` - A `std::fmt::Result` value.
323    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        match self {
325            GeoError::MissingType => write!(formatter, "the map has no `type` field"),
326            GeoError::UnsupportedType(kind) => {
327                write!(formatter, "the map kind `{}` is not supported", kind)
328            }
329            GeoError::MissingGeometry => write!(formatter, "the map feature has no geometry"),
330            GeoError::MalformedGeometry(reason) => {
331                write!(formatter, "the map geometry is malformed: {}", reason)
332            }
333        }
334    }
335}
336
337/// Implements `impl std::error::Error for GeoError {}`.
338impl Error for GeoError {}