1515 FEATURE_BASE_NAME_HEAD ,
1616 FEATURE_BASE_NAME_LANGUAGE ,
1717 TYPE_NAME_DOCUMENT_ANNOTATION ,
18+ TYPE_NAME_ANNOTATION ,
1819 TYPE_NAME_FS_ARRAY ,
1920 TYPE_NAME_FS_LIST ,
2021 TYPE_NAME_SOFA ,
@@ -368,6 +369,78 @@ def add_annotations(self, annotations: Iterable[FeatureStructure]):
368369 """
369370 self .add_all (annotations )
370371
372+ def crop_sofa_string (self , sofa_begin : int , sofa_end : int , overlap : bool = True ):
373+ """Replaces current sofa string with a cutout of the given range. Removes all annotations outside of range,
374+ but keeps annotations that overlap with cutout points by default.
375+
376+ Args:
377+ sofa_begin: The beginning of the cutout sofa.
378+ sofa_end: The end of the cutout sofa.
379+ overlap: If true, keeps overlapping annotations and modifies begin and end of annotation accordingly.
380+
381+ Raises:
382+ ValueError: If cutout indices are invalid.
383+ Note:
384+ Removal performed by this method only removes annotations from the current view's
385+ index. Feature structures that are removed from the view remain in memory and any
386+ references from kept annotations to those feature structures are left intact. Such
387+ transitively referenced feature structures will still be discovered by traversal
388+ (e.g. ``_find_all_fs()``) and included during serialization.
389+
390+ Important: only the annotations that are kept (inside the cut or overlapping
391+ the cut boundaries) have their ``begin``/``end`` offsets adjusted to the new
392+ sofa coordinate space. Feature structures that are removed from the view are
393+ not re-anchored or relocated — they keep their original ``begin``/``end``
394+ values. As a result, serializers may attempt to transcode offsets that fall
395+ outside the new sofa range; the offset converter will emit ``UserWarning``
396+ messages for unmappable offsets but will not raise an exception. If you
397+ require a cascading delete or re-anchoring of transitively referenced feature
398+ structures, perform an explicit graph traversal and removal or implement an
399+ opt-in ``cascade=True`` behavior.
400+ """
401+ if self .sofa_string is None :
402+ raise ValueError ("Cannot crop sofa string: CAS has no sofa string for the current view" )
403+
404+ if 0 <= sofa_begin < sofa_end <= len (self .sofa_string ):
405+ self .sofa_string = self .sofa_string [sofa_begin :sofa_end ]
406+ # Make an explicit snapshot of the current annotations to avoid
407+ # issues when removing/modifying elements during iteration.
408+ for annotation in list (self .select_all ()):
409+ # Determine whether the annotation will be kept and how its
410+ # offsets need to be adjusted. If offsets are adjusted we must
411+ # reindex the annotation (remove then add) so that the
412+ # underlying SortedKeyList remains correctly ordered by the
413+ # updated begin/end values.
414+ if sofa_begin <= annotation .begin and annotation .end <= sofa_end :
415+ # fully contained
416+ self ._current_view .remove_annotation_from_index (annotation )
417+ annotation .begin = annotation .begin - sofa_begin
418+ annotation .end = annotation .end - sofa_begin
419+ self ._current_view .add_annotation_to_index (annotation )
420+ elif overlap and sofa_begin < annotation .end <= sofa_end :
421+ # left overlap (annotation starts before cut)
422+ self ._current_view .remove_annotation_from_index (annotation )
423+ annotation .begin = 0
424+ annotation .end = annotation .end - sofa_begin
425+ self ._current_view .add_annotation_to_index (annotation )
426+ elif overlap and sofa_begin <= annotation .begin < sofa_end :
427+ # right overlap (annotation ends after cut)
428+ self ._current_view .remove_annotation_from_index (annotation )
429+ annotation .begin = annotation .begin - sofa_begin
430+ annotation .end = len (self .sofa_string )
431+ self ._current_view .add_annotation_to_index (annotation )
432+ elif overlap and annotation .begin <= sofa_begin and sofa_end <= annotation .end :
433+ # annotation fully covers the cut
434+ self ._current_view .remove_annotation_from_index (annotation )
435+ annotation .begin = 0
436+ annotation .end = len (self .sofa_string )
437+ self ._current_view .add_annotation_to_index (annotation )
438+ else :
439+ # annotation falls completely outside the cut; remove it
440+ self .remove (annotation )
441+ else :
442+ raise ValueError (f"Invalid indices for begin { sofa_begin } and end { sofa_end } " )
443+
371444 def remove (self , annotation : FeatureStructure ):
372445 """Removes an annotation from an index. This throws if the
373446 annotation was not present.
@@ -387,6 +460,38 @@ def remove_annotation(self, annotation: FeatureStructure):
387460 """
388461 self .remove (annotation )
389462
463+ def remove_annotations_in_range (self , begin : int , end : int , type_ : Optional [Union [Type , str ]] = None ):
464+ """Removes annotations between two indices of the sofa string.
465+
466+ Args:
467+ begin: The beginning of the cutting interval.
468+ end: The end of the cutting interval.
469+ type_: The type or name of the type name whose annotation instances are to be found
470+ Raises:
471+ ValueError: If range indices are invalid.
472+ """
473+
474+ # If no type is provided, operate on annotation-like feature
475+ # structures only (those that have `begin` and `end`) to avoid
476+ # AttributeError for arbitrary FS (e.g., instances of uima.cas.TOP).
477+ if type_ is None :
478+ # Only operate on annotation-like feature structures to avoid
479+ # AttributeError for non-annotation FS present in the view.
480+ annotations = [a for a in self .select_all () if self .typesystem .is_instance_of (a .type , TYPE_NAME_ANNOTATION )]
481+ else :
482+ annotations = self .select (type_ )
483+ if self .sofa_string is None :
484+ raise ValueError ("Cannot remove annotations by range: CAS has no sofa string for the current view" )
485+
486+ if 0 <= begin < end <= len (self .sofa_string ):
487+ # Make an explicit snapshot of the annotations to avoid issues when
488+ # removing elements during iteration (defensive copy).
489+ for annotation in list (annotations ):
490+ if begin <= annotation .begin < annotation .end <= end :
491+ self .remove (annotation )
492+ else :
493+ raise ValueError (f"Invalid indices for begin { begin } and end { end } " )
494+
390495 @deprecation .deprecated (details = "Use annotation.get_covered_text()" )
391496 def get_covered_text (self , annotation : FeatureStructure ) -> str :
392497 """Gets the text that is covered by `annotation`.
@@ -848,17 +953,17 @@ def deep_copy(self, copy_typesystem: bool = False) -> "Cas":
848953 ts = self .typesystem .to_xml ()
849954 ts = load_typesystem (ts )
850955
851- cas_copy = Cas (ts ,
852- document_language = self .document_language ,
853- lenient = self ._lenient ,
854- sofa_mime = self .sofa_mime ,
855- )
956+ cas_copy = Cas (
957+ ts ,
958+ document_language = self .document_language ,
959+ lenient = self ._lenient ,
960+ sofa_mime = self .sofa_mime ,
961+ )
856962
857963 cas_copy ._views = {}
858964 cas_copy ._sofas = {}
859965
860966 for sofa in self .sofas :
861-
862967 sofa_copy = Sofa (
863968 sofaID = sofa .sofaID ,
864969 sofaNum = sofa .sofaNum ,
@@ -883,9 +988,8 @@ def deep_copy(self, copy_typesystem: bool = False) -> "Cas":
883988 referenced_view = {}
884989
885990 for fs in self ._find_all_fs ():
886-
887991 # the referenced view is required when adding the fs to the copied cas later
888- if hasattr (fs , ' sofa' ) and fs .sofa and hasattr (fs , ' xmiID' ) and fs .xmiID :
992+ if hasattr (fs , " sofa" ) and fs .sofa and hasattr (fs , " xmiID" ) and fs .xmiID :
889993 referenced_view [fs .xmiID ] = fs .sofa .sofaID
890994
891995 t = ts .get_type (fs .type .name )
@@ -902,19 +1006,21 @@ def deep_copy(self, copy_typesystem: bool = False) -> "Cas":
9021006 # collect referenced xmiIDs for mapping later
9031007 referenced_list = []
9041008 for item in fs [feature .name ].elements :
905- if hasattr (item , ' xmiID' ) and item .xmiID is not None :
1009+ if hasattr (item , " xmiID" ) and item .xmiID is not None :
9061010 referenced_list .append (item .xmiID )
9071011 referenced_arrays .setdefault (fs .xmiID , {})
9081012 referenced_arrays [fs .xmiID ][feature .name ] = referenced_list
9091013 elif feature .rangeType .name == TYPE_NAME_SOFA :
9101014 # ignore sofa references
9111015 pass
9121016 else :
913- if hasattr (fs [feature .name ], ' xmiID' ) and fs [feature .name ].xmiID is not None :
1017+ if hasattr (fs [feature .name ], " xmiID" ) and fs [feature .name ].xmiID is not None :
9141018 references .setdefault (feature .name , [])
9151019 references [feature .name ].append ((fs .xmiID , fs [feature .name ].xmiID ))
9161020 else :
917- warnings .warn (f"Original non-primitive feature \" { feature .name } \" was and not copied from feature structure { fs .xmiID } ." )
1021+ warnings .warn (
1022+ f'Original non-primitive feature "{ feature .name } " was and not copied from feature structure { fs .xmiID } .'
1023+ )
9181024
9191025 fs_copy .xmiID = fs .xmiID
9201026 all_copied_fs [fs_copy .xmiID ] = fs_copy
@@ -924,19 +1030,21 @@ def deep_copy(self, copy_typesystem: bool = False) -> "Cas":
9241030 for current_ID , reference_ID in pairs :
9251031 try :
9261032 all_copied_fs [current_ID ][feature ] = all_copied_fs [reference_ID ]
927- except KeyError as e :
928- warnings .warn (f"Reference { reference_ID } not found for feature '{ feature } ' of feature structure { current_ID } " )
1033+ except KeyError :
1034+ warnings .warn (
1035+ f"Reference { reference_ID } not found for feature '{ feature } ' of feature structure { current_ID } "
1036+ )
9291037
9301038 # set references for objects in arrays
9311039 for current_ID , arrays in referenced_arrays .items ():
9321040 for feature , referenced_list in arrays .items ():
9331041 elements = [all_copied_fs [reference_ID ] for reference_ID in referenced_list ]
9341042 all_copied_fs [current_ID ][feature ].elements = elements
9351043
936- # add feature structures to the appropriate views
1044+ # add feature structures to the appropriate views (add in xmiID order)
9371045 feature_structures = sorted (all_copied_fs .values (), key = lambda f : f .xmiID , reverse = False )
938- for item in all_copied_fs . values () :
939- if hasattr (item , ' xmiID' ) and item .xmiID is not None :
1046+ for item in feature_structures :
1047+ if hasattr (item , " xmiID" ) and item .xmiID is not None :
9401048 view_name = referenced_view .get (item .xmiID )
9411049 if view_name is not None :
9421050 cas_copy ._current_view = cas_copy ._views [view_name ]
0 commit comments