diff --git a/src/types.rs b/src/types.rs index a216775..0888c85 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,6 @@ use clap::{Parser, Subcommand}; -use geo::{BoundingRect, Geometry, MultiPolygon, Point, Polygon}; +use geo::{Geometry, MultiPolygon}; +use geo::{Point, Polygon}; use geojson::{Feature, FeatureCollection, Value}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -127,8 +128,8 @@ pub struct Marker { pub ty: MarkerType, } -impl Marker { - pub fn to_feature(&self) -> geojson::Feature { +impl ToFeature for Marker { + fn to_feature(&self) -> geojson::Feature { geojson::Feature { geometry: Some(geojson::Geometry::new(Value::Point(vec![ self.coordinates.x(), @@ -171,25 +172,146 @@ pub enum Territory { MultiPolygon(MultiPolygon), } -impl Territory { - pub fn to_feature(&self) -> geojson::Feature { - match self { - Territory::Polygon(p) => geojson::Feature { - geometry: Some(geojson::Geometry::new(p.into())), - properties: None, +impl ToTerritory for MultiPolygon { + fn to_territory(&self) -> Territory { + Territory::MultiPolygon(self.clone()) + } +} - bbox: None, - id: None, - foreign_members: None, - }, - Territory::MultiPolygon(mp) => geojson::Feature { - geometry: Some(geojson::Geometry::new(mp.into())), - properties: None, +impl ToCountryFeature for MultiPolygon { + fn to_country_feature(&self, id: &String, config: &CountryConfig) -> geojson::Feature { + geojson::Feature { + geometry: Some(geojson::Geometry::new((self).into())), + properties: Some( + serde_json::Map::from_iter([ + ("id".to_owned(), json!(id)), + ("type".to_owned(), json!("country")), + ("fill".to_owned(), json!(config.fill)), + ("stroke".to_owned(), json!(config.stroke)), + ("tags".to_owned(), json!(config.tags)), + ]) + .into(), + ), - bbox: None, - id: None, - foreign_members: None, - }, + bbox: None, + id: None, + foreign_members: None, } } } + +impl ToFeatures for Vec { + fn to_features(&self) -> Vec { + self.iter().map(|m| m.to_feature()).collect() + } +} + +impl ToCollection for Vec { + fn to_collection(self) -> geojson::FeatureCollection { + geojson::FeatureCollection { + features: self, + bbox: None, + foreign_members: None, + } + } +} + +impl ToSplitGeo for FeatureCollection { + fn split_geo(&self) -> (Vec, Vec) { + let mut markers: Vec = vec![]; + let mut territories: Vec = vec![]; + + self.features.iter().for_each(|f| { + let properties = f.properties.clone().unwrap(); + + let geometry: Geometry = f.geometry.clone().unwrap().try_into().unwrap(); + + match geometry { + Geometry::Point(p) => { + let ty = match properties + .get("type") + .expect("Missing marker type") + .to_string() + .trim_matches('"') + { + "capital" | "capital-city" => MarkerType::Capital, + "city" => MarkerType::City, + "landmark" => MarkerType::Landmark, + + t => panic!("Invalid marker type: {}", t), + }; + + markers.push(Marker { + coordinates: p, + title: properties + .get("title") + .expect("Missing marker title") + .to_string(), + description: properties + .get("description") + .unwrap_or(&json!("")) + .to_string(), + ty, + }) + } + + Geometry::MultiPolygon(mp) => territories.push(Territory::MultiPolygon(mp)), + + Geometry::Polygon(p) => territories.push(Territory::Polygon(p)), + + _ => panic!("Unexpected geometry type"), + } + }); + + (markers, territories) + } +} + +impl UnsplitGeo for (Vec, Feature) { + fn unsplit_geo(self) -> FeatureCollection { + let (markers, territories) = self; + + let mut features: Vec = markers.to_features(); + features.push(territories); + + features.to_collection() + } +} + +impl ToMultiPolygon for Polygon { + fn to_mp(&self) -> MultiPolygon { + MultiPolygon::new(vec![self.clone()]) + } +} + +pub trait ToFeature { + fn to_feature(&self) -> geojson::Feature; +} + +pub trait ToFeatures { + fn to_features(&self) -> Vec; +} + +pub trait ToCountryFeature { + fn to_country_feature(&self, id: &String, config: &CountryConfig) -> geojson::Feature; +} + +pub trait ToCollection { + fn to_collection(self) -> geojson::FeatureCollection; +} + +pub trait ToSplitGeo { + fn split_geo(&self) -> (Vec, Vec); +} + +pub trait ToTerritory { + fn to_territory(&self) -> Territory; +} + +pub trait UnsplitGeo { + fn unsplit_geo(self) -> FeatureCollection; +} + +pub trait ToMultiPolygon { + fn to_mp(&self) -> MultiPolygon; +} diff --git a/src/utils.rs b/src/utils.rs index f6f21f3..1afc0cb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,12 @@ -use std::{collections::HashMap, fs, path::Path}; +use std::{fs, path::Path}; -use geo::{BooleanOps, Geometry, MultiPolygon}; -use geojson::{Feature, FeatureCollection, GeoJson}; -use serde_json::json; +use geo::{BooleanOps, MultiPolygon}; +use geojson::{FeatureCollection, GeoJson}; -use crate::types::{Config, CountryConfig, CountryData, Marker, MarkerType, Territory}; +use crate::types::{ + Config, CountryConfig, CountryData, Territory, ToCountryFeature, ToMultiPolygon, ToSplitGeo, + UnsplitGeo, +}; pub fn read_config() -> Config { let c = toml::from_str::(&fs::read_to_string("config.toml").unwrap()); @@ -47,88 +49,21 @@ pub fn dissolve_territory( id: String, config: CountryConfig, ) -> FeatureCollection { - let mut markers: Vec = vec![]; - let mut territories: Vec = vec![]; + let (markers, territories) = geo.split_geo(); - geo.features.iter().for_each(|f| { - let properties = f.properties.clone().unwrap(); + let dissolved = territories + .iter() + .fold(MultiPolygon::new(vec![]), |a, b| match b { + Territory::Polygon(p) => a.union(&p.to_mp()), + Territory::MultiPolygon(mp) => a.union(mp), + }) + .to_country_feature(&id, &config); - let geometry: Geometry = f.geometry.clone().unwrap().try_into().unwrap(); - - match geometry { - Geometry::Point(p) => { - let ty = match properties - .get("type") - .expect("Missing marker type") - .to_string() - .trim_matches('"') - { - "capital" | "capital-city" => MarkerType::Capital, - "city" => MarkerType::City, - "landmark" => MarkerType::Landmark, - - t => panic!("Invalid marker type: {}", t), - }; - - markers.push(Marker { - coordinates: p, - title: properties - .get("title") - .expect("Missing marker title") - .to_string(), - description: properties - .get("description") - .unwrap_or(&json!("")) - .to_string(), - ty, - }) - } - - Geometry::MultiPolygon(mp) => territories.push(Territory::MultiPolygon(mp)), - - Geometry::Polygon(p) => territories.push(Territory::Polygon(p)), - - _ => panic!("Unexpected geometry type"), - } - }); - - let territories = territories.iter(); - - let dissolved = territories.fold(MultiPolygon::new(vec![]), |a, b| match b { - Territory::Polygon(p) => a.union(&MultiPolygon::new(vec![p.clone()])), - Territory::MultiPolygon(mp) => a.union(mp), - }); - - // combine markers and dissolved - let mut features: Vec = markers.iter().map(|m| m.to_feature()).collect(); - - features.push(geojson::Feature { - geometry: Some(geojson::Geometry::new((&dissolved).into())), - properties: Some( - serde_json::Map::from_iter([ - ("id".to_owned(), json!(id)), - ("type".to_owned(), json!("country")), - ("fill".to_owned(), json!(config.fill)), - ("stroke".to_owned(), json!(config.stroke)), - ("tags".to_owned(), json!(config.tags)), - ]) - .into(), - ), - - bbox: None, - id: None, - foreign_members: None, - }); - - FeatureCollection { - features, - bbox: None, - foreign_members: None, - } + (markers, dissolved).unsplit_geo() } pub fn hash_hex_color(s: String) -> String { let hex_str = format!("{:x}", xxhash_rust::xxh3::xxh3_64(s.as_bytes())); - format!("#{:6}", hex_str.chars().take(6).collect::()) + format!("#{}", hex_str.chars().take(6).collect::()) }