use geo::{map_coords::MapCoordsInplace, LineString, Point, Polygon};
use geographiclib_rs::{DirectGeodesic, Geodesic};
use geojson::GeoJson;
use std::convert::TryInto;
use std::default::Default;
use std::f64::consts::PI;
use structopt::StructOpt;
pub fn clockboard(
center: Point<f64>,
params: Params,
) -> GeoJson {
let mut zones: Vec<(String, Polygon<f64>)> = Vec::new();
let crs = if params.projected {
None
} else {
Some(Geodesic::wgs84())
};
zones.push((
"A".to_string(),
Polygon::new(
make_circle(
center,
params.distances[0],
params.num_vertices_arc * params.num_segments,
crs,
),
Vec::new(),
),
));
let mut ring_label = 'B';
for pair in params.distances.windows(2) {
let (inner_radius, outer_radius) = (pair[0], pair[1]);
if params.num_segments == 1 {
let outer_ring = make_circle(
center,
outer_radius,
params.num_vertices_arc * params.num_segments,
crs,
);
let inner_ring = make_circle(
center,
inner_radius,
params.num_vertices_arc * params.num_segments,
crs,
);
zones.push((
ring_label.to_string(),
Polygon::new(outer_ring, vec![inner_ring]),
));
} else {
for idx in 0..params.num_segments {
zones.push((
format!("{}{:02}", ring_label, idx + 1),
clock_polygon(
center,
outer_radius,
inner_radius,
params.num_vertices_arc,
params.num_segments,
idx,
crs,
),
));
}
}
ring_label = std::char::from_u32(ring_label as u32 + 1).expect("too may rings");
}
for (_, poly) in &mut zones {
round(poly, params.precision);
}
let geom_collection: geo::GeometryCollection<f64> =
zones.iter().map(|(_, poly)| poly.clone()).collect();
let mut feature_collection = geojson::FeatureCollection::from(&geom_collection);
for (feature, (label, _)) in feature_collection.features.iter_mut().zip(zones) {
let mut properties = serde_json::Map::new();
properties.insert("label".to_string(), label.into());
feature.properties = Some(properties);
}
GeoJson::from(feature_collection)
}
#[derive(StructOpt, Debug)]
#[structopt(name = "zb")]
pub struct Params {
#[structopt(short = "s", long, default_value = "12")]
pub num_segments: usize,
#[structopt(
short,
long,
default_value = "1.0,3.0,6.0,10.0,15.0",
use_delimiter = true
)]
pub distances: Vec<f64>,
#[structopt(short = "v", long, default_value = "10")]
pub num_vertices_arc: usize,
#[structopt(short, long, default_value = "6")]
pub precision: usize,
#[structopt(long)]
pub projected: bool,
}
impl Default for Params {
fn default() -> Self {
Params {
num_segments: 12,
distances: triangular_sequence(5),
num_vertices_arc: 10,
precision: 6,
projected: false,
}
}
}
pub fn triangular_sequence(n: usize) -> Vec<f64> {
(1..=n).map(|i| 0.5 * (i as f64) * (i + 1) as f64).collect()
}
fn arc_point(
num_circles: usize,
idx: usize,
num_segments: usize,
center: Point<f64>,
radius: f64,
crs: Option<Geodesic>,
) -> Point<f64> {
if let Some(crs) = crs {
let offset = 180.0 / (num_segments as f64);
let angle: f64 = 360.0 / (num_circles as f64) * (idx as f64) + offset;
let (y, x) = crs.direct(center.y(), center.x(), angle, radius * 1000.0);
Point::new(x, y)
} else {
let offset = std::f64::consts::PI / (num_segments as f64);
let angle: f64 = 2.0 * PI / (num_circles as f64) * (idx as f64) + offset;
let x = center.x() + radius * angle.sin();
let y = center.y() + radius * angle.cos();
Point::new(x, y)
}
}
fn round(poly: &mut Polygon<f64>, precision: usize) {
let p = 10_usize.pow(precision.try_into().unwrap()) as f64;
poly.map_coords_inplace(|&(x, y)| (f64::trunc(x * p) / p, f64::trunc(y * p) / p))
}
fn make_circle(
center: Point<f64>,
radius: f64,
num_vertices: usize,
crs: Option<Geodesic>,
) -> LineString<f64> {
let circle_points: Vec<Point<f64>> = if let Some(crs) = crs {
(0..num_vertices)
.map(|idx| {
let angle: f64 = 360.0 / (num_vertices as f64) * (idx as f64);
let (y, x) = crs.direct(center.y(), center.x(), angle, radius * 1000.0);
Point::new(x, y)
})
.collect()
} else {
(0..num_vertices)
.map(|idx| {
let angle: f64 = 2.0 * PI / (num_vertices as f64) * (idx as f64);
let x = center.x() + radius * angle.cos();
let y = center.y() + radius * angle.sin();
Point::new(x, y)
})
.collect()
};
LineString::from(circle_points)
}
fn clock_polygon(
center: Point<f64>,
radius_outer: f64,
radius_inner: f64,
num_vertices_arc: usize,
num_segments: usize,
seg: usize,
crs: Option<Geodesic>,
) -> Polygon<f64> {
assert!(radius_outer > radius_inner);
let num_vertices_circle = num_vertices_arc * num_segments;
let idx1 = seg * num_vertices_arc;
let idx2 = 1 + (seg + 1) * num_vertices_arc;
let arcs: Vec<Point<f64>> = (idx1..idx2)
.map(|idx| {
arc_point(
num_vertices_circle,
idx,
num_segments,
center,
radius_outer,
crs,
)
})
.chain((idx1..idx2).rev().map(|idx| {
arc_point(
num_vertices_circle,
idx,
num_segments,
center,
radius_inner,
crs,
)
}))
.collect();
Polygon::new(LineString::from(arcs), vec![])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_triangular_sequence() {
assert!(triangular_sequence(0).is_empty());
assert_eq!(vec![1.0], triangular_sequence(1));
assert_eq!(vec![1.0, 3.0], triangular_sequence(2));
assert_eq!(vec![1.0, 3.0, 6.0], triangular_sequence(3));
assert_eq!(vec![1.0, 3.0, 6.0, 10.0], triangular_sequence(4));
}
#[test]
fn test_number_of_zones() {
let args: Vec<String> = Vec::new();
let params = Params::from_iter(args);
let gj = clockboard(Point::new(0.0, 0.0), params);
if let GeoJson::FeatureCollection(fc) = gj {
assert_eq!(49, fc.features.len());
} else {
panic!("not a feature collection");
}
}
}
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn make_clockboard(lat: f64, lon: f64, distances: Vec<f64>, num_segments: usize) -> String {
let args: Vec<String> = Vec::new();
let mut params = Params::from_iter(args);
params.distances = distances;
params.num_segments = num_segments;
let gj = clockboard(Point::new(lon, lat), params);
serde_json::to_string(&gj).unwrap()
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn generate_triangular_sequence(n: usize) -> Vec<f64> {
triangular_sequence(n)
}