Skip to content

Commit

Permalink
Enable scaling (x, y) coordinates to fit a given range
Browse files Browse the repository at this point in the history
This is useful to make any arbitrary set of geometries passed in fit in
0..360.
  • Loading branch information
Notgnoshi committed May 21, 2024
1 parent 845bc53 commit f0e6a7b
Showing 1 changed file with 95 additions and 25 deletions.
120 changes: 95 additions & 25 deletions tools/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ struct CmdlineOptions {
/// Any affine transformations are applied in the original coordinate space
#[clap(long, conflicts_with = "to_polar")]
from_polar: bool,

/// Scale coordinate 1 (x, or r) to fit in the given range
///
/// If specified, will be applied regardless of whether polar conversion is performed
#[clap(long, num_args = 2)]
range1: Vec<f64>,

/// Scale coordinate 2 (y, or theta) to fit in the given range
///
/// If specified, will be applied regardless of whether polar conversion is performed
#[clap(long, num_args = 2)]
range2: Vec<f64>,
}

fn build_transform(args: &CmdlineOptions, center: Coord) -> AffineTransform {
Expand Down Expand Up @@ -152,6 +164,31 @@ fn build_transform(args: &CmdlineOptions, center: Coord) -> AffineTransform {
transform
}

fn bounding_box(geometries: &[Geometry]) -> Rect {
// Calculate the center of the bounding box; needed to build the AffineTransform
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for geom in geometries.iter() {
let temp = geom.bounding_rect().unwrap_or_else(|| {
panic!(
"Geometry '{}' didn't have a bounding rectangle",
geom.to_wkt()
)
});

let min = temp.min();
let max = temp.max();

min_x = min_x.min(min.x);
min_y = min_y.min(min.y);
max_x = max_x.max(max.x);
max_y = max_y.max(max.y);
}
Rect::new(coord! {x:min_x, y:min_y}, coord! {x:max_x, y:max_y})
}

fn affine_transform(
geometries: impl Iterator<Item = Geometry> + 'static,
args: &CmdlineOptions,
Expand Down Expand Up @@ -181,30 +218,8 @@ fn affine_transform(
// more expensive for large numbers of geometries (has to load all of them into RAM before
// performing the transformations)
TransformCenter::WholeCollection => {
// Read geometries into memory so we can loop over them twice
let geometries: Vec<Geometry<f64>> = geometries.collect();
// Calculate the center of the bounding box; needed to build the AffineTransform
let mut min_x = f64::MAX;
let mut min_y = f64::MAX;
let mut max_x = f64::MIN;
let mut max_y = f64::MIN;
for geom in geometries.iter() {
let temp = geom.bounding_rect().unwrap_or_else(|| {
panic!(
"Geometry '{}' didn't have a bounding rectangle",
geom.to_wkt()
)
});

let min = temp.min();
let max = temp.max();

min_x = min_x.min(min.x);
min_y = min_y.min(min.y);
max_x = max_x.max(max.x);
max_y = max_y.max(max.y);
}
let rect = Rect::new(coord! {x:min_x, y:min_y}, coord! {x:max_x, y:max_y});
let geometries: Vec<_> = geometries.collect();
let rect = bounding_box(&geometries);
let center = rect.center();
let transform = build_transform(args, center);

Expand All @@ -231,6 +246,32 @@ fn to_polar(coord: Coord) -> Coord {
coord! { x: r, y: theta}
}

fn scale_range(src: &[f64; 2], dst: &[f64; 2], v: f64) -> f64 {
(dst[1] - dst[0]) * (v - src[0]) / (src[1] - src[0]) + dst[0]
}

fn scale_coord_range(
bounds: &Rect,
x_dst: Option<&[f64; 2]>,
y_dst: Option<&[f64; 2]>,
coord: Coord,
) -> Coord {
let min = bounds.min();
let max = bounds.max();
let mut x = coord.x;
let mut y = coord.y;

if let Some(dst) = x_dst {
let src = [min.x, max.x];
x = scale_range(&src, dst, coord.x);
}
if let Some(dst) = y_dst {
let src = [min.y, max.y];
y = scale_range(&src, dst, coord.y);
}
coord! {x: x, y: y}
}

fn geoms_coordwise(
geometries: impl Iterator<Item = Geometry>,
transform: impl Fn(Coord) -> Coord + Copy,
Expand All @@ -252,9 +293,38 @@ fn main() {

let reader = get_input_reader(&args.input).unwrap();
let writer = get_output_writer(&args.output).unwrap();
let geometries = read_geometries(reader, &args.input_format); // lazily loaded
let geometries = read_geometries(reader, &args.input_format);
let mut transformed = affine_transform(geometries, &args);

if args.range1.len() == 2 || args.range2.len() == 2 {
let geometries: Vec<_> = transformed.collect();
let bounds = bounding_box(&geometries);

let mut x_dst = None;
let mut y_dst = None;
if args.range1.len() == 2 {
let dst = [args.range1[0], args.range1[1]];
x_dst = Some(dst);
}
if args.range2.len() == 2 {
// If we're converting from polar, then the "y" coordinate is actually theta.
// Use degrees in the CLI args, because it's waaaay easier to do "0 360" than it is
// "0 2PI"
let dst = if args.from_polar {
[args.range2[0].to_radians(), args.range2[1].to_radians()]
} else {
[args.range2[0], args.range2[1]]
};
y_dst = Some(dst);
}

let scaled: Vec<_> = geoms_coordwise(geometries.into_iter(), |coord| {
scale_coord_range(&bounds, x_dst.as_ref(), y_dst.as_ref(), coord)
})
.collect();
transformed = Box::new(scaled.into_iter());
}

if args.to_polar {
transformed = Box::new(geoms_coordwise(transformed, to_polar));
} else if args.from_polar {
Expand Down

0 comments on commit f0e6a7b

Please sign in to comment.