use ifc_model::value::Value;
use ifc_model::EntityId;
use crate::context::{operation_source, OperationSource};
use crate::crs::{geographic_crs, projected_crs, GeographicCrs, LengthUnit};
use crate::error::{GeorefError, GeorefResult};
use crate::slot::map_conversion as base;
use crate::slot::rigid_operation as slot;
use crate::view::GeorefView;
use super::operation::{Operation, OperationKind, ProjectToMap};
const LENGTH: &str = "IFCLENGTHMEASURE";
const PLANE_ANGLE: &str = "IFCPLANEANGLEMEASURE";
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct GeographicOffset {
pub operation: EntityId,
pub source: OperationSource,
pub target_crs: GeographicCrs,
pub first_coordinate: f64,
pub second_coordinate: f64,
pub height: Option<f64>,
}
pub fn resolve_geographic_offset_in(
view: &GeorefView,
id: EntityId,
) -> GeorefResult<GeographicOffset> {
let actual = view.require_known_type(id)?;
let entity = view.model.get(id).ok_or(GeorefError::MissingEntity {
referrer: id,
missing: id,
})?;
if !actual.eq_ignore_ascii_case("IFCRIGIDOPERATION") {
return Err(GeorefError::WrongType {
entity: id,
expected: "IFCRIGIDOPERATION",
actual: actual.to_owned(),
});
}
let op = Operation {
model: view.model,
view: Some(view),
id,
entity,
};
let coordinates = coordinates(&op)?;
if coordinates.measure != PLANE_ANGLE {
return Err(GeorefError::CoordinateMeasureMismatch {
entity: id,
expected: PLANE_ANGLE,
actual: coordinates.measure,
});
}
let source = operation_source(view.model, Some(view), id)?;
let target = op.required_ref(base::TARGET_CRS, "TargetCRS")?;
let target_crs = geographic_crs(view.model, target)?;
Ok(GeographicOffset {
operation: id,
source,
target_crs,
first_coordinate: coordinates.first,
second_coordinate: coordinates.second,
height: coordinates.height,
})
}
pub(super) fn lower(
op: &Operation<'_, '_>,
project_unit: LengthUnit,
) -> GeorefResult<ProjectToMap> {
let coordinates = coordinates(op)?;
if coordinates.measure != LENGTH {
return Err(GeorefError::CoordinateMeasureMismatch {
entity: op.id,
expected: LENGTH,
actual: coordinates.measure,
});
}
let source = operation_source(op.model, op.view, op.id)?;
let target = op.required_ref(base::TARGET_CRS, "TargetCRS")?;
let target_crs = projected_crs(op.model, target)?;
let map_unit = target_crs
.map_unit
.clone()
.unwrap_or_else(|| project_unit.clone());
let metres = map_unit.metres_per_unit;
let translation = [
op.finite(
slot::FIRST_COORDINATE,
"FirstCoordinate",
coordinates.first * metres,
)?,
op.finite(
slot::SECOND_COORDINATE,
"SecondCoordinate",
coordinates.second * metres,
)?,
op.finite(
slot::HEIGHT,
"Height",
coordinates.height.unwrap_or(0.0) * metres,
)?,
];
Ok(ProjectToMap::new(
source,
op.id,
OperationKind::RigidOperation {
height: coordinates.height,
},
target_crs,
[
coordinates.first,
coordinates.second,
coordinates.height.unwrap_or(0.0),
],
(project_unit, map_unit),
1.0,
(1.0, 0.0),
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
translation,
))
}
struct Coordinates {
measure: &'static str,
first: f64,
second: f64,
height: Option<f64>,
}
fn coordinates(op: &Operation<'_, '_>) -> GeorefResult<Coordinates> {
let (first_measure, first) = measure(op, slot::FIRST_COORDINATE, "FirstCoordinate")?;
let (second_measure, second) = measure(op, slot::SECOND_COORDINATE, "SecondCoordinate")?;
let measure = match (first_measure, second_measure) {
(Some(a), Some(b)) if a == b => a,
_ => {
return Err(GeorefError::RuleViolation {
entity: op.id,
rule: "SameCoordinateType",
})
}
};
let height = op
.optional_number(slot::HEIGHT, "Height")?
.map(|value| op.finite(slot::HEIGHT, "Height", value))
.transpose()?;
Ok(Coordinates {
measure,
first: op.finite(slot::FIRST_COORDINATE, "FirstCoordinate", first)?,
second: op.finite(slot::SECOND_COORDINATE, "SecondCoordinate", second)?,
height,
})
}
fn measure(
op: &Operation<'_, '_>,
index: usize,
name: &'static str,
) -> GeorefResult<(Option<&'static str>, f64)> {
let invalid = GeorefError::InvalidAttribute {
entity: op.id,
index,
name,
};
match op.entity.attribute(index) {
None | Some(Value::Null) => Err(GeorefError::MissingAttribute {
entity: op.id,
index,
name,
}),
Some(Value::Typed { type_name, value }) => {
let number = value.as_f64().ok_or(invalid)?;
let branch = match type_name.to_ascii_uppercase().as_str() {
"IFCLENGTHMEASURE" | "IFCPOSITIVELENGTHMEASURE" | "IFCNONNEGATIVELENGTHMEASURE" => {
Some(LENGTH)
}
"IFCPLANEANGLEMEASURE" | "IFCPOSITIVEPLANEANGLEMEASURE" => Some(PLANE_ANGLE),
_ => None,
};
Ok((branch, number))
}
Some(value) => value.as_f64().map(|n| (None, n)).ok_or(invalid),
}
}