use ifc_model::value::Value;
use ifc_model::{EntityId, Model};
use crate::conversion::ProjectToMap;
use crate::error::{GeorefError, GeorefResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NorthReference {
Project,
True {
direction: (f64, f64),
},
Grid {
direction: (f64, f64),
},
}
impl NorthReference {
#[must_use]
pub fn direction(&self) -> (f64, f64) {
match self {
Self::Project => project_north_direction(),
Self::True { direction } | Self::Grid { direction } => *direction,
}
}
}
#[must_use]
pub const fn project_north_direction() -> (f64, f64) {
(0.0, 1.0)
}
pub fn resolve_true_north(model: &Model, context: EntityId) -> GeorefResult<NorthReference> {
let entity = model.get(context).ok_or(GeorefError::MissingEntity {
referrer: context,
missing: context,
})?;
if !entity.is_type("IFCGEOMETRICREPRESENTATIONCONTEXT")
&& !entity.is_type("IFCGEOMETRICREPRESENTATIONSUBCONTEXT")
{
return Err(GeorefError::WrongType {
entity: context,
expected: "IFCGEOMETRICREPRESENTATIONCONTEXT",
actual: entity.type_name.to_string(),
});
}
let direction = match entity.attribute(5).map(Value::unwrap_typed) {
None | Some(Value::Null) => project_north_direction(),
Some(Value::Ref(direction_id)) => {
direction_ratios_2d(model, *direction_id, context, 5, "TrueNorth")?
}
Some(_) => {
return Err(GeorefError::InvalidAttribute {
entity: context,
index: 5,
name: "TrueNorth",
})
}
};
Ok(NorthReference::True { direction })
}
#[must_use]
pub fn grid_north_direction(operation: &ProjectToMap) -> NorthReference {
let (a, b) = operation.x_axis_direction;
NorthReference::Grid { direction: (b, a) }
}
fn direction_ratios_2d(
model: &Model,
direction_id: EntityId,
referrer: EntityId,
index: usize,
name: &'static str,
) -> GeorefResult<(f64, f64)> {
let direction = model.get(direction_id).ok_or(GeorefError::MissingEntity {
referrer,
missing: direction_id,
})?;
if !direction.is_type("IFCDIRECTION") {
return Err(GeorefError::WrongType {
entity: direction_id,
expected: "IFCDIRECTION",
actual: direction.type_name.to_string(),
});
}
let ratios = direction
.attribute(0)
.map(Value::unwrap_typed)
.and_then(Value::as_list)
.ok_or(GeorefError::InvalidAttribute {
entity: referrer,
index,
name,
})?;
let (x, y) = match ratios {
[x, y] => (
x.unwrap_typed()
.as_f64()
.ok_or(GeorefError::InvalidAttribute {
entity: direction_id,
index: 0,
name: "DirectionRatios",
})?,
y.unwrap_typed()
.as_f64()
.ok_or(GeorefError::InvalidAttribute {
entity: direction_id,
index: 0,
name: "DirectionRatios",
})?,
),
_ => {
return Err(GeorefError::InvalidAttribute {
entity: referrer,
index,
name,
})
}
};
let norm = x.hypot(y);
if !norm.is_finite() || norm <= f64::EPSILON {
return Err(GeorefError::NonFiniteDirection {
entity: direction_id,
});
}
Ok((x / norm, y / norm))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use ifc_model::Entity;
use super::*;
use crate::conversion::resolve_project_to_map;
fn id(value: u64) -> EntityId {
EntityId(value)
}
fn r(value: u64) -> Value {
Value::Ref(id(value))
}
fn real(value: f64) -> Value {
Value::Real(value)
}
#[test]
fn true_north_defaults_to_project_north_when_absent() {
let mut model = Model::new();
model.insert(
id(1),
Entity::new("IFCGEOMETRICREPRESENTATIONCONTEXT", vec![]),
);
let north = resolve_true_north(&model, id(1)).expect("default applies");
assert_eq!(north.direction(), (0.0, 1.0));
assert!(matches!(north, NorthReference::True { .. }));
}
#[test]
fn true_north_reads_an_explicit_declared_direction() {
let mut model = Model::new();
model.insert(
id(2),
Entity::new(
"IFCDIRECTION",
vec![Value::List(vec![real(1.0), real(1.0)])],
),
);
model.insert(
id(1),
Entity::new(
"IFCGEOMETRICREPRESENTATIONCONTEXT",
vec![
Value::Null,
Value::Null,
Value::Integer(3),
Value::Null,
Value::Null,
r(2),
],
),
);
let north = resolve_true_north(&model, id(1)).expect("explicit direction resolves");
let root_two = 2.0_f64.sqrt();
let (x, y) = north.direction();
assert!((x - 1.0 / root_two).abs() < 1e-12);
assert!((y - 1.0 / root_two).abs() < 1e-12);
}
#[test]
fn true_north_rejects_a_zero_length_direction() {
let mut model = Model::new();
model.insert(
id(2),
Entity::new(
"IFCDIRECTION",
vec![Value::List(vec![real(0.0), real(0.0)])],
),
);
model.insert(
id(1),
Entity::new(
"IFCGEOMETRICREPRESENTATIONCONTEXT",
vec![
Value::Null,
Value::Null,
Value::Integer(3),
Value::Null,
Value::Null,
r(2),
],
),
);
assert!(matches!(
resolve_true_north(&model, id(1)),
Err(GeorefError::NonFiniteDirection { entity }) if entity == id(2)
));
}
#[test]
fn grid_north_matches_project_north_when_the_map_conversion_has_no_rotation() {
let mut model = Model::new();
model.insert(
id(1),
Entity::new("IFCGEOMETRICREPRESENTATIONCONTEXT", vec![]),
);
model.insert(
id(2),
Entity::new(
"IFCPROJECTEDCRS",
vec![Value::Text(Arc::from("EPSG:25832"))],
),
);
model.insert(
id(4),
Entity::new(
"IFCMAPCONVERSION",
vec![
r(1),
r(2),
real(0.0),
real(0.0),
real(0.0),
real(1.0),
real(0.0),
real(1.0),
],
),
);
let operation = resolve_project_to_map(&model, id(4), 1.0).expect("resolves");
let grid_north = grid_north_direction(&operation);
assert_eq!(grid_north.direction(), (0.0, 1.0));
}
#[test]
fn grid_north_rotates_a_quarter_turn_when_the_map_conversion_does() {
let mut model = Model::new();
model.insert(
id(1),
Entity::new("IFCGEOMETRICREPRESENTATIONCONTEXT", vec![]),
);
model.insert(
id(2),
Entity::new(
"IFCPROJECTEDCRS",
vec![Value::Text(Arc::from("EPSG:25832"))],
),
);
model.insert(
id(4),
Entity::new(
"IFCMAPCONVERSION",
vec![
r(1),
r(2),
real(0.0),
real(0.0),
real(0.0),
real(0.0),
real(1.0),
real(1.0),
],
),
);
let operation = resolve_project_to_map(&model, id(4), 1.0).expect("resolves");
let grid_north = grid_north_direction(&operation);
let (x, y) = grid_north.direction();
assert!((x - 1.0).abs() < 1e-12);
assert!(y.abs() < 1e-12);
}
#[test]
fn project_north_is_always_the_project_y_axis() {
assert_eq!(
NorthReference::Project.direction(),
project_north_direction()
);
assert_eq!(project_north_direction(), (0.0, 1.0));
}
}