Skip to main content

flow_gate_xml/
parser.rs

1use std::collections::HashMap;
2
3use flow_gate_core::{
4    gate::{
5        BooleanGate, BooleanOp, BooleanOperand, EllipsoidDimension, EllipsoidGate, GateKind,
6        PolygonDimension, PolygonGate, RectangleDimension, RectangleGate,
7    },
8    transform::{
9        FASinhTransform, HyperlogTransform, LinearTransform, LogarithmicTransform, LogicleParams,
10        LogicleTransform,
11    },
12    FlowGateError, GateId, GateRegistry, ParameterName, TransformKind,
13};
14use indexmap::IndexMap;
15use quick_xml::{
16    events::{BytesStart, Event},
17    name::{Namespace, ResolveResult},
18    NsReader,
19};
20
21use crate::{
22    make_fcs_binding_name, make_ratio_binding_name,
23    namespace::{parse_bool_attr, NS_DATATYPE, NS_GATING, NS_TRANSFORMS},
24    FlowGateDocument, RatioTransformSpec, SpectrumMatrixSpec,
25};
26
27#[derive(Default)]
28pub struct FlowGateParser {
29    transforms: HashMap<String, TransformKind>,
30    ratio_transforms: HashMap<String, RatioTransformSpec>,
31    spectrum_matrices: HashMap<String, SpectrumMatrixSpec>,
32    gates: IndexMap<GateId, GateKind>,
33}
34
35impl FlowGateParser {
36    pub fn parse_str(xml: &str) -> Result<FlowGateDocument, FlowGateError> {
37        parse_document(xml)
38    }
39}
40
41#[derive(Debug, Clone)]
42enum DimensionSelector {
43    Fcs(String),
44    New(String),
45}
46
47fn parse_boolean_op_block(
48    reader: &mut NsReader<&[u8]>,
49    source: &str,
50    operator_local: &[u8],
51    current_gate_id: &GateId,
52    existing_gates: &IndexMap<GateId, GateKind>,
53) -> Result<(BooleanOp, Vec<BooleanOperand>), FlowGateError> {
54    let op = match operator_local {
55        b"and" => BooleanOp::And,
56        b"or" => BooleanOp::Or,
57        b"not" => BooleanOp::Not,
58        _ => {
59            return Err(FlowGateError::InvalidGate(format!(
60                "Unsupported Boolean operator '{}'",
61                String::from_utf8_lossy(operator_local)
62            )))
63        }
64    };
65
66    let mut operands = Vec::new();
67    let mut buf = Vec::new();
68    loop {
69        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
70            Ok(v) => v,
71            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
72        };
73        match event {
74            Event::Start(e) => {
75                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"gateReference") {
76                    let attrs = attrs_map(reader, source, &e)?;
77                    let gate_id = GateId::from(
78                        required_attr(&attrs, "ref", &[NS_GATING, ""], "gateReference")?
79                            .to_string(),
80                    );
81                    if !existing_gates.contains_key(&gate_id) {
82                        return Err(FlowGateError::UnknownGateReference(
83                            current_gate_id.clone(),
84                            gate_id,
85                        ));
86                    }
87                    let complement = parse_bool_attr(
88                        optional_attr(&attrs, "use-as-complement", &[NS_GATING, ""])
89                            .or_else(|| optional_attr(&attrs, "complement", &[NS_GATING, ""])),
90                        false,
91                    );
92                    operands.push(BooleanOperand {
93                        gate_id,
94                        complement,
95                    });
96                    skip_element(reader, source, &e)?;
97                } else {
98                    skip_element(reader, source, &e)?;
99                }
100            }
101            Event::Empty(e) => {
102                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"gateReference") {
103                    let attrs = attrs_map(reader, source, &e)?;
104                    let gate_id = GateId::from(
105                        required_attr(&attrs, "ref", &[NS_GATING, ""], "gateReference")?
106                            .to_string(),
107                    );
108                    if !existing_gates.contains_key(&gate_id) {
109                        return Err(FlowGateError::UnknownGateReference(
110                            current_gate_id.clone(),
111                            gate_id,
112                        ));
113                    }
114                    let complement = parse_bool_attr(
115                        optional_attr(&attrs, "use-as-complement", &[NS_GATING, ""])
116                            .or_else(|| optional_attr(&attrs, "complement", &[NS_GATING, ""])),
117                        false,
118                    );
119                    operands.push(BooleanOperand {
120                        gate_id,
121                        complement,
122                    });
123                }
124            }
125            Event::End(e)
126                if ns_is(&ns, NS_GATING) && e.name().local_name().as_ref() == operator_local =>
127            {
128                break;
129            }
130            Event::Eof => {
131                return Err(xml_err(
132                    reader,
133                    source,
134                    "Unexpected EOF while reading Boolean operator block",
135                ));
136            }
137            _ => {}
138        }
139        buf.clear();
140    }
141
142    Ok((op, operands))
143}
144
145fn parse_vertex(reader: &mut NsReader<&[u8]>, source: &str) -> Result<(f64, f64), FlowGateError> {
146    let coords = parse_coordinates_block(reader, source, NS_GATING, b"vertex")?;
147    if coords.len() != 2 {
148        return Err(FlowGateError::InvalidGate(format!(
149            "Polygon vertex must contain exactly 2 coordinates, found {}",
150            coords.len()
151        )));
152    }
153    Ok((coords[0], coords[1]))
154}
155
156fn parse_coordinates_block(
157    reader: &mut NsReader<&[u8]>,
158    source: &str,
159    end_ns: &str,
160    end_local: &[u8],
161) -> Result<Vec<f64>, FlowGateError> {
162    let mut values = Vec::new();
163    let mut buf = Vec::new();
164
165    loop {
166        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
167            Ok(v) => v,
168            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
169        };
170        match event {
171            Event::Start(e) => {
172                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate") {
173                    let attrs = attrs_map(reader, source, &e)?;
174                    values.push(parse_required_f64_attr(
175                        &attrs,
176                        "value",
177                        &[NS_DATATYPE, ""],
178                        "coordinate",
179                    )?);
180                    skip_element(reader, source, &e)?;
181                } else {
182                    skip_element(reader, source, &e)?;
183                }
184            }
185            Event::Empty(e) => {
186                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate") {
187                    let attrs = attrs_map(reader, source, &e)?;
188                    values.push(parse_required_f64_attr(
189                        &attrs,
190                        "value",
191                        &[NS_DATATYPE, ""],
192                        "coordinate",
193                    )?);
194                }
195            }
196            Event::Text(t) => {
197                let text = t
198                    .unescape()
199                    .map_err(|e| xml_err(reader, source, format!("XML text decode error: {e}")))?;
200                for token in text.split_whitespace() {
201                    if let Ok(v) = token.parse::<f64>() {
202                        values.push(v);
203                    }
204                }
205            }
206            Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
207                break;
208            }
209            Event::Eof => {
210                return Err(xml_err(
211                    reader,
212                    source,
213                    "Unexpected EOF while reading coordinate block",
214                ));
215            }
216            _ => {}
217        }
218        buf.clear();
219    }
220    Ok(values)
221}
222
223fn parse_covariance_block(
224    reader: &mut NsReader<&[u8]>,
225    source: &str,
226) -> Result<Vec<f64>, FlowGateError> {
227    let mut values = Vec::new();
228    let mut buf = Vec::new();
229    loop {
230        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
231            Ok(v) => v,
232            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
233        };
234        match event {
235            Event::Start(e) => {
236                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"row") {
237                    values.extend(parse_covariance_row(reader, source)?);
238                } else {
239                    skip_element(reader, source, &e)?;
240                }
241            }
242            Event::End(e)
243                if element_is(
244                    &ns,
245                    e.name().local_name().as_ref(),
246                    NS_GATING,
247                    b"covarianceMatrix",
248                ) =>
249            {
250                break;
251            }
252            Event::Eof => {
253                return Err(xml_err(
254                    reader,
255                    source,
256                    "Unexpected EOF while reading covarianceMatrix",
257                ));
258            }
259            _ => {}
260        }
261        buf.clear();
262    }
263    Ok(values)
264}
265
266fn parse_covariance_row(
267    reader: &mut NsReader<&[u8]>,
268    source: &str,
269) -> Result<Vec<f64>, FlowGateError> {
270    let mut values = Vec::new();
271    let mut buf = Vec::new();
272    loop {
273        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
274            Ok(v) => v,
275            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
276        };
277        match event {
278            Event::Start(e) => {
279                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"entry")
280                    || element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate")
281                {
282                    let attrs = attrs_map(reader, source, &e)?;
283                    values.push(parse_required_f64_attr(
284                        &attrs,
285                        "value",
286                        &[NS_DATATYPE, "", NS_GATING],
287                        "entry",
288                    )?);
289                    skip_element(reader, source, &e)?;
290                } else {
291                    skip_element(reader, source, &e)?;
292                }
293            }
294            Event::Empty(e) => {
295                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"entry")
296                    || element_is(&ns, e.local_name().as_ref(), NS_GATING, b"coordinate")
297                {
298                    let attrs = attrs_map(reader, source, &e)?;
299                    values.push(parse_required_f64_attr(
300                        &attrs,
301                        "value",
302                        &[NS_DATATYPE, "", NS_GATING],
303                        "entry",
304                    )?);
305                }
306            }
307            Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"row") => {
308                break;
309            }
310            Event::Eof => {
311                return Err(xml_err(
312                    reader,
313                    source,
314                    "Unexpected EOF while reading covariance row",
315                ));
316            }
317            _ => {}
318        }
319        buf.clear();
320    }
321    Ok(values)
322}
323
324fn parse_transform_kind(
325    reader: &NsReader<&[u8]>,
326    source: &str,
327    local: &[u8],
328    start: &BytesStart<'_>,
329) -> Result<TransformKind, FlowGateError> {
330    let attrs = attrs_map(reader, source, start)?;
331    match local {
332        b"logicle" => {
333            let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "logicle")?;
334            let w = parse_required_f64_attr(&attrs, "W", &[NS_TRANSFORMS, ""], "logicle")?;
335            let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "logicle")?;
336            let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "logicle")?;
337            let params = LogicleParams { t, w, m, a }
338                .validate()
339                .map_err(FlowGateError::InvalidTransformParam)?;
340            Ok(TransformKind::Logicle(LogicleTransform { params }))
341        }
342        b"fasinh" => {
343            let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "fasinh")?;
344            let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "fasinh")?;
345            let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "fasinh")?;
346            Ok(TransformKind::FASinh(FASinhTransform::new(t, m, a)?))
347        }
348        b"log" | b"flog" => {
349            let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "log")?;
350            let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "log")?;
351            Ok(TransformKind::Logarithmic(LogarithmicTransform::new(t, m)?))
352        }
353        b"lin" | b"flin" => {
354            let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "lin")?;
355            let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "lin")?;
356            Ok(TransformKind::Linear(LinearTransform::new(t, a)?))
357        }
358        b"hyperlog" => {
359            let t = parse_required_f64_attr(&attrs, "T", &[NS_TRANSFORMS, ""], "hyperlog")?;
360            let w = parse_required_f64_attr(&attrs, "W", &[NS_TRANSFORMS, ""], "hyperlog")?;
361            let m = parse_required_f64_attr(&attrs, "M", &[NS_TRANSFORMS, ""], "hyperlog")?;
362            let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "hyperlog")?;
363            Ok(TransformKind::Hyperlog(HyperlogTransform::new(t, w, m, a)?))
364        }
365        _ => Err(FlowGateError::InvalidGate(format!(
366            "Unsupported transform element '{}'",
367            String::from_utf8_lossy(local)
368        ))),
369    }
370}
371
372fn parse_transform_ref(
373    attrs: &[AttrRecord],
374    transform_map: &HashMap<String, TransformKind>,
375) -> Result<Option<TransformKind>, FlowGateError> {
376    let candidates = [
377        "transformation-ref",
378        "transformation_ref",
379        "transformationRef",
380        "transformation",
381        "transformRef",
382        "transform_ref",
383    ];
384    for name in candidates {
385        if let Some(value) = optional_attr(attrs, name, &[NS_GATING, ""]) {
386            let transform = transform_map.get(value).copied().ok_or_else(|| {
387                FlowGateError::InvalidGate(format!("Unknown transform reference '{value}'"))
388            })?;
389            return Ok(Some(transform));
390        }
391    }
392    Ok(None)
393}
394
395fn attrs_map(
396    reader: &NsReader<&[u8]>,
397    source: &str,
398    start: &BytesStart<'_>,
399) -> Result<Vec<AttrRecord>, FlowGateError> {
400    let mut out = Vec::new();
401    for attr in start.attributes().with_checks(false) {
402        let attr =
403            attr.map_err(|e| xml_err(reader, source, format!("Invalid XML attribute: {e}")))?;
404        let (resolved_ns, local_name) = reader.resolve_attribute(attr.key);
405        let ns_uri = match resolved_ns {
406            ResolveResult::Bound(Namespace(ns)) => Some(String::from_utf8_lossy(ns).to_string()),
407            ResolveResult::Unbound => None,
408            ResolveResult::Unknown(prefix) => {
409                return Err(FlowGateError::XmlParse(format!(
410                    "Unknown XML namespace prefix '{}' in attribute name",
411                    String::from_utf8_lossy(&prefix)
412                )))
413            }
414        };
415        let value = attr
416            .decode_and_unescape_value(reader.decoder())
417            .map_err(|e| xml_err(reader, source, format!("XML attribute decode error: {e}")))?
418            .into_owned();
419        out.push(AttrRecord {
420            ns_uri,
421            local: String::from_utf8_lossy(local_name.as_ref()).to_string(),
422            value,
423        });
424    }
425    Ok(out)
426}
427
428fn required_attr<'a>(
429    attrs: &'a [AttrRecord],
430    local: &str,
431    allowed_ns: &[&str],
432    element: &str,
433) -> Result<&'a str, FlowGateError> {
434    optional_attr(attrs, local, allowed_ns)
435        .ok_or_else(|| FlowGateError::MissingAttribute(local.to_string(), element.to_string()))
436}
437
438fn optional_attr<'a>(attrs: &'a [AttrRecord], local: &str, allowed_ns: &[&str]) -> Option<&'a str> {
439    for ns in allowed_ns {
440        let ns_opt = if ns.is_empty() { None } else { Some(*ns) };
441        if let Some(hit) = attrs
442            .iter()
443            .find(|a| a.local == local && a.ns_uri.as_deref() == ns_opt)
444        {
445            return Some(hit.value.as_str());
446        }
447    }
448    None
449}
450
451fn parse_required_f64_attr(
452    attrs: &[AttrRecord],
453    local: &str,
454    allowed_ns: &[&str],
455    element: &str,
456) -> Result<f64, FlowGateError> {
457    let raw = required_attr(attrs, local, allowed_ns, element)?;
458    raw.parse::<f64>()
459        .map_err(|_| FlowGateError::InvalidFloat(raw.to_string(), local.to_string()))
460}
461
462fn parse_optional_f64_attr(
463    attrs: &[AttrRecord],
464    local: &str,
465    allowed_ns: &[&str],
466    element: &str,
467) -> Result<Option<f64>, FlowGateError> {
468    match optional_attr(attrs, local, allowed_ns) {
469        Some(raw) if raw.trim().is_empty() => Ok(None),
470        Some(raw) => raw.parse::<f64>().map(Some).map_err(|_| {
471            FlowGateError::InvalidFloat(raw.to_string(), format!("{element}:{local}"))
472        }),
473        None => Ok(None),
474    }
475}
476
477fn skip_element(
478    reader: &mut NsReader<&[u8]>,
479    source: &str,
480    start: &BytesStart<'_>,
481) -> Result<(), FlowGateError> {
482    let mut buf = Vec::new();
483    reader
484        .read_to_end_into(start.name(), &mut buf)
485        .map_err(|e| xml_err(reader, source, format!("XML skip error: {e}")))?;
486    Ok(())
487}
488
489fn element_is(
490    resolved_ns: &ResolveResult<'_>,
491    local: &[u8],
492    expected_ns: &str,
493    expected_local: &[u8],
494) -> bool {
495    ns_is(resolved_ns, expected_ns) && local == expected_local
496}
497
498fn ns_is(resolved_ns: &ResolveResult<'_>, expected_ns: &str) -> bool {
499    matches!(resolved_ns, ResolveResult::Bound(Namespace(ns)) if *ns == expected_ns.as_bytes())
500}
501
502fn xml_err(reader: &NsReader<&[u8]>, source: &str, message: impl AsRef<str>) -> FlowGateError {
503    let pos = reader.error_position() as usize;
504    let (line, col) = byte_pos_to_line_col(source, pos);
505    FlowGateError::XmlParse(format!(
506        "{} (line {}, column {})",
507        message.as_ref(),
508        line,
509        col
510    ))
511}
512
513fn byte_pos_to_line_col(source: &str, pos: usize) -> (usize, usize) {
514    let mut line = 1usize;
515    let mut col = 1usize;
516    for &b in source.as_bytes().iter().take(pos.min(source.len())) {
517        if b == b'\n' {
518            line += 1;
519            col = 1;
520        } else {
521            col += 1;
522        }
523    }
524    (line, col)
525}
526
527#[derive(Debug, Clone)]
528struct AttrRecord {
529    ns_uri: Option<String>,
530    local: String,
531    value: String,
532}
533
534fn is_gate_element(local: &[u8]) -> bool {
535    matches!(
536        local,
537        b"RectangleGate" | b"PolygonGate" | b"EllipsoidGate" | b"BooleanGate" | b"QuadrantGate"
538    )
539}
540
541fn parse_dimension_selector(
542    reader: &NsReader<&[u8]>,
543    source: &str,
544    start: &BytesStart<'_>,
545) -> Result<DimensionSelector, FlowGateError> {
546    let local = start.local_name();
547    let local = local.as_ref();
548    if !matches!(local, b"parameter" | b"fcs-dimension" | b"new-dimension") {
549        return Err(xml_err(
550            reader,
551            source,
552            format!(
553                "Unsupported dimension element '{}'",
554                String::from_utf8_lossy(local)
555            ),
556        ));
557    }
558
559    let attrs = attrs_map(reader, source, start)?;
560    if local == b"new-dimension" {
561        let ratio_id = required_attr(
562            &attrs,
563            "transformation-ref",
564            &[NS_DATATYPE, ""],
565            "new-dimension",
566        )?;
567        Ok(DimensionSelector::New(ratio_id.to_string()))
568    } else {
569        let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
570        Ok(DimensionSelector::Fcs(name.to_string()))
571    }
572}
573
574fn dimension_selector_to_parameter(
575    compensation_ref: &str,
576    selector: DimensionSelector,
577) -> ParameterName {
578    match selector {
579        DimensionSelector::Fcs(name) => make_fcs_binding_name(compensation_ref, &name),
580        DimensionSelector::New(ratio_id) => make_ratio_binding_name(compensation_ref, &ratio_id),
581    }
582}
583
584fn parse_dimension_ref(
585    reader: &mut NsReader<&[u8]>,
586    source: &str,
587    attrs: &[AttrRecord],
588    context: &str,
589    end_ns: &str,
590    end_local: &[u8],
591) -> Result<ParameterName, FlowGateError> {
592    let compensation_ref = optional_attr(attrs, "compensation-ref", &[NS_GATING, ""])
593        .unwrap_or("uncompensated")
594        .to_string();
595
596    let mut selector: Option<DimensionSelector> = None;
597    let mut buf = Vec::new();
598
599    loop {
600        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
601            Ok(v) => v,
602            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
603        };
604        match event {
605            Event::Start(e) => {
606                if ns_is(&ns, NS_DATATYPE)
607                    && matches!(
608                        e.local_name().as_ref(),
609                        b"parameter" | b"fcs-dimension" | b"new-dimension"
610                    )
611                {
612                    if selector.is_some() {
613                        return Err(FlowGateError::InvalidGate(format!(
614                            "{context} contains multiple dimension selectors"
615                        )));
616                    }
617                    selector = Some(parse_dimension_selector(reader, source, &e)?);
618                    skip_element(reader, source, &e)?;
619                } else {
620                    skip_element(reader, source, &e)?;
621                }
622            }
623            Event::Empty(e) => {
624                if ns_is(&ns, NS_DATATYPE)
625                    && matches!(
626                        e.local_name().as_ref(),
627                        b"parameter" | b"fcs-dimension" | b"new-dimension"
628                    )
629                {
630                    if selector.is_some() {
631                        return Err(FlowGateError::InvalidGate(format!(
632                            "{context} contains multiple dimension selectors"
633                        )));
634                    }
635                    selector = Some(parse_dimension_selector(reader, source, &e)?);
636                }
637            }
638            Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
639                break;
640            }
641            Event::Eof => {
642                return Err(xml_err(
643                    reader,
644                    source,
645                    format!("Unexpected EOF while reading {context}"),
646                ));
647            }
648            _ => {}
649        }
650        buf.clear();
651    }
652
653    let selector = selector.ok_or_else(|| {
654        FlowGateError::InvalidGate(format!("{context} is missing fcs-dimension/new-dimension"))
655    })?;
656    Ok(dimension_selector_to_parameter(&compensation_ref, selector))
657}
658
659fn parse_value_content(reader: &mut NsReader<&[u8]>, source: &str) -> Result<f64, FlowGateError> {
660    let mut value: Option<f64> = None;
661    let mut buf = Vec::new();
662
663    loop {
664        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
665            Ok(v) => v,
666            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
667        };
668        match event {
669            Event::Start(e) => {
670                let attrs = attrs_map(reader, source, &e)?;
671                if let Some(v) = parse_optional_f64_attr(
672                    &attrs,
673                    "value",
674                    &[NS_DATATYPE, "", NS_GATING, NS_TRANSFORMS],
675                    "value",
676                )? {
677                    value = Some(v);
678                }
679                skip_element(reader, source, &e)?;
680            }
681            Event::Empty(e) => {
682                let attrs = attrs_map(reader, source, &e)?;
683                if let Some(v) = parse_optional_f64_attr(
684                    &attrs,
685                    "value",
686                    &[NS_DATATYPE, "", NS_GATING, NS_TRANSFORMS],
687                    "value",
688                )? {
689                    value = Some(v);
690                }
691            }
692            Event::Text(t) => {
693                let text = t
694                    .unescape()
695                    .map_err(|e| xml_err(reader, source, format!("XML text decode error: {e}")))?;
696                let trimmed = text.trim();
697                if !trimmed.is_empty() {
698                    let parsed = trimmed.parse::<f64>().map_err(|_| {
699                        FlowGateError::InvalidFloat(trimmed.to_string(), "value".to_string())
700                    })?;
701                    value = Some(parsed);
702                }
703            }
704            Event::End(e)
705                if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"value") =>
706            {
707                break;
708            }
709            Event::Eof => {
710                return Err(xml_err(
711                    reader,
712                    source,
713                    "Unexpected EOF while reading <value>",
714                ));
715            }
716            _ => {}
717        }
718        buf.clear();
719    }
720
721    value.ok_or_else(|| FlowGateError::InvalidGate("Missing numeric value in <value>".to_string()))
722}
723
724fn parse_fcs_dimension_list(
725    reader: &mut NsReader<&[u8]>,
726    source: &str,
727    end_ns: &str,
728    end_local: &[u8],
729) -> Result<Vec<ParameterName>, FlowGateError> {
730    let mut out = Vec::new();
731    let mut buf = Vec::new();
732    loop {
733        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
734            Ok(v) => v,
735            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
736        };
737        match event {
738            Event::Start(e) => {
739                if ns_is(&ns, NS_DATATYPE)
740                    && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
741                {
742                    let attrs = attrs_map(reader, source, &e)?;
743                    let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
744                    out.push(ParameterName::from(name.to_string()));
745                    skip_element(reader, source, &e)?;
746                } else {
747                    skip_element(reader, source, &e)?;
748                }
749            }
750            Event::Empty(e) => {
751                if ns_is(&ns, NS_DATATYPE)
752                    && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
753                {
754                    let attrs = attrs_map(reader, source, &e)?;
755                    let name = required_attr(&attrs, "name", &[NS_DATATYPE, ""], "fcs-dimension")?;
756                    out.push(ParameterName::from(name.to_string()));
757                }
758            }
759            Event::End(e) if element_is(&ns, e.name().local_name().as_ref(), end_ns, end_local) => {
760                break;
761            }
762            Event::Eof => {
763                return Err(xml_err(
764                    reader,
765                    source,
766                    "Unexpected EOF while reading fcs-dimension list",
767                ));
768            }
769            _ => {}
770        }
771        buf.clear();
772    }
773    Ok(out)
774}
775
776fn parse_spectrum_row(
777    reader: &mut NsReader<&[u8]>,
778    source: &str,
779) -> Result<Vec<f64>, FlowGateError> {
780    let mut values = Vec::new();
781    let mut buf = Vec::new();
782    loop {
783        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
784            Ok(v) => v,
785            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
786        };
787        match event {
788            Event::Start(e) => {
789                if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"coefficient") {
790                    let attrs = attrs_map(reader, source, &e)?;
791                    values.push(parse_required_f64_attr(
792                        &attrs,
793                        "value",
794                        &[NS_TRANSFORMS, "", NS_DATATYPE],
795                        "coefficient",
796                    )?);
797                    skip_element(reader, source, &e)?;
798                } else {
799                    skip_element(reader, source, &e)?;
800                }
801            }
802            Event::Empty(e) => {
803                if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"coefficient") {
804                    let attrs = attrs_map(reader, source, &e)?;
805                    values.push(parse_required_f64_attr(
806                        &attrs,
807                        "value",
808                        &[NS_TRANSFORMS, "", NS_DATATYPE],
809                        "coefficient",
810                    )?);
811                }
812            }
813            Event::End(e)
814                if element_is(
815                    &ns,
816                    e.name().local_name().as_ref(),
817                    NS_TRANSFORMS,
818                    b"spectrum",
819                ) =>
820            {
821                break;
822            }
823            Event::Eof => {
824                return Err(xml_err(
825                    reader,
826                    source,
827                    "Unexpected EOF while reading <spectrum>",
828                ));
829            }
830            _ => {}
831        }
832        buf.clear();
833    }
834    Ok(values)
835}
836
837fn parse_quadrant_definition(
838    reader: &mut NsReader<&[u8]>,
839    source: &str,
840    start: &BytesStart<'_>,
841) -> Result<(GateId, Vec<(String, f64)>), FlowGateError> {
842    let attrs = attrs_map(reader, source, start)?;
843    let id = GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], "Quadrant")?.to_string());
844    let mut positions: Vec<(String, f64)> = Vec::new();
845    let mut buf = Vec::new();
846    loop {
847        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
848            Ok(v) => v,
849            Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
850        };
851        match event {
852            Event::Start(e) => {
853                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"position") {
854                    let pos_attrs = attrs_map(reader, source, &e)?;
855                    let divider_ref =
856                        required_attr(&pos_attrs, "divider_ref", &[NS_GATING, ""], "position")?
857                            .to_string();
858                    let location = parse_required_f64_attr(
859                        &pos_attrs,
860                        "location",
861                        &[NS_GATING, ""],
862                        "position",
863                    )?;
864                    positions.push((divider_ref, location));
865                    skip_element(reader, source, &e)?;
866                } else {
867                    skip_element(reader, source, &e)?;
868                }
869            }
870            Event::Empty(e) => {
871                if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"position") {
872                    let pos_attrs = attrs_map(reader, source, &e)?;
873                    let divider_ref =
874                        required_attr(&pos_attrs, "divider_ref", &[NS_GATING, ""], "position")?
875                            .to_string();
876                    let location = parse_required_f64_attr(
877                        &pos_attrs,
878                        "location",
879                        &[NS_GATING, ""],
880                        "position",
881                    )?;
882                    positions.push((divider_ref, location));
883                }
884            }
885            Event::End(e)
886                if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"Quadrant") =>
887            {
888                break;
889            }
890            Event::Eof => {
891                return Err(xml_err(
892                    reader,
893                    source,
894                    "Unexpected EOF while reading <Quadrant>",
895                ));
896            }
897            _ => {}
898        }
899        buf.clear();
900    }
901
902    if positions.is_empty() {
903        return Err(FlowGateError::InvalidGate(format!(
904            "Quadrant '{}' does not define any positions",
905            id
906        )));
907    }
908
909    Ok((id, positions))
910}
911
912fn interval_for_location(
913    values: &[f64],
914    location: f64,
915) -> Result<(Option<f64>, Option<f64>), FlowGateError> {
916    if !location.is_finite() {
917        return Err(FlowGateError::InvalidGate(
918            "Quadrant position location must be finite".to_string(),
919        ));
920    }
921    if values.is_empty() {
922        return Err(FlowGateError::InvalidGate(
923            "Quadrant divider must define at least one value".to_string(),
924        ));
925    }
926
927    let mut sorted: Vec<f64> = values.to_vec();
928    if sorted.iter().any(|v| !v.is_finite()) {
929        return Err(FlowGateError::InvalidGate(
930            "Quadrant divider values must be finite".to_string(),
931        ));
932    }
933    sorted.sort_by(|a, b| a.total_cmp(b));
934    sorted.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
935
936    if location < sorted[0] {
937        return Ok((None, Some(sorted[0])));
938    }
939    for pair in sorted.windows(2) {
940        let lo = pair[0];
941        let hi = pair[1];
942        if location >= lo && location < hi {
943            return Ok((Some(lo), Some(hi)));
944        }
945    }
946    Ok((
947        Some(*sorted.last().ok_or_else(|| {
948            FlowGateError::InvalidGate(
949                "interval_for_location produced no valid values after filtering".to_string(),
950            )
951        })?),
952        None,
953    ))
954}
955
956pub fn parse_document(xml: &str) -> Result<FlowGateDocument, FlowGateError> {
957    let mut reader = NsReader::from_str(xml);
958    reader.config_mut().trim_text(true);
959    reader.config_mut().expand_empty_elements = true;
960
961    let mut parser = FlowGateParser::default();
962    let mut buf = Vec::new();
963
964    loop {
965        let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
966            Ok(v) => v,
967            Err(e) => return Err(xml_err(&reader, xml, format!("XML read error: {e}"))),
968        };
969        match event {
970            Event::Start(e) => {
971                let local_name = e.local_name();
972                let local = local_name.as_ref();
973                if element_is(&ns, local, NS_TRANSFORMS, b"transformation") {
974                    parser.parse_transformation_block(&mut reader, xml, &e)?;
975                } else if element_is(&ns, local, NS_TRANSFORMS, b"spectrumMatrix") {
976                    parser.parse_spectrum_matrix_block(&mut reader, xml, &e)?;
977                } else if element_is(&ns, local, NS_GATING, b"Gate") {
978                    parser.parse_gate_block(&mut reader, xml, &e)?;
979                } else if ns_is(&ns, NS_GATING) && is_gate_element(local) {
980                    parser.parse_standalone_gate(&mut reader, xml, &e)?;
981                } else if element_is(&ns, local, NS_GATING, b"GatingML")
982                    || element_is(&ns, local, NS_GATING, b"Gating-ML")
983                {
984                    // Root container; continue scanning nested elements.
985                } else {
986                    skip_element(&mut reader, xml, &e)?;
987                }
988            }
989            Event::Empty(_) => {}
990            Event::Eof => break,
991            _ => {}
992        }
993        buf.clear();
994    }
995
996    let gate_registry = GateRegistry::new(parser.gates)?;
997    Ok(FlowGateDocument {
998        transforms: parser.transforms,
999        ratio_transforms: parser.ratio_transforms,
1000        spectrum_matrices: parser.spectrum_matrices,
1001        gate_registry,
1002        source_xml: Some(xml.to_string()),
1003    })
1004}
1005
1006impl FlowGateParser {
1007    fn parse_transformation_block(
1008        &mut self,
1009        reader: &mut NsReader<&[u8]>,
1010        source: &str,
1011        start: &BytesStart<'_>,
1012    ) -> Result<(), FlowGateError> {
1013        let attrs = attrs_map(reader, source, start)?;
1014        let id = required_attr(&attrs, "id", &[NS_TRANSFORMS, ""], "transformation")?.to_string();
1015        let mut selected_scale: Option<TransformKind> = None;
1016        let mut selected_ratio: Option<RatioTransformSpec> = None;
1017
1018        let mut buf = Vec::new();
1019        loop {
1020            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1021                Ok(v) => v,
1022                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1023            };
1024            match event {
1025                Event::Start(e) if ns_is(&ns, NS_TRANSFORMS) => {
1026                    if e.local_name().as_ref() == b"fratio" {
1027                        if selected_scale.is_some() || selected_ratio.is_some() {
1028                            return Err(FlowGateError::InvalidGate(format!(
1029                                "Transformation '{id}' contains multiple payload elements"
1030                            )));
1031                        }
1032                        selected_ratio = Some(self.parse_ratio_transform(reader, source, &e, &id)?);
1033                    } else {
1034                        if selected_scale.is_some() || selected_ratio.is_some() {
1035                            return Err(FlowGateError::InvalidGate(format!(
1036                                "Transformation '{id}' contains multiple payload elements"
1037                            )));
1038                        }
1039                        selected_scale = Some(parse_transform_kind(
1040                            reader,
1041                            source,
1042                            e.local_name().as_ref(),
1043                            &e,
1044                        )?);
1045                        skip_element(reader, source, &e)?;
1046                    }
1047                }
1048                Event::Empty(e) => {
1049                    if ns_is(&ns, NS_TRANSFORMS) {
1050                        if e.local_name().as_ref() == b"fratio" {
1051                            return Err(FlowGateError::InvalidGate(
1052                                "fratio requires two fcs-dimension sub-elements".to_string(),
1053                            ));
1054                        }
1055                        if selected_scale.is_some() || selected_ratio.is_some() {
1056                            return Err(FlowGateError::InvalidGate(format!(
1057                                "Transformation '{id}' contains multiple payload elements"
1058                            )));
1059                        }
1060                        selected_scale = Some(parse_transform_kind(
1061                            reader,
1062                            source,
1063                            e.local_name().as_ref(),
1064                            &e,
1065                        )?);
1066                    }
1067                }
1068                Event::End(e)
1069                    if ns_is(&ns, NS_TRANSFORMS)
1070                        && e.name().local_name().as_ref() == b"transformation" =>
1071                {
1072                    break;
1073                }
1074                Event::Eof => {
1075                    return Err(xml_err(
1076                        reader,
1077                        source,
1078                        "Unexpected EOF while reading <transformation>",
1079                    ));
1080                }
1081                Event::Start(e) => {
1082                    skip_element(reader, source, &e)?;
1083                }
1084                _ => {}
1085            }
1086            buf.clear();
1087        }
1088
1089        match (selected_scale, selected_ratio) {
1090            (Some(scale), None) => {
1091                self.transforms.insert(id, scale);
1092                Ok(())
1093            }
1094            (None, Some(ratio)) => {
1095                self.ratio_transforms.insert(id, ratio);
1096                Ok(())
1097            }
1098            (None, None) => Err(xml_err(
1099                reader,
1100                source,
1101                "Transformation block does not contain a supported transform element",
1102            )),
1103            (Some(_), Some(_)) => Err(FlowGateError::InvalidGate(
1104                "Transformation cannot contain both scale and ratio payloads".to_string(),
1105            )),
1106        }
1107    }
1108
1109    fn parse_ratio_transform(
1110        &self,
1111        reader: &mut NsReader<&[u8]>,
1112        source: &str,
1113        start: &BytesStart<'_>,
1114        id: &str,
1115    ) -> Result<RatioTransformSpec, FlowGateError> {
1116        let attrs = attrs_map(reader, source, start)?;
1117        let a = parse_required_f64_attr(&attrs, "A", &[NS_TRANSFORMS, ""], "fratio")?;
1118        let b = parse_required_f64_attr(&attrs, "B", &[NS_TRANSFORMS, ""], "fratio")?;
1119        let c = parse_required_f64_attr(&attrs, "C", &[NS_TRANSFORMS, ""], "fratio")?;
1120        let mut dims: Vec<ParameterName> = Vec::new();
1121
1122        let mut buf = Vec::new();
1123        loop {
1124            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1125                Ok(v) => v,
1126                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1127            };
1128            match event {
1129                Event::Start(e) => {
1130                    if ns_is(&ns, NS_DATATYPE)
1131                        && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
1132                    {
1133                        let param_attrs = attrs_map(reader, source, &e)?;
1134                        let name = required_attr(
1135                            &param_attrs,
1136                            "name",
1137                            &[NS_DATATYPE, ""],
1138                            "fcs-dimension",
1139                        )?;
1140                        dims.push(ParameterName::from(name.to_string()));
1141                        skip_element(reader, source, &e)?;
1142                    } else {
1143                        skip_element(reader, source, &e)?;
1144                    }
1145                }
1146                Event::Empty(e) => {
1147                    if ns_is(&ns, NS_DATATYPE)
1148                        && matches!(e.local_name().as_ref(), b"fcs-dimension" | b"parameter")
1149                    {
1150                        let param_attrs = attrs_map(reader, source, &e)?;
1151                        let name = required_attr(
1152                            &param_attrs,
1153                            "name",
1154                            &[NS_DATATYPE, ""],
1155                            "fcs-dimension",
1156                        )?;
1157                        dims.push(ParameterName::from(name.to_string()));
1158                    }
1159                }
1160                Event::End(e)
1161                    if ns_is(&ns, NS_TRANSFORMS) && e.name().local_name().as_ref() == b"fratio" =>
1162                {
1163                    break;
1164                }
1165                Event::Eof => {
1166                    return Err(xml_err(
1167                        reader,
1168                        source,
1169                        "Unexpected EOF while reading fratio transformation",
1170                    ));
1171                }
1172                _ => {}
1173            }
1174            buf.clear();
1175        }
1176
1177        if dims.len() != 2 {
1178            return Err(FlowGateError::InvalidGate(format!(
1179                "fratio transformation '{id}' requires exactly 2 dimensions, found {}",
1180                dims.len()
1181            )));
1182        }
1183
1184        Ok(RatioTransformSpec {
1185            id: id.to_string(),
1186            numerator: dims.remove(0),
1187            denominator: dims.remove(0),
1188            a,
1189            b,
1190            c,
1191        })
1192    }
1193
1194    fn parse_spectrum_matrix_block(
1195        &mut self,
1196        reader: &mut NsReader<&[u8]>,
1197        source: &str,
1198        start: &BytesStart<'_>,
1199    ) -> Result<(), FlowGateError> {
1200        let attrs = attrs_map(reader, source, start)?;
1201        let id = required_attr(&attrs, "id", &[NS_TRANSFORMS, ""], "spectrumMatrix")?.to_string();
1202        let matrix_inverted_already = parse_bool_attr(
1203            optional_attr(&attrs, "matrix-inverted-already", &[NS_TRANSFORMS, ""]),
1204            false,
1205        );
1206        let mut fluorochromes: Vec<ParameterName> = Vec::new();
1207        let mut detectors: Vec<ParameterName> = Vec::new();
1208        let mut rows: Vec<Vec<f64>> = Vec::new();
1209
1210        let mut buf = Vec::new();
1211        loop {
1212            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1213                Ok(v) => v,
1214                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1215            };
1216            match event {
1217                Event::Start(e) => {
1218                    if element_is(
1219                        &ns,
1220                        e.local_name().as_ref(),
1221                        NS_TRANSFORMS,
1222                        b"fluorochromes",
1223                    ) {
1224                        fluorochromes = parse_fcs_dimension_list(
1225                            reader,
1226                            source,
1227                            NS_TRANSFORMS,
1228                            b"fluorochromes",
1229                        )?;
1230                    } else if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"detectors")
1231                    {
1232                        detectors =
1233                            parse_fcs_dimension_list(reader, source, NS_TRANSFORMS, b"detectors")?;
1234                    } else if element_is(&ns, e.local_name().as_ref(), NS_TRANSFORMS, b"spectrum") {
1235                        rows.push(parse_spectrum_row(reader, source)?);
1236                    } else {
1237                        skip_element(reader, source, &e)?;
1238                    }
1239                }
1240                Event::End(e)
1241                    if element_is(
1242                        &ns,
1243                        e.name().local_name().as_ref(),
1244                        NS_TRANSFORMS,
1245                        b"spectrumMatrix",
1246                    ) =>
1247                {
1248                    break;
1249                }
1250                Event::Eof => {
1251                    return Err(xml_err(
1252                        reader,
1253                        source,
1254                        "Unexpected EOF while reading spectrumMatrix",
1255                    ));
1256                }
1257                _ => {}
1258            }
1259            buf.clear();
1260        }
1261
1262        if fluorochromes.is_empty() || detectors.is_empty() {
1263            return Err(FlowGateError::InvalidGate(format!(
1264                "spectrumMatrix '{id}' must define fluorochromes and detectors"
1265            )));
1266        }
1267        if rows.len() != fluorochromes.len() {
1268            return Err(FlowGateError::InvalidGate(format!(
1269                "spectrumMatrix '{id}' has {} rows but {} fluorochromes",
1270                rows.len(),
1271                fluorochromes.len()
1272            )));
1273        }
1274        for row in &rows {
1275            if row.len() != detectors.len() {
1276                return Err(FlowGateError::InvalidGate(format!(
1277                    "spectrumMatrix '{id}' row has {} coefficients but {} detectors",
1278                    row.len(),
1279                    detectors.len()
1280                )));
1281            }
1282        }
1283
1284        let mut coefficients = Vec::with_capacity(fluorochromes.len() * detectors.len());
1285        for row in rows {
1286            coefficients.extend(row);
1287        }
1288
1289        self.spectrum_matrices.insert(
1290            id.clone(),
1291            SpectrumMatrixSpec {
1292                id,
1293                fluorochromes,
1294                detectors,
1295                coefficients,
1296                matrix_inverted_already,
1297            },
1298        );
1299        Ok(())
1300    }
1301
1302    fn parse_gate_block(
1303        &mut self,
1304        reader: &mut NsReader<&[u8]>,
1305        source: &str,
1306        start: &BytesStart<'_>,
1307    ) -> Result<(), FlowGateError> {
1308        let attrs = attrs_map(reader, source, start)?;
1309        let id = GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], "Gate")?.to_string());
1310        let parent_id = optional_attr(&attrs, "parent_id", &[NS_GATING, ""])
1311            .map(|s| GateId::from(s.to_string()));
1312        let mut parsed_gate: Option<GateKind> = None;
1313
1314        let mut buf = Vec::new();
1315        loop {
1316            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1317                Ok(v) => v,
1318                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1319            };
1320            match event {
1321                Event::Start(e) => {
1322                    if ns_is(&ns, NS_GATING) {
1323                        parsed_gate = Some(match e.local_name().as_ref() {
1324                            b"RectangleGate" => {
1325                                GateKind::Rectangle(Box::new(self.parse_rectangle_gate(
1326                                    reader,
1327                                    source,
1328                                    id.clone(),
1329                                    parent_id.clone(),
1330                                )?))
1331                            }
1332                            b"PolygonGate" => GateKind::Polygon(self.parse_polygon_gate(
1333                                reader,
1334                                source,
1335                                id.clone(),
1336                                parent_id.clone(),
1337                            )?),
1338                            b"EllipsoidGate" => GateKind::Ellipsoid(self.parse_ellipsoid_gate(
1339                                reader,
1340                                source,
1341                                id.clone(),
1342                                parent_id.clone(),
1343                            )?),
1344                            b"BooleanGate" => GateKind::Boolean(self.parse_boolean_gate(
1345                                reader,
1346                                source,
1347                                id.clone(),
1348                                parent_id.clone(),
1349                            )?),
1350                            _ => {
1351                                skip_element(reader, source, &e)?;
1352                                continue;
1353                            }
1354                        });
1355                    } else {
1356                        skip_element(reader, source, &e)?;
1357                    }
1358                }
1359                Event::End(e)
1360                    if ns_is(&ns, NS_GATING) && e.name().local_name().as_ref() == b"Gate" =>
1361                {
1362                    break;
1363                }
1364                Event::Eof => {
1365                    return Err(xml_err(
1366                        reader,
1367                        source,
1368                        "Unexpected EOF while reading <Gate>",
1369                    ));
1370                }
1371                _ => {}
1372            }
1373            buf.clear();
1374        }
1375
1376        let gate = parsed_gate.ok_or_else(|| {
1377            FlowGateError::InvalidGate("Gate block does not contain a supported gate".to_string())
1378        })?;
1379        self.gates.insert(id, gate);
1380        Ok(())
1381    }
1382
1383    fn parse_standalone_gate(
1384        &mut self,
1385        reader: &mut NsReader<&[u8]>,
1386        source: &str,
1387        start: &BytesStart<'_>,
1388    ) -> Result<(), FlowGateError> {
1389        let attrs = attrs_map(reader, source, start)?;
1390        let gate_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string();
1391        let id =
1392            GateId::from(required_attr(&attrs, "id", &[NS_GATING, ""], &gate_name)?.to_string());
1393        let parent_id = optional_attr(&attrs, "parent_id", &[NS_GATING, ""])
1394            .map(|s| GateId::from(s.to_string()));
1395
1396        match start.local_name().as_ref() {
1397            b"RectangleGate" => {
1398                let gate = self.parse_rectangle_gate(reader, source, id.clone(), parent_id)?;
1399                self.gates.insert(id, GateKind::Rectangle(Box::new(gate)));
1400            }
1401            b"PolygonGate" => {
1402                let gate = self.parse_polygon_gate(reader, source, id.clone(), parent_id)?;
1403                self.gates.insert(id, GateKind::Polygon(gate));
1404            }
1405            b"EllipsoidGate" => {
1406                let gate = self.parse_ellipsoid_gate(reader, source, id.clone(), parent_id)?;
1407                self.gates.insert(id, GateKind::Ellipsoid(gate));
1408            }
1409            b"BooleanGate" => {
1410                let gate = self.parse_boolean_gate(reader, source, id.clone(), parent_id)?;
1411                self.gates.insert(id, GateKind::Boolean(gate));
1412            }
1413            b"QuadrantGate" => {
1414                for (qid, gate) in self.parse_quadrant_gate(reader, source, parent_id)? {
1415                    self.gates.insert(qid, gate);
1416                }
1417            }
1418            _ => {
1419                skip_element(reader, source, start)?;
1420            }
1421        }
1422
1423        Ok(())
1424    }
1425
1426    fn parse_quadrant_gate(
1427        &self,
1428        reader: &mut NsReader<&[u8]>,
1429        source: &str,
1430        parent_id: Option<GateId>,
1431    ) -> Result<Vec<(GateId, GateKind)>, FlowGateError> {
1432        let mut dividers: HashMap<String, RectangleDimension> = HashMap::new();
1433        let mut divider_values: HashMap<String, Vec<f64>> = HashMap::new();
1434        let mut quadrants: Vec<(GateId, Vec<(String, f64)>)> = Vec::new();
1435        let mut buf = Vec::new();
1436
1437        loop {
1438            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1439                Ok(v) => v,
1440                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1441            };
1442            match event {
1443                Event::Start(e) => {
1444                    if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"divider") {
1445                        let (divider_id, dim, values) =
1446                            self.parse_quadrant_divider(reader, source, &e)?;
1447                        dividers.insert(divider_id.clone(), dim);
1448                        divider_values.insert(divider_id, values);
1449                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"Quadrant") {
1450                        quadrants.push(parse_quadrant_definition(reader, source, &e)?);
1451                    } else {
1452                        skip_element(reader, source, &e)?;
1453                    }
1454                }
1455                Event::End(e)
1456                    if element_is(
1457                        &ns,
1458                        e.name().local_name().as_ref(),
1459                        NS_GATING,
1460                        b"QuadrantGate",
1461                    ) =>
1462                {
1463                    break;
1464                }
1465                Event::Eof => {
1466                    return Err(xml_err(
1467                        reader,
1468                        source,
1469                        "Unexpected EOF while reading QuadrantGate",
1470                    ));
1471                }
1472                _ => {}
1473            }
1474            buf.clear();
1475        }
1476
1477        let mut out = Vec::new();
1478        for (qid, positions) in quadrants {
1479            let mut dims = Vec::with_capacity(positions.len());
1480            for (divider_id, location) in positions {
1481                let base_dim = dividers.get(&divider_id).ok_or_else(|| {
1482                    FlowGateError::InvalidGate(format!(
1483                        "Quadrant '{}' references unknown divider '{}'",
1484                        qid, divider_id
1485                    ))
1486                })?;
1487                let values = divider_values.get(&divider_id).ok_or_else(|| {
1488                    FlowGateError::InvalidGate(format!("Divider '{}' has no values", divider_id))
1489                })?;
1490                let (min, max) = interval_for_location(values, location)?;
1491                dims.push(RectangleDimension {
1492                    parameter: base_dim.parameter.clone(),
1493                    transform: base_dim.transform,
1494                    min,
1495                    max,
1496                });
1497            }
1498            let gate = RectangleGate::new(qid.clone(), parent_id.clone(), dims)?;
1499            out.push((qid, GateKind::Rectangle(Box::new(gate))));
1500        }
1501
1502        Ok(out)
1503    }
1504
1505    fn parse_quadrant_divider(
1506        &self,
1507        reader: &mut NsReader<&[u8]>,
1508        source: &str,
1509        start: &BytesStart<'_>,
1510    ) -> Result<(String, RectangleDimension, Vec<f64>), FlowGateError> {
1511        let attrs = attrs_map(reader, source, start)?;
1512        let divider_id = required_attr(&attrs, "id", &[NS_GATING, ""], "divider")?.to_string();
1513        let transform = parse_transform_ref(&attrs, &self.transforms)?;
1514        let compensation_ref = optional_attr(&attrs, "compensation-ref", &[NS_GATING, ""])
1515            .unwrap_or("uncompensated")
1516            .to_string();
1517        let mut selector: Option<DimensionSelector> = None;
1518        let mut values = Vec::<f64>::new();
1519        let mut buf = Vec::new();
1520        loop {
1521            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1522                Ok(v) => v,
1523                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1524            };
1525            match event {
1526                Event::Start(e) => {
1527                    if ns_is(&ns, NS_DATATYPE)
1528                        && matches!(
1529                            e.local_name().as_ref(),
1530                            b"parameter" | b"fcs-dimension" | b"new-dimension"
1531                        )
1532                    {
1533                        selector = Some(parse_dimension_selector(reader, source, &e)?);
1534                        skip_element(reader, source, &e)?;
1535                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"value") {
1536                        values.push(parse_value_content(reader, source)?);
1537                    } else {
1538                        skip_element(reader, source, &e)?;
1539                    }
1540                }
1541                Event::Empty(e) => {
1542                    if ns_is(&ns, NS_DATATYPE)
1543                        && matches!(
1544                            e.local_name().as_ref(),
1545                            b"parameter" | b"fcs-dimension" | b"new-dimension"
1546                        )
1547                    {
1548                        selector = Some(parse_dimension_selector(reader, source, &e)?);
1549                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"value") {
1550                        let value_attrs = attrs_map(reader, source, &e)?;
1551                        values.push(parse_required_f64_attr(
1552                            &value_attrs,
1553                            "value",
1554                            &[NS_DATATYPE, "", NS_GATING],
1555                            "value",
1556                        )?);
1557                    }
1558                }
1559                Event::End(e)
1560                    if element_is(&ns, e.name().local_name().as_ref(), NS_GATING, b"divider") =>
1561                {
1562                    break;
1563                }
1564                Event::Eof => {
1565                    return Err(xml_err(
1566                        reader,
1567                        source,
1568                        "Unexpected EOF while reading divider",
1569                    ));
1570                }
1571                _ => {}
1572            }
1573            buf.clear();
1574        }
1575        let selector = selector.ok_or_else(|| {
1576            FlowGateError::InvalidGate(format!(
1577                "Divider '{divider_id}' is missing dimension reference"
1578            ))
1579        })?;
1580        let parameter = dimension_selector_to_parameter(&compensation_ref, selector);
1581        Ok((
1582            divider_id,
1583            RectangleDimension {
1584                parameter,
1585                transform,
1586                min: None,
1587                max: None,
1588            },
1589            values,
1590        ))
1591    }
1592
1593    fn parse_rectangle_gate(
1594        &self,
1595        reader: &mut NsReader<&[u8]>,
1596        source: &str,
1597        id: GateId,
1598        parent_id: Option<GateId>,
1599    ) -> Result<RectangleGate, FlowGateError> {
1600        let mut dimensions = Vec::new();
1601        let mut buf = Vec::new();
1602        loop {
1603            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1604                Ok(v) => v,
1605                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1606            };
1607            match event {
1608                Event::Start(e) => {
1609                    if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1610                        dimensions.push(self.parse_rectangle_dimension(reader, source, &e)?);
1611                    } else {
1612                        skip_element(reader, source, &e)?;
1613                    }
1614                }
1615                Event::End(e)
1616                    if element_is(
1617                        &ns,
1618                        e.name().local_name().as_ref(),
1619                        NS_GATING,
1620                        b"RectangleGate",
1621                    ) =>
1622                {
1623                    break;
1624                }
1625                Event::Eof => {
1626                    return Err(xml_err(
1627                        reader,
1628                        source,
1629                        "Unexpected EOF while reading RectangleGate",
1630                    ));
1631                }
1632                _ => {}
1633            }
1634            buf.clear();
1635        }
1636        RectangleGate::new(id, parent_id, dimensions)
1637    }
1638
1639    fn parse_rectangle_dimension(
1640        &self,
1641        reader: &mut NsReader<&[u8]>,
1642        source: &str,
1643        start: &BytesStart<'_>,
1644    ) -> Result<RectangleDimension, FlowGateError> {
1645        let attrs = attrs_map(reader, source, start)?;
1646        let min = parse_optional_f64_attr(&attrs, "min", &[NS_GATING, ""], "dimension")?;
1647        let max = parse_optional_f64_attr(&attrs, "max", &[NS_GATING, ""], "dimension")?;
1648        let transform = parse_transform_ref(&attrs, &self.transforms)?;
1649        let parameter = parse_dimension_ref(
1650            reader,
1651            source,
1652            &attrs,
1653            "rectangle dimension",
1654            NS_GATING,
1655            b"dimension",
1656        )?;
1657        Ok(RectangleDimension {
1658            parameter,
1659            transform,
1660            min,
1661            max,
1662        })
1663    }
1664
1665    fn parse_polygon_gate(
1666        &self,
1667        reader: &mut NsReader<&[u8]>,
1668        source: &str,
1669        id: GateId,
1670        parent_id: Option<GateId>,
1671    ) -> Result<PolygonGate, FlowGateError> {
1672        let mut dimensions = Vec::new();
1673        let mut vertices = Vec::new();
1674        let mut buf = Vec::new();
1675
1676        loop {
1677            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1678                Ok(v) => v,
1679                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1680            };
1681            match event {
1682                Event::Start(e) => {
1683                    if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1684                        dimensions.push(self.parse_polygon_dimension(reader, source, &e)?);
1685                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"vertex") {
1686                        vertices.push(parse_vertex(reader, source)?);
1687                    } else {
1688                        skip_element(reader, source, &e)?;
1689                    }
1690                }
1691                Event::End(e)
1692                    if element_is(
1693                        &ns,
1694                        e.name().local_name().as_ref(),
1695                        NS_GATING,
1696                        b"PolygonGate",
1697                    ) =>
1698                {
1699                    break;
1700                }
1701                Event::Eof => {
1702                    return Err(xml_err(
1703                        reader,
1704                        source,
1705                        "Unexpected EOF while reading PolygonGate",
1706                    ));
1707                }
1708                _ => {}
1709            }
1710            buf.clear();
1711        }
1712
1713        if dimensions.len() != 2 {
1714            return Err(FlowGateError::InvalidGate(format!(
1715                "PolygonGate requires exactly 2 dimensions, found {}",
1716                dimensions.len()
1717            )));
1718        }
1719
1720        PolygonGate::new(
1721            id,
1722            parent_id,
1723            dimensions.remove(0),
1724            dimensions.remove(0),
1725            vertices,
1726        )
1727    }
1728
1729    fn parse_polygon_dimension(
1730        &self,
1731        reader: &mut NsReader<&[u8]>,
1732        source: &str,
1733        start: &BytesStart<'_>,
1734    ) -> Result<PolygonDimension, FlowGateError> {
1735        let attrs = attrs_map(reader, source, start)?;
1736        let transform = parse_transform_ref(&attrs, &self.transforms)?;
1737        let parameter = parse_dimension_ref(
1738            reader,
1739            source,
1740            &attrs,
1741            "polygon dimension",
1742            NS_GATING,
1743            b"dimension",
1744        )?;
1745        Ok(PolygonDimension {
1746            parameter,
1747            transform,
1748        })
1749    }
1750
1751    fn parse_ellipsoid_gate(
1752        &self,
1753        reader: &mut NsReader<&[u8]>,
1754        source: &str,
1755        id: GateId,
1756        parent_id: Option<GateId>,
1757    ) -> Result<EllipsoidGate, FlowGateError> {
1758        let mut dimensions = Vec::new();
1759        let mut mean = Vec::new();
1760        let mut covariance_upper = Vec::new();
1761        let mut distance_sq: Option<f64> = None;
1762        let mut buf = Vec::new();
1763
1764        loop {
1765            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1766                Ok(v) => v,
1767                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1768            };
1769            match event {
1770                Event::Start(e) => {
1771                    if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"dimension") {
1772                        dimensions.push(self.parse_ellipsoid_dimension(reader, source, &e)?);
1773                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"mean") {
1774                        mean = parse_coordinates_block(reader, source, NS_GATING, b"mean")?;
1775                    } else if element_is(
1776                        &ns,
1777                        e.local_name().as_ref(),
1778                        NS_GATING,
1779                        b"covarianceMatrix",
1780                    ) {
1781                        covariance_upper = parse_covariance_block(reader, source)?;
1782                    } else if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"distanceSquare")
1783                    {
1784                        let attrs = attrs_map(reader, source, &e)?;
1785                        distance_sq = Some(parse_required_f64_attr(
1786                            &attrs,
1787                            "value",
1788                            &[NS_DATATYPE, ""],
1789                            "distanceSquare",
1790                        )?);
1791                        skip_element(reader, source, &e)?;
1792                    } else {
1793                        skip_element(reader, source, &e)?;
1794                    }
1795                }
1796                Event::Empty(e)
1797                    if element_is(&ns, e.local_name().as_ref(), NS_GATING, b"distanceSquare") =>
1798                {
1799                    let attrs = attrs_map(reader, source, &e)?;
1800                    distance_sq = Some(parse_required_f64_attr(
1801                        &attrs,
1802                        "value",
1803                        &[NS_DATATYPE, ""],
1804                        "distanceSquare",
1805                    )?);
1806                }
1807                Event::End(e)
1808                    if element_is(
1809                        &ns,
1810                        e.name().local_name().as_ref(),
1811                        NS_GATING,
1812                        b"EllipsoidGate",
1813                    ) =>
1814                {
1815                    break;
1816                }
1817                Event::Eof => {
1818                    return Err(xml_err(
1819                        reader,
1820                        source,
1821                        "Unexpected EOF while reading EllipsoidGate",
1822                    ));
1823                }
1824                _ => {}
1825            }
1826            buf.clear();
1827        }
1828
1829        let distance_sq = distance_sq.ok_or_else(|| {
1830            FlowGateError::MissingAttribute("value".to_string(), "distanceSquare".to_string())
1831        })?;
1832        let n = dimensions.len();
1833        if covariance_upper.len() == n * n {
1834            EllipsoidGate::new_general_covariance(
1835                id,
1836                parent_id,
1837                dimensions,
1838                mean,
1839                &covariance_upper,
1840                distance_sq,
1841            )
1842        } else {
1843            EllipsoidGate::new(
1844                id,
1845                parent_id,
1846                dimensions,
1847                mean,
1848                &covariance_upper,
1849                distance_sq,
1850            )
1851        }
1852    }
1853
1854    fn parse_ellipsoid_dimension(
1855        &self,
1856        reader: &mut NsReader<&[u8]>,
1857        source: &str,
1858        start: &BytesStart<'_>,
1859    ) -> Result<EllipsoidDimension, FlowGateError> {
1860        let attrs = attrs_map(reader, source, start)?;
1861        let transform = parse_transform_ref(&attrs, &self.transforms)?;
1862        let parameter = parse_dimension_ref(
1863            reader,
1864            source,
1865            &attrs,
1866            "ellipsoid dimension",
1867            NS_GATING,
1868            b"dimension",
1869        )?;
1870        Ok(EllipsoidDimension {
1871            parameter,
1872            transform,
1873        })
1874    }
1875
1876    fn parse_boolean_gate(
1877        &self,
1878        reader: &mut NsReader<&[u8]>,
1879        source: &str,
1880        current_gate_id: GateId,
1881        parent_id: Option<GateId>,
1882    ) -> Result<BooleanGate, FlowGateError> {
1883        let mut op: Option<BooleanOp> = None;
1884        let mut operands: Vec<BooleanOperand> = Vec::new();
1885        let mut buf = Vec::new();
1886
1887        loop {
1888            let (ns, event) = match reader.read_resolved_event_into(&mut buf) {
1889                Ok(v) => v,
1890                Err(e) => return Err(xml_err(reader, source, format!("XML read error: {e}"))),
1891            };
1892            match event {
1893                Event::Start(e) => {
1894                    if ns_is(&ns, NS_GATING)
1895                        && matches!(e.local_name().as_ref(), b"and" | b"or" | b"not")
1896                    {
1897                        let (parsed_op, parsed_operands) = parse_boolean_op_block(
1898                            reader,
1899                            source,
1900                            e.local_name().as_ref(),
1901                            &current_gate_id,
1902                            &self.gates,
1903                        )?;
1904                        op = Some(parsed_op);
1905                        operands = parsed_operands;
1906                    } else {
1907                        skip_element(reader, source, &e)?;
1908                    }
1909                }
1910                Event::End(e)
1911                    if element_is(
1912                        &ns,
1913                        e.name().local_name().as_ref(),
1914                        NS_GATING,
1915                        b"BooleanGate",
1916                    ) =>
1917                {
1918                    break;
1919                }
1920                Event::Eof => {
1921                    return Err(xml_err(
1922                        reader,
1923                        source,
1924                        "Unexpected EOF while reading BooleanGate",
1925                    ));
1926                }
1927                _ => {}
1928            }
1929            buf.clear();
1930        }
1931
1932        let op = op.ok_or_else(|| {
1933            FlowGateError::InvalidGate("BooleanGate missing required boolean operator".to_string())
1934        })?;
1935        BooleanGate::new(current_gate_id, parent_id, op, operands)
1936    }
1937}