1use std::fmt;
27use std::sync::Arc;
28
29use axioval_ir::ObjectId;
30
31use crate::relationships::{
32 CompleteRelationshipSelection, RelationshipQuery, RelationshipSelectionError,
33 RelationshipSelectionRequest, RelationshipSelectionService, RelationshipSelectionServiceHandle,
34 SemanticRelationship, validate_selection,
35};
36use crate::session::{SnapshotBoundService, SourceSnapshot};
37
38pub const DERIVED_RELATIONSHIP_PREFIX: &str = "axioval:derived.";
40
41#[derive(Clone, Copy, Debug, PartialEq)]
43pub enum Derivation {
44 ContainedInSpace {
50 horizontal_metres: f64,
52 vertical_metres: f64,
54 },
55 AdjacentSpace {
59 reach_metres: f64,
61 },
62 OverlappingGroupSpace {
67 minimum_ratio: f64,
69 vertical_metres: f64,
71 },
72 SpansLevel {
78 overlap_metres: f64,
80 },
81 Intersects,
90}
91
92const CONTAINED_IN_SPACE: &str = "contained-in-space";
93const ADJACENT_SPACE: &str = "adjacent-space";
94const OVERLAPPING_GROUP_SPACE: &str = "overlapping-group-space";
95const SPANS_LEVEL: &str = "spans-level";
96const INTERSECTS: &str = "intersects";
97
98impl Derivation {
99 pub fn parse(
107 relationship: &SemanticRelationship,
108 ) -> Result<Option<Self>, RelationshipSelectionError> {
109 let Some(rest) = relationship
110 .as_str()
111 .strip_prefix(DERIVED_RELATIONSHIP_PREFIX)
112 else {
113 return Ok(None);
114 };
115 let mut parts = rest.split(';');
116 let name = parts.next().unwrap_or_default();
117 let mut parameters: Vec<(&str, f64)> = Vec::new();
118 for part in parts {
119 let (key, value) = part
120 .split_once('=')
121 .ok_or(RelationshipSelectionError::InvalidRequest)?;
122 let value: f64 = value
123 .trim()
124 .parse()
125 .map_err(|_| RelationshipSelectionError::InvalidRequest)?;
126 let key = key.trim();
127 if !value.is_finite() || value < 0.0 || parameters.iter().any(|(seen, _)| *seen == key)
128 {
129 return Err(RelationshipSelectionError::InvalidRequest);
130 }
131 parameters.push((key, value));
132 }
133 let allowed: &[&str] = match name {
134 CONTAINED_IN_SPACE => &["horizontal", "vertical"],
135 ADJACENT_SPACE => &["reach"],
136 OVERLAPPING_GROUP_SPACE => &["ratio", "vertical"],
137 SPANS_LEVEL => &["overlap"],
138 INTERSECTS => &[],
139 _ => return Err(RelationshipSelectionError::InvalidRequest),
140 };
141 if parameters.iter().any(|(key, _)| !allowed.contains(key)) {
142 return Err(RelationshipSelectionError::InvalidRequest);
143 }
144 let get = |key: &str, default: f64| {
145 parameters
146 .iter()
147 .find(|(seen, _)| *seen == key)
148 .map_or(default, |(_, value)| *value)
149 };
150 let derivation = match name {
151 CONTAINED_IN_SPACE => Self::ContainedInSpace {
152 horizontal_metres: get("horizontal", 0.0),
153 vertical_metres: get("vertical", 0.0),
154 },
155 ADJACENT_SPACE => Self::AdjacentSpace {
156 reach_metres: get("reach", 1.0),
157 },
158 SPANS_LEVEL => Self::SpansLevel {
159 overlap_metres: get("overlap", 1.0),
160 },
161 INTERSECTS => Self::Intersects,
162 _ => Self::OverlappingGroupSpace {
163 minimum_ratio: get("ratio", 0.5),
164 vertical_metres: get("vertical", 0.0),
165 },
166 };
167 match derivation {
168 Self::AdjacentSpace { reach_metres } if reach_metres <= 0.0 => {
169 Err(RelationshipSelectionError::InvalidRequest)
170 }
171 Self::SpansLevel { overlap_metres } if overlap_metres <= 0.0 => {
172 Err(RelationshipSelectionError::InvalidRequest)
173 }
174 Self::OverlappingGroupSpace { minimum_ratio, .. }
175 if minimum_ratio <= 0.0 || minimum_ratio > 1.0 =>
176 {
177 Err(RelationshipSelectionError::InvalidRequest)
178 }
179 derivation => Ok(Some(derivation)),
180 }
181 }
182
183 #[must_use]
187 pub fn name(&self) -> String {
188 let name = match self {
189 Self::ContainedInSpace { .. } => CONTAINED_IN_SPACE,
190 Self::AdjacentSpace { .. } => ADJACENT_SPACE,
191 Self::OverlappingGroupSpace { .. } => OVERLAPPING_GROUP_SPACE,
192 Self::SpansLevel { .. } => SPANS_LEVEL,
193 Self::Intersects => INTERSECTS,
194 };
195 format!("{DERIVED_RELATIONSHIP_PREFIX}{name}")
196 }
197}
198
199impl fmt::Display for Derivation {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}", self.name())?;
203 match self {
204 Self::ContainedInSpace {
205 horizontal_metres,
206 vertical_metres,
207 } => write!(
208 f,
209 ";horizontal={horizontal_metres};vertical={vertical_metres}"
210 ),
211 Self::AdjacentSpace { reach_metres } => write!(f, ";reach={reach_metres}"),
212 Self::OverlappingGroupSpace {
213 minimum_ratio,
214 vertical_metres,
215 } => write!(f, ";ratio={minimum_ratio};vertical={vertical_metres}"),
216 Self::SpansLevel { overlap_metres } => write!(f, ";overlap={overlap_metres}"),
217 Self::Intersects => Ok(()),
218 }
219 }
220}
221
222const SAME_LEVEL: &str = "same-level";
223
224#[derive(Clone, Copy, Debug, PartialEq)]
236pub enum LevelMatch {
237 Elevation {
239 tolerance_metres: f64,
241 },
242 Name,
244}
245
246#[derive(Clone, Debug, Default, PartialEq)]
248pub struct LevelFacts {
249 pub elevation_metres: Option<f64>,
251 pub name: Option<String>,
253}
254
255impl LevelMatch {
256 pub fn parse(relationship: &str) -> Result<Option<Self>, RelationshipSelectionError> {
265 let Some(rest) = relationship.strip_prefix(DERIVED_RELATIONSHIP_PREFIX) else {
266 return Ok(None);
267 };
268 let mut parts = rest.split(';');
269 if parts.next() != Some(SAME_LEVEL) {
270 return Ok(None);
271 }
272 let (mut by, mut tolerance) = (None, None);
273 for part in parts {
274 let (key, value) = part
275 .split_once('=')
276 .ok_or(RelationshipSelectionError::InvalidRequest)?;
277 match (key.trim(), value.trim()) {
278 ("by", value) if by.is_none() => by = Some(value.to_owned()),
279 ("tolerance", value) if tolerance.is_none() => {
280 let metres: f64 = value
281 .parse()
282 .map_err(|_| RelationshipSelectionError::InvalidRequest)?;
283 if !metres.is_finite() || metres < 0.0 {
284 return Err(RelationshipSelectionError::InvalidRequest);
285 }
286 tolerance = Some(metres);
287 }
288 _ => return Err(RelationshipSelectionError::InvalidRequest),
289 }
290 }
291 match (by.as_deref(), tolerance) {
292 (None | Some("elevation"), tolerance) => Ok(Some(Self::Elevation {
293 tolerance_metres: tolerance.unwrap_or(0.0),
294 })),
295 (Some("name"), None) => Ok(Some(Self::Name)),
296 _ => Err(RelationshipSelectionError::InvalidRequest),
297 }
298 }
299
300 #[must_use]
307 pub fn same(&self, left: &LevelFacts, right: &LevelFacts) -> Option<bool> {
308 match self {
309 Self::Elevation { tolerance_metres } => {
310 let (left, right) = (left.elevation_metres?, right.elevation_metres?);
311 if !left.is_finite() || !right.is_finite() {
312 return None;
313 }
314 let rounding = 1e-9 * left.abs().max(right.abs()).max(1.0);
315 Some((left - right).abs() <= tolerance_metres + rounding)
316 }
317 Self::Name => Some(left.name.as_ref()? == right.name.as_ref()?),
318 }
319 }
320}
321
322impl fmt::Display for LevelMatch {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 write!(f, "{DERIVED_RELATIONSHIP_PREFIX}{SAME_LEVEL}")?;
326 match self {
327 Self::Elevation { tolerance_metres } => {
328 write!(f, ";by=elevation;tolerance={tolerance_metres}")
329 }
330 Self::Name => write!(f, ";by=name"),
331 }
332 }
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
339pub enum AdjacentSide {
340 Positive,
342 Negative,
344}
345
346impl AdjacentSide {
347 #[must_use]
349 pub fn symbol(self) -> char {
350 match self {
351 Self::Positive => '+',
352 Self::Negative => '-',
353 }
354 }
355
356 #[must_use]
358 pub fn opposite(self) -> Self {
359 match self {
360 Self::Positive => Self::Negative,
361 Self::Negative => Self::Positive,
362 }
363 }
364
365 fn from_symbol(symbol: char) -> Option<Self> {
366 match symbol {
367 '+' => Some(Self::Positive),
368 '-' => Some(Self::Negative),
369 _ => None,
370 }
371 }
372}
373
374impl fmt::Display for AdjacentSide {
375 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376 write!(f, "{}", self.symbol())
377 }
378}
379
380#[must_use]
397pub fn adjacent_side(
398 locator: &str,
399 subject: &ObjectId,
400 space: Option<&ObjectId>,
401) -> Option<AdjacentSide> {
402 let rest = locator
403 .strip_prefix(DERIVED_RELATIONSHIP_PREFIX)?
404 .strip_prefix(ADJACENT_SPACE)?;
405 let (tolerances, record) = rest.split_once(':')?;
407 if !(tolerances.is_empty() || tolerances.starts_with(';')) {
408 return None;
409 }
410 let head = match space {
411 Some(space) => format!("{subject}->{space}:side="),
412 None => format!("{subject}:side="),
413 };
414 let tail = record.strip_prefix(&head)?;
415 let side = AdjacentSide::from_symbol(tail.chars().next()?)?;
416 if space.is_none() && !tail.contains(":outside") {
417 return None;
418 }
419 Some(side)
420}
421
422pub trait DerivedRelationshipService: Send + Sync {
429 fn source_snapshots(&self) -> &[SourceSnapshot] {
431 &[]
432 }
433 fn derive(
439 &self,
440 derivation: &Derivation,
441 request: &RelationshipSelectionRequest,
442 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError>;
443}
444
445#[derive(Clone)]
447pub struct DerivedRelationshipServiceHandle(Arc<dyn DerivedRelationshipService>);
448
449impl DerivedRelationshipServiceHandle {
450 #[must_use]
452 pub fn new(service: Arc<dyn DerivedRelationshipService>) -> Self {
453 Self(service)
454 }
455
456 pub fn select(
465 &self,
466 request: &RelationshipSelectionRequest,
467 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
468 let derivation = Derivation::parse(request.query().relationship())?
469 .ok_or(RelationshipSelectionError::InvalidRequest)?;
470 let selection = validate_selection(request, self.0.derive(&derivation, request)?)?;
471 let name = derivation.name();
472 if selection
473 .evidence()
474 .iter()
475 .any(|item| !item.locator.starts_with(&name))
476 {
477 return Err(RelationshipSelectionError::InexactEvidence);
478 }
479 Ok(selection)
480 }
481}
482
483impl SnapshotBoundService for DerivedRelationshipServiceHandle {
484 fn source_snapshots(&self) -> &[SourceSnapshot] {
485 self.0.source_snapshots()
486 }
487}
488
489pub(crate) struct RoutedRelationships {
492 pub(crate) semantic: Option<RelationshipSelectionServiceHandle>,
493 pub(crate) derived: DerivedRelationshipServiceHandle,
494 pub(crate) snapshots: Vec<SourceSnapshot>,
495}
496
497impl RelationshipSelectionService for RoutedRelationships {
498 fn source_snapshots(&self) -> &[SourceSnapshot] {
499 &self.snapshots
500 }
501
502 fn select(
503 &self,
504 request: &RelationshipSelectionRequest,
505 ) -> Result<CompleteRelationshipSelection, RelationshipSelectionError> {
506 if is_derived(request.query()) {
507 return self.derived.select(request);
508 }
509 match &self.semantic {
510 Some(semantic) => semantic.select(request),
511 None => Err(RelationshipSelectionError::Unavailable(
512 "no semantic relationship service is registered".into(),
513 )),
514 }
515 }
516}
517
518fn is_derived(query: &RelationshipQuery) -> bool {
519 query
520 .relationship()
521 .as_str()
522 .starts_with(DERIVED_RELATIONSHIP_PREFIX)
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn parse(identity: &str) -> Result<Option<Derivation>, RelationshipSelectionError> {
530 Derivation::parse(&SemanticRelationship::try_new(identity).unwrap())
531 }
532
533 #[test]
534 fn identities_name_the_derivation_and_fix_its_tolerances() {
535 assert_eq!(parse("IfcRelAggregates"), Ok(None));
536 assert_eq!(
537 parse("axioval:derived.contained-in-space"),
538 Ok(Some(Derivation::ContainedInSpace {
539 horizontal_metres: 0.0,
540 vertical_metres: 0.0
541 }))
542 );
543 let nearest = parse("axioval:derived.contained-in-space;vertical=0.5;horizontal=0.25")
544 .unwrap()
545 .unwrap();
546 assert_eq!(
547 nearest.to_string(),
548 "axioval:derived.contained-in-space;horizontal=0.25;vertical=0.5"
549 );
550 assert_eq!(
551 parse("axioval:derived.adjacent-space")
552 .unwrap()
553 .unwrap()
554 .to_string(),
555 "axioval:derived.adjacent-space;reach=1"
556 );
557 assert_eq!(
558 parse("axioval:derived.overlapping-group-space;ratio=0.9")
559 .unwrap()
560 .unwrap()
561 .to_string(),
562 "axioval:derived.overlapping-group-space;ratio=0.9;vertical=0"
563 );
564 assert_eq!(
565 parse("axioval:derived.spans-level"),
566 Ok(Some(Derivation::SpansLevel {
567 overlap_metres: 1.0
568 }))
569 );
570 assert_eq!(
571 parse("axioval:derived.spans-level;overlap=0.5")
572 .unwrap()
573 .unwrap()
574 .to_string(),
575 "axioval:derived.spans-level;overlap=0.5"
576 );
577 assert_eq!(
578 parse("axioval:derived.spans-level;overlap=0"),
579 Err(RelationshipSelectionError::InvalidRequest)
580 );
581 assert_eq!(
582 parse("axioval:derived.spans-level;reach=1"),
583 Err(RelationshipSelectionError::InvalidRequest)
584 );
585 let intersects = parse("axioval:derived.intersects").unwrap().unwrap();
586 assert_eq!(intersects, Derivation::Intersects);
587 assert_eq!(intersects.to_string(), "axioval:derived.intersects");
588 assert_eq!(intersects.name(), "axioval:derived.intersects");
589 assert_eq!(
590 parse("axioval:derived.intersects;depth=0.1"),
591 Err(RelationshipSelectionError::InvalidRequest)
592 );
593 }
594
595 #[test]
596 fn adjacency_locators_record_the_side_of_each_space_and_each_outside_face() {
597 let source = axioval_ir::SourceId::new("ifc-step", "model.ifc").unwrap();
598 let door = ObjectId::new(source.clone(), "#76").unwrap();
599 let room = ObjectId::new(source.clone(), "#16").unwrap();
600 let other = ObjectId::new(source, "#26").unwrap();
601 let identity = "axioval:derived.adjacent-space;reach=1";
602 let edge = format!("{identity}:{door}->{room}:side=+(1.000000,0.000000):entered=0.000000");
604 let outside = format!("{identity}:{door}:side=-(1.000000,0.000000):outside:reach=1");
605 assert_eq!(
606 adjacent_side(&edge, &door, Some(&room)),
607 Some(AdjacentSide::Positive)
608 );
609 assert_eq!(adjacent_side(&edge, &door, Some(&other)), None);
610 assert_eq!(adjacent_side(&edge, &door, None), None);
611 assert_eq!(
612 adjacent_side(&outside, &door, None),
613 Some(AdjacentSide::Negative)
614 );
615 assert_eq!(adjacent_side(&outside, &door, Some(&room)), None);
616 assert_eq!(adjacent_side(&outside, &room, None), None);
617 let contained = format!(
619 "axioval:derived.contained-in-space;horizontal=0;vertical=0:{door}->{room}:side=+"
620 );
621 assert_eq!(adjacent_side(&contained, &door, Some(&room)), None);
622 let scan = format!("{identity}:derived-from:{door}:2 space(s)");
623 assert_eq!(adjacent_side(&scan, &door, None), None);
624 assert_eq!(AdjacentSide::Positive.opposite(), AdjacentSide::Negative);
625 }
626
627 #[test]
628 fn a_malformed_identity_is_an_invalid_request() {
629 for identity in [
630 "axioval:derived.",
631 "axioval:derived.nearest-thing",
632 "axioval:derived.adjacent-space;reach",
633 "axioval:derived.adjacent-space;reach=0",
634 "axioval:derived.adjacent-space;reach=-1",
635 "axioval:derived.adjacent-space;reach=inf",
636 "axioval:derived.adjacent-space;ratio=0.5",
637 "axioval:derived.contained-in-space;vertical=1;vertical=2",
638 "axioval:derived.overlapping-group-space;ratio=1.5",
639 "axioval:derived.overlapping-group-space;ratio=0",
640 ] {
641 assert_eq!(
642 parse(identity),
643 Err(RelationshipSelectionError::InvalidRequest),
644 "{identity}"
645 );
646 }
647 }
648}