use ifc_model::{Entity, EntityId, Model, Transaction, Value};
use crate::authoring::invalid;
use crate::GeorefResult;
#[derive(Debug, Clone, Copy, Default)]
pub struct GeographicCrsDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub geodetic_datum: Option<&'a str>,
pub prime_meridian: Option<&'a str>,
pub angle_unit: Option<EntityId>,
pub height_unit: Option<EntityId>,
}
pub fn create_geographic_crs(
tx: &mut Transaction,
draft: GeographicCrsDraft<'_>,
) -> GeorefResult<EntityId> {
if let Some(name) = draft.name {
if name.trim().is_empty() {
return Err(invalid("IFCGEOGRAPHICCRS", "Name", name));
}
}
Ok(tx.create(Entity::new(
"IFCGEOGRAPHICCRS",
vec![
draft.name.map_or(Value::Null, |v| Value::Text(v.into())),
draft
.description
.map_or(Value::Null, |v| Value::Text(v.into())),
draft
.geodetic_datum
.map_or(Value::Null, |v| Value::Text(v.into())),
draft
.prime_meridian
.map_or(Value::Null, |v| Value::Text(v.into())),
draft.angle_unit.map_or(Value::Null, Value::Ref),
draft.height_unit.map_or(Value::Null, Value::Ref),
],
)))
}
#[derive(Debug, Clone, Copy)]
pub struct MapConversionDraft {
pub source_crs: EntityId,
pub target_crs: EntityId,
pub eastings: f64,
pub northings: f64,
pub orthogonal_height: f64,
pub x_axis: Option<(f64, f64)>,
pub scale: Option<f64>,
}
fn resolved_type(tx: &Transaction, model: &Model, target: EntityId) -> Option<String> {
tx.edits()
.iter()
.rev()
.find_map(|edit| match edit {
ifc_model::Edit::Create { id, entity } if *id == target => {
Some(entity.type_name.as_ref().to_owned())
}
_ => None,
})
.or_else(|| model.get(target).map(|e| e.type_name.as_ref().to_owned()))
}
fn check_conversion(
entity: &'static str,
tx: &Transaction,
model: &Model,
draft: &MapConversionDraft,
) -> GeorefResult<()> {
let finite = [
("Eastings", draft.eastings),
("Northings", draft.northings),
("OrthogonalHeight", draft.orthogonal_height),
];
for (attribute, value) in finite {
if !value.is_finite() {
return Err(invalid(entity, attribute, value.to_string()));
}
}
match resolved_type(tx, model, draft.target_crs).as_deref() {
Some("IFCPROJECTEDCRS") => {}
Some(other) => {
return Err(invalid(
entity,
"TargetCRS",
format!("expected IFCPROJECTEDCRS, found {other}"),
))
}
None => {
return Err(invalid(
entity,
"TargetCRS",
"target does not resolve to a staged entity",
))
}
}
if let Some((abscissa, ordinate)) = draft.x_axis {
if !abscissa.is_finite() || !ordinate.is_finite() {
return Err(invalid(
entity,
"XAxisAbscissa",
format!("expected a finite direction, got ({abscissa}, {ordinate})"),
));
}
if abscissa == 0.0 && ordinate == 0.0 {
return Err(invalid(
entity,
"XAxisAbscissa",
"a zero-length vector gives no rotation",
));
}
}
if let Some(scale) = draft.scale {
if !scale.is_finite() || scale == 0.0 {
return Err(invalid(
entity,
"Scale",
format!("expected a non-zero finite scale, got {scale}"),
));
}
}
Ok(())
}
fn conversion_attributes(draft: &MapConversionDraft) -> Vec<Value> {
let (abscissa, ordinate) = match draft.x_axis {
Some((a, o)) => (Value::Real(a), Value::Real(o)),
None => (Value::Null, Value::Null),
};
vec![
Value::Ref(draft.source_crs),
Value::Ref(draft.target_crs),
Value::Real(draft.eastings),
Value::Real(draft.northings),
Value::Real(draft.orthogonal_height),
abscissa,
ordinate,
draft.scale.map_or(Value::Null, Value::Real),
]
}
pub fn create_map_conversion(
tx: &mut Transaction,
model: &Model,
draft: MapConversionDraft,
) -> GeorefResult<EntityId> {
check_conversion("IFCMAPCONVERSION", tx, model, &draft)?;
Ok(tx.create(Entity::new(
"IFCMAPCONVERSION",
conversion_attributes(&draft),
)))
}
pub fn create_map_conversion_scaled(
tx: &mut Transaction,
model: &Model,
draft: MapConversionDraft,
factors: (f64, f64, f64),
) -> GeorefResult<EntityId> {
check_conversion("IFCMAPCONVERSIONSCALED", tx, model, &draft)?;
let (x, y, z) = factors;
for (attribute, value) in [("FactorX", x), ("FactorY", y), ("FactorZ", z)] {
if !value.is_finite() || value == 0.0 {
return Err(invalid(
"IFCMAPCONVERSIONSCALED",
attribute,
format!("expected a non-zero finite factor, got {value}"),
));
}
}
let mut attributes = conversion_attributes(&draft);
attributes.extend([Value::Real(x), Value::Real(y), Value::Real(z)]);
Ok(tx.create(Entity::new("IFCMAPCONVERSIONSCALED", attributes)))
}
pub fn create_rigid_operation(
tx: &mut Transaction,
source_crs: EntityId,
target_crs: EntityId,
coordinates: (f64, f64),
height: Option<f64>,
) -> GeorefResult<EntityId> {
let (first, second) = coordinates;
for (attribute, value) in [("FirstCoordinate", first), ("SecondCoordinate", second)] {
if !value.is_finite() {
return Err(invalid("IFCRIGIDOPERATION", attribute, value.to_string()));
}
}
if let Some(value) = height {
if !value.is_finite() {
return Err(invalid("IFCRIGIDOPERATION", "Height", value.to_string()));
}
}
Ok(tx.create(Entity::new(
"IFCRIGIDOPERATION",
vec![
Value::Ref(source_crs),
Value::Ref(target_crs),
Value::Real(first),
Value::Real(second),
height.map_or(Value::Null, Value::Real),
],
)))
}
pub fn create_well_known_text(
tx: &mut Transaction,
model: &Model,
well_known_text: &str,
coordinate_reference_system: EntityId,
) -> GeorefResult<EntityId> {
const ENTITY: &str = "IFCWELLKNOWNTEXT";
const CRS_TYPES: &[&str] = &[
"IFCPROJECTEDCRS",
"IFCGEOGRAPHICCRS",
"IFCCOORDINATEREFERENCESYSTEM",
];
if well_known_text.trim().is_empty() {
return Err(invalid(ENTITY, "WellKnownText", well_known_text));
}
let actual = tx
.edits()
.iter()
.rev()
.find_map(|edit| match edit {
ifc_model::Edit::Create { id, entity } if *id == coordinate_reference_system => {
Some(entity.type_name.as_ref().to_owned())
}
_ => None,
})
.or_else(|| {
model
.get(coordinate_reference_system)
.map(|entity| entity.type_name.as_ref().to_owned())
})
.unwrap_or_default();
if !CRS_TYPES
.iter()
.any(|kind| actual.eq_ignore_ascii_case(kind))
{
return Err(invalid(ENTITY, "CoordinateReferenceSystem", actual));
}
Ok(tx.create(Entity::new(
ENTITY,
vec![
Value::Text(well_known_text.into()),
Value::Ref(coordinate_reference_system),
],
)))
}