//----------------------------------------------------------------------------//
// //
// S t a f f I n f o //
// //
//----------------------------------------------------------------------------//
// <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.grid;
import omr.glyph.facets.Glyph;
import omr.glyph.ui.AttachmentHolder;
import omr.glyph.ui.BasicAttachmentHolder;
import omr.math.GeoPath;
import omr.math.LineUtil;
import omr.math.ReversePathIterator;
import omr.run.Orientation;
import omr.score.entity.Staff;
import omr.sheet.NotePosition;
import omr.sheet.Scale;
import omr.util.HorizontalSide;
import static omr.util.HorizontalSide.*;
import omr.util.VerticalSide;
import static omr.util.VerticalSide.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Class {@code StaffInfo} handles the physical informations of a staff
* with its lines.
* Note: All methods are meant to provide correct results, regardless of the
* actual number of lines in the staff instance.
*
* @author Hervé Bitteur
*/
public class StaffInfo
implements AttachmentHolder
{
//~ Static fields/initializers ---------------------------------------------
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(StaffInfo.class);
/** To sort by staff id. */
public static final Comparator<StaffInfo> byId = new Comparator<StaffInfo>()
{
@Override
public int compare (StaffInfo o1,
StaffInfo o2)
{
return Integer.compare(o1.id, o2.id);
}
};
//~ Instance fields --------------------------------------------------------
//
/** Sequence of the staff lines. (from top to bottom) */
private final List<LineInfo> lines;
/**
* Scale specific to this staff. [not used actually]
* (since different staves in a page may exhibit different scales)
*/
private Scale specificScale;
/** Top limit of staff related area. (left to right) */
private GeoPath topLimit = null;
/** Bottom limit of staff related area. (left to right) */
private GeoPath bottomLimit = null;
/** Staff id. counted from 1 within the sheet */
private final int id;
/** Information about left bar line. */
private BarInfo leftBar;
/** Left extrema. */
private double left;
/** Information about right bar line. */
private BarInfo rightBar;
/** Right extrema. */
private double right;
/** The area around the staff, lazily computed. */
private GeoPath area;
/** Map of ledgers nearby. */
private final Map<Integer, SortedSet<Glyph>> ledgerMap = new TreeMap<>();
/** Corresponding staff entity in the score hierarchy. */
private Staff scoreStaff;
/** Potential attachments. */
private AttachmentHolder attachments = new BasicAttachmentHolder();
//~ Constructors -----------------------------------------------------------
//
//-----------//
// StaffInfo //
//-----------//
/**
* Create info about a staff, with its contained staff lines.
*
* @param id the id of the staff
* @param left abscissa of the left side
* @param right abscissa of the right side
* @param specificScale specific scale detected for this staff
* @param lines the sequence of contained staff lines
*/
public StaffInfo (int id,
double left,
double right,
Scale specificScale,
List<LineInfo> lines)
{
this.id = id;
this.left = (int) Math.rint(left);
this.right = (int) Math.rint(right);
this.specificScale = specificScale;
this.lines = lines;
}
//~ Methods ----------------------------------------------------------------
//
//---------------//
// addAttachment //
//---------------//
@Override
public void addAttachment (String id,
Shape attachment)
{
attachments.addAttachment(id, attachment);
}
//-----------//
// addLedger //
//-----------//
/**
* Add a ledger to the collection (which is lazily created)
*
* @param ledger the ledger to add
* @param index the staff-based index for ledger line
*/
public void addLedger (Glyph ledger,
int index)
{
if (ledger == null) {
throw new IllegalArgumentException("Cannot register a null ledger");
}
SortedSet<Glyph> ledgerSet = ledgerMap.get(index);
if (ledgerSet == null) {
ledgerSet = new TreeSet<>(Glyph.byAbscissa);
ledgerMap.put(index, ledgerSet);
}
ledgerSet.add(ledger);
}
//-----------//
// addLedger //
//-----------//
/**
* Add a ledger to the collection, computing line index from
* glyph pitch position.
*
* @param ledger the ledger to add
*/
public void addLedger (Glyph ledger)
{
if (ledger == null) {
throw new IllegalArgumentException("Cannot register a null ledger");
}
addLedger(ledger,
getLedgerLineIndex(pitchPositionOf(ledger.getCentroid())));
}
//--------------//
// removeLedger //
//--------------//
/**
* Remove a legder from staff collection.
*
* @param ledger the ledger to remove
* @return true if actually removed, false if not found
*/
public boolean removeLedger (Glyph ledger)
{
if (ledger == null) {
throw new IllegalArgumentException("Cannot remove a null ledger");
}
// Browse all staff ledger indices
for (SortedSet<Glyph> ledgerSet : ledgerMap.values()) {
if (ledgerSet.remove(ledger)) {
return true;
}
}
// Not found
logger.debug("Could not find ledger {}", ledger.idString());
return false;
}
//------//
// dump //
//------//
/**
* A utility meant for debugging.
*/
public void dump ()
{
System.out.println(
"StaffInfo" + getId() + " left=" + left + " right=" + right);
int i = 0;
for (LineInfo line : lines) {
System.out.println(" LineInfo" + i++ + " " + line.toString());
}
}
//-------------//
// getAbscissa //
//-------------//
/**
* Report the staff abscissa, on the provided side.
*
* @param side provided side
* @return the staff abscissa
*/
public double getAbscissa (HorizontalSide side)
{
if (side == HorizontalSide.LEFT) {
return left;
} else {
return right;
}
}
//---------//
// getArea //
//---------//
/**
* Report the lazily computed area defined by the staff limits.
*
* @return the whole staff area
*/
public GeoPath getArea ()
{
if (area == null) {
area = new GeoPath();
area.append(topLimit, false);
area.append(
ReversePathIterator.getReversePathIterator(bottomLimit),
true);
area.closePath();
}
return area;
}
//---------------//
// getAreaBounds //
//---------------//
/**
* Report the bounding box of the staff area.
*
* @return the lazily computed bounding box
*/
public Rectangle2D getAreaBounds ()
{
return getArea().getBounds2D();
}
//----------------//
// getAttachments //
//----------------//
@Override
public Map<String, Shape> getAttachments ()
{
return attachments.getAttachments();
}
//--------//
// getBar //
//--------//
/**
* Report the barline, if any, on the provided side
*
* @param side proper horizontal side
* @return the bar on the provided side, if any
*/
public BarInfo getBar (HorizontalSide side)
{
if (side == HorizontalSide.LEFT) {
return leftBar;
} else {
return rightBar;
}
}
//------------------//
// getClosestLedger //
//------------------//
/**
* Report the closest ledger (if any) between provided point and
* this staff.
*
* @param point the provided point
* @return the closest ledger found, or null
*/
public IndexedLedger getClosestLedger (Point2D point)
{
IndexedLedger bestLedger = null;
double top = getFirstLine().yAt(point.getX());
double bottom = getLastLine().yAt(point.getX());
double rawPitch = (4.0d * ((2 * point.getY()) - bottom - top)) / (bottom
- top);
if (Math.abs(rawPitch) <= 5) {
return null;
}
int interline = specificScale.getInterline();
Rectangle2D searchBox;
if (rawPitch < 0) {
searchBox = new Rectangle2D.Double(
point.getX(),
point.getY(),
0,
top - point.getY() + 1);
} else {
searchBox = new Rectangle2D.Double(
point.getX(),
bottom,
0,
point.getY() - bottom + 1);
}
//searchBox.grow(interline, interline);
searchBox.setRect(
searchBox.getX() - interline,
searchBox.getY() - interline,
searchBox.getWidth() + (2 * interline),
searchBox.getHeight() + (2 * interline));
// Browse all staff ledgers
Set<IndexedLedger> foundLedgers = new HashSet<>();
for (Map.Entry<Integer, SortedSet<Glyph>> entry : ledgerMap.entrySet()) {
for (Glyph ledger : entry.getValue()) {
if (ledger.getBounds().intersects(searchBox)) {
foundLedgers.add(new IndexedLedger(ledger, entry.getKey()));
}
}
}
if (!foundLedgers.isEmpty()) {
// Use the closest ledger
double bestDist = Double.MAX_VALUE;
for (IndexedLedger iLedger : foundLedgers) {
Point2D center = iLedger.glyph.getAreaCenter();
double dist = Math.abs(center.getY() - point.getY());
if (dist < bestDist) {
bestDist = dist;
bestLedger = iLedger;
}
}
}
return bestLedger;
}
//----------------//
// getClosestLine //
//----------------//
/**
* Report the staff line which is closest to the provided point.
*
* @param point the provided point
* @return the closest line found
*/
public LineInfo getClosestLine (Point2D point)
{
double pos = pitchPositionOf(point);
int idx = (int) Math.rint((pos + (lines.size() - 1)) / 2);
if (idx < 0) {
idx = 0;
} else if (idx > (lines.size() - 1)) {
idx = lines.size() - 1;
}
return lines.get(idx);
}
//----------//
// getGapTo //
//----------//
/**
* Report the vertical gap between staff and the provided glyph.
*
* @param glyph the provided glyph
* @return 0 if the glyph intersects the staff, otherwise the vertical
* distance from staff to closest edge of the glyph
*/
public int getGapTo (Glyph glyph)
{
Point center = glyph.getAreaCenter();
int staffTop = getFirstLine().yAt(center.x);
int staffBot = getLastLine().yAt(center.x);
int glyphTop = glyph.getBounds().y;
int glyphBot = glyphTop + glyph.getBounds().height - 1;
// Check overlap
int top = Math.max(glyphTop, staffTop);
int bot = Math.min(glyphBot, staffBot);
if (top <= bot) {
return 0;
}
// No overlap, compute distance
int dist = Integer.MAX_VALUE;
dist = Math.min(dist, Math.abs(staffTop - glyphTop));
dist = Math.min(dist, Math.abs(staffTop - glyphBot));
dist = Math.min(dist, Math.abs(staffBot - glyphTop));
dist = Math.min(dist, Math.abs(staffBot - glyphBot));
return dist;
}
//----------------//
// getEndingSlope //
//----------------//
/**
* Report mean ending slope, on the provided side.
* We discard highest and lowest absolute slopes, and return the average
* values for the remaining ones.
*
* @param side which side to select (left or right)
* @return a "mean" value
*/
public double getEndingSlope (HorizontalSide side)
{
List<Double> slopes = new ArrayList<>(lines.size());
for (LineInfo l : lines) {
FilamentLine line = (FilamentLine) l;
slopes.add(line.getSlope(side));
}
Collections.sort(
slopes,
new Comparator<Double>()
{
@Override
public int compare (Double o1,
Double o2)
{
return Double.compare(Math.abs(o1), Math.abs(o2));
}
});
double sum = 0;
for (Double slope : slopes.subList(1, slopes.size() - 1)) {
sum += slope;
}
return sum / (slopes.size() - 2);
}
//--------------//
// getFirstLine //
//--------------//
/**
* Report the first line in the series.
*
* @return the first line
*/
public LineInfo getFirstLine ()
{
return lines.get(0);
}
//-----------//
// getHeight //
//-----------//
/**
* Report the mean height of the staff, between first and last line.
*
* @return the mean staff height
*/
public int getHeight ()
{
return getSpecificScale().getInterline() * (lines.size() - 1);
}
//-------//
// getId //
//-------//
/**
* Report the staff id, counted from 1 in the sheet.
*
* @return the staff id
*/
public int getId ()
{
return id;
}
//-------------//
// getLastLine //
//-------------//
/**
* Report the last line in the series.
*
* @return the last line
*/
public LineInfo getLastLine ()
{
return lines.get(lines.size() - 1);
}
//--------------//
// getLedgerMap //
//--------------//
public Map<Integer, SortedSet<Glyph>> getLedgerMap ()
{
return ledgerMap;
}
//------------//
// getLedgers //
//------------//
/**
* Report the ordered set of ledgers, if any, for a given pitch value.
*
* @param lineIndex the precise line index that specifies algebraic
* distance from staff
* @return the proper set of ledgers, or null
*/
public SortedSet<Glyph> getLedgers (int lineIndex)
{
return ledgerMap.get(lineIndex);
}
//-------------//
// getLimitAtX //
//-------------//
/**
* Report the precise ordinate of staff area limit, on the provided
* vertical side.
*
* @param side the provided vertical side
* @param x the provided abscissa
* @return the ordinate of staff limit
*/
public double getLimitAtX (VerticalSide side,
double x)
{
GeoPath limit = (side == TOP) ? topLimit : bottomLimit;
return limit.yAtX(x);
}
//----------//
// getLines //
//----------//
/**
* Report the sequence of lines.
*
* @return the list of lines in this staff
*/
public List<LineInfo> getLines ()
{
return lines;
}
//-------------//
// getLinesEnd //
//-------------//
/**
* Report the ending abscissa of the staff lines.
*
* @param side desired horizontal side
* @return the abscissa corresponding to lines extrema
*/
public double getLinesEnd (HorizontalSide side)
{
if (side == HorizontalSide.LEFT) {
double linesLeft = Integer.MAX_VALUE;
for (LineInfo line : lines) {
linesLeft = Math.min(linesLeft, line.getEndPoint(LEFT).getX());
}
return linesLeft;
} else {
double linesRight = Integer.MIN_VALUE;
for (LineInfo line : lines) {
linesRight = Math.max(
linesRight,
line.getEndPoint(RIGHT).getX());
}
return linesRight;
}
}
//----------------//
// getMidOrdinate //
//----------------//
/**
* Report an approximate ordinate of staff ending, on the provided
* horizontal side.
*
* @param side provided side
* @return the middle ordinate of staff ending
*/
public double getMidOrdinate (HorizontalSide side)
{
return (getFirstLine().getEndPoint(side).getY() + getLastLine().
getEndPoint(side).getY()) / 2;
}
//-----------------//
// getNotePosition //
//-----------------//
/**
* Report the precise position for a note-like entity with respect
* to this staff, taking ledgers (if any) into account.
*
* @param point the absolute location of the provided note
* @return the detailed note position
*/
public NotePosition getNotePosition (Point2D point)
{
double pitch = pitchPositionOf(point);
IndexedLedger bestLedger = null;
// If we are rather far from the staff, try getting help from ledgers
if (Math.abs(pitch) > lines.size()) {
bestLedger = getClosestLedger(point);
if (bestLedger != null) {
Point2D center = bestLedger.glyph.getAreaCenter();
int ledgerPitch = getLedgerPitchPosition(bestLedger.index);
double deltaPitch = (2d * (point.getY() - center.getY())) / specificScale.
getInterline();
pitch = ledgerPitch + deltaPitch;
}
}
return new NotePosition(this, pitch, bestLedger);
}
//------------------------//
// getLedgerPitchPosition //
//------------------------//
/**
* Report the pitch position of a ledger WRT the related staff
*
* @param lineIndex the ledger line index
* @return the ledger pitch position
*/
public static int getLedgerPitchPosition (int lineIndex)
{
// // Safer, for the time being...
// if (getStaff()
// .getLines()
// .size() != 5) {
// throw new RuntimeException("Only 5-line staves are supported");
// }
if (lineIndex > 0) {
return 4 + (2 * lineIndex);
} else {
return -4 + (2 * lineIndex);
}
}
//--------------------//
// getLedgerLineIndex //
//--------------------//
/**
* Compute staff-based line index, based on provided pitch position
*
* @param pitchPosition the provided pitch position
* @return the computed line index
*/
public static int getLedgerLineIndex (double pitchPosition)
{
if (pitchPosition > 0) {
return (int) Math.rint(pitchPosition / 2) - 2;
} else {
return (int) Math.rint(pitchPosition / 2) + 2;
}
}
//---------------//
// getScoreStaff //
//---------------//
/**
* Report the related score staff entity.
*
* @return the corresponding scoreStaff
*/
public Staff getScoreStaff ()
{
return scoreStaff;
}
//------------------//
// getSpecificScale //
//------------------//
/**
* Report the <b>specific</b> staff scale, which may have a
* different interline value than the page average.
*
* @return the staff scale
*/
public Scale getSpecificScale ()
{
if (specificScale != null) {
// Return the specific scale of this staff
return specificScale;
} else {
// Return the scale of the sheet
logger.warn("No specific scale available");
return null;
}
}
//--------------//
// intersection //
//--------------//
/**
* Report the approximate point where a provided vertical stick
* crosses this staff.
*
* @param stick the rather vertical stick
* @return the crossing point
*/
public Point2D intersection (Glyph stick)
{
LineInfo midLine = lines.get(lines.size() / 2);
return LineUtil.intersection(
midLine.getEndPoint(LEFT),
midLine.getEndPoint(RIGHT),
stick.getStartPoint(Orientation.VERTICAL),
stick.getStopPoint(Orientation.VERTICAL));
}
//-----------------//
// pitchPositionOf //
//-----------------//
/**
* Compute an approximation of the pitch position of a pixel point,
* since it is based only on distance to staff, with no
* consideration for ledgers.
*
* @param pt the pixel point
* @return the pitch position
*/
public double pitchPositionOf (Point2D pt)
{
double top = getFirstLine().yAt(pt.getX());
double bottom = getLastLine().yAt(pt.getX());
return ((lines.size() - 1) * ((2 * pt.getY()) - bottom - top)) / (bottom
- top);
}
//-------------------//
// removeAttachments //
//-------------------//
@Override
public int removeAttachments (String prefix)
{
return attachments.removeAttachments(prefix);
}
//--------//
// render //
//--------//
/**
* Paint the staff lines.
*
* @param g the graphics context
* @return true if something has been actually drawn
*/
public boolean render (Graphics2D g)
{
LineInfo firstLine = getFirstLine();
LineInfo lastLine = getLastLine();
if ((firstLine != null) && (lastLine != null)) {
if (g.getClipBounds().intersects(getAreaBounds())) {
// Draw the left and right vertical lines
for (HorizontalSide side : HorizontalSide.values()) {
Point2D first = firstLine.getEndPoint(side);
Point2D last = lastLine.getEndPoint(side);
g.draw(new Line2D.Double(first, last));
}
// Draw each horizontal line in the set
for (LineInfo line : lines) {
line.render(g);
}
return true;
}
}
return false;
}
//-------------------//
// renderAttachments //
//-------------------//
@Override
public void renderAttachments (Graphics2D g)
{
attachments.renderAttachments(g);
}
//-------------//
// setAbscissa //
//-------------//
/**
* Set the staff abscissa of the provided side.
*
* @param side provided side
* @param val abscissa of staff end
*/
public void setAbscissa (HorizontalSide side,
double val)
{
if (side == HorizontalSide.LEFT) {
left = val;
} else {
right = val;
}
}
//--------//
// setBar //
//--------//
/**
* Set a barline on the provided side
*
* @param side proper horizontal side
* @param bar the bar to set
*/
public void setBar (HorizontalSide side,
BarInfo bar)
{
if (side == HorizontalSide.LEFT) {
this.leftBar = bar;
} else {
this.rightBar = bar;
}
}
//----------//
// setLimit //
//----------//
/**
* Define the limit of the staff area, on the provided vertical side.
*
* @param side proper vertical side
* @param limit assigned limit
*/
public void setLimit (VerticalSide side,
GeoPath limit)
{
logger.debug("staff#{} setLimit {} {}", id, side, limit);
if (side == TOP) {
topLimit = limit;
} else {
bottomLimit = limit;
}
// Invalidate area, so that it gets recomputed when needed
area = null;
}
//---------------//
// setScoreStaff //
//---------------//
/**
* Remember the related score staff entity.
*
* @param scoreStaff the corresponding scoreStaff to set
*/
public void setScoreStaff (Staff scoreStaff)
{
this.scoreStaff = scoreStaff;
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("{StaffInfo");
sb.append(" id=").append(getId());
sb.append(" left=").append((float) left);
sb.append(" right=").append((float) right);
if (specificScale != null) {
sb.append(" specificScale=").append(specificScale.getInterline());
}
if (leftBar != null) {
sb.append(" leftBar:").append(leftBar);
}
if (rightBar != null) {
sb.append(" rightBar:").append(rightBar);
}
sb.append("}");
return sb.toString();
}
//---------------//
// IndexedLedger //
//---------------//
public static class IndexedLedger
{
/** The ledger glyph. */
public final Glyph glyph;
/** Staff-based line index. (-1, -2, ... above, +1, +2, ... below) */
public final int index;
public IndexedLedger (Glyph ledger,
int index)
{
this.glyph = ledger;
this.index = index;
}
}
}