diff --git a/Cargo.toml b/Cargo.toml index 553e4cd..0ce23b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde_json = "1.0.143" [workspace.lints.rust] # It's always good to write as much documentation as possible missing_docs = "warn" +unreachable_pub = "warn" [workspace.lints.clippy] # `clippy::all` is already on by default. It implies the following: @@ -93,3 +94,8 @@ multiple_unsafe_ops_per_block = "allow" # i dont write docs with punctuation but you can still tell what im saying doc_paragraphs_missing_punctuation = "allow" + +# Along with `unreachable_pub = "warn"` this enforces the practice of explicitly defining visibility. +# Therefore, if a symbol isn't truly public to the outside world then it should be qualified with +# `pub(crate)`, `pub(super)`, etc. +redundant_pub_crate = "allow" diff --git a/crates/total-viewsheds/src/compute/area_of_interest.rs b/crates/total-viewsheds/src/compute/area_of_interest.rs index 9bbcdf4..b0b9a20 100644 --- a/crates/total-viewsheds/src/compute/area_of_interest.rs +++ b/crates/total-viewsheds/src/compute/area_of_interest.rs @@ -5,7 +5,7 @@ use color_eyre::Result; use geo::Contains as _; /// `Pruner` -pub struct Pruner { +pub(crate) struct Pruner { /// Width of the DEM in points width: u32, /// The DEM coordinates for the Area of Interest. @@ -14,12 +14,12 @@ pub struct Pruner { impl Pruner { /// Instantiate. - pub const fn new(width: u32, polygon: geo::Polygon) -> Self { + pub(crate) const fn new(width: u32, polygon: geo::Polygon) -> Self { Self { width, polygon } } /// Convert user-provided lon/lat coordinates to a polygon. - pub fn lonlat_coords_to_polygon( + pub(crate) fn lonlat_coords_to_polygon( points: Vec<(f32, f32)>, metadata: &tvs_lib::metadata::MetaData, ) -> Result { @@ -41,7 +41,7 @@ impl Pruner { } /// Can the given DEM ID be ignored in computations. - pub fn is_prunable(&self, dem_id: i64) -> bool { + pub(crate) fn is_prunable(&self, dem_id: i64) -> bool { let dem_coord = self.convert_dem_id_to_coord(dem_id); !self.polygon.contains(&dem_coord.0) } @@ -52,7 +52,7 @@ impl Pruner { reason = "We're only dealing with a max of the DEM's width" )] /// Convert a DEM 1D index to a 2D coordinate. - pub fn convert_dem_id_to_coord(&self, dem_id: i64) -> tvs_lib::dem::Coordinate { + pub(crate) fn convert_dem_id_to_coord(&self, dem_id: i64) -> tvs_lib::dem::Coordinate { let x = dem_id.rem_euclid(self.width.into()) as f64; let y = dem_id.div_euclid(self.width.into()) as f64; tvs_lib::dem::Coordinate(geo::coord! {x: x, y: y}) diff --git a/crates/total-viewsheds/src/compute/kernel.rs b/crates/total-viewsheds/src/compute/kernel.rs index 3522e43..26ce83b 100644 --- a/crates/total-viewsheds/src/compute/kernel.rs +++ b/crates/total-viewsheds/src/compute/kernel.rs @@ -7,7 +7,7 @@ use geo::HasDimensions as _; use itertools::izip; /// The data output by a single angle. -pub struct OutputData { +pub(crate) struct OutputData { /// The visibile surface area. pub surfaces: Vec, /// The longest lines of sight. @@ -57,7 +57,7 @@ const DEFAULT_UNROLL: usize = const { /// `kernel` will calculate the longest line of sight heatmap for a given angle and elevation map /// assuming that the maximum line of sight is `max_los` -pub fn kernel( +pub(crate) fn kernel( db_worker: &crate::storage::worker::Worker, elevation_map: &[i16], output: &mut OutputData, diff --git a/crates/total-viewsheds/src/compute/los.rs b/crates/total-viewsheds/src/compute/los.rs index fbda642..404fe68 100644 --- a/crates/total-viewsheds/src/compute/los.rs +++ b/crates/total-viewsheds/src/compute/los.rs @@ -1,6 +1,7 @@ /// `LineOfSight` abstracts the implementation of line of sight calculations to /// any "carry through" that can be materialized into a (f32, f32). -pub trait LineOfSight, LOS: Angle + PrefixMax + Accumulate> { +pub(crate) trait LineOfSight, LOS: Angle + PrefixMax + Accumulate> +{ /// `line_of_sight` calculates a line of sight for the given `pov_height` /// and outputs a triple of the surface area, longest line of sight in meters /// and a vector of bools of which @@ -9,7 +10,7 @@ pub trait LineOfSight, LOS: Angle + PrefixMax + Accumul /// `Angle` abstracts the angle calculation between a pov and all the elevation data within /// its band of sight -pub trait Angle { +pub(crate) trait Angle { /// `calculate_angles` calculates the angle from the `pov_height` to a given elevation fn calculate_angles( pov_height: f32, @@ -23,7 +24,7 @@ pub trait Angle { /// `Accumulate` accumulates the surface area visible and longest line of sight /// in a pair of (f32, f32). `Accumulate` doesn't care about the implementation details /// of accumulation so long as the `Output` type can be materialized to (f32, f32) -pub trait Accumulate> { +pub(crate) trait Accumulate> { /// `accumulate` accumulates the surface area using the distances by comparing /// whether a point at a distance is visible (angle > prefix) /// If `output_sector` is true, it should output a bitmap of which distances are visible @@ -38,7 +39,7 @@ pub trait Accumulate> { } /// `PrefixMax` calculates the prefix maximum of the given angles -pub trait PrefixMax { +pub(crate) trait PrefixMax { /// `prefix_max` calculates the prefix max of the fn prefix_max(highest: f32, angles_in: &[f32], angles_out: &mut [f32]); } diff --git a/crates/total-viewsheds/src/compute/rotation.rs b/crates/total-viewsheds/src/compute/rotation.rs index b291b85..dcb17ed 100644 --- a/crates/total-viewsheds/src/compute/rotation.rs +++ b/crates/total-viewsheds/src/compute/rotation.rs @@ -14,7 +14,7 @@ use std::rc::Rc; reason = "rotation should not be out of bounds" )] #[expect(clippy::expect_used, reason = "invriants broken if options not none")] -pub fn lines( +pub(crate) fn lines( elevs: &[i16], max_los: usize, angle: f64, diff --git a/crates/total-viewsheds/src/compute/rotator.rs b/crates/total-viewsheds/src/compute/rotator.rs index b3b9190..cb42478 100644 --- a/crates/total-viewsheds/src/compute/rotator.rs +++ b/crates/total-viewsheds/src/compute/rotator.rs @@ -10,7 +10,7 @@ /// * 180°: (`width_index` - x, `width_index` - y) // * 270°: (y, `width_index` - x) #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Quadrant { +pub(crate) enum Quadrant { /// Top-right quadrant: 0° to 90° TopRight, /// Top-left quadrant: 90° to 180° @@ -55,7 +55,7 @@ impl From for Quadrant { } /// Rotator struct. -pub struct Rotator { +pub(crate) struct Rotator { /// The centre of the DEM in relative coordinates. centre: (f64, f64), /// The index of the last point in a row/column of the DEM. @@ -72,7 +72,7 @@ pub struct Rotator { impl Rotator { /// Instantiate. - pub fn new(angle: f64, width: isize) -> Self { + pub(crate) fn new(angle: f64, width: isize) -> Self { assert!( (0.0f64..360.0f64).contains(&angle), "Angle {angle} must be in range 0..360" @@ -109,7 +109,7 @@ impl Rotator { /// we'd just make the angle negative here. But seeing as the rest of the maths involved in /// skew rotation is based in positive angles it's better to achieve the same rotation /// through a positive but inverted angle. - pub fn invert_angle(angle: f64) -> f64 { + pub(crate) fn invert_angle(angle: f64) -> f64 { if angle > 0.0f64 { 360.0f64 - angle } else { @@ -122,7 +122,7 @@ impl Rotator { /// /// Note that each shear step _must_ be rounded. This is essential to achieving a bijective /// mapping. - pub fn rotate(&self, x: isize, y: isize) -> (isize, isize) { + pub(crate) fn rotate(&self, x: isize, y: isize) -> (isize, isize) { let (x_centre, y_centre) = self.centre; let (quadrant_x, quadrant_y) = self.rotate_to_quadrant(x, y); diff --git a/crates/total-viewsheds/src/compute/unrolled_los.rs b/crates/total-viewsheds/src/compute/unrolled_los.rs index b77ddc7..de4ab7c 100644 --- a/crates/total-viewsheds/src/compute/unrolled_los.rs +++ b/crates/total-viewsheds/src/compute/unrolled_los.rs @@ -27,7 +27,7 @@ fn generate_distances(max_los: usize, refraction: f32, scale: f32) -> (Vec, /// Unroll holds an unrolled heatmap and unrolled longest line of sight calculation /// Since in Line of Sight-land max/addition are commutative, then Unroll will be materialized /// into (f32, f32) -pub struct UnrollVector +pub(crate) struct UnrollVector where [(); UNROLL * VECTOR_WIDTH]:, { @@ -40,7 +40,7 @@ where } /// `UnrolledLOS` implements an Unrolled `LineOfSight` calculation -pub struct UnrolledVectorLos { +pub(crate) struct UnrolledVectorLos { /// `angles` holds a buffer for line of sight angles to be put into /// which is exactly `max_los+1` long angles: Vec, @@ -81,7 +81,7 @@ where impl UnrolledVectorLos { /// `new` initializes a new `UnrolledLOS`, and precalculates all the distances /// and earth curvature adjustments - pub fn new(max_los: usize, refraction: f32, scale: f32) -> Self { + pub(crate) fn new(max_los: usize, refraction: f32, scale: f32) -> Self { assert_eq!( max_los % VECTOR_WIDTH, 0, diff --git a/crates/total-viewsheds/src/compute/vector_intrinsics.rs b/crates/total-viewsheds/src/compute/vector_intrinsics.rs index b7e4183..a854ee1 100644 --- a/crates/total-viewsheds/src/compute/vector_intrinsics.rs +++ b/crates/total-viewsheds/src/compute/vector_intrinsics.rs @@ -7,7 +7,7 @@ use std::simd::{Mask, Simd, f32x4}; /// `VectorMax` performs an element-wise SIMD max of floats, allowing for architecture /// specific implementations -pub trait VectorMax { +pub(crate) trait VectorMax { /// `max` computes an element-wise maximum from lhs and rhs, assuming neither contain NaNs /// or -0.0 fn max(lhs: Simd, rhs: Simd) -> Simd; @@ -15,7 +15,7 @@ pub trait VectorMax { /// `VectorGreater` performs a SIMD greater than of floats, allowing for architecture /// specific implementations -pub trait VectorGreater { +pub(crate) trait VectorGreater { /// gt computes an element-wise maximum from lhs and rhs, assuming neither contain NaNs /// or -0.0 fn gt(lhs: Simd, rhs: Simd) -> Mask; @@ -23,7 +23,7 @@ pub trait VectorGreater { /// `VectorLos` is an implementation of the internals of `PrefixMax`, `Angle`, and `Accumulate` /// for Portable SIMD -pub struct VectorLos; +pub(crate) struct VectorLos; impl VectorMax for VectorLos { #[inline] @@ -333,7 +333,7 @@ impl Angle for VectorLos { /// `DEFAULT_VECTOR_LENGTH` determines the CPU Kernel's default vector length based off /// the architecture that the binary is built for -pub const DEFAULT_VECTOR_LENGTH: usize = const { +pub(crate) const DEFAULT_VECTOR_LENGTH: usize = const { if cfg!(target_feature = "avx512f") { 16 } else if cfg!(any(test, feature = "ring_data")) { diff --git a/crates/total-viewsheds/src/config.rs b/crates/total-viewsheds/src/config.rs index a249c07..b7fd5a5 100644 --- a/crates/total-viewsheds/src/config.rs +++ b/crates/total-viewsheds/src/config.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; #[command( about = "Generate _all_ the viewsheds for a given Digital Elevation Model, therefore the total viewsheds." )] -pub struct Config { +pub(crate) struct Config { #[command(subcommand)] /// The subcommand. pub command: Commands, @@ -18,7 +18,7 @@ pub struct Config { /// CLI subcommand. #[derive(clap::Subcommand, Debug)] -pub enum Commands { +pub(crate) enum Commands { /// Run main computations. Compute(Compute), /// Reconstruct a viewshed. @@ -35,7 +35,7 @@ pub enum Commands { /// Arguments to the `compute` subcommand. #[derive(clap::Parser, Debug, Default)] -pub struct Compute { +pub(crate) struct Compute { // TODO: make this "reserved rings" and add support to the kernel so that the user can get // feedback of the actual number needed. // @@ -154,7 +154,7 @@ pub struct Compute { /// Arguments to the `viewshed` subcommand. #[derive(clap::Parser, Debug)] -pub struct Viewshed { +pub(crate) struct Viewshed { /// Path or directory where viewshed DB was saved. #[arg(value_name = "Path to existing database")] pub db_path: std::path::PathBuf, @@ -170,7 +170,7 @@ pub struct Viewshed { /// Arguments to the `post-process` subcommand. #[derive(clap::Parser, Debug)] -pub struct PostProcess { +pub(crate) struct PostProcess { /// Directory where viewshed DBs were saved. #[arg(value_name = "Path to existing databases")] pub db_dir: std::path::PathBuf, @@ -211,7 +211,7 @@ fn is_perfect_square(argument: &str) -> Result { /// Where to run the computations. #[derive(clap::ValueEnum, Clone, Debug, Default)] -pub enum Backend { +pub(crate) enum Backend { /// Optimised cache-efficient CPU kernel #[default] CPU, @@ -221,7 +221,7 @@ pub enum Backend { /// Which calculations to process. #[derive(clap::ValueEnum, Clone, Debug, PartialEq, Eq)] -pub enum Process { +pub(crate) enum Process { /// Calculate everything. All, /// Compute all the polar segments saving them to disk so that they can be used to later @@ -231,7 +231,7 @@ pub enum Process { /// Where to run the computations. #[derive(clap::ValueEnum, Clone, Debug, Copy, Default)] -pub enum HeatmapNormalisation { +pub(crate) enum HeatmapNormalisation { /// Just scale between 0 and 1 UnitScale, /// Scale between 0 and 1 with an exponential factor. diff --git a/crates/total-viewsheds/src/dump_usage.rs b/crates/total-viewsheds/src/dump_usage.rs index 55bc641..1527ac3 100644 --- a/crates/total-viewsheds/src/dump_usage.rs +++ b/crates/total-viewsheds/src/dump_usage.rs @@ -5,7 +5,7 @@ use color_eyre::Result; /// main dump usage function #[expect(clippy::print_stdout, reason = "We need to output the usage.")] -pub fn dump_full_usage_for_readme() -> Result<()> { +pub(crate) fn dump_full_usage_for_readme() -> Result<()> { use clap::CommandFactory as _; let mut command = crate::config::Config::command(); diff --git a/crates/total-viewsheds/src/los_pack.rs b/crates/total-viewsheds/src/los_pack.rs index 3916bab..287e810 100644 --- a/crates/total-viewsheds/src/los_pack.rs +++ b/crates/total-viewsheds/src/los_pack.rs @@ -10,7 +10,7 @@ const U10_MAX: u32 = (1 << 10) - 1; #[derive(Default, Debug, Clone, Copy)] /// Line of sight data that can be packed within 32 bits. -pub struct LineOfSightPacked(f32); +pub(crate) struct LineOfSightPacked(f32); #[expect( clippy::big_endian_bytes, @@ -21,7 +21,7 @@ impl LineOfSightPacked { /// /// f32 is better than u32 because I imagine that floats are just more widely recognised in general. /// At the end of the day, they're just bits that are neither valid f32 nor u32. - pub fn new(distance: u32, angle: u16) -> Result { + pub(crate) fn new(distance: u32, angle: u16) -> Result { if distance > U22_MAX { color_eyre::eyre::bail!("{} is greater than u22 max {U22_MAX}", distance); } @@ -37,7 +37,7 @@ impl LineOfSightPacked { /// create a packed line of sight without checking if the values fit within /// the packed bit widths - pub unsafe fn new_unchecked(distance: u32, angle: u16) -> Self { + pub(crate) unsafe fn new_unchecked(distance: u32, angle: u16) -> Self { let angle_u32 = u32::from(angle); let distance_bits = (distance & U22_MAX) << 10u32; @@ -54,25 +54,25 @@ impl LineOfSightPacked { } /// Distance in meters from the point of view to the visible point. - pub const fn distance(self) -> u32 { + pub(crate) const fn distance(self) -> u32 { (self.to_u32() >> 10u32) & U22_MAX } /// The angle of the line of sight from the point of view. #[expect(clippy::as_conversions, reason = "(self & 2^10) < 2^16")] - pub const fn angle(self) -> u16 { + pub(crate) const fn angle(self) -> u16 { (self.to_u32() & U10_MAX) as u16 } /// Get the raw f32 value. It doesn't represent any useful data, it's just the f32 view of the /// packed data. - pub const fn as_f32(self) -> f32 { + pub(crate) const fn as_f32(self) -> f32 { self.0 } /// calculate the longer packed line of sight, and when distances are equal /// choose the smaller angle - pub const fn max(self, rhs: Self) -> Self { + pub(crate) const fn max(self, rhs: Self) -> Self { if rhs.distance() > self.distance() { return rhs; } diff --git a/crates/total-viewsheds/src/main.rs b/crates/total-viewsheds/src/main.rs index dfedfa7..8bb6314 100644 --- a/crates/total-viewsheds/src/main.rs +++ b/crates/total-viewsheds/src/main.rs @@ -2,12 +2,11 @@ #![feature(portable_simd)] #![feature(specialization)] #![feature(mpmc_channel)] +#![feature(generic_const_exprs)] #![expect( incomplete_features, reason = "our usage isn't crazy and unlikely to break" )] -#![feature(generic_const_exprs)] -#![expect(clippy::pub_use, reason = "I admit I don't understand the other way.")] #![cfg_attr( test, expect( @@ -40,7 +39,7 @@ mod workers; /// cpu implements a CPU kernel for the longest line of sight mod compute { - pub mod area_of_interest; + pub(crate) mod area_of_interest; /// los contains all the traits necessary for implementing a line of sight algorithm mod los; @@ -49,7 +48,7 @@ mod compute { mod rotator; /// kernel is the exported kernel module - pub mod kernel; + pub(crate) mod kernel; /// `unrolled_los` holds a fully implemented los trait for unrolled vectorization mod unrolled_los; @@ -57,31 +56,30 @@ mod compute { /// `vector_intrinsics` holds all the vector-related LOS intrinsics mod vector_intrinsics; - pub use kernel::kernel; + pub(crate) use kernel::kernel; } /// Database for viewsheds mod storage { - pub mod db; - pub mod engine; - pub mod segments; - pub mod worker; + pub(crate) mod db; + pub(crate) mod engine; + pub(crate) mod segments; + pub(crate) mod worker; } /// Various ways to output data. mod output { - pub mod ascii; - pub mod bresenham; - pub mod png; - pub mod tiff; + mod ascii; + mod bresenham; + pub(crate) mod png; + pub(crate) mod tiff; /// Load, parse and reconstruct euclidean polygon viewsheds from their raw polar segments. - pub mod viewsheds { - pub mod growable_polygon; - pub mod joiner_common; - pub mod joiner_final; - pub mod segment_polygon; - pub mod viewshed; + pub(crate) mod viewsheds { + mod growable_polygon; + mod joiner; + mod segment_polygon; + pub(crate) mod viewshed; } } @@ -209,5 +207,5 @@ fn compute(config: &config::Compute) -> Result<()> { #[cfg(test)] mod tests { - pub mod fixtures; + pub(crate) mod fixtures; } diff --git a/crates/total-viewsheds/src/output/ascii.rs b/crates/total-viewsheds/src/output/ascii.rs index fb6f72e..d451f08 100644 --- a/crates/total-viewsheds/src/output/ascii.rs +++ b/crates/total-viewsheds/src/output/ascii.rs @@ -7,7 +7,7 @@ use geo::CoordsIter as _; use crate::output::viewsheds::viewshed::Viewshed; -pub fn make_viewshed( +pub(crate) fn make_viewshed( elevations: &[i16], viewshed_pov: geo::Coord, config: crate::run::Config, @@ -57,7 +57,10 @@ pub fn make_viewshed( rasterise_multi_polygon(&viewshed, dem.width) } -pub fn rasterise_multi_polygon(multi_polygon: &geo::MultiPolygon, width: u32) -> Vec { +pub(crate) fn rasterise_multi_polygon( + multi_polygon: &geo::MultiPolygon, + width: u32, +) -> Vec { let scale = std::env::var("ASCII_TEST_SIZE") .unwrap_or_else(|_| "2".to_owned()) .parse::() @@ -164,7 +167,7 @@ pub fn rasterise_multi_polygon(multi_polygon: &geo::MultiPolygon, width: u32) -> ascii } -pub fn rasterise(mut multi_polygon: geo::MultiPolygon) -> Vec { +pub(crate) fn rasterise(mut multi_polygon: geo::MultiPolygon) -> Vec { let width = 12u32; let centre = f64::from(width.div_euclid(2)); for polygons in multi_polygon.iter_mut() { @@ -192,7 +195,7 @@ pub fn rasterise(mut multi_polygon: geo::MultiPolygon) -> Vec { } #[expect(clippy::print_stderr, reason = "This is for tests")] -pub fn assert_rasterised(actual: &[String], expected: &[&str]) { +pub(crate) fn assert_rasterised(actual: &[String], expected: &[&str]) { if actual != expected { eprintln!("Actual:"); eprint!("{}", actual.join("\n")); diff --git a/crates/total-viewsheds/src/output/bresenham.rs b/crates/total-viewsheds/src/output/bresenham.rs index f4e49dc..7c9109c 100644 --- a/crates/total-viewsheds/src/output/bresenham.rs +++ b/crates/total-viewsheds/src/output/bresenham.rs @@ -2,7 +2,7 @@ #[derive(Clone, Copy, Debug)] /// A raster coodinate. -pub struct RasterCoord { +pub(crate) struct RasterCoord { /// The x coordinate. pub x: i32, /// The y coordinate. @@ -20,7 +20,7 @@ impl From<(i32, i32)> for RasterCoord { /// Iterator that yields (x, y) pixels on a line using Bresenham's algorithm. /// -pub struct Bresenham { +pub(crate) struct Bresenham { /// The current start of the line. Updates as we move through the rasterisation. from: RasterCoord, /// Where the line ends. @@ -38,7 +38,7 @@ pub struct Bresenham { impl Bresenham { /// Instantiate. - pub fn new(from: RasterCoord, to: RasterCoord) -> Self { + pub(crate) fn new(from: RasterCoord, to: RasterCoord) -> Self { let delta = RasterCoord { x: (to.x - from.x).abs(), y: (to.y - from.y).abs(), diff --git a/crates/total-viewsheds/src/output/png.rs b/crates/total-viewsheds/src/output/png.rs index 238189e..4215268 100644 --- a/crates/total-viewsheds/src/output/png.rs +++ b/crates/total-viewsheds/src/output/png.rs @@ -2,7 +2,7 @@ use color_eyre::{Result, eyre::ContextCompat as _}; /// Convert an array of floats to a grayscale heatmap. -pub fn save( +pub(crate) fn save( data: &[f32], width: u32, height: u32, diff --git a/crates/total-viewsheds/src/output/tiff.rs b/crates/total-viewsheds/src/output/tiff.rs index 1de965d..9213b15 100644 --- a/crates/total-viewsheds/src/output/tiff.rs +++ b/crates/total-viewsheds/src/output/tiff.rs @@ -3,7 +3,7 @@ use color_eyre::Result; /// Save an array of `f32`s (total surfaces, longest lines of sight) to a `.tiff` file. -pub fn save(dem: &tvs_lib::dem::DEM, data: &[f32], path: &std::path::PathBuf) -> Result<()> { +pub(crate) fn save(dem: &tvs_lib::dem::DEM, data: &[f32], path: &std::path::PathBuf) -> Result<()> { let driver = gdal::DriverManager::get_driver_by_name("GTiff")?; let mut dataset = driver.create_with_band_type::( diff --git a/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs b/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs index 995c314..1edf30d 100644 --- a/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs +++ b/crates/total-viewsheds/src/output/viewsheds/growable_polygon.rs @@ -6,7 +6,7 @@ use color_eyre::eyre::{ContextCompat as _, Result, bail}; /// The `u32` values in the variants are for storing the polar distance from the centre. This saves /// having to do trigonometry to figure out if two openings are touching. #[derive(Eq, PartialEq, Debug, Clone)] -pub enum Opening { +pub(crate) enum Opening { /// No opening. We could also just use `Option::None`, but unwrapping it isn't so ergonomic. Null, /// This i for polygons that start at 0 degrees. It's possible that another polygon (or even @@ -29,7 +29,7 @@ pub enum Opening { /// A vertex is a single point in a polygon. #[derive(Debug, Clone)] -pub struct Vertex { +pub(crate) struct Vertex { /// The coordinate of the vertex in euclidian space. pub coordinate: geo::Coord, /// The kind of opening that this vertex represents. @@ -39,14 +39,14 @@ pub struct Vertex { impl Vertex { /// Is the vertex at the centre of the viewshed? The centre is a special place, it is a /// zero-length opening that isn't nulled if no segments/polygons touch it for a given angle. - pub fn is_centre(&self) -> bool { + pub(crate) fn is_centre(&self) -> bool { self.coordinate.x.abs() == 0.0 && self.coordinate.y.abs() == 0.0 } } /// A polygon that grows, but only from its anti-clockwise facing side. #[derive(Debug, Clone)] -pub struct GrowablePolygon { +pub(crate) struct GrowablePolygon { /// The main exterior vertices. pub vertices: Vec, /// Interiore holes within the polygon. @@ -66,7 +66,10 @@ pub struct GrowablePolygon { impl GrowablePolygon { /// Create a new growable polygon. It always begins with a single polar segment converted to /// its euclidean coordinates. - pub fn new(segment_vertices: &super::segment_polygon::Vertices, angle: f32) -> Self { + pub(crate) fn new( + segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, + angle: f32, + ) -> Self { let mut vertices = Vec::new(); for (segment_index, segment_vertex) in segment_vertices.vertices.iter().enumerate() { let opening = match segment_index { @@ -117,7 +120,10 @@ impl GrowablePolygon { /// w└──┘x w└──┘x /// /// (abcde) + (wxyz) = (wxyzcdeb) - fn new_for_insertion(segment_vertices: &super::segment_polygon::Vertices, angle: f32) -> Self { + fn new_for_insertion( + segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, + angle: f32, + ) -> Self { let mut polygon = Self::new(segment_vertices, angle); #[expect( @@ -140,18 +146,16 @@ impl GrowablePolygon { /// polygon as follows: /// * `Opening::NewStart/NewEnd` become `Opening::Start/End`. /// * `Opening::Start/End` become `Opening::Null`. - pub fn downgrade_openings(&mut self) -> Result<()> { + pub(crate) fn downgrade_openings(&mut self) -> Result<()> { tracing::trace!("Downgrading openings"); let mut iterator = self.vertices.iter_mut().rev(); while let Some(vertex) = iterator.next() { match vertex.opening { - super::growable_polygon::Opening::Null - | super::growable_polygon::Opening::GenesisStart(_) - | super::growable_polygon::Opening::GenesisEnd(_) => {} - super::growable_polygon::Opening::End(_) => { + Opening::Null | Opening::GenesisStart(_) | Opening::GenesisEnd(_) => {} + Opening::End(_) => { bail!("Dangling `Opening`"); } - super::growable_polygon::Opening::Start(_) => { + Opening::Start(_) => { let Some(next) = iterator.next() else { bail!("`Opening::Start` without adjacent end"); }; @@ -159,10 +163,10 @@ impl GrowablePolygon { vertex.opening = Opening::Null; next.opening = Opening::Null; } - super::growable_polygon::Opening::NewStart(at) => { + Opening::NewStart(at) => { vertex.opening = Opening::Start(at); } - super::growable_polygon::Opening::NewEnd(at) => { + Opening::NewEnd(at) => { vertex.opening = Opening::End(at); } } @@ -201,9 +205,9 @@ impl GrowablePolygon { } /// Insert a segment into the polygon. - pub fn join_segment( + pub(crate) fn join_segment( &mut self, - segment_vertices: &super::segment_polygon::Vertices, + segment_vertices: &crate::output::viewsheds::segment_polygon::Vertices, vertices_range: std::ops::Range, angle: f32, ) -> Result<()> { @@ -239,7 +243,7 @@ impl GrowablePolygon { /// We know that a segment always has `NewEnd` and `NewStart` openings. Therefore we can /// calculate the index "x" at which the new polygon should be inserted by finding `NewEnd` and /// substracting 1. - pub fn join_non_starting_polygon( + pub(crate) fn join_non_starting_polygon( &mut self, base_vertices_range: std::ops::Range, joining_polygon: &mut Self, @@ -292,7 +296,7 @@ impl GrowablePolygon { } /// Insert a starting polygon (from angle 0) into a final polygon (from angle ~360). - pub fn join_starting_polygon( + pub(crate) fn join_starting_polygon( &mut self, base_vertices_range: std::ops::Range, joining_vertices_range: std::ops::Range, @@ -330,7 +334,7 @@ impl GrowablePolygon { } /// Join a polygon into itself. - pub fn join_self( + pub(crate) fn join_self( &mut self, left_range: std::ops::Range, right_range: std::ops::Range, @@ -350,7 +354,7 @@ impl GrowablePolygon { } /// Extract the starting vertex of an opening that has just been joined to. - pub fn extract_old_start(&mut self, index: usize) -> Result { + pub(crate) fn extract_old_start(&mut self, index: usize) -> Result { let vertex = self .vertices .get_mut(index) @@ -376,12 +380,15 @@ impl GrowablePolygon { /// Is a segment and a polygon, or 2 polygons, touching? We decide by whether their openings are /// overlapping. - pub const fn is_touching(left: &std::ops::Range, right: &std::ops::Range) -> bool { + pub(crate) const fn is_touching( + left: &std::ops::Range, + right: &std::ops::Range, + ) -> bool { left.start < right.end && right.start < left.end } /// Convert the polygon to the `geo` crate's representation. Ready for exporting to `GeoJSON`. - pub fn to_geo_polygon(&self) -> geo::Polygon { + pub(crate) fn to_geo_polygon(&self) -> geo::Polygon { let holes: Vec = self .holes .iter() @@ -417,7 +424,7 @@ impl GrowablePolygon { /// Dedupe vertices, but allow destroying opening metadata. This is useful right at the very end /// of the reconstruction. - pub fn dedup_vertices_ignore_openings(&mut self) { + pub(crate) fn dedup_vertices_ignore_openings(&mut self) { self.vertices.dedup_by(|left, right| { Self::are_coordinates_within_tolerance(&left.coordinate, &right.coordinate) }); diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner.rs b/crates/total-viewsheds/src/output/viewsheds/joiner.rs new file mode 100644 index 0000000..38abecb --- /dev/null +++ b/crates/total-viewsheds/src/output/viewsheds/joiner.rs @@ -0,0 +1,60 @@ +//! Join segments and polygons to existing growable polygons. + +mod last; +mod normal; + +use color_eyre::Result; + +/// Keeps track of active and completed polygons within a viewshed. +#[derive(Default)] +struct Joiner { + /// Polygons that don't intersect with the current angle. They don't need to be checked against + /// new segments. + completed: Vec, + /// Polygons that intersect with the current angle. They must be checked to see if any of their + /// openings touch any of the segments of the current angle. + active: Vec, +} + +/// Build a viewshed of euclidean polygons from polar segments. +pub(crate) fn build( + data: &[Vec], + dem_scale: f32, +) -> Result { + #[expect( + clippy::as_conversions, + clippy::cast_precision_loss, + reason = "The angle count should never strain the f32 mantissa" + )] + let angle_count = data.len() as f32; + let angle_scale = angle_count / 360.0; + + let mut joiner = Joiner { + completed: Vec::new(), + active: Vec::new(), + }; + + for (anglish, segments) in data.iter().enumerate() { + #[expect( + clippy::as_conversions, + clippy::cast_precision_loss, + reason = "The angle count should never strain the f32 mantissa" + )] + let angle = anglish as f32 / angle_scale; + + joiner.build_angle(angle, angle_scale, segments, dem_scale)?; + } + + joiner.build_final_angle()?; + joiner.move_all_active_polygons_to_completed(); + + let mut geo_polygons = Vec::new(); + for mut raw_polygon in joiner.completed { + raw_polygon.dedup_vertices_ignore_openings(); + let polygon = raw_polygon.to_geo_polygon(); + tracing::trace!("Final viewshed, adding polygon: {polygon:?}"); + geo_polygons.push(polygon); + } + + Ok(geo::MultiPolygon::new(geo_polygons)) +} diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner_final.rs b/crates/total-viewsheds/src/output/viewsheds/joiner/last.rs similarity index 91% rename from crates/total-viewsheds/src/output/viewsheds/joiner_final.rs rename to crates/total-viewsheds/src/output/viewsheds/joiner/last.rs index 97c0ada..7b17047 100644 --- a/crates/total-viewsheds/src/output/viewsheds/joiner_final.rs +++ b/crates/total-viewsheds/src/output/viewsheds/joiner/last.rs @@ -5,7 +5,9 @@ use color_eyre::eyre::{ContextCompat as _, Result, bail}; use itertools::Itertools as _; -impl super::joiner_common::Joiner { +use crate::output::viewsheds::growable_polygon::Opening; + +impl super::Joiner { /// The final angle faces the challenge of joining polygons together from the first angle. pub(crate) fn build_final_angle(&mut self) -> Result<()> { tracing::trace!("Building viewshed for final angle"); @@ -101,19 +103,19 @@ impl super::joiner_common::Joiner { let mut iterator = vertices_clone.iter().enumerate().rev(); while let Some((index, vertex)) = iterator.next() { match vertex.opening { - super::growable_polygon::Opening::Null - | super::growable_polygon::Opening::NewStart(_) - | super::growable_polygon::Opening::NewEnd(_) - | super::growable_polygon::Opening::Start(_) - | super::growable_polygon::Opening::End(_) => (), - super::growable_polygon::Opening::GenesisStart(_) => { + Opening::Null + | Opening::NewStart(_) + | Opening::NewEnd(_) + | Opening::Start(_) + | Opening::End(_) => (), + Opening::GenesisStart(_) => { bail!("Dangling `Opening::GenesisStart`"); } - super::growable_polygon::Opening::GenesisEnd(start) => { + Opening::GenesisEnd(start) => { let Some(next) = iterator.next() else { bail!("`Opening::GenesisEnd` without a following vertex"); }; - let super::growable_polygon::Opening::GenesisStart(end) = next.1.opening else { + let Opening::GenesisStart(end) = next.1.opening else { tracing::error!( "Bad opening ({:?}) in polygon: {vertices_clone:#?}", next.1.opening @@ -182,20 +184,19 @@ impl super::joiner_common::Joiner { let mut iterator = base_polygon.vertices.iter_mut().enumerate().rev(); while let Some((index, vertex)) = iterator.next() { match vertex.opening { - super::growable_polygon::Opening::Start(opening_start) => { + Opening::Start(opening_start) => { let Some((index_next, vertex_next)) = iterator.next() else { bail!("Opening without following vertex"); }; - let super::growable_polygon::Opening::End(opening_end) = vertex_next.opening - else { + let Opening::End(opening_end) = vertex_next.opening else { bail!("Opening `Start` not followed by opening `End`"); }; let base_opening_range = opening_start..opening_end; tracing::trace!("Checking touching for vertex ({:?})...", vertex); - if super::growable_polygon::GrowablePolygon::is_touching( + if crate::output::viewsheds::growable_polygon::GrowablePolygon::is_touching( &base_opening_range, &joining_opening_range, ) { @@ -215,14 +216,14 @@ impl super::joiner_common::Joiner { ); } } - super::growable_polygon::Opening::End(_) => { + Opening::End(_) => { bail!("Unexpected opening `End` reached"); } - super::growable_polygon::Opening::Null - | super::growable_polygon::Opening::GenesisStart(_) - | super::growable_polygon::Opening::GenesisEnd(_) - | super::growable_polygon::Opening::NewStart(_) - | super::growable_polygon::Opening::NewEnd(_) => (), + Opening::Null + | Opening::GenesisStart(_) + | Opening::GenesisEnd(_) + | Opening::NewStart(_) + | Opening::NewEnd(_) => (), } } @@ -260,12 +261,11 @@ impl super::joiner_common::Joiner { #[cfg(test)] mod test { - #[test] fn final_segment_joins_starting_segment_simple() { crate::setup_logging().unwrap(); let part = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[part.clone(), vec![], vec![], vec![], vec![], part], 1.0, ) @@ -291,7 +291,7 @@ mod test { #[test] fn inherit_holes_final() { crate::setup_logging().unwrap(); - let joined = crate::output::viewsheds::joiner_common::Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ vec![crate::storage::segments::Segment::new(0, 3)], vec![ @@ -332,7 +332,7 @@ mod test { crate::storage::segments::Segment::new(0, 1), crate::storage::segments::Segment::new(2, 1), ]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[starting, vec![], vec![], vec![], vec![], long], 1.0, ) @@ -363,7 +363,7 @@ mod test { crate::storage::segments::Segment::new(2, 1), crate::storage::segments::Segment::new(4, 1), ]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[starting, long.clone(), vec![], vec![], vec![], long], 1.0, ) @@ -391,7 +391,7 @@ mod test { crate::setup_logging().unwrap(); let long = vec![crate::storage::segments::Segment::new(0, 4)]; let short = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ short.clone(), long.clone(), @@ -428,7 +428,7 @@ mod test { crate::setup_logging().unwrap(); let part = vec![crate::storage::segments::Segment::new(2, 2)]; let ring = vec![part; 10]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build(&ring, 1.0).unwrap(); + let joined = crate::output::viewsheds::joiner::build(&ring, 1.0).unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", @@ -455,7 +455,7 @@ mod test { crate::storage::segments::Segment::new(3, 1), ]; let ring = vec![part; 10]; - let joined = crate::output::viewsheds::joiner_common::Joiner::build(&ring, 1.0).unwrap(); + let joined = crate::output::viewsheds::joiner::build(&ring, 1.0).unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", diff --git a/crates/total-viewsheds/src/output/viewsheds/joiner_common.rs b/crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs similarity index 88% rename from crates/total-viewsheds/src/output/viewsheds/joiner_common.rs rename to crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs index 2fa1de3..22b8396 100644 --- a/crates/total-viewsheds/src/output/viewsheds/joiner_common.rs +++ b/crates/total-viewsheds/src/output/viewsheds/joiner/normal.rs @@ -1,66 +1,14 @@ -//! Join the visible segments from each computed angle into the viewshed. These "common" joinings -//! are the ones the occur for every angle other than the final one. Therefore these should simpler +//! Join the visible segments from each computed angle into the viewshed. These "normal" joinings +//! are the ones that occur for every angle other than the final one. Therefore these should simpler //! and faster. use color_eyre::eyre::{Result, bail}; -/// Keeps track of active and completed polygons within a viewshed. -#[derive(Default)] -pub struct Joiner { - /// Polygons that don't intersect with the current angle. They don't need to be checked against - /// new segments. - pub completed: Vec, - /// Polygons that intersect with the current angle. They must be checked to see if any of their - /// openings touch any of the segments of the current angle. - pub active: Vec, -} - -impl Joiner { - /// Build a viewshed of euclidean polygons from polar segments. - pub fn build( - data: &[Vec], - dem_scale: f32, - ) -> Result { - #[expect( - clippy::as_conversions, - clippy::cast_precision_loss, - reason = "The angle count should never strain the f32 mantissa" - )] - let angle_count = data.len() as f32; - let angle_scale = angle_count / 360.0; - - let mut joiner = Self { - completed: Vec::new(), - active: Vec::new(), - }; - - for (anglish, segments) in data.iter().enumerate() { - #[expect( - clippy::as_conversions, - clippy::cast_precision_loss, - reason = "The angle count should never strain the f32 mantissa" - )] - let angle = anglish as f32 / angle_scale; - - joiner.build_angle(angle, angle_scale, segments, dem_scale)?; - } - - joiner.build_final_angle()?; - joiner.move_all_active_polygons_to_completed(); - - let mut geo_polygons = Vec::new(); - for mut raw_polygon in joiner.completed { - raw_polygon.dedup_vertices_ignore_openings(); - let polygon = raw_polygon.to_geo_polygon(); - tracing::trace!("Final viewshed, adding polygon: {polygon:?}"); - geo_polygons.push(polygon); - } - - Ok(geo::MultiPolygon::new(geo_polygons)) - } +use crate::output::viewsheds::growable_polygon::Opening; +impl super::Joiner { /// Build the viewshed for a single angle. - fn build_angle( + pub(crate) fn build_angle( &mut self, angle: f32, angle_scale: f32, @@ -80,7 +28,7 @@ impl Joiner { let mut new_polygons = Vec::new(); - let polygoner = super::segment_polygon::SegmentPolygon { + let polygoner = crate::output::viewsheds::segment_polygon::SegmentPolygon { dem_scale, angle, angle_scale, @@ -160,14 +108,17 @@ impl Joiner { /// When a segment doesn't touch anything it becomes its own independent polygon. fn create_new_polygon_from_untouched_segment( - polygon_segment: &super::segment_polygon::Vertices, + polygon_segment: &crate::output::viewsheds::segment_polygon::Vertices, angle: f32, - ) -> Result { + ) -> Result { tracing::debug!( "Segment not touching anything at angle {angle}, \ so making it its own polygon: {polygon_segment:?}" ); - let mut polygon = super::growable_polygon::GrowablePolygon::new(polygon_segment, angle); + let mut polygon = crate::output::viewsheds::growable_polygon::GrowablePolygon::new( + polygon_segment, + angle, + ); if angle == 0.0 { polygon.is_created_at_angle_0 = true; } @@ -195,7 +146,7 @@ impl Joiner { } /// Once all angles have been checked, we can move all active polygons to "completed". - fn move_all_active_polygons_to_completed(&mut self) { + pub(crate) fn move_all_active_polygons_to_completed(&mut self) { let active = self.active.drain(..); self.completed.extend(active); } @@ -244,7 +195,7 @@ impl Joiner { fn join_segment( &mut self, growable_polygon_index: usize, - segment: &super::segment_polygon::Vertices, + segment: &crate::output::viewsheds::segment_polygon::Vertices, maybe_joining_polygon_index: Option, angle: f32, ) -> Result { @@ -262,13 +213,12 @@ impl Joiner { let mut iterator = base_polygon.vertices.iter_mut().enumerate().rev(); while let Some((index, vertex)) = iterator.next() { match vertex.opening { - super::growable_polygon::Opening::Start(opening_start) => { + Opening::Start(opening_start) => { let Some((index_next, vertex_next)) = iterator.next() else { bail!("Opening without following vertex"); }; - let super::growable_polygon::Opening::End(opening_end) = vertex_next.opening - else { + let Opening::End(opening_end) = vertex_next.opening else { bail!("Opening start not followed by opening end"); }; @@ -278,7 +228,7 @@ impl Joiner { "Checking touch for polygon {growable_polygon_index} at opening index: \ {index}, distances: {opening_range:?}", ); - if super::growable_polygon::GrowablePolygon::is_touching( + if crate::output::viewsheds::growable_polygon::GrowablePolygon::is_touching( &segment.distances, &opening_range, ) { @@ -296,14 +246,14 @@ impl Joiner { ); } } - super::growable_polygon::Opening::End(_) => { + Opening::End(_) => { bail!("Unexpected opening end reached"); } - super::growable_polygon::Opening::Null - | super::growable_polygon::Opening::GenesisStart(_) - | super::growable_polygon::Opening::GenesisEnd(_) - | super::growable_polygon::Opening::NewStart(_) - | super::growable_polygon::Opening::NewEnd(_) => (), + Opening::Null + | Opening::GenesisStart(_) + | Opening::GenesisEnd(_) + | Opening::NewStart(_) + | Opening::NewEnd(_) => (), } } @@ -342,8 +292,8 @@ impl Joiner { growable_polygon_index: usize, maybe_joining_polygon_index: Option, ) -> ( - &mut super::growable_polygon::GrowablePolygon, - Option<&mut super::growable_polygon::GrowablePolygon>, + &mut crate::output::viewsheds::growable_polygon::GrowablePolygon, + Option<&mut crate::output::viewsheds::growable_polygon::GrowablePolygon>, ) { if let Some(joining_polygon_index) = maybe_joining_polygon_index { #[expect( @@ -369,13 +319,15 @@ impl Joiner { #[cfg(test)] mod test { - use super::*; - #[test] fn multiple_angles() { crate::setup_logging().unwrap(); let segment = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = Joiner::build(&[segment.clone(), segment.clone(), segment], 1.0).unwrap(); + let joined = crate::output::viewsheds::joiner::build( + &[segment.clone(), segment.clone(), segment], + 1.0, + ) + .unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", @@ -402,7 +354,7 @@ mod test { crate::storage::segments::Segment::new(0, 2), crate::storage::segments::Segment::new(3, 1), ]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ main.clone(), main.clone(), @@ -436,7 +388,7 @@ mod test { fn multiple_polygons() { crate::setup_logging().unwrap(); let segment = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ segment.clone(), vec![], @@ -471,7 +423,7 @@ mod test { fn multiple_polygons_not_touching() { crate::setup_logging().unwrap(); let segment = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ segment.clone(), vec![], @@ -507,7 +459,7 @@ mod test { crate::setup_logging().unwrap(); let main = vec![crate::storage::segments::Segment::new(0, 4)]; let variance = vec![crate::storage::segments::Segment::new(0, 2)]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[main.clone(), main.clone(), main.clone(), main, variance], 1.0, ) @@ -539,7 +491,11 @@ mod test { crate::storage::segments::Segment::new(3, 1), ]; let bottom = vec![crate::storage::segments::Segment::new(0, 2)]; - let joined = Joiner::build(&[main.clone(), pair, bottom, main.clone(), main], 1.0).unwrap(); + let joined = crate::output::viewsheds::joiner::build( + &[main.clone(), pair, bottom, main.clone(), main], + 1.0, + ) + .unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", @@ -563,7 +519,7 @@ mod test { crate::setup_logging().unwrap(); let main = vec![crate::storage::segments::Segment::new(0, 4)]; let top = vec![crate::storage::segments::Segment::new(2, 2)]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ main.clone(), top.clone(), @@ -598,8 +554,11 @@ mod test { crate::setup_logging().unwrap(); let main = vec![crate::storage::segments::Segment::new(0, 4)]; let lid = vec![crate::storage::segments::Segment::new(2, 1)]; - let joined = - Joiner::build(&[main.clone(), main.clone(), lid, main.clone(), main], 1.0).unwrap(); + let joined = crate::output::viewsheds::joiner::build( + &[main.clone(), main.clone(), lid, main.clone(), main], + 1.0, + ) + .unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", @@ -627,7 +586,7 @@ mod test { crate::storage::segments::Segment::new(2, 1), crate::storage::segments::Segment::new(4, 1), ]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[main.clone(), main.clone(), main.clone(), struts, main], 1.0, ) @@ -658,7 +617,7 @@ mod test { crate::storage::segments::Segment::new(4, 1), ]; let long = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ polygons.clone(), polygons.clone(), @@ -701,7 +660,9 @@ mod test { let first = vec![higher_but_made_first.clone()]; let second = vec![lower, higher_but_made_first]; let long = vec![crate::storage::segments::Segment::new(0, 5)]; - let joined = Joiner::build(&[vec![], first, second, long, vec![]], 1.0).unwrap(); + let joined = + crate::output::viewsheds::joiner::build(&[vec![], first, second, long, vec![]], 1.0) + .unwrap(); let actual = crate::output::ascii::rasterise(joined); let expected = [ "████████████████████████", @@ -724,7 +685,7 @@ mod test { fn inherit_holes_common() { crate::setup_logging().unwrap(); - let joined = Joiner::build( + let joined = crate::output::viewsheds::joiner::build( &[ vec![], vec![crate::storage::segments::Segment::new(0, 3)], diff --git a/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs b/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs index bd68e7a..ceb072d 100644 --- a/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs +++ b/crates/total-viewsheds/src/output/viewsheds/segment_polygon.rs @@ -2,7 +2,7 @@ /// The 5 vertices of a segment and their distances from the centre. #[derive(Debug)] -pub struct Vertices { +pub(crate) struct Vertices { /// The 5 vertices of a segment. The 4 corners plus a copy of the first vertex to close the /// polygon. pub vertices: [geo::Coord; 5], @@ -11,7 +11,7 @@ pub struct Vertices { } /// `SegmentPolygon` -pub struct SegmentPolygon { +pub(crate) struct SegmentPolygon { /// Scale of DEM data. pub dem_scale: f32, /// The current sector angle. @@ -22,7 +22,7 @@ pub struct SegmentPolygon { impl SegmentPolygon { /// Make a single polygon representing a visible region of the planet. - pub fn make(&self, opening_index: u32, closing_index: u32) -> Vertices { + pub(crate) fn make(&self, opening_index: u32, closing_index: u32) -> Vertices { let opening_coord = self.index_to_coordinate(opening_index); let closing_coord = self.index_to_coordinate(closing_index); diff --git a/crates/total-viewsheds/src/output/viewsheds/viewshed.rs b/crates/total-viewsheds/src/output/viewsheds/viewshed.rs index 9725f62..1501d66 100644 --- a/crates/total-viewsheds/src/output/viewsheds/viewshed.rs +++ b/crates/total-viewsheds/src/output/viewsheds/viewshed.rs @@ -7,10 +7,10 @@ use color_eyre::eyre::Result; /// but metric projections are not globally correct. So reprojecting to the _viewshed's_ centre /// just gives us that little bit more accuracy, especially for larger DEMs. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Coordinate(pub geo::Coord); +pub(crate) struct Coordinate(pub geo::Coord); /// `Viewshed` -pub struct Viewshed<'viewshed> { +pub(crate) struct Viewshed<'viewshed> { /// The DEM used to compute the final data. pub dem: &'viewshed tvs_lib::dem::DEM, /// Coordinate of the observer for the viewshed we want to reconstruct. @@ -19,7 +19,7 @@ pub struct Viewshed<'viewshed> { impl Viewshed<'_> { /// Reconstruct a viewshed. - pub fn reconstruct( + pub(crate) fn reconstruct( db_path: std::path::PathBuf, requested_lonlat: tvs_lib::projector::LonLatCoord, ) -> Result<(tvs_lib::dem::Coordinate, geo::MultiPolygon)> { @@ -85,7 +85,7 @@ impl Viewshed<'_> { color_eyre::eyre::bail!("Point of view ({:?}) is not calculable", viewshed.pov_coord); } - let multi_polygon = super::joiner_common::Joiner::build(&segments, viewshed.dem.scale)?; + let multi_polygon = crate::output::viewsheds::joiner::build(&segments, viewshed.dem.scale)?; tracing::info!("Viewshed reconstructed in {:?}.", start.elapsed()); Ok((pov_dem_coord, multi_polygon)) @@ -142,7 +142,7 @@ impl Viewshed<'_> { } /// Save the viewshed to disk. - pub fn save( + pub(crate) fn save( viewshed: geo::MultiPolygon, output_directory: &std::path::Path, viewshed_latlon: tvs_lib::projector::LonLatCoord, @@ -163,7 +163,7 @@ impl Viewshed<'_> { #[cfg(test)] impl Viewshed<'_> { /// Convert from the viewshed projection to DEM coordinates. - pub fn convert_viewshed_coord_to_dem_coord( + pub(crate) fn convert_viewshed_coord_to_dem_coord( &self, viewshed_coord: Coordinate, ) -> Result { diff --git a/crates/total-viewsheds/src/post_process.rs b/crates/total-viewsheds/src/post_process.rs index 88fa74f..b59e1b4 100644 --- a/crates/total-viewsheds/src/post_process.rs +++ b/crates/total-viewsheds/src/post_process.rs @@ -4,7 +4,7 @@ use color_eyre::Result; use rayon::iter::{IntoParallelRefIterator as _, ParallelIterator as _}; /// Run post-processing tasks. -pub fn run(config: &crate::config::PostProcess) -> Result<()> { +pub(crate) fn run(config: &crate::config::PostProcess) -> Result<()> { #[expect( clippy::redundant_closure_for_method_calls, reason = "It's too verbose" diff --git a/crates/total-viewsheds/src/pre_process.rs b/crates/total-viewsheds/src/pre_process.rs index 8ecb543..43401de 100644 --- a/crates/total-viewsheds/src/pre_process.rs +++ b/crates/total-viewsheds/src/pre_process.rs @@ -5,7 +5,9 @@ use std::collections::HashMap; use color_eyre::eyre::{ContextCompat as _, Result}; /// Create a fast lookup for deciding which points should have their viewsheds saved. -pub fn create_biggest_tvs_subgrid(config: &crate::config::Compute) -> Result> { +pub(crate) fn create_biggest_tvs_subgrid( + config: &crate::config::Compute, +) -> Result> { let neighbourhood_size = config .only_save_biggest_viewsheds .context("Must specify --only-save-biggest-viewsheds")?; @@ -20,7 +22,7 @@ pub fn create_biggest_tvs_subgrid(config: &crate::config::Compute) -> Result Result> { @@ -51,7 +53,11 @@ pub fn find_biggest_total_surfaces( } /// Find which neighbourhood a given index is in. -pub const fn get_neighbourhood_id(index: i64, global_width: i64, neighbourhood_size: i64) -> i64 { +pub(crate) const fn get_neighbourhood_id( + index: i64, + global_width: i64, + neighbourhood_size: i64, +) -> i64 { let global_x = index.rem_euclid(global_width); let global_y = index.div_euclid(global_width); diff --git a/crates/total-viewsheds/src/run.rs b/crates/total-viewsheds/src/run.rs index 3b3919d..8a497d6 100644 --- a/crates/total-viewsheds/src/run.rs +++ b/crates/total-viewsheds/src/run.rs @@ -4,7 +4,7 @@ use color_eyre::Result; use std::path::PathBuf; /// Handles all the computations. -pub struct Compute<'compute> +pub(crate) struct Compute<'compute> where 'static: 'compute, { @@ -20,7 +20,7 @@ where #[derive(Clone)] /// Configuration for computing. -pub struct Config { +pub(crate) struct Config { /// The height of the observer that views viewsheds. pub observer_height: f32, /// What to compute. @@ -51,7 +51,7 @@ pub struct Config { impl<'compute> Compute<'compute> { /// Instantiate. - pub fn new(config: Config, dem: &'compute mut tvs_lib::dem::DEM) -> Result { + pub(crate) fn new(config: Config, dem: &'compute mut tvs_lib::dem::DEM) -> Result { if Self::is_process_viewsheds(&config.process) && !cfg!(any(test, feature = "ring_data")) { color_eyre::eyre::bail!( "Viewshed storage is only supported with the ring_data feature, \ @@ -73,13 +73,13 @@ impl<'compute> Compute<'compute> { } /// Are we computing viewsheds? - pub fn is_process_viewsheds(process: &[crate::config::Process]) -> bool { + pub(crate) fn is_process_viewsheds(process: &[crate::config::Process]) -> bool { Self::is_process_everything(process) || process.contains(&crate::config::Process::Viewsheds) } /// Render a heatmap and `.tiff` file of the total surface areas for each point within the computable area of the /// DEM. - pub fn render_total_surfaces(&self) -> Result<()> { + pub(crate) fn render_total_surfaces(&self) -> Result<()> { let Some(output_dir) = &self.config.output_directory else { return Ok(()); }; @@ -107,7 +107,7 @@ impl<'compute> Compute<'compute> { /// Render a heatmap and `.tiff` of the longest lines of sight for each point within the computable area of the /// DEM. - pub fn render_longest_lines(&self) -> Result<()> { + pub(crate) fn render_longest_lines(&self) -> Result<()> { let Some(output_dir) = &self.config.output_directory else { return Ok(()); }; @@ -156,11 +156,11 @@ impl<'compute> Compute<'compute> { } #[cfg(test)] -pub mod test { +pub(crate) mod test { use super::*; use googletest::prelude::*; - pub fn make_dem(elevations: &[i16]) -> tvs_lib::dem::DEM { + pub(crate) fn make_dem(elevations: &[i16]) -> tvs_lib::dem::DEM { let width = elevations.len().isqrt() as u32; let mut dem = tvs_lib::dem::DEM::new( tvs_lib::projector::LonLatCoord((33.33, 33.33).into()), @@ -173,14 +173,14 @@ pub mod test { dem } - pub fn default_metadata() -> tvs_lib::metadata::MetaData { + pub(crate) fn default_metadata() -> tvs_lib::metadata::MetaData { tvs_lib::metadata::MetaData { scale: 1.0, ..Default::default() } } - pub fn big_dem_metadata() -> tvs_lib::metadata::MetaData { + pub(crate) fn big_dem_metadata() -> tvs_lib::metadata::MetaData { tvs_lib::metadata::MetaData { width: 12, max_line_of_sight: 4, @@ -189,7 +189,7 @@ pub mod test { } } - pub fn default_config(temp_db: &tempfile::NamedTempFile) -> Config { + pub(crate) fn default_config(temp_db: &tempfile::NamedTempFile) -> Config { Config { observer_height: 0.8, process: vec![crate::config::Process::Viewsheds], @@ -207,7 +207,7 @@ pub mod test { } } - pub fn compute(dem: &mut tvs_lib::dem::DEM, config: Config) -> Compute<'_> { + pub(crate) fn compute(dem: &mut tvs_lib::dem::DEM, config: Config) -> Compute<'_> { let mut compute = Compute::new(config, dem).unwrap(); compute.run().unwrap(); compute diff --git a/crates/total-viewsheds/src/storage/db.rs b/crates/total-viewsheds/src/storage/db.rs index b440e67..09f7794 100644 --- a/crates/total-viewsheds/src/storage/db.rs +++ b/crates/total-viewsheds/src/storage/db.rs @@ -5,7 +5,7 @@ use std::path::Path; /// How we look up polar segments in the database. #[derive(Debug)] -pub enum ID { +pub(crate) enum ID { /// The precise DEM ID of the polar segment. DEM(i64), /// The neighbourhood ID within which a single DEM ID has been isolated. @@ -13,20 +13,20 @@ pub enum ID { } /// Sqlite DB connection details. -pub struct DB { +pub(crate) struct DB { /// A connection to Sqlite connection: rusqlite::Connection, } impl DB { /// Instantitate. - pub fn new>(db_path: P) -> Result { + pub(crate) fn new>(db_path: P) -> Result { let connection = rusqlite::Connection::open(db_path)?; Ok(Self { connection }) } /// Save the metadata. - pub fn save_metadata(&self, metadata: &tvs_lib::metadata::MetaData) -> Result<()> { + pub(crate) fn save_metadata(&self, metadata: &tvs_lib::metadata::MetaData) -> Result<()> { tracing::debug!("Saving metadata to {:?}...", self.connection.path()); self.connection.execute( @@ -49,7 +49,7 @@ impl DB { } /// Load the metadata. - pub fn load_metadata(&self) -> Result { + pub(crate) fn load_metadata(&self) -> Result { tracing::debug!("Loading metadata from {:?}...", self.connection.path()); let metadata_string: String = @@ -62,7 +62,10 @@ impl DB { } /// Load all the polar segments for a given ID. - pub fn load_segments(&self, id: &ID) -> Result<(Vec>, i64)> { + pub(crate) fn load_segments( + &self, + id: &ID, + ) -> Result<(Vec>, i64)> { tracing::debug!( "Loading polar segments for {id:?} from {:?}...", self.connection.path() diff --git a/crates/total-viewsheds/src/storage/engine.rs b/crates/total-viewsheds/src/storage/engine.rs index 7f35921..5f86335 100644 --- a/crates/total-viewsheds/src/storage/engine.rs +++ b/crates/total-viewsheds/src/storage/engine.rs @@ -6,7 +6,7 @@ use std::sync::mpsc; use std::thread; /// `Engine` is a thread-safe trait to store `PolarSegments` for a given `tvs_id` -pub trait Engine +pub(crate) trait Engine where Self: Send + Sync, { @@ -20,7 +20,7 @@ where } /// `NoopEngine` stores no `PolarSegments`, it exists for testing purposes -pub struct Noop; +pub(crate) struct Noop; impl Engine for Noop { fn store_segments( &self, @@ -48,7 +48,7 @@ impl Engine for Noop { /// /// Because of the large amount of data and need to optimize for quick writes t is not crash safe, /// meaning if your program crashes the underlying sql database will likely be corrupted -pub struct Sqlite { +pub(crate) struct Sqlite { /// `worker_handle` holds the `JoinHandle` for a worker thread which is reading /// from the Receiver end of `sender`'s channel. /// It uses an `Option` so that it can `take` the inner `JoinHandle` inside drop, @@ -60,7 +60,7 @@ pub struct Sqlite { impl Sqlite { /// Create a new `SqliteEngine` storing the database at `path` - pub fn new>(path: P) -> Self { + pub(crate) fn new>(path: P) -> Self { let (tx, rx) = mpsc::sync_channel(1024); // make an owned copy of Path so that it can be moved into the worker diff --git a/crates/total-viewsheds/src/storage/segments.rs b/crates/total-viewsheds/src/storage/segments.rs index 4f13906..ed4a8d8 100644 --- a/crates/total-viewsheds/src/storage/segments.rs +++ b/crates/total-viewsheds/src/storage/segments.rs @@ -4,11 +4,11 @@ /// `Segment` is the rho portion of a line segment in polar coordinates /// as (`rho`: u16, `delta_rho`: u16) which are packed into a single u32 for storage #[derive(Clone, Default)] -pub struct Segment(pub u32); +pub(crate) struct Segment(pub u32); impl Segment { /// `new` creates a `Segment` the segment's start point and the distance - pub fn new(start: u16, distance: u16) -> Self { + pub(crate) fn new(start: u16, distance: u16) -> Self { // pack start/distsance into a u32 in the format of (start|distance) let wide_start: u32 = start.into(); let wide_distance: u32 = distance.into(); @@ -20,7 +20,7 @@ impl Segment { clippy::as_conversions, reason = "the top 16 bits are guaranteed to be 0" )] - pub const fn start(&self) -> u16 { + pub(crate) const fn start(&self) -> u16 { (self.0 >> 16) as u16 } @@ -30,15 +30,11 @@ impl Segment { clippy::cast_possible_truncation, reason = "the top 16 bits are guaranteed to be 0" )] - pub const fn distance(&self) -> u16 { + pub(crate) const fn distance(&self) -> u16 { self.0 as u16 } } -#[expect( - clippy::module_name_repetitions, - reason = "That's the name we use in the DB" -)] /// `PolarSegments` holds the degree of a line of sight and the list /// of visible `Segments` which is constucted through a Run Length /// Encoding algorithm. @@ -47,7 +43,7 @@ impl Segment { /// 1. The kernel is run with higher angular resolutions. /// 2. The rotation maths causes some `dem_id`/angles to have multiple pairs. #[derive(Clone)] -pub struct PolarSegments { +pub(crate) struct PolarSegments { /// `degree` is a whole degree in the range [0, 359] pub degree: u16, /// `visible_segments` is a list of segments visible for a given @@ -66,7 +62,7 @@ impl PolarSegments { clippy::indexing_slicing, reason = "we want to panic if out of indexes are oob" )] - pub fn from_bools(degree: u16, bitmap: &[bool]) -> Self { + pub(crate) fn from_bools(degree: u16, bitmap: &[bool]) -> Self { let mut visible_segments: Vec = Vec::with_capacity(1); let char_slice: &[u8] = bytemuck::cast_slice(bitmap); diff --git a/crates/total-viewsheds/src/storage/worker.rs b/crates/total-viewsheds/src/storage/worker.rs index f529214..05e3b68 100644 --- a/crates/total-viewsheds/src/storage/worker.rs +++ b/crates/total-viewsheds/src/storage/worker.rs @@ -4,7 +4,7 @@ /// A wrapper for a storage `Engine`. It converts bitmaps from the kernel into a `PolarSegments` /// to communicate. Keeping it a concrete struct lets us hide the underlying engine from the user /// but gives us flexibility to NOOP storage for testing purposes -pub struct Worker { +pub(crate) struct Worker { /// `engine` is the underlying storage engine for our `Storage` struct. /// We keep it `Box` so that Storage doesn't have a generic type parameter /// because: @@ -15,7 +15,7 @@ pub struct Worker { impl Worker { /// `new_noop` initializes a Worker with a dummy engine for testing - pub fn new_noop() -> Self { + pub(crate) fn new_noop() -> Self { Self { engine: Box::new(super::engine::Noop), } @@ -23,7 +23,7 @@ impl Worker { /// `new` creates a new Worker with Sqlite as its backing when /// the `test` or `ring_data` feature is enabled, otherwise returning a noop - pub fn new>(path: P) -> Self { + pub(crate) fn new>(path: P) -> Self { if !cfg!(any(test, feature = "ring_data")) { return Self { engine: Box::new(crate::storage::engine::Noop), @@ -36,7 +36,7 @@ impl Worker { } /// `store_bitmap` converts a bitmap into `PolarSegments` and uses its `Engine` to store it - pub fn store_bitmap( + pub(crate) fn store_bitmap( &self, dem_id: u64, neighbourhood_id: Option, @@ -57,7 +57,7 @@ impl Worker { /// /// All segments sent to `recv` will be run in the same transaction to save on transaction /// overhead. This means that any panic or error will end in a corrupted database -pub fn writer>( +pub(crate) fn writer>( path: P, recv: std::sync::mpsc::Receiver<(u64, Option, super::segments::PolarSegments)>, ) -> Result<(), rusqlite::Error> { diff --git a/crates/total-viewsheds/src/tests/fixtures.rs b/crates/total-viewsheds/src/tests/fixtures.rs index 598c13b..e554e62 100644 --- a/crates/total-viewsheds/src/tests/fixtures.rs +++ b/crates/total-viewsheds/src/tests/fixtures.rs @@ -3,7 +3,7 @@ use color_eyre::eyre::Result; /// The nodata value from NASA's SRTM data. -pub const NODATA: f32 = -32768.0; +pub(crate) const NODATA: f32 = -32768.0; // This is a little map to help orient yourself when looking at the results: // @@ -21,7 +21,7 @@ pub const NODATA: f32 = -32768.0; #[inline] #[must_use] /// A DEM tile with a symmetrical single peak. -pub fn single_peak_dem() -> Vec { +pub(crate) fn single_peak_dem() -> Vec { vec![ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, @@ -55,7 +55,7 @@ pub fn single_peak_dem() -> Vec { #[must_use] #[rustfmt::skip] /// A bigger DEM for making viewsheds. -pub fn bigger_dem() -> Vec { +pub(crate) fn bigger_dem() -> Vec { let x = 15; #[expect( clippy::as_conversions, @@ -82,7 +82,7 @@ pub fn bigger_dem() -> Vec { } /// Create the TVS heatmap that a compute run outputs. -pub fn create_tvs_tiff(data: Vec) -> Result { +pub(crate) fn create_tvs_tiff(data: Vec) -> Result { let width = data.len().isqrt(); let driver = gdal::DriverManager::get_driver_by_name("MEM")?; let dataset = driver.create_with_band_type::("", width, width, 1)?; diff --git a/crates/total-viewsheds/src/tile.rs b/crates/total-viewsheds/src/tile.rs index e38ab3d..5826770 100644 --- a/crates/total-viewsheds/src/tile.rs +++ b/crates/total-viewsheds/src/tile.rs @@ -7,7 +7,7 @@ use color_eyre::eyre::{ContextCompat as _, Result}; /// The basic raw data needed to compute total viewsheds. -pub struct Tile { +pub(crate) struct Tile { /// The width of the tile. pub width: u32, /// The size of single elevation sample in metres. @@ -20,7 +20,7 @@ pub struct Tile { impl Tile { /// Load a tile. - pub fn load(config: &crate::config::Compute) -> Result { + pub(crate) fn load(config: &crate::config::Compute) -> Result { if !config.input.exists() { color_eyre::eyre::bail!("Input file not found: {}", config.input.display()); } diff --git a/crates/total-viewsheds/src/workers.rs b/crates/total-viewsheds/src/workers.rs index 7f84de9..8eb452c 100644 --- a/crates/total-viewsheds/src/workers.rs +++ b/crates/total-viewsheds/src/workers.rs @@ -75,7 +75,7 @@ fn compilation_worker( /// create a db at path P only if `is_db_worker`, otherwise return /// a noop worker -pub fn init_worker>( +pub(crate) fn init_worker>( path: P, meta_data: &tvs_lib::metadata::MetaData, is_db_worker: bool, @@ -93,7 +93,7 @@ pub fn init_worker>( impl super::run::Compute<'_> { /// `run_parallel` runs the CPU kernel in parallel #[expect(clippy::expect_used, reason = "We need to panic on failure")] - pub fn run(&mut self) -> Result<()> { + pub(crate) fn run(&mut self) -> Result<()> { let max_los = usize::try_from(self.dem.max_los_as_points)?; let elevations = &self.dem.elevations;