feat: dissolve territory

This commit is contained in:
Artemy 2024-04-25 14:03:08 +03:00
parent b1f5e42a46
commit 1ab51d33d3
10 changed files with 347 additions and 48 deletions

7
Cargo.lock generated
View file

@ -116,6 +116,7 @@ dependencies = [
"toml", "toml",
"toml_edit", "toml_edit",
"wax", "wax",
"xxhash-rust",
] ]
[[package]] [[package]]
@ -736,6 +737,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "xxhash-rust"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.32" version = "0.7.32"

View file

@ -15,6 +15,8 @@ toml = "0.8.12"
toml_edit = "0.22.12" toml_edit = "0.22.12"
wax = "0.6.0" wax = "0.6.0"
xxhash-rust = { version = "0.8.10", features = ["xxh3"] }
[[bin]] [[bin]]
name = "cimengine" name = "cimengine"
path = "src/main.rs" path = "src/main.rs"

60
src/build.rs Normal file
View file

@ -0,0 +1,60 @@
use std::{fs, path::Path};
use geojson::{Feature, FeatureCollection};
use serde_json::json;
use wax::Glob;
use crate::{
types::CountryData,
utils::{get_country, read_config},
};
pub fn build() {
let config = read_config();
for processing_item in config.processing {
let mut features: Vec<Feature> = vec![];
let out_folder = Path::new(&processing_item.output_folder);
let mut countries: Vec<CountryData> = vec![];
for country_id in &config.main.layers {
let mut country = get_country(country_id.to_owned());
// TODO: Add tags support
features.append(&mut country.geo.features);
countries.push(country);
}
// TODO: Add country_rewrite support
// TODO: let countries = vec![rewrite_info];
// TODO: Add nature support
let feature_collection = FeatureCollection {
bbox: None,
features,
foreign_members: None,
}
.to_string();
let countries = serde_json::to_string(&serde_json::Map::from_iter(
countries
.iter()
.map(|country| (country.id.clone(), json!(country.config))),
))
.unwrap();
fs::create_dir_all(out_folder).unwrap();
fs::write(out_folder.join("geo.geojson"), feature_collection).unwrap();
fs::write(out_folder.join("countries.json"), countries).unwrap();
if let Some(public) = processing_item.public {
let public = serde_json::to_string(&public).unwrap();
fs::write(out_folder.join("public.json"), public).unwrap();
}
}
}

View file

@ -1 +0,0 @@
pub fn build() {}

View file

@ -4,6 +4,7 @@ mod build;
mod init; mod init;
mod new; mod new;
mod types; mod types;
mod utils;
use types::Commands; use types::Commands;

View file

