mirror of
https://github.com/CIMEngine/cimengine-build-tools.git
synced 2024-11-05 20:53:58 +03:00
feat: dissolve territory
This commit is contained in:
parent
b1f5e42a46
commit
1ab51d33d3
10 changed files with 347 additions and 48 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -116,6 +116,7 @@ dependencies = [
|
|||
"toml",
|
||||
"toml_edit",
|
||||
"wax",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -736,6 +737,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
|
|
|
@ -15,6 +15,8 @@ toml = "0.8.12"
|
|||
toml_edit = "0.22.12"
|
||||
wax = "0.6.0"
|
||||
|
||||
xxhash-rust = { version = "0.8.10", features = ["xxh3"] }
|
||||
|
||||
[[bin]]
|
||||
name = "cimengine"
|
||||
path = "src/main.rs"
|
||||
|
|
60
src/build.rs
Normal file
60
src/build.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub fn build() {}
|
|
@ -4,6 +4,7 @@ mod build;
|
|||
mod init;
|
||||
mod new;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
use types::Commands;
|
||||
|
||||
|
|
29
src/new.rs
29
src/new.rs
|
@ -1,7 +1,10 @@
|
|||
use std::{fs, path::Path};
|
||||
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) {
|
||||
match cmd {
|
||||
|
@ -12,39 +15,37 @@ pub fn new(cmd: NewCommands) {
|
|||
foundation_date,
|
||||
flag,
|
||||
about,
|
||||
fill,
|
||||
stroke,
|
||||
} => {
|
||||
let name = name.unwrap_or_default();
|
||||
let description = description.unwrap_or_default();
|
||||
let foundation_date = foundation_date.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(),
|
||||
description,
|
||||
foundation_date,
|
||||
flag,
|
||||
about,
|
||||
tags: Some(vec![]),
|
||||
fill,
|
||||
stroke,
|
||||
tags: None,
|
||||
};
|
||||
|
||||
let config = fs::read_to_string("config.toml").unwrap();
|
||||
|
||||
// Validate config
|
||||
let c = toml::from_str::<Config>(&config);
|
||||
|
||||
match c {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
panic!("Invalid config: {}", err);
|
||||
}
|
||||
};
|
||||
read_config();
|
||||
|
||||
// Get actual config
|
||||
let mut config = config.parse::<DocumentMut>().unwrap();
|
||||
|
||||
// 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 {
|
||||
layers.push(&id);
|
||||
|
@ -53,7 +54,7 @@ pub fn new(cmd: NewCommands) {
|
|||
panic!("layers is not an array");
|
||||
};
|
||||
|
||||
config["country"]["layers"] = value(layers);
|
||||
config["main"]["layers"] = value(layers);
|
||||
|
||||
fs::write("config.toml", config.to_string()).unwrap();
|
||||
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
[country]
|
||||
[main]
|
||||
# Order matters when building countries. This affects the processing of area intersections
|
||||
layers = ["sample_country_id"]
|
||||
|
||||
[[processing]]
|
||||
output_folder = "out"
|
||||
countries_file = "countries.json"
|
||||
geo_file = "geo.geojson"
|
||||
output_folder = "./out/map"
|
||||
|
||||
# generate_colors = false
|
||||
# show_markers = false
|
||||
|
||||
# Information for public repository in cimengine. See: https://github.com/CIMEngine/MapList
|
||||
# [processing.public]
|
||||
# name = "Sample Map"
|
||||
# description = "This is a sample map"
|
||||
# geo = "https://example.com/geo.geojson"
|
||||
# countries = "https://example.com/countries.json"
|
||||
|
||||
|
@ -22,9 +20,14 @@ geo_file = "geo.geojson"
|
|||
# include = [] # [] = not filtered
|
||||
# exclude = ["test", "test2"]
|
||||
|
||||
# Rewrite properties of all countries. All fields are optional.
|
||||
# [processing.countries_rewrite]
|
||||
# name = "name"
|
||||
# color = "#000000"
|
||||
# description = "description"
|
||||
# foundation_date = "2024-01-01"
|
||||
# flag = "https://example.com/flag.png"
|
||||
# about = "https://example.com/about.html"
|
||||
|
||||
# Nature layers
|
||||
[[nature]]
|
||||
|
|
|
@ -2,4 +2,8 @@ name = "Sample Country"
|
|||
description = "This is a sample country"
|
||||
foundation_date = "2024-01-01"
|
||||
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"]
|
||||
|
|
142
src/types.rs
142
src/types.rs
|
@ -1,5 +1,8 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use geo::{BoundingRect, Geometry, MultiPolygon, Point, Polygon};
|
||||
use geojson::{Feature, FeatureCollection, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "cimengine", bin_name = "cimengine")]
|
||||
|
@ -37,71 +40,156 @@ pub enum NewCommands {
|
|||
/// Create new country
|
||||
Country {
|
||||
id: String,
|
||||
#[clap(short, long)]
|
||||
#[clap(long)]
|
||||
name: Option<String>,
|
||||
#[clap(short, long)]
|
||||
#[clap(long)]
|
||||
description: Option<String>,
|
||||
#[clap(short, long)]
|
||||
#[clap(long)]
|
||||
foundation_date: Option<String>,
|
||||
#[clap(long)]
|
||||
flag: Option<String>,
|
||||
#[clap(short, long)]
|
||||
#[clap(long)]
|
||||
about: Option<String>,
|
||||
#[clap(long)]
|
||||
fill: Option<String>,
|
||||
#[clap(long)]
|
||||
stroke: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
country: CountryConfig,
|
||||
processing: Vec<ProcessingConfig>,
|
||||
pub main: MainConfig,
|
||||
pub processing: Vec<ProcessingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CountryConfig {
|
||||
layers: Vec<String>,
|
||||
pub struct MainConfig {
|
||||
pub layers: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ProcessingConfig {
|
||||
generate_colors: Option<bool>,
|
||||
show_markers: Option<bool>,
|
||||
output_folder: String,
|
||||
countries_file: String,
|
||||
geo_file: String,
|
||||
pub show_markers: Option<bool>,
|
||||
pub output_folder: String,
|
||||
|
||||
tags: Option<ProcessingTagsConfig>,
|
||||
countries_rewrite: Option<CountryRewriteConfig>,
|
||||
public: Option<PublicConfig>,
|
||||
pub tags: Option<ProcessingTagsConfig>,
|
||||
pub countries_rewrite: Option<CountryRewriteConfig>,
|
||||
pub public: Option<PublicConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ProcessingTagsConfig {
|
||||
include: Option<Vec<String>>,
|
||||
exclude: Option<Vec<String>>,
|
||||
pub include: Option<Vec<String>>,
|
||||
pub exclude: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CountryRewriteConfig {
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
foundation_date: Option<String>,
|
||||
flag: Option<String>,
|
||||
about: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub foundation_date: Option<String>,
|
||||
pub flag: Option<String>,
|
||||
pub about: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct PublicConfig {
|
||||
name: String,
|
||||
geo: String,
|
||||
countries: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub geo: String,
|
||||
pub countries: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Country {
|
||||
pub struct CountryConfig {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub foundation_date: String,
|
||||
pub flag: String,
|
||||
pub fill: String,
|
||||
pub stroke: String,
|
||||
pub about: Option<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
134
src/utils.rs
Normal 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>())
|
||||
}
|
Loading…
Reference in a new issue