def processContigRange(self, ema_range): """ Process a contigous range of measures give an MEI doc and an EmaExpression.EmaMeasureRange object """ # get all the spanners for total extension of meaure selection # (including gap measures, if present) # NB: Doing this for every range may be inefficient for larger files spanners = self.getMultiMeasureSpanners(ema_range.measures[0].idx - 1, ema_range.measures[-1].idx - 1) # Let's start with measures for i, ema_m in enumerate(ema_range.measures): is_first_m = i == 0 is_last_m = i == len(ema_range.measures) - 1 # Get requested measure measure = self.measures[ema_m.idx - 1] events = measure.getChildren() # determine current measure beat info meter = None for change in self.timeChanges: if int(change) + 1 <= ema_m.idx: meter = self.beatsInfo[change] # Get list of staff numbers in current measure sds = [ int(sd.getAttribute("n").getValue()) for sd in measure.getClosestStaffDefs() ] # Set aside selected staff numbers s_nos = [] # Proceed to locate requested staves for ema_s in ema_m.staves: if ema_s.number not in sds: # CAREFUL: there may be issues with "silent" staves # that may be defined by missing from current measure. # TODO: Write test, fix. raise BadApiRequest("Requested staff is not defined") s_nos.append(ema_s.number) for s_i, staff in enumerate(measure.getChildrenByName("staff")): s_no = s_i if staff.hasAttribute("n"): s_no = int(staff.getAttribute("n").getValue()) if s_no in s_nos: ema_s = ema_m.staves[s_nos.index(s_no)] # Get other elements affecting the staff, e.g. slurs around = [] for el in list(events): if self._isInStaff(el, s_no): around.append(el) # Create sets of elements marked for selection, removal and # cutting. Elements are not removed or cut immediately to make # sure that beat calcualtions are accurate. marked_as_selected = MeiElementSet() marked_as_space = MeiElementSet() marked_for_removal = MeiElementSet() marked_for_cutting = MeiElementSet() # Now locate the requested beat ranges within the staff for b_i, ema_beat_range in enumerate(ema_s.beat_ranges): is_first_b = i == 0 is_last_b = i == len(ema_s.beat_ranges) - 1 def _remove(el): """ Determine whether a removed element needs to be converted to a space or removed altogether""" if is_last_b and is_last_m: marked_for_removal.add(el) else: marked_as_space.add(el) # shorten them names tstamp_first = ema_beat_range.tstamp_first tstamp_final = ema_beat_range.tstamp_final co = self.ema_exp.completenessOptions # check that the requested beats actually fit in the meter if tstamp_first > int(meter["count"])+1 or \ tstamp_final > int(meter["count"])+1: raise BadApiRequest( "Request beat is out of measure bounds") # Find all descendants with att.duration.musical (@dur) for layer in staff.getDescendantsByName("layer"): cur_beat = 1.0 is_first_match = True for el in layer.getDescendants(): if el.hasAttribute( "dur" ) and not el.hasAttribute("grace"): dur = self._calculateDur(el, meter) # TODO still problems with non-consecutive beat ranges # e.g. @1@3 # exclude descendants at and in between tstamps if cur_beat >= tstamp_first: # We round to 4 decimal places to avoid issues caused by # tuplet-related calculations, which are admittedly not # well expressed in floating numbers. if round(cur_beat, 4) <= round( tstamp_final, 4): marked_as_selected.add(el) if is_first_match and "cut" in co: marked_for_cutting.add(el) is_first_match = False # discard from removal set if it had # been placed there from other beat # range marked_as_space.discard(el) # Cut the duration of the last element # if completeness = cut needs_cut = cur_beat + dur > tstamp_final + 1 if needs_cut and "cut" in co: marked_for_cutting.add(el) is_first_match = False elif not marked_as_selected.get(el): _remove(el) elif not marked_as_selected.get(el): marked_as_space.add(el) # continue cur_beat += dur # select elements affecting the staff occurring # within beat range for event in around: if not marked_as_selected.get(event): if event.hasAttribute("tstamp"): ts = float( event.getAttribute( "tstamp").getValue()) if ts < 1 and "cut" not in self.ema_exp.completenessOptions: ts = 1 ts2_att = None if event.hasAttribute("tstamp2"): ts2_att = event.getAttribute("tstamp2") if ts > tstamp_final or ( not ts2_att and ts < tstamp_first): marked_for_removal.add(event) elif ts2_att: ts2 = ts2_att.getValue() if "+" not in ts2: if ts2 < tstamp_first: marked_for_removal.add(event) elif ts2 == tstamp_final: marked_as_selected.add(event) marked_for_removal.discard( event) if ts < tstamp_first and ts2 >= tstamp_final: marked_as_selected.add(event) marked_for_removal.discard( event) else: marked_for_removal.add(event) else: marked_as_selected.add(event) else: marked_as_selected.add(event) elif event.hasAttribute("startid"): startid = (event.getAttribute( "startid").getValue().replace("#", "")) target = self.doc.getElementById(startid) if not target: msg = """Unsupported Encoding: attribute startid on element {0} does not point to any element in the document.""".format( event.getName()) raise UnsupportedEncoding( re.sub(r'\s+', ' ', msg.strip())) # Make sure the target event is in the same measure event_m = event.getAncestor( "measure").getId() target_m = target.getAncestor( "measure").getId() if not event_m == target_m: msg = """Unsupported Encoding: attribute startid on element {0} does not point to an element in the same measure.""".format( event.getName()) raise UnsupportedEncoding( re.sub(r'\s+', ' ', msg.strip())) else: if marked_as_selected.get(target): marked_as_selected.add(event) marked_for_removal.discard(event) elif not event.hasAttribute("endid"): marked_for_removal.add(event) else: # Skip if event starts after latest # selected element with duration pos = target.getPositionInDocument( ) is_ahead = False for i in reversed( marked_as_selected. getElements()): if i.hasAttribute("dur"): if pos > i.getPositionInDocument( ): marked_for_removal.add( event) is_ahead = True break if not is_ahead: # last chance to keep it: # must start before and end after # latest selected element with duration endid = ( event.getAttribute("endid") .getValue().replace( "#", "")) target2 = self.doc.getElementById( endid) if marked_as_selected.get( target2): marked_as_selected.add( event) marked_for_removal.discard( event) else: pos2 = target2.getPositionInDocument( ) for i in reversed( marked_as_selected. getElements()): if i.hasAttribute( "dur"): if pos2 > i.getPositionInDocument( ): marked_as_selected.add( event) marked_for_removal.discard( event) else: marked_for_removal.add( event) break # Remove elements marked for removal if "highlight" not in self.ema_exp.completenessOptions: for el in marked_for_removal: el.getParent().removeChild(el) # Replace elements marked as spaces with actual spaces, # unless completion = nospace, then remove the elements. for el in marked_as_space: parent = el.getParent() if "nospace" not in self.ema_exp.completenessOptions: space = MeiElement("space") space.setId(el.id) space.addAttribute(el.getAttribute("dur")) if el.getAttribute("dots"): space.addAttribute(el.getAttribute("dots")) elif el.getChildrenByName("dot"): dots = str(len( el.getChildrenByName("dot"))) space.addAttribute( MeiAttribute("dots", dots)) parent.addChildBefore(el, space) el.getParent().removeChild(el) else: for el in marked_as_selected: # add to list if self.highlight_el: cur_plist = self.highlight_el.getAttribute( "plist").getValue() space = "" if len(cur_plist) > 0: space = " " val = cur_plist + space + "#" + el.getId() self.highlight_el.addAttribute("plist", val) else: if "highlight" not in self.ema_exp.completenessOptions: # Remove this staff and its attached events staff.getParent().removeChild(staff) for el in list(events): if self._isInStaff(el, s_no): el.getParent().removeChild(el) # At the first measure, also add relevant multi-measure spanners # for each selected staff if is_first_m: if "highlight" not in self.ema_exp.completenessOptions: for evs in spanners.values(): for event_id in evs: ev = self.doc.getElementById(event_id) # Spanners starting outside of beat ranges # may be already gone if ev: # Determine staff of event for id changes for ema_s in ema_m.staves: staff_no = self._isInStaff( ev, ema_s.number) if staff_no and staff_no in s_nos: # If the event is attached to more than one staff, just # consider it attached to the its first one staff_no = staff_no[0] staff = None for staff_candidate in measure.getDescendantsByName( "staff"): if staff_candidate.hasAttribute("n"): n = int( staff_candidate.getAttribute( "n").getValue()) if n == staff_no: staff = staff_candidate # Truncate event to start at the beginning of the beat range if ev.hasAttribute("startid"): # Set startid to the first event still on staff, # at the first available layer try: layer = (staff.getChildrenByName( "layer")) first_id = layer[0].getChildren( )[0].getId() ev.getAttribute( "startid").setValue("#" + first_id) except IndexError: msg = """ Unsupported encoding. Omas attempted to adjust the starting point of a selected multi-measure element that starts before the selection, but the staff or layer could not be located. """ msg = re.sub( r'\s+', ' ', msg.strip()) raise UnsupportedEncoding(msg) if ev.hasAttribute("tstamp"): # Set tstamp to first in beat selection tstamp_first = 0 for e_s in ema_m.staves: if e_s.number == staff_no: tstamp_first = e_s.beat_ranges[ 0].tstamp_first ev.getAttribute("tstamp").setValue( str(tstamp_first)) # Truncate to end of range if completeness = cut # (actual beat cutting will happen when beat ranges are procesed) if "cut" in self.ema_exp.completenessOptions: if ev.hasAttribute("tstamp2"): att = ev.getAttribute("tstamp2") t2 = att.getValue() p = re.compile(r"([1-9]+)(?=m\+)") multimeasure = p.match(t2) if multimeasure: new_val = len(mm) - 1 att.setValue( p.sub(str(new_val), t2)) # Otherwise adjust tspan2 value to correct distance. # E.g. given 4 measures with a spanner originating # in 1 and ending in 4 and a selection of measures 2 and 3, # change @tspan2 from 3m+X to 2m+X else: if ev.hasAttribute("tstamp2"): att = ev.getAttribute("tstamp2") t2 = att.getValue() p = re.compile(r"([1-9]+)(?=m\+)") multimeasure = p.match(t2) if multimeasure: dis = evs[event_id]["distance"] new_val = int( multimeasure.group( 1)) - dis att.setValue( p.sub(str(new_val), t2)) # move element to first measure and add it to selected # events "around" the staff. ev.moveTo(measure) else: # Add spanners to annotated selection for evs in spanners.values(): for event_id in evs: if self.highlight_el: cur_plist = self.highlight_el.getAttribute( "plist").getValue() space = "" if len(cur_plist) > 0: space = " " val = cur_plist + space + "#" + event_id self.highlight_el.addAttribute("plist", val) return self.doc
def processContigRange(self, ema_range): """ Process a contigous range of measures give an MEI doc and an EmaExpression.EmaMeasureRange object """ # get all the spanners for total extension of meaure selection # (including gap measures, if present) # NB: Doing this for every range may be inefficient for larger files spanners = self.getMultiMeasureSpanners(ema_range.measures[0].idx-1, ema_range.measures[-1].idx-1) # Let's start with measures for i, ema_m in enumerate(ema_range.measures): is_first_m = i == 0 is_last_m = i == len(ema_range.measures)-1 # Get requested measure measure = self.measures[ema_m.idx-1] events = measure.getChildren() # determine current measure beat info meter = None for change in self.timeChanges: if int(change)+1 <= ema_m.idx: meter = self.beatsInfo[change] # Get list of staff numbers in current measure sds = [int(sd.getAttribute("n").getValue()) for sd in measure.getClosestStaffDefs()] # Set aside selected staff numbers s_nos = [] # Proceed to locate requested staves for ema_s in ema_m.staves: if ema_s.number not in sds: # CAREFUL: there may be issues with "silent" staves # that may be defined by missing from current measure. # TODO: Write test, fix. raise BadApiRequest("Requested staff is not defined") s_nos.append(ema_s.number) for s_i, staff in enumerate(measure.getChildrenByName("staff")): s_no = s_i if staff.hasAttribute("n"): s_no = int(staff.getAttribute("n").getValue()) if s_no in s_nos: ema_s = ema_m.staves[s_nos.index(s_no)] # Get other elements affecting the staff, e.g. slurs around = [] for el in list(events): if self._isInStaff(el, s_no): around.append(el) # Create sets of elements marked for selection, removal and # cutting. Elements are not removed or cut immediately to make # sure that beat calcualtions are accurate. marked_as_selected = MeiElementSet() marked_as_space = MeiElementSet() marked_for_removal = MeiElementSet() marked_for_cutting = MeiElementSet() # Now locate the requested beat ranges within the staff for b_i, ema_beat_range in enumerate(ema_s.beat_ranges): is_first_b = i == 0 is_last_b = i == len(ema_s.beat_ranges)-1 def _remove(el): """ Determine whether a removed element needs to be converted to a space or removed altogether""" if is_last_b and is_last_m: marked_for_removal.add(el) else: marked_as_space.add(el) # shorten them names tstamp_first = ema_beat_range.tstamp_first tstamp_final = ema_beat_range.tstamp_final co = self.ema_exp.completenessOptions # check that the requested beats actually fit in the meter if tstamp_first > int(meter["count"])+1 or \ tstamp_final > int(meter["count"])+1: raise BadApiRequest( "Request beat is out of measure bounds") # Find all descendants with att.duration.musical (@dur) for layer in staff.getDescendantsByName("layer"): cur_beat = 1.0 is_first_match = True for el in layer.getDescendants(): if el.hasAttribute("dur") and not el.hasAttribute("grace"): dur = self._calculateDur(el, meter) # TODO still problems with non-consecutive beat ranges # e.g. @1@3 # exclude descendants at and in between tstamps if cur_beat >= tstamp_first: # We round to 4 decimal places to avoid issues caused by # tuplet-related calculations, which are admittedly not # well expressed in floating numbers. if round(cur_beat, 4) <= round(tstamp_final, 4): marked_as_selected.add(el) if is_first_match and "cut" in co: marked_for_cutting.add(el) is_first_match = False # discard from removal set if it had # been placed there from other beat # range marked_as_space.discard(el) # Cut the duration of the last element # if completeness = cut needs_cut = cur_beat+dur > tstamp_final+1 if needs_cut and "cut" in co: marked_for_cutting.add(el) is_first_match = False elif not marked_as_selected.get(el): _remove(el) elif not marked_as_selected.get(el): marked_as_space.add(el) # continue cur_beat += dur # select elements affecting the staff occurring # within beat range for event in around: if not marked_as_selected.get(event): if event.hasAttribute("tstamp"): ts = float(event.getAttribute("tstamp").getValue()) if ts < 1 and "cut" not in self.ema_exp.completenessOptions: ts = 1 ts2_att = None if event.hasAttribute("tstamp2"): ts2_att = event.getAttribute("tstamp2") if ts > tstamp_final or (not ts2_att and ts < tstamp_first): marked_for_removal.add(event) elif ts2_att: ts2 = ts2_att.getValue() if "+" not in ts2: if ts2 < tstamp_first: marked_for_removal.add(event) elif ts2 == tstamp_final: marked_as_selected.add(event) marked_for_removal.discard(event) if ts < tstamp_first and ts2 >= tstamp_final: marked_as_selected.add(event) marked_for_removal.discard(event) else: marked_for_removal.add(event) else: marked_as_selected.add(event) else: marked_as_selected.add(event) elif event.hasAttribute("startid"): startid = ( event.getAttribute("startid") .getValue() .replace("#", "") ) target = self.doc.getElementById(startid) if not target: msg = """Unsupported Encoding: attribute startid on element {0} does not point to any element in the document.""".format( event.getName()) raise UnsupportedEncoding( re.sub(r'\s+', ' ', msg.strip())) # Make sure the target event is in the same measure event_m = event.getAncestor("measure").getId() target_m = target.getAncestor("measure").getId() if not event_m == target_m: msg = """Unsupported Encoding: attribute startid on element {0} does not point to an element in the same measure.""".format( event.getName()) raise UnsupportedEncoding( re.sub(r'\s+', ' ', msg.strip())) else: if marked_as_selected.get(target): marked_as_selected.add(event) marked_for_removal.discard(event) elif not event.hasAttribute("endid"): marked_for_removal.add(event) else: # Skip if event starts after latest # selected element with duration pos = target.getPositionInDocument() is_ahead = False for i in reversed(marked_as_selected.getElements()): if i.hasAttribute("dur"): if pos > i.getPositionInDocument(): marked_for_removal.add(event) is_ahead = True break if not is_ahead: # last chance to keep it: # must start before and end after # latest selected element with duration endid = ( event.getAttribute("endid") .getValue() .replace("#", "") ) target2 = self.doc.getElementById(endid) if marked_as_selected.get(target2): marked_as_selected.add(event) marked_for_removal.discard(event) else: pos2 = target2.getPositionInDocument() for i in reversed(marked_as_selected.getElements()): if i.hasAttribute("dur"): if pos2 > i.getPositionInDocument(): marked_as_selected.add(event) marked_for_removal.discard(event) else: marked_for_removal.add(event) break # Remove elements marked for removal for el in marked_for_removal: el.getParent().removeChild(el) # Replace elements marked as spaces with actual spaces, # unless completion = nospace, then remove the elements. for el in marked_as_space: parent = el.getParent() if "nospace" not in self.ema_exp.completenessOptions: space = MeiElement("space") space.setId(el.id) space.addAttribute(el.getAttribute("dur")) if el.getAttribute("dots"): space.addAttribute(el.getAttribute("dots")) elif el.getChildrenByName("dot"): dots = str(len(el.getChildrenByName("dot"))) space.addAttribute(MeiAttribute("dots", dots)) parent.addChildBefore(el, space) el.getParent().removeChild(el) else: # Remove this staff and its attached events staff.getParent().removeChild(staff) for el in list(events): if self._isInStaff(el, s_no): el.getParent().removeChild(el) # At the first measure, also add relevant multi-measure spanners # for each selected staff if is_first_m: for evs in spanners.values(): for event_id in evs: ev = self.doc.getElementById(event_id) # Spanners starting outside of beat ranges # may be already gone if ev: # Determine staff of event for id changes for ema_s in ema_m.staves: staff_no = self._isInStaff(ev, ema_s.number) if staff_no and staff_no in s_nos: # If the event is attached to more than one staff, just # consider it attached to the its first one staff_no = staff_no[0] staff = None for staff_candidate in measure.getDescendantsByName("staff"): if staff_candidate.hasAttribute("n"): n = int(staff_candidate.getAttribute("n").getValue()) if n == staff_no: staff = staff_candidate # Truncate event to start at the beginning of the beat range if ev.hasAttribute("startid"): # Set startid to the first event still on staff, # at the first available layer try: layer = ( staff.getChildrenByName("layer") ) first_id = layer[0].getChildren()[0].getId() ev.getAttribute("startid").setValue("#"+first_id) except IndexError: msg = """ Unsupported encoding. Omas attempted to adjust the starting point of a selected multi-measure element that starts before the selection, but the staff or layer could not be located. """ msg = re.sub(r'\s+', ' ', msg.strip()) raise UnsupportedEncoding(msg) if ev.hasAttribute("tstamp"): # Set tstamp to first in beat selection tstamp_first = 0 for e_s in ema_m.staves: if e_s.number == staff_no: tstamp_first = e_s.beat_ranges[0].tstamp_first ev.getAttribute("tstamp").setValue(str(tstamp_first)) # Truncate to end of range if completeness = cut # (actual beat cutting will happen when beat ranges are procesed) if "cut" in self.ema_exp.completenessOptions: if ev.hasAttribute("tstamp2"): att = ev.getAttribute("tstamp2") t2 = att.getValue() p = re.compile(r"([1-9]+)(?=m\+)") multimeasure = p.match(t2) if multimeasure: new_val = len(mm) - 1 att.setValue(p.sub(str(new_val), t2)) # Otherwise adjust tspan2 value to correct distance. # E.g. given 4 measures with a spanner originating # in 1 and ending in 4 and a selection of measures 2 and 3, # change @tspan2 from 3m+X to 2m+X else: if ev.hasAttribute("tstamp2"): att = ev.getAttribute("tstamp2") t2 = att.getValue() p = re.compile(r"([1-9]+)(?=m\+)") multimeasure = p.match(t2) if multimeasure: dis = evs[event_id]["distance"] new_val = int(multimeasure.group(1)) - dis att.setValue(p.sub(str(new_val), t2)) # move element to first measure and add it to selected # events "around" the staff. ev.moveTo(measure) return self.doc