//----------------------------------------------------------------------------//
// //
// S l u r //
// //
//----------------------------------------------------------------------------//
// <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.ConstantSet;
import omr.glyph.facets.Glyph;
import omr.math.Circle;
import omr.score.visitor.ScoreVisitor;
import omr.sheet.Scale;
import omr.util.Predicate;
import omr.util.TreeNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
/**
* Class {@code Slur} encapsulates a slur (a curve) in a system.
* A slur is used for a tie (2 notes with the same octave & step) or for
* just a phrase embracing several notes.
*
* @author Hervé Bitteur
*/
public class Slur
extends PartNode
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(Slur.class);
/** To order slurs vertically within a measure. */
public static final Comparator<Slur> verticalComparator = new Comparator<Slur>()
{
@Override
public int compare (Slur s1,
Slur s2)
{
return Double.compare(s1.getCurve().getY1(), s2.getCurve().getY1());
}
};
/**
* Predicate for a slur not connected on both ends.
*/
public static final Predicate<Slur> isOrphan = new Predicate<Slur>()
{
@Override
public boolean check (Slur slur)
{
return slur.getLeftNote() == null || slur.getRightNote() == null;
}
};
/** Predicate for an orphan slur at the end of its system/part. */
public static final Predicate<Slur> isEndingOrphan = new Predicate<Slur>()
{
@Override
public boolean check (Slur slur)
{
if (slur.getRightNote() == null) {
// Check we are in last measure
Point2D p2 = slur.getCurve().getP2();
Measure measure = slur.getPart().getMeasureAt(
new Point((int) p2.getX(), (int) p2.getY()));
if (measure == slur.getPart().getLastMeasure()) {
// Check slur ends in last measure half
if (p2.getX() > measure.getCenter().x) {
return true;
}
}
}
return false;
}
};
/** Predicate for an orphan slur at the beginning of its system/part. */
public static final Predicate<Slur> isBeginningOrphan = new Predicate<Slur>()
{
@Override
public boolean check (Slur slur)
{
if (slur.getLeftNote() == null) {
// Check we are in first measure
Point2D p1 = slur.getCurve().getP1();
Measure measure = slur.getPart().getMeasureAt(
new Point((int) p1.getX(), (int) p1.getY()));
if (measure == slur.getPart().getFirstMeasure()) {
// Check slur begins in first measure half
if (p1.getX() < measure.getCenter().x) {
return true;
}
}
}
return false;
}
};
//~ Instance fields --------------------------------------------------------
//
/** Underlying glyph. */
private final Glyph glyph;
/** Underlying curve. */
private final CubicCurve2D curve;
/** Note on left side, if any. */
private final Note leftNote;
/** Note on right side, if any. */
private final Note rightNote;
/** Slur extension on left side, if any. */
private Slur leftExtension;
/** Slur extension on right side, if any. */
private Slur rightExtension;
/** Placement / orientation. */
private final boolean below;
/** Is a Tie (else a plain slur). */
private boolean tie;
//~ Constructors -----------------------------------------------------------
//------//
// Slur //
//------//
/**
* Create a slur with all the specified parameters.
*
* @param part the containing system part
* @param glyph the underlying glyph
* @param curve the underlying bezier curve
* @param below true if below, false if above
* @param leftNote the note on the left
* @param rightNote the note on the right
*/
public Slur (SystemPart part,
Glyph glyph,
CubicCurve2D curve,
boolean below,
Note leftNote,
Note rightNote)
{
super(part);
this.glyph = glyph;
this.curve = curve;
this.below = below;
this.leftNote = leftNote;
this.rightNote = rightNote;
// Link embraced notes to this slur instance
if (leftNote != null) {
leftNote.addSlur(this);
}
if (rightNote != null) {
rightNote.addSlur(this);
}
// Tie ?
tie = isTie(leftNote, rightNote);
}
//~ Methods ----------------------------------------------------------------
//---------//
// destroy //
//---------//
/**
* Destroy this slur instance.
*/
public void destroy ()
{
if (glyph != null) {
glyph.setShape(null);
glyph.clearTranslations();
}
if (leftNote != null) {
leftNote.removeSlur(this);
}
if (rightNote != null) {
rightNote.removeSlur(this);
}
getParent().getChildren().remove(this);
}
//----------//
// populate //
//----------//
/**
* Given a glyph (potentially representing a Slur), allocate the Slur
* entity that corresponds to this glyph.
*
* @param glyph The glyph to process
* @param system The system which will contain the allocated Slur
*/
public static void populate (Glyph glyph,
ScoreSystem system)
{
if (glyph.isVip()) {
logger.info("Slur. populate {}", glyph.idString());
}
// Compute the approximating circle
Circle circle = system.getInfo().getSlurInspector().getCircle(glyph);
CubicCurve2D curve = circle.getCurve();
// Safer
if (curve == null) {
logger.debug("No curve found for slur candidate #{}", glyph.getId());
return;
}
// Retrieve & sort nodes (notes or chords) on both ends of the slur
List<MeasureNode> leftNodes = new ArrayList<>();
List<MeasureNode> rightNodes = new ArrayList<>();
boolean below = retrieveEmbracedNotes(
glyph,
system,
curve,
leftNodes,
rightNodes);
// Now choose the most relevant note, if any, on each slur end
End leftEnd = null;
End rightEnd = null;
switch (leftNodes.size()) {
case 0:
switch (rightNodes.size()) {
case 0:
break;
case 1:
rightEnd = new End(rightNodes.get(0));
break;
default:
rightEnd = new End(rightNodes.get(0)); // Why not?
}
break;
case 1:
leftEnd = new End(leftNodes.get(0));
switch (rightNodes.size()) {
case 0:
break;
case 1:
rightEnd = new End(rightNodes.get(0));
break;
default:
for (MeasureNode node : rightNodes) {
rightEnd = new End(node);
if (leftEnd.stemDir == rightEnd.stemDir) {
break;
}
}
}
break;
default:
switch (rightNodes.size()) {
case 0:
leftEnd = new End(leftNodes.get(0)); // Why not?
break;
case 1:
rightEnd = new End(rightNodes.get(0));
for (MeasureNode node : leftNodes) {
leftEnd = new End(node);
if (leftEnd.stemDir == rightEnd.stemDir) {
break;
}
}
break;
default: // N left & P right
leftEnd = new End(leftNodes.get(0)); // Why not?
rightEnd = new End(rightNodes.get(0)); // Why not?
}
}
// Should we allocate the slur entity?
if ((leftEnd != null) || (rightEnd != null)) {
SystemPart part = (leftEnd != null) ? leftEnd.note.getPart()
: rightEnd.note.getPart();
if (leftEnd != null && rightEnd != null
&& leftEnd.note == rightEnd.note) {
// Slur looping on the same note!
logger.debug("Looping slur {}", glyph.idString());
glyph.setShape(null);
} else {
Slur slur = new Slur(
part,
glyph,
curve,
below,
(leftEnd != null) ? leftEnd.note : null,
(rightEnd != null) ? rightEnd.note : null);
glyph.setTranslation(slur);
logger.debug(slur.toString());
}
} else {
system.addError(glyph, "Slur with no embraced notes");
}
}
//--------//
// accept //
//--------//
@Override
public boolean accept (ScoreVisitor visitor)
{
return visitor.visit(this);
}
//----------//
// addError //
//----------//
@Override
public void addError (String text)
{
super.addError(glyph, text);
}
//-----------//
// canExtend //
//-----------//
/**
* Check whether this slur can extend the prevSlur of the preceding
* system.
*
* @param prevSlur the slur candidate in the preceding system
* @return true if connection is possible
*/
public boolean canExtend (Slur prevSlur)
{
return (this.leftExtension == null)
&& (prevSlur.rightExtension == null)
&& this.isCompatibleWith(prevSlur);
}
//-----------//
// connectTo //
//-----------//
/**
* Make the connection with another slur in the previous system.
*
* @param prevSlur slur at the end of previous system
*/
public void connectTo (Slur prevSlur)
{
// Cross-extensions
this.leftExtension = prevSlur;
prevSlur.rightExtension = this;
// Tie?
boolean isATie = haveSameHeight(prevSlur.leftNote, this.rightNote);
prevSlur.tie = isATie;
this.tie = isATie;
logger.debug("{} connection #{} -> #{}",
isATie ? "Tie" : "Slur", prevSlur.glyph.getId(), glyph.getId());
}
//----------//
// getCurve //
//----------//
/**
* Report the curve of the slur.
*
* @return the curve to draw
*/
public CubicCurve2D getCurve ()
{
return curve;
}
//-------//
// getId //
//-------//
/**
* Report the slur id (as the id of the underlying glyph).
*
* @return the id of the underlying glyph
*/
public int getId ()
{
return glyph.getId();
}
//------------------//
// getLeftExtension //
//------------------//
/**
* Report the slur (if any) at the end of previous system that could
* be considered as an extension of this slur.
*
* @return the connected slur on left, or null if none
*/
public Slur getLeftExtension ()
{
return leftExtension;
}
//-------------//
// getLeftNote //
//-------------//
/**
* Report the note (if any) embraced by the left side of this slur
*
* @return the embraced note, or null
*/
public Note getLeftNote ()
{
return leftNote;
}
//-------------------//
// getRightExtension //
//-------------------//
/**
* Report the slur (if any) at the beginning of next system that
* could be considered as an extension of this slur.
*
* @return the connected slur on right, or null if none
*/
public Slur getRightExtension ()
{
return rightExtension;
}
//--------------//
// getRightNote //
//--------------//
/**
* Report the note (if any) embraced by the right side of this slur.
*
* @return the embraced note, or null
*/
public Note getRightNote ()
{
return rightNote;
}
//---------//
// isBelow //
//---------//
/**
* Report whether the placement of this slur is below the embraced notes.
*
* @return true if below, false if above
*/
public boolean isBelow ()
{
return below;
}
//-------//
// isTie //
//-------//
/**
* Report whether this slur is actually a tie (a slur between
* similar notes).
*
* @return true if is a Tie, false otherwise
*/
public boolean isTie ()
{
return tie;
}
//--------------------//
// resetLeftExtension //
//--------------------//
/**
* Reset to null the left extension of this slur.
*/
public void resetLeftExtension ()
{
leftExtension = null;
}
//---------------------//
// resetRightExtension //
//---------------------//
/**
* Reset to null the right extension of this slur.
*/
public void resetRightExtension ()
{
rightExtension = null;
}
//---------------------//
// getTranslationLinks //
//---------------------//
@Override
public List<Line2D> getTranslationLinks (Glyph glyph)
{
List<Line2D> links = new ArrayList<>();
if (leftNote != null) {
Point to = leftNote.getReferencePoint();
links.add(new Line2D.Double(curve.getP1(), to));
}
if (rightNote != null) {
Point to = rightNote.getReferencePoint();
links.add(new Line2D.Double(curve.getP2(), to));
}
return links;
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder();
if (tie) {
sb.append("{Tie");
} else {
sb.append("{Slur");
}
try {
sb.append("#").append(glyph.getId());
sb.append(" P1[").append((int) Math.rint(curve.getX1())).append(",").
append((int) Math.rint(curve.getY1())).append("]");
sb.append(" C1[").append((int) Math.rint(curve.getCtrlX1())).append(
",").append((int) Math.rint(curve.getCtrlY1())).append("]");
sb.append(" C2[").append((int) Math.rint(curve.getCtrlX2())).append(
",").append((int) Math.rint(curve.getCtrlY2())).append("]");
sb.append(" P2[").append((int) Math.rint(curve.getX2())).append(",").
append((int) Math.rint(curve.getY2())).append("]");
if (leftNote != null) {
sb.append(" L=").append(leftNote.getStep()).append(leftNote.
getOctave());
} else if ((leftExtension != null)
&& (leftExtension.leftNote != null)) {
sb.append(" LE=").append(leftExtension.leftNote.getStep()).
append(leftExtension.leftNote.getOctave());
}
if (rightNote != null) {
sb.append(" R=").append(rightNote.getStep()).append(rightNote.
getOctave());
} else if ((rightExtension != null)
&& (rightExtension.rightNote != null)) {
sb.append(" RE=").append(rightExtension.rightNote.getStep()).
append(rightExtension.rightNote.getOctave());
}
} catch (NullPointerException e) {
sb.append(" INVALID");
}
sb.append("}");
return sb.toString();
}
//-------------//
// filterNodes //
//-------------//
/**
* Keep in the provided collection of nodes the very first ones that
* cannot be separated via the normal node comparator.
*
* @param nodes the collection of nodes found in the neighborhood
* @param ref the reference point to compare distance from
*/
private static void filterNodes (List<MeasureNode> nodes,
Point ref)
{
if (nodes.size() > 1) {
NodeComparator comparator = new NodeComparator(ref);
Collections.sort(nodes, comparator);
// Keep only the minimum number of nodes
MeasureNode prevNode = null;
for (Iterator<MeasureNode> it = nodes.iterator(); it.hasNext();) {
MeasureNode currNode = it.next();
if (prevNode != null) {
if (comparator.compare(prevNode, currNode) != 0) {
// Discard this node
it.remove();
// And the following ones as well
for (; it.hasNext();) {
it.next();
it.remove();
}
return;
}
}
prevNode = currNode;
}
}
}
//----------------//
// haveSameHeight //
//----------------//
/**
* Check whether two notes represent the same pitch (same octave,
* same step, same alteration).
* This is needed to detects tie slurs.
*
* @param n1 one note
* @param n2 the other note
* @return true if the notes are equivalent.
*/
private static boolean haveSameHeight (Note n1,
Note n2)
{
return (n1 != null) && (n2 != null) && (n1.getStep() == n2.getStep())
&& (n1.getOctave() == n2.getOctave());
// TODO: what about alteration, if we have not processed them yet ???
}
//---------//
// isBelow //
//---------//
/**
* Report whether the provided curve is below the notes (turned
* upwards) or above the notes (turned downwards).
*
* @param curve the provided curve to check
* @return true if below, false if above
*/
private static boolean isBelow (CubicCurve2D curve)
{
// Determine arc orientation (above or below)
final double DX = curve.getX2() - curve.getX1();
final double DY = curve.getY2() - curve.getY1();
final double power = (curve.getCtrlX1() * DY)
- (curve.getCtrlY1() * DX) - (curve.getX1() * DY)
+ (curve.getY1() * DX);
return power < 0;
}
//------------------//
// isCompatibleWith //
//------------------//
/**
* Check whether two slurs to-be-connected are roughly compatible
* with each other (same staff id, and pitch positions not too
* different).
*
* @param prevSlur the previous slur
* @return true if found compatible
*/
private boolean isCompatibleWith (Slur prevSlur)
{
// Retrieve prev staff, using the left point of the prev slur
Staff prevStaff = prevSlur.getPart().getStaffAt(
new Point(
(int) prevSlur.curve.getX1(),
(int) prevSlur.curve.getY1()));
// Retrieve staff, using the right point of the slur
Staff staff = getPart().getStaffAt(
new Point((int) curve.getX2(), (int) curve.getY2()));
if (prevStaff.getId() != staff.getId()) {
logger.debug(
"prevSlur#{} prevStaff:{} slur#{} staff:{} different staff id",
prevSlur.getId(), prevStaff.getId(), getId(), staff.getId());
return false;
}
// Retrieve prev position, using the right point of the prev slur
double prevPp = prevStaff.pitchPositionOf(
new Point(
(int) prevSlur.curve.getX2(),
(int) prevSlur.curve.getY2()));
// Retrieve position, using the left point of the slur
Point pt = new Point(
(int) curve.getX1(),
(int) curve.getY1());
double pp = staff.pitchPositionOf(pt);
// Compare staves and pitch positions (very roughly)
double deltaPitch = pp - prevPp;
boolean res = Math.abs(deltaPitch) <= (constants.maxDeltaY.getValue() * 2);
logger.debug("prevSlur#{} slur#{} deltaPitch:{} res:{}",
prevSlur.getId(), this.getId(), deltaPitch, res);
return res;
}
//-----------------------//
// retrieveEmbracedNotes //
//-----------------------//
/**
* Retrieve the notes that are embraced on the left side and on the
* right side of a slur glyph.
*
* @param system the containing system
* @param curve the slur underlying curve
* @param leftNodes output: the ordered list of notes found on left side
* @param rightNodes output: the ordered list of notes found on right side
* @return true if the placement is 'below'
*/
private static boolean retrieveEmbracedNotes (Glyph glyph,
ScoreSystem system,
CubicCurve2D curve,
List<MeasureNode> leftNodes,
List<MeasureNode> rightNodes)
{
boolean below = isBelow(curve);
// Determine left and right search areas
final Scale scale = system.getScale();
final int dx = scale.toPixels(constants.areaDx);
final int dy = scale.toPixels(constants.areaDy);
final int xMg = scale.toPixels(constants.areaXMargin);
final Rectangle leftRect = new Rectangle(
(int) Math.rint(curve.getX1() - dx),
(int) Math.rint(curve.getY1()),
dx + xMg,
dy);
final Rectangle rightRect = new Rectangle(
(int) Math.rint(curve.getX2() - xMg),
(int) Math.rint(curve.getY2()),
dx + xMg,
dy);
if (below) {
leftRect.y -= dy;
rightRect.y -= dy;
}
// Visualize these rectangles (for visual debug)
glyph.addAttachment("|^", leftRect);
glyph.addAttachment("^|", rightRect);
// System > Part > Measure > Chord > Note
for (TreeNode pNode : system.getParts()) {
SystemPart part = (SystemPart) pNode;
for (TreeNode mNode : part.getMeasures()) {
Measure measure = (Measure) mNode;
for (TreeNode cNode : measure.getChords()) {
Chord chord = (Chord) cNode;
if (!chord.getNotes().isEmpty()) {
if (leftRect.contains(chord.getTailLocation())) {
leftNodes.add(chord);
}
if (rightRect.contains(chord.getTailLocation())) {
rightNodes.add(chord);
}
for (TreeNode nNode : chord.getNotes()) {
Note note = (Note) nNode;
if (leftRect.contains(note.getCenter())) {
leftNodes.add(note);
}
if (rightRect.contains(note.getCenter())) {
rightNodes.add(note);
}
}
}
}
}
}
// Sort the collections of nodes, and keep only the closest ones
filterNodes(
leftNodes,
new Point((int) curve.getX1(), (int) curve.getY1()));
filterNodes(
rightNodes,
new Point((int) curve.getX2(), (int) curve.getY2()));
return below;
}
//-------//
// isTie //
//-------//
private boolean isTie (Note leftNote,
Note rightNote)
{
if (!haveSameHeight(leftNote, rightNote)) {
return false;
}
// Check that we are not embracing several chords of the same bean group
Chord leftChord = leftNote.getChord();
BeamGroup leftGroup = leftChord.getBeamGroup();
Chord rightChord = rightNote.getChord();
BeamGroup rightGroup = rightChord.getBeamGroup();
if (leftGroup != null && leftGroup == rightGroup) {
return false;
}
return true;
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Scale.Fraction areaDx = new Scale.Fraction(
2,
"Abscissa extension when looking for embraced notes");
Scale.Fraction areaDy = new Scale.Fraction(
5.5,
"Ordinate extension when looking for embraced notes");
Scale.Fraction areaXMargin = new Scale.Fraction(
0.7,
"Abscissa inside margin when looking for embraced notes");
Scale.Fraction maxDeltaY = new Scale.Fraction(
4,
"Maximum difference in interlines between connecting slurs");
}
//-----//
// End //
//-----//
/**
* Note information on one end of a slur.
*/
private static class End
{
//~ Instance fields ----------------------------------------------------
// The precise note embraced by the slur on this side
final Note note;
// The related chord stem direction
final int stemDir;
//~ Constructors -------------------------------------------------------
public End (MeasureNode node)
{
if (node instanceof Note) {
note = (Note) node;
} else {
Chord chord = (Chord) node;
// Take the last note (closest to the tail)
note = (Note) chord.getNotes().get(chord.getNotes().size() - 1);
}
stemDir = note.getChord().getStemDir();
}
}
//----------------//
// NodeComparator //
//----------------//
/**
* Class {@code NodeComparator} implements a Node comparator, where
* nodes are sorted according to the ordinate of the left point
* (whether its'a Note or a chord tail location, from top to bottom).
*/
private static final class NodeComparator
implements Comparator<MeasureNode>,
Serializable
{
//~ Instance fields ----------------------------------------------------
final Point ref;
//~ Constructors -------------------------------------------------------
public NodeComparator (Point ref)
{
this.ref = ref;
}
//~ Methods ------------------------------------------------------------
@Override
public int compare (MeasureNode n1,
MeasureNode n2)
{
Point p1 = (n1 instanceof Chord)
? ((Chord) n1).getTailLocation() : n1.getCenter();
Point p2 = (n2 instanceof Chord)
? ((Chord) n2).getTailLocation() : n2.getCenter();
return Double.compare(p1.distance(ref), p2.distance(ref));
}
}
}