diff --git a/src/api/annotation.rs b/src/api/annotation.rs index 460e3b6..9142f48 100644 --- a/src/api/annotation.rs +++ b/src/api/annotation.rs @@ -243,7 +243,12 @@ impl<'store> ResultItem<'store, Annotation> { textselections.push(tsel); } } - Some(textselections.into_iter().collect()) + //important check because into_iter().collect() [from_iter()] panics if passed an empty iter! + if textselections.is_empty() { + None + } else { + Some(textselections.into_iter().collect()) + } } /// Groups text selections targeting the same resource together in a TextSelectionSet. diff --git a/src/api/textselection.rs b/src/api/textselection.rs index bed1c7d..5ed8b34 100644 --- a/src/api/textselection.rs +++ b/src/api/textselection.rs @@ -1171,7 +1171,7 @@ impl<'store> FromIterator> for ResultTextSelectionSe .collect(); ResultTextSelectionSet { tset, - rootstore: store.expect("Iterator may not be empty"), + rootstore: store.expect("Iterator may not be empty"), //TODO: this is suboptimal, it will panic when an empty iterator is passed! } } } diff --git a/src/api/transpose.rs b/src/api/transpose.rs index c3111db..0562bad 100644 --- a/src/api/transpose.rs +++ b/src/api/transpose.rs @@ -1,3 +1,5 @@ +use std::collections::VecDeque; + use crate::api::*; use crate::datavalue::DataValue; use crate::selector::{Offset, OffsetMode, SelectorBuilder}; @@ -116,21 +118,58 @@ impl<'store> Transposable<'store> for ResultTextSelectionSet<'store> { let mut relative_offsets = Vec::new(); let mut selectors_per_side: SmallVec<[Vec>; 2]> = SmallVec::new(); + let resource = self.resource(); + let mut source_found = false; let mut simple_transposition = true; //falsify + let mut tselbuffer: VecDeque = + self.inner().iter().map(|x| x.clone()).collect(); //MAYBE TODO: slightly waste of time/space if the transposition turns out to be a simple transposition rather than a complex one + // match the textselectionset against the sides in a complex transposition (or ascertain that we are dealing with a simple transposition instead) // the source side that matches can never be the same as the target side that is mappped to - for annotation in via.annotations_in_targets(AnnotationDepth::One) { - simple_transposition = false; - if let Some(refset) = annotation.textselectionset_in(self.resource()) { - //TODO + while let Some(tsel) = tselbuffer.pop_front() { + for (side_i, annotation) in via.annotations_in_targets(AnnotationDepth::One).enumerate() + { + simple_transposition = false; + if selectors_per_side.len() <= side_i { + selectors_per_side.push(Vec::new()); + } + if let Some(refset) = annotation.textselectionset_in(self.resource()) { + // We may have multiple text selections to transpose (all must be found) + for reftsel in refset.iter() { + if reftsel.resource() == resource + && (source_side.is_none() || source_side == Some(side_i)) + { + if let Some((intersection, remainder, _)) = + tsel.intersection(reftsel.inner()) + { + source_side = Some(side_i); + source_found = true; //source_side might have been pre-set so we need this extra flag + let relative_offset = intersection + .relative_offset(reftsel.inner(), OffsetMode::default()) + .expect("intersection offset must be valid"); + relative_offsets.push(relative_offset); + selectors_per_side[side_i].push(SelectorBuilder::TextSelector( + resource.handle().into(), + intersection.into(), + )); + if let Some(remainder) = remainder { + //the text selection was not matched/consumed entirely + //add the remainder of the text selection back to the buffer + tselbuffer.push_front(remainder); + } + break; + } + } + } + } + } + if simple_transposition { + break; } } if simple_transposition { - let resource = self.resource(); - let mut source_found = false; - // We may have multiple text selections to transpose (all must be found) for tsel in self.inner().iter() { // each text selection in a simple transposition corresponds to a side @@ -196,6 +235,39 @@ impl<'store> Transposable<'store> for ResultTextSelectionSet<'store> { } } } + } else { + //complex transposition + if !tselbuffer.is_empty() { + return Err(StamError::TransposeError( + format!( + "Not all source fragments were found in the complex transposition {}, not enough to transpose", + via.id().unwrap_or("(no-id)"), + ), + "", + )); + } + + // now map the targets (there may be multiple target sides) + for (side_i, annotation) in via.annotations_in_targets(AnnotationDepth::One).enumerate() + { + if selectors_per_side.len() <= side_i { + selectors_per_side.push(Vec::new()); + } + if source_side != Some(side_i) { + for reftsel in annotation.textselections() { + let resource = reftsel.resource().handle(); + for offset in relative_offsets.iter() { + let mapped_tsel = reftsel.textselection(&offset)?; + let mapped_selector: SelectorBuilder<'static> = + SelectorBuilder::TextSelector( + resource.into(), + mapped_tsel.inner().into(), + ); + selectors_per_side[side_i].push(mapped_selector); + } + } + } + } } match selectors_per_side[source_side.expect("source side must exist at this point")].len() { diff --git a/tests/api.rs b/tests/api.rs index 333e71b..c0a157c 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -2147,3 +2147,58 @@ fn transpose_over_simple_transposition() -> Result<(), StamError> { assert_eq!(tsel.end(), 5, "transposed offset check"); Ok(()) } + +#[test] +#[cfg(feature = "transpose")] +fn transpose_over_complex_transposition() -> Result<(), StamError> { + let mut store = setup_example_8b()?; + store.annotate( + AnnotationBuilder::new() + .with_id("A3") + .with_data("mydataset", "species", "homo sapiens") + .with_target(SelectorBuilder::textselector( + "humanrights", + Offset::simple(4, 9), + )), + )?; + let transposition = store.annotation("ComplexTransposition1").or_fail()?; + let source = store.annotation("A3").or_fail()?; + assert_eq!( + source.text_simple(), + Some("human"), + "sanity check for source annotation" + ); + let config = TransposeConfig { + transposition_id: Some("NewTransposition".to_string()), + target_side_ids: vec!["A3t".to_string()], + ..Default::default() + }; + let targets = + store.annotate_from_iter(source.transpose(&transposition, config)?.into_iter())?; + assert_eq!(targets.len(), 2); + let new_transposition = store.annotation("NewTransposition").or_fail()?; + assert_eq!( + new_transposition + .annotations_in_targets(AnnotationDepth::One) + .count(), + 2, + "new transposition must have two target annotations (source annotation and transposed annotation)" + ); + let source = store.annotation("A3").or_fail()?; //reobtain (otherwise borrow checker complains after mutation) + let transposed = store.annotation("A3t").or_fail()?; + eprintln!("{:?}", transposed); + assert_eq!( + transposed.text_simple(), + source.text_simple(), + "transposed annotation must reference same text as source" + ); + let tsel = transposed.textselections().next().unwrap(); + assert_eq!( + tsel.resource().id(), + Some("warhol"), + "transposed annotation must reference the target resource" + ); + assert_eq!(tsel.begin(), 0, "transposed offset check"); + assert_eq!(tsel.end(), 5, "transposed offset check"); + Ok(()) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 117a45b..9b14e0e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -628,7 +628,7 @@ pub fn setup_example_8b() -> Result { )? .with_annotation( AnnotationBuilder::new() - .with_id("SimpleTransposition1") + .with_id("ComplexTransposition1") .with_target(SelectorBuilder::DirectionalSelector(vec![ SelectorBuilder::annotationselector("A1", None), SelectorBuilder::annotationselector("A2", None),