@ -1,7 +1,10 @@
use std::{fs, path::Path}; use std::{fs, path::Path};
use toml_edit::{value, DocumentMut, Value}; use toml_edit::{value, DocumentMut, Value};
use crate::types::{Config, Country, NewCommands}; use crate::{
types::{CountryConfig, NewCommands},
utils::{hash_hex_color, read_config},
};
pub fn new(cmd: NewCommands) { pub fn new(cmd: NewCommands) {
match cmd { match cmd {
@ -12,39 +15,37 @@ pub fn new(cmd: NewCommands) {
foundation_date, foundation_date,
flag, flag,
about, about,
fill,
stroke,
} => { } => {
let name = name.unwrap_or_default(); let name = name.unwrap_or_default();
let description = description.unwrap_or_default(); let description = description.unwrap_or_default();
let foundation_date = foundation_date.unwrap_or_default(); let foundation_date = foundation_date.unwrap_or_default();
let flag = flag.unwrap_or_default(); let flag = flag.unwrap_or_default();
let about = about; let fill = fill.unwrap_or_else(|| hash_hex_color(id.clone() + "_fill"));
let stroke = stroke.unwrap_or_else(|| hash_hex_color(id.clone() + "_stroke"));
let country = Country { let country = CountryConfig {
name: name.clone(), name: name.clone(),
description, description,
foundation_date, foundation_date,
flag, flag,
about, about,
tags: Some(vec![]), fill,
stroke,
tags: None,
}; };
let config = fs::read_to_string("config.toml").unwrap(); let config = fs::read_to_string("config.toml").unwrap();
// Validate config // Validate config
let c = toml::from_str::<Config>(&config); read_config();
match c {
Ok(c) => c,
Err(err) => {
panic!("Invalid config: {}", err);
}
};
// Get actual config // Get actual config
let mut config = config.parse::<DocumentMut>().unwrap(); let mut config = config.parse::<DocumentMut>().unwrap();
// Add country to layers // Add country to layers
let layers = config["country"]["layers"].clone().into_value().unwrap(); let layers = config["main"]["layers"].clone().into_value().unwrap();
let layers = if let Value::Array(mut layers) = layers { let layers = if let Value::Array(mut layers) = layers {
layers.push(&id); layers.push(&id);
@ -53,7 +54,7 @@ pub fn new(cmd: NewCommands) {
panic!("layers is not an array"); panic!("layers is not an array");
}; };
config["country"]["layers"] = value(layers); config["main"]["layers"] = value(layers);
fs::write("config.toml", config.to_string()).unwrap(); fs::write("config.toml", config.to_string()).unwrap();

View file

@ -1,18 +1,16 @@
[country] [main]
# Order matters when building countries. This affects the processing of area intersections # Order matters when building countries. This affects the processing of area intersections
layers = ["sample_country_id"] layers = ["sample_country_id"]
[[processing]] [[processing]]
output_folder = "out" output_folder = "./out/map"
countries_file = "countries.json"
geo_file = "geo.geojson"
# generate_colors = false
# show_markers = false # show_markers = false
# Information for public repository in cimengine. See: https://github.com/CIMEngine/MapList # Information for public repository in cimengine. See: https://github.com/CIMEngine/MapList
# [processing.public] # [processing.public]
# name = "Sample Map" # name = "Sample Map"
# description = "This is a sample map"
# geo = "https://example.com/geo.geojson" # geo = "https://example.com/geo.geojson"
# countries = "https://example.com/countries.json" # countries = "https://example.com/countries.json"
@ -22,9 +20,14 @@ geo_file = "geo.geojson"
# include = [] # [] = not filtered # include = [] # [] = not filtered
# exclude = ["test", "test2"] # exclude = ["test", "test2"]
# Rewrite properties of all countries. All fields are optional.
# [processing.countries_rewrite] # [processing.countries_rewrite]
# name = "name" # name = "name"
# color = "#000000" # color = "#000000"
# description = "description"
# foundation_date = "2024-01-01"
# flag = "https://example.com/flag.png"
# about = "https://example.com/about.html"
# Nature layers # Nature layers
[[nature]] [[nature]]

View file

@ -2,4 +2,8 @@ name = "Sample Country"
description = "This is a sample country" description = "This is a sample country"
foundation_date = "2024-01-01" foundation_date = "2024-01-01"
flag = "https://example.com/flag.png" flag = "https://example.com/flag.png"
about = "https://example.com/about.html" fill = "#000000"
stroke = "#000000"
# about = "https://example.com/about.html"
# tags = ["test", "test2"]

View file

@ -1,5 +1,8 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use geo::{BoundingRect, Geometry, MultiPolygon, Point, Polygon};
use geojson::{Feature, FeatureCollection, Value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "cimengine", bin_name = "cimengine")] #[command(name = "cimengine", bin_name = "cimengine")]
@ -37,71 +40,156 @@ pub enum NewCommands {
/// Create new country /// Create new country
Country { Country {
id: String, id: String,
#[clap(short, long)] #[clap(long)]
name: Option<String>, name: Option<String>,
#[clap(short, long)] #[clap(long)]
description: Option<String>, description: Option<String>,
#[clap(short, long)] #[clap(long)]
foundation_date: Option<String>, foundation_date: Option<String>,
#[clap(long)] #[clap(long)]
flag: Option<String>, flag: Option<String>,
#[clap(short, long)] #[clap(long)]
about: Option<String>, about: Option<String>,
#[clap(long)]
fill: Option<String>,
#[clap(long)]
stroke: Option<String>,
}, },
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config { pub struct Config {
country: CountryConfig, pub main: MainConfig,
processing: Vec<ProcessingConfig>, pub processing: Vec<ProcessingConfig>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CountryConfig { pub struct MainConfig {
layers: Vec<String>, pub layers: Vec<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProcessingConfig { pub struct ProcessingConfig {
generate_colors: Option<bool>, pub show_markers: Option<bool>,
show_markers: Option<bool>, pub output_folder: String,
output_folder: String,
countries_file: String,
geo_file: String,
tags: Option<ProcessingTagsConfig>, pub tags: Option<ProcessingTagsConfig>,
countries_rewrite: Option<CountryRewriteConfig>, pub countries_rewrite: Option<CountryRewriteConfig>,
public: Option<PublicConfig>, pub public: Option<PublicConfig>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProcessingTagsConfig { pub struct ProcessingTagsConfig {
include: Option<Vec<String>>, pub include: Option<Vec<String>>,
exclude: Option<Vec<String>>, pub exclude: Option<Vec<String>>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CountryRewriteConfig { pub struct CountryRewriteConfig {
name: Option<String>, pub name: Option<String>,
description: Option<String>, pub description: Option<String>,
foundation_date: Option<String>, pub foundation_date: Option<String>,
flag: Option<String>, pub flag: Option<String>,
about: Option<String>, pub about: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PublicConfig { pub struct PublicConfig {
name: String, pub name: String,
geo: String, pub description: String,
countries: String, pub geo: String,
pub countries: String,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Country { pub struct CountryConfig {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub foundation_date: String, pub foundation_date: String,
pub flag: String, pub flag: String,
pub fill: String,
pub stroke: String,
pub about: Option<String>, pub about: Option<String>,
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CountryData {
pub id: String,
pub config: CountryConfig,
pub geo: FeatureCollection,
}
pub struct Marker {
pub coordinates: Point,
pub title: String,
pub description: String,
pub ty: MarkerType,
}
impl Marker {
pub fn to_feature(&self) -> geojson::Feature {
geojson::Feature {
geometry: Some(geojson::Geometry::new(Value::Point(vec![
self.coordinates.x(),
self.coordinates.y(),
]))),
properties: Some(
serde_json::Map::from_iter([
("title".to_owned(), json!(self.title)),
("description".to_owned(), json!(self.description)),
("marker-type".to_owned(), json!(self.ty.to_str())),
])
.into(),
),
bbox: None,
id: None,
foreign_members: None,
}
}
}
pub enum MarkerType {
Capital,
City,
Landmark,
}
impl MarkerType {
pub fn to_str(&self) -> &'static str {
match self {
MarkerType::Capital => "capital",
MarkerType::City => "city",
MarkerType::Landmark => "landmark-0",
}
}
}
pub enum Territory {
Polygon(Polygon),
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,
bbox: None,
id: None,
foreign_members: None,
},
Territory::MultiPolygon(mp) => geojson::Feature {
geometry: Some(geojson::Geometry::new(mp.into())),
properties: None,
bbox: None,
id: None,
foreign_members: None,
},
}
}
}

134
src/utils.rs Normal file
View file

@ -0,0 +1,134 @@
use std::{collections::HashMap, fs, path::Path};
use geo::{BooleanOps, Geometry, MultiPolygon};
use geojson::{Feature, FeatureCollection, GeoJson};
use serde_json::json;
use crate::types::{Config, CountryConfig, CountryData, Marker, MarkerType, Territory};
pub fn read_config() -> Config {
let c = toml::from_str::<Config>(&fs::read_to_string("config.toml").unwrap());
match c {
Ok(c) => c,
Err(err) => panic!("Invalid config: {}", err),
}
}
pub fn get_country(id: String) -> CountryData {
let country_folder = Path::new(".").join("countries").join(&id);
let config = toml::from_str::<CountryConfig>(
&fs::read_to_string(country_folder.join("country.toml")).unwrap(),
);
let config = match config {
Ok(c) => c,
Err(err) => panic!("Invalid config: {}", err),
};
let geo_str = fs::read_to_string(country_folder.join("country.geojson")).unwrap();
let geo: GeoJson = geo_str.parse().unwrap();
let geo = match geo {
GeoJson::FeatureCollection(coll) => coll,
_ => panic!("Invalid geojson, expected FeatureCollection"),
};
CountryData {
id: id.clone(),
config: config.clone(),
geo: dissolve_territory(geo, id, config),
}
}
pub fn dissolve_territory(
geo: FeatureCollection,
id: String,
config: CountryConfig,
) -> FeatureCollection {
let mut markers: Vec<Marker> = vec![];
let mut territories: Vec<Territory> = vec![];
geo.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"),
}
});
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<Feature> = 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,
}
}
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::<String>())
}