use std::fmt;
use std::sync::Arc;
use axioval_ir::ObjectId;
use crate::relationships::{
CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
SemanticRelationship, validate_selection,
};
use crate::session::{SnapshotBoundService, SourceSnapshot};
pub const DERIVED_RELATIONSHIP_PREFIX: &str = "axioval:derived.";
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Derivation {
ContainedInSpace {
horizontal_metres: f64,
vertical_metres: f64,
},
AdjacentSpace {
reach_metres: f64,
},
OverlappingGroupSpace {
minimum_ratio: f64,
vertical_metres: f64,
},
SpansLevel {
overlap_metres: f64,
},
Intersects,
}
const CONTAINED_IN_SPACE: &str = "contained-in-space";
const ADJACENT_SPACE: &str = "adjacent-space";
const OVERLAPPING_GROUP_SPACE: &str = "overlapping-group-space";
const SPANS_LEVEL: &str = "spans-level";
const INTERSECTS: &str = "intersects";
impl Derivation {
pub fn parse(
relationship: &SemanticRelationship,
) -> Result<Option<Self>, RelationshipSelectionError> {
let Some(rest) = relationship
.as_str()
.strip_prefix(DERIVED_RELATIONSHIP_PREFIX)
else {
return Ok(None);
};
let mut parts = rest.split(';');
let name = parts.next().unwrap_or_default();
let mut parameters: Vec<(&str, f64)> = Vec::new();
for part in parts {
let (key, value) = part
.split_once('=')
.ok_or(RelationshipSelectionError::InvalidRequest)?;
let value: f64 = value
.trim()
.parse()
.map_err(|_| RelationshipSelectionError::InvalidRequest)?;
let key = key.trim();
if !value.is_finite() || value < 0.0 || parameters.iter().any(|(seen, _)| *seen == key)
{
return Err(RelationshipSelectionError::InvalidRequest);
}
parameters.push((key, value));
}
let allowed: &[&str] = match name {
CONTAINED_IN_SPACE => &["horizontal", "vertical"],
ADJACENT_SPACE => &["reach"],
OVERLAPPING_GROUP_SPACE => &["ratio", "vertical"],
SPANS_LEVEL => &["overlap"],
INTERSECTS => &[],
_ => return Err(RelationshipSelectionError::InvalidRequest),
};
if parameters.iter().any(|(key, _)| !allowed.contains(key)) {
return Err(RelationshipSelectionError::InvalidRequest);
}
let get = |key: &str, default: f64| {
parameters
.iter()
.find(|(seen, _)| *seen == key)
.map_or(default, |(_, value)| *value)
};
let derivation = match name {
CONTAINED_IN_SPACE => Self::ContainedInSpace {
horizontal_metres: get("horizontal", 0.0),
vertical_metres: get("vertical", 0.0),
},
ADJACENT_SPACE => Self::AdjacentSpace {
reach_metres: get("reach", 1.0),
},
SPANS_LEVEL => Self::SpansLevel {
overlap_metres: get("overlap", 1.0),
},
INTERSECTS => Self::Intersects,
_ => Self::OverlappingGroupSpace {
minimum_ratio: get("ratio", 0.5),
vertical_metres: get("vertical", 0.0),
},
};
match derivation {
Self::AdjacentSpace { reach_metres } if reach_metres <= 0.0 => {
Err(RelationshipSelectionError::InvalidRequest)
}
Self::SpansLevel { overlap_metres } if overlap_metres <= 0.0 => {
Err(RelationshipSelectionError::InvalidRequest)
}
Self::OverlappingGroupSpace { minimum_ratio, .. }
if minimum_ratio <= 0.0 || minimum_ratio > 1.0 =>
{
Err(RelationshipSelectionError::InvalidRequest)
}
derivation => Ok(Some(derivation)),
}
}
#[must_use]
pub fn name(&self) -> String {
let name = match self {
Self::ContainedInSpace { .. } => CONTAINED_IN_SPACE,
Self::AdjacentSpace { .. } => ADJACENT_SPACE,
Self::OverlappingGroupSpace { .. } => OVERLAPPING_GROUP_SPACE,
Self::SpansLevel { .. } => SPANS_LEVEL,
Self::Intersects => INTERSECTS,
};
format!("{DERIVED_RELATIONSHIP_PREFIX}{name}")
}
}
impl fmt::Display for Derivation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())?;
match self {
Self::ContainedInSpace {
horizontal_metres,
vertical_metres,
} => write!(
f,
";horizontal={horizontal_metres};vertical={vertical_metres}"
),
Self::AdjacentSpace { reach_metres } => write!(f, ";reach={reach_metres}"),
Self::OverlappingGroupSpace {
minimum_ratio,
vertical_metres,
} => write!(f, ";ratio={minimum_ratio};vertical={vertical_metres}"),
Self::SpansLevel { overlap_metres } => write!(f, ";overlap={overlap_metres}"),
Self::Intersects => Ok(()),
}
}
}
const SAME_LEVEL: &str = "same-level";
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LevelMatch {
Elevation {
tolerance_metres: f64,
},
Name,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LevelFacts {
pub elevation_metres: Option<f64>,
pub name: Option<String>,
}
impl LevelMatch {
pub fn parse(relationship: &str) -> Result<Option<Self>, RelationshipSelectionError> {
let Some(rest) = relationship.strip_prefix(DERIVED_RELATIONSHIP_PREFIX) else {
return Ok(None);
};
let mut parts = rest.split(';');
if parts.next() != Some(SAME_LEVEL) {
return Ok(None);
}
let (mut by, mut tolerance) = (None, None);
for part in parts {
let (key, value) = part
.split_once('=')
.ok_or(RelationshipSelectionError::InvalidRequest)?;
match (key.trim(), value.trim()) {
("by", value) if by.is_none() => by = Some(value.to_owned()),
("tolerance", value) if tolerance.is_none() => {
let metres: f64 = value
.parse()
.map_err(|_| RelationshipSelectionError::InvalidRequest)?;
if !metres.is_finite() || metres < 0.0 {
return Err(RelationshipSelectionError::InvalidRequest);
}
tolerance = Some(metres);
}
_ => return Err(RelationshipSelectionError::InvalidRequest),
}
}
match (by.as_deref(), tolerance) {
(None | Some("elevation"), tolerance) => Ok(Some(Self::Elevation {
tolerance_metres: tolerance.unwrap_or(0.0),
})),
(Some("name"), None) => Ok(Some(Self::Name)),
_ => Err(RelationshipSelectionError::InvalidRequest),
}
}
#[must_use]
pub fn same(&self, left: &LevelFacts, right: &LevelFacts) -> Option<bool> {
match self {
Self::Elevation { tolerance_metres } => {
let (left, right) = (left.elevation_metres?, right.elevation_metres?);
if !left.is_finite() || !right.is_finite() {
return None;
}
let rounding = 1e-9 * left.abs().max(right.abs()).max(1.0);
Some((left - right).abs() <= tolerance_metres + rounding)
}
Self::Name => Some(left.name.as_ref()? == right.name.as_ref()?),
}
}
}
impl fmt::Display for LevelMatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{DERIVED_RELATIONSHIP_PREFIX}{SAME_LEVEL}")?;
match self {
Self::Elevation { tolerance_metres } => {
write!(f, ";by=elevation;tolerance={tolerance_metres}")
}
Self::Name => write!(f, ";by=name"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AdjacentSide {
Positive,
Negative,
}
impl AdjacentSide {
#[must_use]
pub fn symbol(self) -> char {
match self {
Self::Positive => '+',
Self::Negative => '-',
}
}
#[must_use]
pub fn opposite(self) -> Self {
match self {
Self::Positive => Self::Negative,
Self::Negative => Self::Positive,
}
}
fn from_symbol(symbol: char) -> Option<Self> {
match symbol {
'+' => Some(Self::Positive),
'-' => Some(Self::Negative),
_ => None,
}
}
}
impl fmt::Display for AdjacentSide {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.symbol())
}
}
#[must_use]
pub fn adjacent_side(
locator: &str,
subject: &ObjectId,
space: Option<&ObjectId>,
) -> Option<AdjacentSide> {
let rest = locator
.strip_prefix(DERIVED_RELATIONSHIP_PREFIX)?
.strip_prefix(ADJACENT_SPACE)?;
let (tolerances, record) = rest.split_once(':')?;
if !(tolerances.is_empty() || tolerances.starts_with(';')) {
return None;
}
let head = match space {
Some(space) => format!("{subject}->{space}:side="),
None => format!("{subject}:side="),
};
let tail = record.strip_prefix(&head)?;
let side = AdjacentSide::from_symbol(tail.chars().next()?)?;
if space.is_none() && !tail.contains(":outside") {
return None;
}
Some(side)
}
pub trait DerivedRelationshipService: Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot] {
&[]
}
fn derive(
&self,
derivation: &Derivation,
request: &RelationshipSelectionRequest,
) -> Result<CompleteRelationshipSelection, RelationshipSelectionError>;
}
#[derive(Clone)]
pub struct DerivedRelationshipServiceHandle(Arc<dyn DerivedRelationshipService>);
impl DerivedRelationshipServiceHandle {
#[must_use]
pub fn new(service: Arc<dyn DerivedRelationshipService>) -> Self {
Self(service)
}
pub fn select(
&self,
request: &RelationshipSelectionRequest,
) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
let derivation = Derivation::parse(request.query().relationship())?
.ok_or(RelationshipSelectionError::InvalidRequest)?;
let selection = validate_selection(request, self.0.derive(&derivation, request)?)?;
let name = derivation.name();
if selection
.evidence()
.iter()
.any(|item| !item.locator.starts_with(&name))
{
return Err(RelationshipSelectionError::InexactEvidence);
}
Ok(selection)
}
}
impl SnapshotBoundService for DerivedRelationshipServiceHandle {
fn source_snapshots(&self) -> &[SourceSnapshot] {
self.0.source_snapshots()
}
}
pub(crate) struct RoutedRelationships {
pub(crate) semantic: Option<RelationshipSelectionServiceHandle>,
pub(crate) derived: DerivedRelationshipServiceHandle,
pub(crate) snapshots: Vec<SourceSnapshot>,
}
impl RelationshipSelectionService for RoutedRelationships {
fn source_snapshots(&self) -> &[SourceSnapshot] {
&self.snapshots
}
fn select(
&self,
request: &RelationshipSelectionRequest,
) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
if is_derived(request.query()) {
return self.derived.select(request);
}
match &self.semantic {
Some(semantic) => semantic.select(request),
None => Err(RelationshipSelectionError::Unavailable(
"no semantic relationship service is registered".into(),
)),
}
}
}
fn is_derived(query: &RelationshipQuery) -> bool {
query
.relationship()
.as_str()
.starts_with(DERIVED_RELATIONSHIP_PREFIX)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(identity: &str) -> Result<Option<Derivation>, RelationshipSelectionError> {
Derivation::parse(&SemanticRelationship::try_new(identity).unwrap())
}
#[test]
fn identities_name_the_derivation_and_fix_its_tolerances() {
assert_eq!(parse("IfcRelAggregates"), Ok(None));
assert_eq!(
parse("axioval:derived.contained-in-space"),
Ok(Some(Derivation::ContainedInSpace {
horizontal_metres: 0.0,
vertical_metres: 0.0
}))
);
let nearest = parse("axioval:derived.contained-in-space;vertical=0.5;horizontal=0.25")
.unwrap()
.unwrap();
assert_eq!(
nearest.to_string(),
"axioval:derived.contained-in-space;horizontal=0.25;vertical=0.5"
);
assert_eq!(
parse("axioval:derived.adjacent-space")
.unwrap()
.unwrap()
.to_string(),
"axioval:derived.adjacent-space;reach=1"
);
assert_eq!(
parse("axioval:derived.overlapping-group-space;ratio=0.9")
.unwrap()
.unwrap()
.to_string(),
"axioval:derived.overlapping-group-space;ratio=0.9;vertical=0"
);
assert_eq!(
parse("axioval:derived.spans-level"),
Ok(Some(Derivation::SpansLevel {
overlap_metres: 1.0
}))
);
assert_eq!(
parse("axioval:derived.spans-level;overlap=0.5")
.unwrap()
.unwrap()
.to_string(),
"axioval:derived.spans-level;overlap=0.5"
);
assert_eq!(
parse("axioval:derived.spans-level;overlap=0"),
Err(RelationshipSelectionError::InvalidRequest)
);
assert_eq!(
parse("axioval:derived.spans-level;reach=1"),
Err(RelationshipSelectionError::InvalidRequest)
);
let intersects = parse("axioval:derived.intersects").unwrap().unwrap();
assert_eq!(intersects, Derivation::Intersects);
assert_eq!(intersects.to_string(), "axioval:derived.intersects");
assert_eq!(intersects.name(), "axioval:derived.intersects");
assert_eq!(
parse("axioval:derived.intersects;depth=0.1"),
Err(RelationshipSelectionError::InvalidRequest)
);
}
#[test]
fn adjacency_locators_record_the_side_of_each_space_and_each_outside_face() {
let source = axioval_ir::SourceId::new("ifc-step", "model.ifc").unwrap();
let door = ObjectId::new(source.clone(), "#76").unwrap();
let room = ObjectId::new(source.clone(), "#16").unwrap();
let other = ObjectId::new(source, "#26").unwrap();
let identity = "axioval:derived.adjacent-space;reach=1";
let edge = format!("{identity}:{door}->{room}:side=+(1.000000,0.000000):entered=0.000000");
let outside = format!("{identity}:{door}:side=-(1.000000,0.000000):outside:reach=1");
assert_eq!(
adjacent_side(&edge, &door, Some(&room)),
Some(AdjacentSide::Positive)
);
assert_eq!(adjacent_side(&edge, &door, Some(&other)), None);
assert_eq!(adjacent_side(&edge, &door, None), None);
assert_eq!(
adjacent_side(&outside, &door, None),
Some(AdjacentSide::Negative)
);
assert_eq!(adjacent_side(&outside, &door, Some(&room)), None);
assert_eq!(adjacent_side(&outside, &room, None), None);
let contained = format!(
"axioval:derived.contained-in-space;horizontal=0;vertical=0:{door}->{room}:side=+"
);
assert_eq!(adjacent_side(&contained, &door, Some(&room)), None);
let scan = format!("{identity}:derived-from:{door}:2 space(s)");
assert_eq!(adjacent_side(&scan, &door, None), None);
assert_eq!(AdjacentSide::Positive.opposite(), AdjacentSide::Negative);
}
#[test]
fn a_malformed_identity_is_an_invalid_request() {
for identity in [
"axioval:derived.",
"axioval:derived.nearest-thing",
"axioval:derived.adjacent-space;reach",
"axioval:derived.adjacent-space;reach=0",
"axioval:derived.adjacent-space;reach=-1",
"axioval:derived.adjacent-space;reach=inf",
"axioval:derived.adjacent-space;ratio=0.5",
"axioval:derived.contained-in-space;vertical=1;vertical=2",
"axioval:derived.overlapping-group-space;ratio=1.5",
"axioval:derived.overlapping-group-space;ratio=0",
] {
assert_eq!(
parse(identity),
Err(RelationshipSelectionError::InvalidRequest),
"{identity}"
);
}
}
}