//----------------------------------------------------------------------------//
// //
// B e a m G r o u p //
// //
//----------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr"> //
// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
// This software is released under the GNU General Public License. //
// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
//----------------------------------------------------------------------------//
// </editor-fold>
package omr.score.entity;
import omr.constant.Constant;
import omr.constant.ConstantSet;
import omr.math.Line;
import omr.math.Rational;
import omr.sheet.Scale;
import omr.util.HorizontalSide;
import omr.util.Navigable;
import omr.util.TreeNode;
import omr.util.Vip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Class {@code BeamGroup} represents a group of related beams.
* It handles the level of each beam within the group.
* The contained beams are sorted in increasing order from stem/chord tail to
* stem/chord head
*
* @author Hervé Bitteur
*/
public class BeamGroup
implements Vip
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(BeamGroup.class);
// /** A Beam comparator based on level. (within the same group only) */
// private static final Comparator<Beam> byLevel = new Comparator<Beam>()
// {
// @Override
// public int compare (Beam b1,
// Beam b2)
// {
// if (b1 == b2) {
// return 0;
// }
//
// // Find a common chord, and use reverse order from head location
// for (Chord chord : b1.getChords()) {
// if (b2.getChords().contains(chord)) {
// int x = chord.getStem().getLocation().x;
// int y = b1.getLine().yAtX(x);
// int yOther = b2.getLine().yAtX(x);
// int yHead = chord.getHeadLocation().y;
//
// int result = Integer.signum(
// Math.abs(yHead - yOther) - Math.abs(yHead - y));
//
// if (result == 0) {
// // This should not happen
// // logger.warn(
// // other.getContextString() + " equality between " +
// // this.toLongString() + " and " + other.toLongString());
// // logger.warn(
// // "Beam comparison data " + "x=" + x + " y=" + y +
// // " yOther=" + yOther + " yHead=" + yHead);
// b1.addError(chord.getStem(), "Weird beam configuration");
// }
//
// return result;
// }
// }
//
// // No common chord
// }
// };
//~ Instance fields --------------------------------------------------------
//
/** (Debug) flag this object as VIP. */
private boolean vip;
/** Id for debug mainly. */
private final int id;
/** Containing measure. */
@Navigable(false)
private final Measure measure;
/** Collection of contained beams. */
private List<Beam> beams = new ArrayList<>();
/** Same voice for all chords of this beam group. */
private Voice voice;
//~ Constructors -----------------------------------------------------------
//-----------//
// BeamGroup //
//-----------//
/**
* Creates a new instance of BeamGroup.
*
* @param measure the containing measure
*/
public BeamGroup (Measure measure)
{
this.measure = measure;
measure.addGroup(this);
id = measure.getBeamGroups().indexOf(this) + 1;
logger.debug("{} Created {}", measure.getContextString(), this);
}
//~ Methods ----------------------------------------------------------------
//----------//
// populate //
//----------//
/**
* Populate all the BeamGroup instances for a given measure.
*
* @param measure the containing measure
*/
public static void populate (Measure measure)
{
// Link beams to chords
for (TreeNode node : measure.getBeams()) {
Beam beam = (Beam) node;
beam.linkChords();
}
// Build beam groups for this measure
for (TreeNode node : measure.getBeams()) {
Beam beam = (Beam) node;
beam.determineGroup();
}
// Close the connections between chords/stems and beams
for (TreeNode node : measure.getBeams()) {
Beam beam = (Beam) node;
beam.closeConnections();
}
// Separate illegal beam groups
BeamGroup.SplitOrder split;
// In case something goes wrong, use an upper limit to loop
int loopNb = constants.maxSplitLoops.getValue();
while ((split = checkBeamGroups(measure)) != null) {
if (--loopNb < 0) {
measure.addError("Loop detected in BeamGroup split");
break;
}
split.group.splitGroup(split);
}
// Dump results
if (logger.isDebugEnabled()) {
logger.debug(measure.getContextString());
for (BeamGroup group : measure.getBeamGroups()) {
logger.debug(" {}", group);
}
}
// Harmonize the slopes of all beams within each beam group
for (BeamGroup group : measure.getBeamGroups()) {
group.align();
}
}
//---------//
// addBeam //
//---------//
/**
* Include a beam as part of this group.
*
* @param beam the beam to include
*/
public void addBeam (Beam beam)
{
if (!beams.add(beam)) {
beam.addError(beam + " already in " + this);
}
if (beam.isVip()) {
setVip();
}
if (isVip() || logger.isDebugEnabled()) {
logger.info("{} Added {} to {}",
measure.getContextString(), beam, this);
}
}
//-------------------//
// computeStartTimes //
//-------------------//
/**
* Compute start times for all chords of this beam group,
* assuming the first chord of the group already has its
* startTime set.
*/
public void computeStartTimes ()
{
Chord prevChord = null;
for (Chord chord : getChords()) {
if (prevChord != null) {
try {
// Here we must check for interleaved rest
Note rest = Chord.lookupRest(prevChord, chord);
if (rest != null) {
rest.getChord().setStartTime(prevChord.getEndTime());
chord.setStartTime(rest.getChord().getEndTime());
} else {
chord.setStartTime(prevChord.getEndTime());
}
} catch (Exception ex) {
chord.addError(
"Cannot compute chord time based on previous chord");
}
} else {
if (chord.getStartTime() == null) {
chord.addError(
"Computing beam group times with first chord not set");
}
}
prevChord = chord;
}
}
//----------//
// getBeams //
//----------//
/**
* Report the beams that are part of this group.
*
* @return the collection of contained beams
*/
public List<Beam> getBeams ()
{
return beams;
}
//-----------//
// getChords //
//-----------//
/**
* Report the x-ordered collection of chords that are grouped by
* this beam group.
*
* @return the (perhaps empty) collection of 'beamed' chords.
*/
public List<Chord> getChords ()
{
List<Chord> chords = new ArrayList<>();
for (Beam beam : getBeams()) {
for (Chord chord : beam.getChords()) {
if (!chords.contains(chord)) {
chords.add(chord);
}
}
}
Collections.sort(chords, Chord.byAbscissa);
return chords;
}
//--------------//
// getLastChord //
//--------------//
/**
* Report the last chord on the right.
*
* @return the last chord
*/
public Chord getLastChord ()
{
List<Chord> chords = getChords();
if (!chords.isEmpty()) {
return chords.get(chords.size() - 1);
} else {
return null;
}
}
//-------------//
// getDuration //
//-------------//
/**
* Report the total duration of the sequence of chords within this
* group.
*
* @return the total group duration, perhaps null
*/
public Rational getDuration ()
{
Rational duration = null;
SortedSet<Chord> chords = new TreeSet<>(Chord.byAbscissa);
for (Beam beam : beams) {
for (Chord chord : beam.getChords()) {
chords.add(chord);
}
}
for (Chord chord : chords) {
Rational dur = chord.getDuration();
if (dur != null) {
if (duration != null) {
duration = duration.plus(dur);
} else {
duration = dur;
}
}
}
return duration;
}
//-------//
// getId //
//-------//
/**
* Report the group id (unique within the measure, starting from 1).
*
* @return the group id
*/
public int getId ()
{
return id;
}
//----------//
// getLevel //
//----------//
/**
* Report the level of a beam within its containing BeamGroup.
*
* @param beam the given beam (assumed to be part of this group)
* @return the beam level within the group, counted from 1
*/
public int getLevel (Beam beam)
{
int level = 1;
for (Beam b : beams) {
if (b == beam) {
return level;
} else {
level++;
}
}
// This should not happen
beam.addError("Unable to find beam in its group. size=" + beams.size());
return 0;
}
//-------//
// isVip //
//-------//
@Override
public boolean isVip ()
{
return vip;
}
//------------//
// removeBeam //
//------------//
/**
* Remove a beam from this group (in order to assign the beam to
* another group).
*
* @param beam the beam to remove
*/
public void removeBeam (Beam beam)
{
if (!beams.remove(beam)) {
beam.addError(beam + " not found in " + this);
}
}
//--------//
// setVip //
//--------//
@Override
public void setVip ()
{
vip = true;
}
//----------//
// setVoice //
//----------//
/**
* Assign a voice to this beam group.
*
* @param voice the voice to assign
*/
public void setVoice (Voice voice)
{
// Already done?
if (this.voice == null) {
this.voice = voice;
// Formard this information to the beamed chords
// Including the interleaved rests if any
Chord prevChord = null;
for (Chord chord : getChords()) {
if (prevChord != null) {
// Here we must check for interleaved rest
Note rest = Chord.lookupRest(prevChord, chord);
if (rest != null) {
rest.getChord().setVoice(voice);
}
}
chord.setVoice(voice);
prevChord = chord;
}
} else if (!this.voice.equals(voice)) {
getChords().get(0).addError(
"Group. Reassigning voice from " + this.voice + " to " + voice
+ " in " + this);
}
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder();
sb.append("{BeamGroup#").append(id).append(" beams[");
if (beams != null) {
for (Beam beam : beams) {
sb.append(beam).append(" ");
}
}
sb.append("]").append("}");
return sb.toString();
}
//-------//
// align //
//-------//
/**
* Force all beams (and beam items) to use the same slope within
* that beam group.
*/
private void align ()
{
// Retrieve the longest beam and use its slope
double bestLength = 0;
Beam bestBeam = null;
for (Beam beam : beams) {
// Extrema points of Beam hooks are not reliable, skip them
if (beam.isHook()) {
continue;
}
double length = beam.getPoint(HorizontalSide.LEFT).distance(beam.
getPoint(HorizontalSide.RIGHT));
if (length > bestLength) {
bestLength = length;
bestBeam = beam;
}
}
if (bestBeam != null) {
double slope = bestBeam.getLine().getSlope();
for (Beam beam : beams) {
Point left = beam.getPoint(HorizontalSide.LEFT);
Point right = beam.getPoint(HorizontalSide.RIGHT);
double yMid = (left.y + right.y) / 2d;
double dy = (right.x - left.x) * slope;
left.y = (int) Math.rint(yMid - (dy / 2));
right.y = (int) Math.rint(yMid + (dy / 2));
beam.setPoint(HorizontalSide.LEFT, left);
beam.setPoint(HorizontalSide.RIGHT, right);
}
}
}
//-----------------//
// checkBeamGroups //
//-----------------//
/**
* Check all the BeamGroup instances of the given measure, to find
* the first split if any to perform.
*
* @param measure the given measure
* @return the first split parameters, or null if everything is OK
*/
private static SplitOrder checkBeamGroups (Measure measure)
{
for (BeamGroup group : measure.getBeamGroups()) {
SplitOrder split = group.checkForSplit();
if (split != null) {
return split;
}
}
return null;
}
//---------------//
// checkForSplit //
//---------------//
/**
* Run a consistency check on the group, and detect when a group
* has to be split.
*
* @return the split order parameters, or null if no split is needed
*/
private SplitOrder checkForSplit ()
{
// Make sure all chords are part of the same group
// We check the vertical distance between any chord and the beams
// above or below the chord.
for (Chord chord : getChords()) {
Rectangle chordBox = chord.getBox();
for (Beam beam : beams) {
// Beam hooks are not concerned
if (beam.isHook()) {
continue;
}
// Skip beams attached to this chord
if (beam.getChords().contains(chord)) {
continue;
}
// Check abscissa overlap
Rectangle beamBox = beam.getBox();
int xOverlap = Math.min(chordBox.x + chordBox.width,
beamBox.x + beamBox.width)
- Math.max(chordBox.x, beamBox.x);
if (xOverlap <= 0) {
continue;
}
// Check vertical gap
Line line = beam.getLine();
Point tail = chord.getTailLocation();
int lineY = line.yAtX(tail.x);
int yOverlap = Math.min(lineY, chordBox.y + chordBox.height)
- Math.max(lineY, chordBox.y);
if (yOverlap >= 0) {
continue;
}
int tailDy = Math.abs(lineY - tail.y);
double normedDy = chord.getScale().pixelsToFrac(tailDy);
double maxChordDy = constants.maxChordDy.getValue();
if (normedDy > maxChordDy) {
logger.debug("Vertical gap between {} and {}, {} vs {}",
chord, beam, normedDy, maxChordDy);
// Split the beam group here
return new SplitOrder(this, chord);
}
}
}
return null; // everything is OK
}
//------------//
// splitChord //
//------------//
/**
* We actually split the chord which embraces the two beam groups.
* At this point, each beam has been moved to its proper group, either
* this (old) group or the (new) alienGroup.
* What remains to be done is to split the pivot chord between the two.
*
* @param pivotChord the chord to split
* @param alienGroup the new beam group (based on alienChord)
*/
private void splitChord (Chord pivotChord,
BeamGroup alienGroup)
{
logger.debug("Shared : {}", pivotChord);
// Create a clone of pivotChord (w/o any beam initially)
Chord cloneChord = pivotChord.duplicate();
List<Beam> alienBeams = alienGroup.getBeams();
for (Iterator<Beam> bit = pivotChord.getBeams().iterator(); bit.hasNext();) {
Beam beam = bit.next();
if (alienBeams.contains(beam)) {
// Cut the link chord -> beam
bit.remove();
// Cut the link chord <- beam
beam.removeChord(pivotChord);
// Link beam to cloneChord
cloneChord.addBeam(beam);
beam.addChord(cloneChord);
}
}
logger.debug("Remaining : {}", pivotChord);
logger.debug("Alien : {}", cloneChord);
}
//------------//
// splitGroup //
//------------//
/**
* Actually split a group in two, according to the split parameters.
*
* @param split the split parameters
*/
private void splitGroup (SplitOrder split)
{
logger.debug("processing {}", split);
// The group on alienChord side
BeamGroup chordGroup = new BeamGroup(measure);
// Check all former beams: any beam linked to the alienChord should be
// moved to the chordGroup.
List<Beam> chordBeams = new ArrayList<>(); // To avoid concurrent modifs
for (Beam beam : beams) {
if (beam.getChords().contains(split.alienChord)) {
chordBeams.add(beam);
}
}
// Now make the switch
for (Beam beam : chordBeams) {
beam.switchToGroup(chordGroup);
}
// Detect the chord which is shared by the two groups
// And duplicate this chord for both groups
for (Beam aBeam : chordGroup.beams) {
for (Chord aChord : aBeam.getChords()) {
// Compare with chords left in group
for (Beam beam : this.beams) {
for (Chord chord : beam.getChords()) {
// Is this (alien) chord also part of the old Group ?
if (chord == aChord) {
splitChord(chord, chordGroup);
return;
}
}
}
}
}
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Constant.Integer maxSplitLoops = new Constant.Integer(
"loops",
10,
"Maximum number of loops allowed for splitting beam groups");
Scale.Fraction maxChordDy = new Scale.Fraction(
0.5,
"Maximum vertical gap between a chord and a beam");
}
//------------//
// SplitOrder //
//------------//
/**
* Class {@code SplitOrder} records a beam group split order.
* Splitting must be separate from browsing to avoid concurrent modification
* of collections
*/
private static class SplitOrder
{
//~ Instance fields ----------------------------------------------------
/** The beam group to be split. */
final BeamGroup group;
/** A chord of this group, where multiplicity was detected. */
final Chord alienChord;
//~ Constructors -------------------------------------------------------
public SplitOrder (BeamGroup group,
Chord alienChord)
{
this.group = group;
this.alienChord = alienChord;
}
//~ Methods ------------------------------------------------------------
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder();
sb.append("{Split");
sb.append("\n\tgroup=").append(group);
sb.append("\n\talienChord=").append(alienChord);
sb.append("}");
return sb.toString();
}
}
}