ifc_georef/context/
source.rs1use ifc_model::value::Value;
24use ifc_model::{EntityId, Model};
25
26use crate::error::{GeorefError, GeorefResult};
27use crate::slot::map_conversion as slot;
28use crate::view::GeorefView;
29
30pub(crate) const HAS_COORDINATE_OPERATION: &str =
32 "HasCoordinateOperation : SET [0:1] OF IfcCoordinateOperation";
33
34const UNPINNED_OPERATIONS: &[&str] = &[
37 "IFCMAPCONVERSION",
38 "IFCMAPCONVERSIONSCALED",
39 "IFCRIGIDOPERATION",
40];
41
42const UNPINNED_CRS: &[&str] = &["IFCPROJECTEDCRS", "IFCGEOGRAPHICCRS"];
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum OperationSource {
49 Context(EntityId),
53 CoordinateReferenceSystem(EntityId),
56}
57
58impl OperationSource {
59 #[must_use]
61 pub const fn entity(&self) -> EntityId {
62 match self {
63 Self::Context(id) | Self::CoordinateReferenceSystem(id) => *id,
64 }
65 }
66}
67
68pub fn resolve_operation_source(
85 view: &GeorefView,
86 operation: EntityId,
87) -> GeorefResult<OperationSource> {
88 let actual = view.require_known_type(operation)?;
89 if !view.schema.is_a(actual, "IfcCoordinateOperation") {
90 return Err(GeorefError::WrongType {
91 entity: operation,
92 expected: "IFCCOORDINATEOPERATION",
93 actual: actual.to_owned(),
94 });
95 }
96 operation_source(view.model, Some(view), operation)
97}
98
99pub fn coordinate_operation_for(
112 view: &GeorefView,
113 source: EntityId,
114) -> GeorefResult<Option<EntityId>> {
115 let model = view.model;
116 if model.get(source).is_none() {
117 return Err(GeorefError::MissingEntity {
118 referrer: source,
119 missing: source,
120 });
121 }
122 classify(model, Some(view), source)?;
123 let operations = operations_from(model, Some(view), source);
124 match operations.as_slice() {
125 [] => Ok(None),
126 [one] => {
127 forbid_sub_context(model, source)?;
128 Ok(Some(*one))
129 }
130 _ => Err(GeorefError::RuleViolation {
131 entity: source,
132 rule: HAS_COORDINATE_OPERATION,
133 }),
134 }
135}
136
137pub(crate) fn operation_source(
140 model: &Model,
141 view: Option<&GeorefView>,
142 operation: EntityId,
143) -> GeorefResult<OperationSource> {
144 let entity = model.get(operation).ok_or(GeorefError::MissingEntity {
145 referrer: operation,
146 missing: operation,
147 })?;
148 let source = match entity.attribute(slot::SOURCE_CRS) {
149 None | Some(Value::Null) => {
150 return Err(GeorefError::MissingAttribute {
151 entity: operation,
152 index: slot::SOURCE_CRS,
153 name: "SourceCRS",
154 })
155 }
156 Some(value) => value.as_ref_id().ok_or(GeorefError::InvalidAttribute {
157 entity: operation,
158 index: slot::SOURCE_CRS,
159 name: "SourceCRS",
160 })?,
161 };
162 if model.get(source).is_none() {
163 return Err(GeorefError::MissingEntity {
164 referrer: operation,
165 missing: source,
166 });
167 }
168 let classified = classify(model, view, source)?;
169 forbid_sub_context(model, source)?;
170 if operations_from(model, view, source).len() > 1 {
171 return Err(GeorefError::RuleViolation {
172 entity: source,
173 rule: HAS_COORDINATE_OPERATION,
174 });
175 }
176 Ok(classified)
177}
178
179fn classify(
181 model: &Model,
182 view: Option<&GeorefView>,
183 id: EntityId,
184) -> GeorefResult<OperationSource> {
185 let entity = model.get(id).ok_or(GeorefError::MissingEntity {
186 referrer: id,
187 missing: id,
188 })?;
189 let type_name = entity.type_name.as_ref();
190 let (is_context, is_crs) = match view {
191 Some(view) => {
192 let declared = view.require_known_type(id)?;
193 (
194 view.schema
195 .is_a(declared, "IfcGeometricRepresentationContext"),
196 view.schema.is_a(declared, "IfcCoordinateReferenceSystem"),
197 )
198 }
199 None => (
200 entity.is_type("IFCGEOMETRICREPRESENTATIONCONTEXT")
201 || entity.is_type("IFCGEOMETRICREPRESENTATIONSUBCONTEXT"),
202 UNPINNED_CRS.iter().any(|crs| entity.is_type(crs)),
203 ),
204 };
205 if is_context {
206 Ok(OperationSource::Context(id))
207 } else if is_crs {
208 Ok(OperationSource::CoordinateReferenceSystem(id))
209 } else {
210 Err(GeorefError::WrongType {
211 entity: id,
212 expected: "IFCCOORDINATEREFERENCESYSTEM or IFCGEOMETRICREPRESENTATIONCONTEXT",
213 actual: type_name.to_owned(),
214 })
215 }
216}
217
218fn forbid_sub_context(model: &Model, source: EntityId) -> GeorefResult<()> {
220 match model.get(source) {
221 Some(entity) if entity.is_type("IFCGEOMETRICREPRESENTATIONSUBCONTEXT") => {
222 Err(GeorefError::RuleViolation {
223 entity: source,
224 rule: "NoCoordOperation",
225 })
226 }
227 _ => Ok(()),
228 }
229}
230
231fn operations_from(model: &Model, view: Option<&GeorefView>, source: EntityId) -> Vec<EntityId> {
234 let types: Vec<&str> = match view {
235 Some(view) => view.schema.subtypes("IfcCoordinateOperation"),
236 None => UNPINNED_OPERATIONS.to_vec(),
237 };
238 let mut found: Vec<EntityId> = types
239 .into_iter()
240 .flat_map(|type_name| model.of_type(type_name))
241 .filter(|(_, operation)| operation.reference(slot::SOURCE_CRS) == Some(source))
242 .map(|(id, _)| id)
243 .collect();
244 found.sort_unstable();
245 found.dedup();
246 found
247}