#[cfg(feature = "transform")]
use axiolid_core::Transform3;
use ifc_model::value::Value;
use ifc_model::{Entity, EntityId, Model};
use crate::context::OperationSource;
use crate::crs::{LengthUnit, ProjectedCrs};
use crate::error::{GeorefError, GeorefResult};
use crate::view::GeorefView;
use super::{map, rigid};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum OperationKind {
MapConversion,
MapConversionScaled {
factors: (f64, f64, f64),
},
RigidOperation {
height: Option<f64>,
},
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ProjectToMap {
pub source_crs: EntityId,
pub source: OperationSource,
pub operation: EntityId,
pub kind: OperationKind,
pub target_crs: ProjectedCrs,
#[cfg(feature = "transform")]
pub transform: Transform3,
pub eastings: f64,
pub northings: f64,
pub orthogonal_height: f64,
pub project_unit: LengthUnit,
pub map_unit: LengthUnit,
pub declared_scale: f64,
pub x_axis_direction: (f64, f64),
linear: [[f64; 3]; 3],
translation: [f64; 3],
}
impl ProjectToMap {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
source: OperationSource,
operation: EntityId,
kind: OperationKind,
target_crs: ProjectedCrs,
authored: [f64; 3],
units: (LengthUnit, LengthUnit),
declared_scale: f64,
x_axis_direction: (f64, f64),
linear: [[f64; 3]; 3],
translation: [f64; 3],
) -> Self {
let (project_unit, map_unit) = units;
Self {
source_crs: source.entity(),
source,
operation,
kind,
target_crs,
#[cfg(feature = "transform")]
transform: transform3(&linear, &translation),
eastings: authored[0],
northings: authored[1],
orthogonal_height: authored[2],
project_unit,
map_unit,
declared_scale,
x_axis_direction,
linear,
translation,
}
}
#[must_use]
pub fn map_point(&self, point: [f64; 3]) -> [f64; 3] {
let [c0, c1, c2] = self.linear;
let t = self.translation;
std::array::from_fn(|i| c0[i] * point[0] + c1[i] * point[1] + c2[i] * point[2] + t[i])
}
#[must_use]
pub fn linear_part(&self) -> [[f64; 3]; 3] {
self.linear
}
#[must_use]
pub fn declared_map_unit(&self) -> Option<&LengthUnit> {
self.target_crs.map_unit.as_ref()
}
#[must_use]
pub fn translation(&self) -> [f64; 3] {
self.translation
}
}
#[cfg(feature = "transform")]
fn transform3(linear: &[[f64; 3]; 3], translation: &[f64; 3]) -> Transform3 {
use axiolid_core::{Mat3, Vec3};
let column = |c: &[f64; 3]| Vec3::new(c[0], c[1], c[2]);
Transform3::from_mat3_translation(
Mat3::from_cols(column(&linear[0]), column(&linear[1]), column(&linear[2])),
column(translation),
)
}
pub fn resolve_project_to_map(
model: &Model,
id: EntityId,
project_metres_per_unit: f64,
) -> GeorefResult<ProjectToMap> {
match GeorefView::for_model(model) {
Ok(view) => resolve_project_to_map_in(&view, id, project_metres_per_unit),
Err(_) => dispatch(model, None, id, project_metres_per_unit),
}
}
pub fn resolve_project_to_map_in(
view: &GeorefView,
id: EntityId,
project_metres_per_unit: f64,
) -> GeorefResult<ProjectToMap> {
view.require_known_type(id)?;
dispatch(view.model, Some(view), id, project_metres_per_unit)
}
fn dispatch(
model: &Model,
view: Option<&GeorefView>,
id: EntityId,
project_metres_per_unit: f64,
) -> GeorefResult<ProjectToMap> {
let entity = model.get(id).ok_or(GeorefError::MissingEntity {
referrer: id,
missing: id,
})?;
if !project_metres_per_unit.is_finite() || project_metres_per_unit <= 0.0 {
return Err(GeorefError::InvalidUnit {
entity: id,
detail: "project length scale must be finite and positive",
});
}
let project_unit = LengthUnit {
name: "PROJECT_LENGTH_UNIT".into(),
metres_per_unit: project_metres_per_unit,
};
let actual_type = entity.type_name.to_ascii_uppercase();
let operation = Operation {
model,
view,
id,
entity,
};
match actual_type.as_str() {
"IFCMAPCONVERSION" => map::lower(&operation, project_unit, false),
"IFCMAPCONVERSIONSCALED" => map::lower(&operation, project_unit, true),
"IFCRIGIDOPERATION" => rigid::lower(&operation, project_unit),
_ => Err(GeorefError::WrongType {
entity: id,
expected: "IFCCOORDINATEOPERATION",
actual: entity.type_name.to_string(),
}),
}
}
pub(super) struct Operation<'m, 'v> {
pub model: &'m Model,
pub view: Option<&'v GeorefView<'m>>,
pub id: EntityId,
pub entity: &'m Entity,
}
impl Operation<'_, '_> {
pub fn required_ref(&self, index: usize, name: &'static str) -> GeorefResult<EntityId> {
self.entity
.reference(index)
.ok_or(GeorefError::MissingAttribute {
entity: self.id,
index,
name,
})
}
pub fn required_number(&self, index: usize, name: &'static str) -> GeorefResult<f64> {
self.entity
.attribute(index)
.and_then(|v| v.unwrap_typed().as_f64())
.ok_or(GeorefError::MissingAttribute {
entity: self.id,
index,
name,
})
}
pub fn optional_number(&self, index: usize, name: &'static str) -> GeorefResult<Option<f64>> {
match self.entity.attribute(index).map(Value::unwrap_typed) {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_f64()
.map(Some)
.ok_or(GeorefError::InvalidAttribute {
entity: self.id,
index,
name,
}),
}
}
pub fn finite(&self, index: usize, name: &'static str, value: f64) -> GeorefResult<f64> {
if value.is_finite() {
Ok(value)
} else {
Err(GeorefError::InvalidAttribute {
entity: self.id,
index,
name,
})
}
}
}