use ifc_model::value::Value;
use ifc_model::{EntityId, Model};
use crate::error::{GeorefError, GeorefResult};
use crate::slot::map_conversion as slot;
use crate::view::GeorefView;
pub(crate) const HAS_COORDINATE_OPERATION: &str =
"HasCoordinateOperation : SET [0:1] OF IfcCoordinateOperation";
const UNPINNED_OPERATIONS: &[&str] = &[
"IFCMAPCONVERSION",
"IFCMAPCONVERSIONSCALED",
"IFCRIGIDOPERATION",
];
const UNPINNED_CRS: &[&str] = &["IFCPROJECTEDCRS", "IFCGEOGRAPHICCRS"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OperationSource {
Context(EntityId),
CoordinateReferenceSystem(EntityId),
}
impl OperationSource {
#[must_use]
pub const fn entity(&self) -> EntityId {
match self {
Self::Context(id) | Self::CoordinateReferenceSystem(id) => *id,
}
}
}
pub fn resolve_operation_source(
view: &GeorefView,
operation: EntityId,
) -> GeorefResult<OperationSource> {
let actual = view.require_known_type(operation)?;
if !view.schema.is_a(actual, "IfcCoordinateOperation") {
return Err(GeorefError::WrongType {
entity: operation,
expected: "IFCCOORDINATEOPERATION",
actual: actual.to_owned(),
});
}
operation_source(view.model, Some(view), operation)
}
pub fn coordinate_operation_for(
view: &GeorefView,
source: EntityId,
) -> GeorefResult<Option<EntityId>> {
let model = view.model;
if model.get(source).is_none() {
return Err(GeorefError::MissingEntity {
referrer: source,
missing: source,
});
}
classify(model, Some(view), source)?;
let operations = operations_from(model, Some(view), source);
match operations.as_slice() {
[] => Ok(None),
[one] => {
forbid_sub_context(model, source)?;
Ok(Some(*one))
}
_ => Err(GeorefError::RuleViolation {
entity: source,
rule: HAS_COORDINATE_OPERATION,
}),
}
}
pub(crate) fn operation_source(
model: &Model,
view: Option<&GeorefView>,
operation: EntityId,
) -> GeorefResult<OperationSource> {
let entity = model.get(operation).ok_or(GeorefError::MissingEntity {
referrer: operation,
missing: operation,
})?;
let source = match entity.attribute(slot::SOURCE_CRS) {
None | Some(Value::Null) => {
return Err(GeorefError::MissingAttribute {
entity: operation,
index: slot::SOURCE_CRS,
name: "SourceCRS",
})
}
Some(value) => value.as_ref_id().ok_or(GeorefError::InvalidAttribute {
entity: operation,
index: slot::SOURCE_CRS,
name: "SourceCRS",
})?,
};
if model.get(source).is_none() {
return Err(GeorefError::MissingEntity {
referrer: operation,
missing: source,
});
}
let classified = classify(model, view, source)?;
forbid_sub_context(model, source)?;
if operations_from(model, view, source).len() > 1 {
return Err(GeorefError::RuleViolation {
entity: source,
rule: HAS_COORDINATE_OPERATION,
});
}
Ok(classified)
}
fn classify(
model: &Model,
view: Option<&GeorefView>,
id: EntityId,
) -> GeorefResult<OperationSource> {
let entity = model.get(id).ok_or(GeorefError::MissingEntity {
referrer: id,
missing: id,
})?;
let type_name = entity.type_name.as_ref();
let (is_context, is_crs) = match view {
Some(view) => {
let declared = view.require_known_type(id)?;
(
view.schema
.is_a(declared, "IfcGeometricRepresentationContext"),
view.schema.is_a(declared, "IfcCoordinateReferenceSystem"),
)
}
None => (
entity.is_type("IFCGEOMETRICREPRESENTATIONCONTEXT")
|| entity.is_type("IFCGEOMETRICREPRESENTATIONSUBCONTEXT"),
UNPINNED_CRS.iter().any(|crs| entity.is_type(crs)),
),
};
if is_context {
Ok(OperationSource::Context(id))
} else if is_crs {
Ok(OperationSource::CoordinateReferenceSystem(id))
} else {
Err(GeorefError::WrongType {
entity: id,
expected: "IFCCOORDINATEREFERENCESYSTEM or IFCGEOMETRICREPRESENTATIONCONTEXT",
actual: type_name.to_owned(),
})
}
}
fn forbid_sub_context(model: &Model, source: EntityId) -> GeorefResult<()> {
match model.get(source) {
Some(entity) if entity.is_type("IFCGEOMETRICREPRESENTATIONSUBCONTEXT") => {
Err(GeorefError::RuleViolation {
entity: source,
rule: "NoCoordOperation",
})
}
_ => Ok(()),
}
}
fn operations_from(model: &Model, view: Option<&GeorefView>, source: EntityId) -> Vec<EntityId> {
let types: Vec<&str> = match view {
Some(view) => view.schema.subtypes("IfcCoordinateOperation"),
None => UNPINNED_OPERATIONS.to_vec(),
};
let mut found: Vec<EntityId> = types
.into_iter()
.flat_map(|type_name| model.of_type(type_name))
.filter(|(_, operation)| operation.reference(slot::SOURCE_CRS) == Some(source))
.map(|(id, _)| id)
.collect();
found.sort_unstable();
found.dedup();
found
}