use axioval_ir::{Evidence, ObjectId};
use crate::LengthInterval;
use crate::door_leaves::{SweptDoor, tidy_swept};
use crate::free_space::{ElevationBand, FreeSpaceError};
use crate::services::reviewable_exact_evidence;
#[derive(Clone, Debug, PartialEq)]
pub struct CirculationRequest {
scope: ObjectId,
entrances: Vec<ObjectId>,
components: Vec<ObjectId>,
obstacles: Vec<ObjectId>,
width_metres: f64,
height_metres: f64,
tolerance_metres: f64,
swept: Vec<SweptDoor>,
merged: Vec<ObjectId>,
band_from_metres: f64,
}
impl CirculationRequest {
pub fn try_new(
scope: ObjectId,
entrances: Vec<ObjectId>,
components: Vec<ObjectId>,
obstacles: Vec<ObjectId>,
width_metres: f64,
height_metres: f64,
tolerance_metres: f64,
) -> Result<Self, FreeSpaceError> {
let positive = |value: f64| value.is_finite() && value > 0.0;
if !(positive(width_metres)
&& positive(height_metres)
&& tolerance_metres.is_finite()
&& tolerance_metres >= 0.0)
{
return Err(FreeSpaceError::InvalidClearanceShape);
}
let tidy = |mut list: Vec<ObjectId>| {
list.retain(|object| object != &scope);
list.sort();
list.dedup();
list
};
let entrances = tidy(entrances);
let components = tidy(components);
let mut obstacles = tidy(obstacles);
obstacles.retain(|object| entrances.binary_search(object).is_err());
Ok(Self {
scope,
entrances,
components,
obstacles,
width_metres,
height_metres,
tolerance_metres,
swept: Vec::new(),
merged: Vec::new(),
band_from_metres: 0.0,
})
}
pub fn with_merged_scopes(mut self, mut merged: Vec<ObjectId>) -> Result<Self, FreeSpaceError> {
merged.retain(|object| object != &self.scope);
merged.sort();
merged.dedup();
if merged.iter().any(|object| {
self.entrances.binary_search(object).is_ok()
|| self.components.binary_search(object).is_ok()
}) {
return Err(FreeSpaceError::MergedScopeConflict);
}
self.obstacles
.retain(|object| merged.binary_search(object).is_err());
self.merged = merged;
Ok(self)
}
#[must_use]
pub fn merged_scopes(&self) -> &[ObjectId] {
&self.merged
}
pub fn with_band_from(mut self, from_metres: f64) -> Result<Self, FreeSpaceError> {
ElevationBand::try_new(from_metres, self.height_metres)?;
self.band_from_metres = from_metres;
Ok(self)
}
pub fn with_swept_doors(mut self, swept: Vec<SweptDoor>) -> Result<Self, FreeSpaceError> {
let mut swept = tidy_swept(swept).ok_or(FreeSpaceError::ConflictingSweptDoors)?;
swept.retain(|door| self.entrances.binary_search(door.door()).is_err());
self.swept = swept;
Ok(self)
}
#[must_use]
pub fn swept_doors(&self) -> &[SweptDoor] {
&self.swept
}
#[must_use]
pub fn scope(&self) -> &ObjectId {
&self.scope
}
#[must_use]
pub fn entrances(&self) -> &[ObjectId] {
&self.entrances
}
#[must_use]
pub fn components(&self) -> &[ObjectId] {
&self.components
}
#[must_use]
pub fn obstacles(&self) -> &[ObjectId] {
&self.obstacles
}
#[must_use]
pub fn width_metres(&self) -> f64 {
self.width_metres
}
#[must_use]
pub fn height_metres(&self) -> f64 {
self.height_metres
}
#[must_use]
pub fn tolerance_metres(&self) -> f64 {
self.tolerance_metres
}
#[must_use]
pub fn band(&self) -> ElevationBand {
ElevationBand::try_new(self.band_from_metres, self.height_metres)
.unwrap_or_else(|_| unreachable!("the start lies below the positive, finite height"))
}
#[must_use]
pub fn subjects(&self) -> Vec<ObjectId> {
let mut subjects: Vec<ObjectId> = self
.entrances
.iter()
.chain(&self.components)
.cloned()
.collect();
subjects.sort();
subjects.dedup();
subjects
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CirculationNodeKind {
End,
Path,
Junction,
Isolated,
}
impl CirculationNodeKind {
fn admits(self, degree: usize) -> bool {
match self {
Self::End => degree == 1,
Self::Path => degree == 2,
Self::Junction => degree >= 3,
Self::Isolated => degree == 0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CirculationNode {
point: [f64; 3],
kind: CirculationNodeKind,
piece: usize,
half_width: LengthInterval,
}
impl CirculationNode {
pub fn try_new(
point: [f64; 3],
kind: CirculationNodeKind,
piece: usize,
half_width: LengthInterval,
) -> Result<Self, FreeSpaceError> {
if !point.iter().all(|value| value.is_finite()) {
return Err(FreeSpaceError::Unavailable(
"a circulation node's coordinates must be finite".into(),
));
}
Ok(Self {
point,
kind,
piece,
half_width,
})
}
#[must_use]
pub fn point(&self) -> [f64; 3] {
self.point
}
#[must_use]
pub fn kind(&self) -> CirculationNodeKind {
self.kind
}
#[must_use]
pub fn piece(&self) -> usize {
self.piece
}
#[must_use]
pub fn half_width(&self) -> LengthInterval {
self.half_width
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CirculationContact {
subject: ObjectId,
reached: Vec<(usize, Option<usize>)>,
possible: Vec<usize>,
}
impl CirculationContact {
#[must_use]
pub fn new(
subject: ObjectId,
mut reached: Vec<(usize, Option<usize>)>,
mut possible: Vec<usize>,
) -> Self {
reached.sort_unstable();
reached.dedup_by_key(|(piece, _)| *piece);
possible.sort_unstable();
possible.dedup();
Self {
subject,
reached,
possible,
}
}
#[must_use]
pub fn subject(&self) -> &ObjectId {
&self.subject
}
#[must_use]
pub fn reached(&self) -> &[(usize, Option<usize>)] {
&self.reached
}
#[must_use]
pub fn reaches(&self, piece: usize) -> bool {
self.reached.iter().any(|(reached, _)| *reached == piece)
}
#[must_use]
pub fn possible(&self) -> &[usize] {
&self.possible
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CirculationMap {
request: CirculationRequest,
pieces: usize,
possible_pieces: usize,
nodes: Vec<CirculationNode>,
edges: Vec<(usize, usize)>,
spacing_metres: f64,
contacts: Vec<CirculationContact>,
unmapped: Vec<(usize, String)>,
evidence: Evidence,
}
impl CirculationMap {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
request: CirculationRequest,
pieces: usize,
possible_pieces: usize,
nodes: Vec<CirculationNode>,
mut edges: Vec<(usize, usize)>,
spacing_metres: f64,
contacts: Vec<CirculationContact>,
mut unmapped: Vec<(usize, String)>,
evidence: Evidence,
) -> Result<Self, FreeSpaceError> {
let malformed = |why: &str| FreeSpaceError::Unavailable(format!("circulation map: {why}"));
if !reviewable_exact_evidence(&evidence) {
return Err(FreeSpaceError::InexactPlacementEvidence);
}
if !(spacing_metres.is_finite() && spacing_metres > 0.0) {
return Err(malformed("the sample spacing must be positive"));
}
if nodes.iter().any(|node| node.piece >= pieces) {
return Err(malformed("a node lies in no piece"));
}
unmapped.sort();
unmapped.dedup_by_key(|(piece, _)| *piece);
if unmapped
.iter()
.any(|(piece, _)| *piece >= pieces || nodes.iter().any(|node| node.piece == *piece))
{
return Err(malformed("an unmapped piece is not there or has nodes"));
}
edges.sort_unstable();
edges.dedup();
let mut degree = vec![0usize; nodes.len()];
for &(a, b) in &edges {
if a >= b || b >= nodes.len() || nodes[a].piece != nodes[b].piece {
return Err(malformed("an edge joins no two nodes of one piece"));
}
degree[a] += 1;
degree[b] += 1;
}
if nodes
.iter()
.zip(°ree)
.any(|(node, °ree)| !node.kind.admits(degree))
{
return Err(malformed("a node's kind does not match its neighbours"));
}
let subjects = request.subjects();
if contacts.len() != subjects.len()
|| contacts
.iter()
.zip(&subjects)
.any(|(contact, subject)| &contact.subject != subject)
{
return Err(malformed("the contacts are not the request's subjects"));
}
for contact in &contacts {
for &(piece, node) in &contact.reached {
if piece >= pieces
|| node.is_some_and(|node| node >= nodes.len() || nodes[node].piece != piece)
{
return Err(malformed(
"a contact names a piece or node that is not there",
));
}
}
if contact
.possible
.iter()
.any(|&piece| piece >= possible_pieces)
{
return Err(malformed(
"a contact names a possible piece that is not there",
));
}
}
Ok(Self {
request,
pieces,
possible_pieces,
nodes,
edges,
spacing_metres,
contacts,
unmapped,
evidence,
})
}
#[must_use]
pub fn request(&self) -> &CirculationRequest {
&self.request
}
#[must_use]
pub fn pieces(&self) -> usize {
self.pieces
}
#[must_use]
pub fn possible_pieces(&self) -> usize {
self.possible_pieces
}
#[must_use]
pub fn nodes(&self) -> &[CirculationNode] {
&self.nodes
}
#[must_use]
pub fn edges(&self) -> &[(usize, usize)] {
&self.edges
}
#[must_use]
pub fn neighbours(&self, node: usize) -> Vec<usize> {
let mut out: Vec<usize> = self
.edges
.iter()
.filter_map(|&(a, b)| match (a == node, b == node) {
(true, _) => Some(b),
(_, true) => Some(a),
_ => None,
})
.collect();
out.sort_unstable();
out
}
#[must_use]
pub fn spacing_metres(&self) -> f64 {
self.spacing_metres
}
#[must_use]
pub fn contacts(&self) -> &[CirculationContact] {
&self.contacts
}
#[must_use]
pub fn contact(&self, subject: &ObjectId) -> Option<&CirculationContact> {
self.contacts
.iter()
.find(|contact| &contact.subject == subject)
}
#[must_use]
pub fn unmapped(&self) -> &[(usize, String)] {
&self.unmapped
}
#[must_use]
pub fn unmapped_reason(&self, piece: usize) -> Option<&str> {
self.unmapped
.iter()
.find(|(unmapped, _)| *unmapped == piece)
.map(|(_, why)| why.as_str())
}
#[must_use]
pub fn evidence(&self) -> &Evidence {
&self.evidence
}
}