//----------------------------------------------------------------------------// // // // L i n e C l u s t e 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.grid; import omr.glyph.Glyphs; import omr.lag.Section; import omr.run.Orientation; import omr.util.GeoUtil; import omr.util.Vip; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; /** * Class {@code LineCluster} is meant to aggregate instances of * {@link Filament} that are linked by {@link FilamentComb} instances * and thus a cluster represents a staff candidate. * * @author Hervé Bitteur */ public class LineCluster implements Vip { //~ Static fields/initializers --------------------------------------------- /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(LineCluster.class); /** For comparing LineCluster instances on their true length */ public static final Comparator<LineCluster> reverseLengthComparator = new Comparator<LineCluster>() { @Override public int compare (LineCluster c1, LineCluster c2) { // Sort on reverse length return Double.compare(c2.getTrueLength(), c1.getTrueLength()); } }; //~ Instance fields -------------------------------------------------------- /** Id for debug */ private final int id; /** Interline for this cluster */ private final int interline; /** Reference to cluster this one has been included into, if any */ private LineCluster parent; /** Composing lines, ordered by their relative position (ordinate) */ private SortedMap<Integer, FilamentLine> lines; /** (Cached) bounding box of this cluster */ private Rectangle contourBox; /** CLuster true length */ private Integer trueLength; /** For debugging */ private boolean vip = false; //~ Constructors ----------------------------------------------------------- //-------------// // LineCluster // //-------------// /** * Creates a new LineCluster object. * * @param seed the first filament of the cluster */ public LineCluster (int interline, LineFilament seed) { if (logger.isDebugEnabled() || seed.isVip()) { logger.info("Creating cluster with F{}", seed.getId()); if (seed.isVip()) { setVip(); } } this.interline = interline; this.id = seed.getId(); lines = new TreeMap<>(); include(seed, 0); } //~ Methods ---------------------------------------------------------------- //---------// // destroy // //---------// /** * Remove the link back from filaments to this cluster. */ public void destroy () { for (FilamentLine line : lines.values()) { line.fil.setCluster(null, 0); line.fil.getCombs().clear(); } } //-------------// // getAncestor // //-------------// /** * Report the top ancestor of this cluster. * * @return the cluster ancestor */ public LineCluster getAncestor () { LineCluster cluster = this; while (cluster.parent != null) { cluster = cluster.parent; } return cluster; } //-----------// // getBounds // //-----------// public Rectangle getBounds () { if (contourBox == null) { Rectangle box = null; for (FilamentLine line : getLines()) { if (box == null) { box = new Rectangle(line.getBounds()); } else { box.add(line.getBounds()); } } contourBox = box; } if (contourBox != null) { return new Rectangle(contourBox); } else { return null; } } //-----------// // getCenter // //-----------// /** * Report the center of cluster. * * @return the center */ public Point getCenter () { Rectangle box = getBounds(); return new Point( box.x + (box.width / 2), box.y + (box.height / 2)); } //--------------// // getFirstLine // //--------------// public FilamentLine getFirstLine () { return lines.get(lines.firstKey()); } //-------// // getId // //-------// /** * @return the id */ public int getId () { return id; } //--------------// // getInterline // //--------------// /** * @return the interline */ public int getInterline () { return interline; } //-------------// // getLastLine // //-------------// public FilamentLine getLastLine () { return lines.get(lines.lastKey()); } //----------// // getLines // //----------// public Collection<FilamentLine> getLines () { return lines.values(); } //-----------// // getParent // //-----------// /** * @return the parent */ public LineCluster getParent () { return parent; } //-------------// // getPointsAt // //-------------// /** * Report the sequence of points that correspond to a provided * abscissa. * * @param x the provided abscissa * @param xMargin maximum abscissa margin for horizontal extrapolation * @param interline the standard interline value, used for vertical * extrapolations * @return the sequence of cluster points, from top to bottom, with perhaps * some holes indicated by null values */ public List<Point2D> getPointsAt (double x, int xMargin, int interline, double globalSlope) { SortedMap<Integer, Point2D> points = new TreeMap<>(); List<Integer> holes = new ArrayList<>(); for (Entry<Integer, FilamentLine> entry : lines.entrySet()) { int pos = entry.getKey(); FilamentLine line = entry.getValue(); if (line.isWithinRange(x)) { points.put( pos, new Point2D.Double(x, line.yAt(x))); } else { holes.add(pos); } } // Interpolate or extrapolate the missing values if any for (int pos : holes) { Integer prevPos = null; Double prevVal = null; for (int p = pos - 1; p >= lines.firstKey(); p--) { Point2D pt = points.get(p); if (pt != null) { prevPos = p; prevVal = pt.getY(); break; } } Integer nextPos = null; Double nextVal = null; for (int p = pos + 1; p <= lines.lastKey(); p++) { Point2D pt = points.get(p); if (pt != null) { nextPos = p; nextVal = pt.getY(); break; } } Double y = null; // Interpolate vertically if ((prevPos != null) && (nextPos != null)) { y = prevVal + (((pos - prevPos) * (nextVal - prevVal)) / (nextPos - prevPos)); } else { // Extrapolate vertically, only for one interline max if ((prevPos != null) && ((pos - prevPos) == 1)) { y = prevVal + interline; } else if ((nextPos != null) && ((nextPos - pos) == 1)) { y = nextVal - interline; } else { // Extrapolate horizontally on a short distance FilamentLine line = lines.get(pos); Point2D point = (x <= line.getStartPoint().getX()) ? line.getStartPoint() : line.getStopPoint(); double dx = x - point.getX(); if (Math.abs(dx) <= xMargin) { y = point.getY() + (dx * globalSlope); } } } points.put(pos, (y != null) ? new Point2D.Double(x, y) : null); } return new ArrayList<>(points.values()); } //---------// // getSize // //---------// public int getSize () { return lines.size(); } //-----------// // getStarts // //-----------// public List<Point2D> getStarts () { List<Point2D> points = new ArrayList<>(getSize()); for (FilamentLine line : lines.values()) { points.add(line.getStartPoint()); } return points; } //----------// // getStops // //----------// public List<Point2D> getStops () { List<Point2D> points = new ArrayList<>(getSize()); for (FilamentLine line : lines.values()) { points.add(line.getStopPoint()); } return points; } //---------------// // getTrueLength // //---------------// /** * Report a measurement of the cluster length. * * @return the mean true length of cluster lines */ public int getTrueLength () { if (trueLength == null) { // Determine mean true line length in this cluster int meanTrueLength = 0; for (FilamentLine line : lines.values()) { meanTrueLength += line.fil.trueLength(); } meanTrueLength /= lines.size(); logger.debug("TrueLength: {} for {}", meanTrueLength, this); trueLength = meanTrueLength; } return trueLength; } //------------------------// // includeFilamentByIndex // //------------------------// /** * Include a filament to this cluster, using the provided relative * line index counted from zero (rather than the line position). * Check this room is "free" on the cluster line * * @param filament the filament to include * @param index the zero-based line index * @return true if there was room for inclusion */ public boolean includeFilamentByIndex (LineFilament filament, int index) { final Rectangle filBox = filament.getBounds(); int i = 0; for (Entry<Integer, FilamentLine> entry : lines.entrySet()) { if (i++ == index) { FilamentLine line = entry.getValue(); // Check for horizontal room // For filaments one above the other, check resulting thickness for (Section section : line.fil.getMembers()) { // Horizontal overlap? Rectangle sctBox = section.getBounds(); int overlap = GeoUtil.xOverlap(filBox, sctBox); if (overlap > 0) { // Check resulting thickness double thickness = Glyphs.getThicknessAt( Math.max(filBox.x, sctBox.x) + overlap / 2, Orientation.HORIZONTAL, filament, line.fil); if (thickness > line.fil.getScale().getMaxFore()) { if (filament.isVip() || logger.isDebugEnabled()) { logger.info("No room for {} in {}", filament, this); } return false; } } } line.add(filament); filament.setCluster(this, entry.getKey()); invalidateCache(); return true; } } return false; // Should not happen } //-------// // isVip // //-------// @Override public boolean isVip () { return vip; } //-----------// // mergeWith // //-----------// public void mergeWith (LineCluster that, int deltaPos) { include( that, deltaPos + (this.lines.firstKey() - that.lines.firstKey())); } //--------// // render // //--------// public void render (Graphics2D g) { for (FilamentLine line : lines.values()) { line.render(g); } } //---------------// // renumberLines // //---------------// /** * Renumber the remaining lines counting from zero. */ public void renumberLines () { // Renumbering int firstPos = lines.firstKey(); if (firstPos != 0) { SortedMap<Integer, FilamentLine> newLines = new TreeMap<>(); for (Entry<Integer, FilamentLine> entry : lines.entrySet()) { int pos = entry.getKey(); int newPos = pos - firstPos; FilamentLine line = entry.getValue(); line.fil.setCluster(this, newPos); newLines.put(newPos, new FilamentLine(line.fil)); } lines = newLines; } invalidateCache(); } // //-----------// // // Constants // // //-----------// // private static final class Constants // extends ConstantSet // { // //~ Instance fields ---------------------------------------------------- // // final Constant.Ratio minTrueLength = new Constant.Ratio( // 0.4, // "Minimum true length ratio to keep a line in a cluster"); // } //--------// // setVip // //--------// @Override public void setVip () { vip = true; } //----------// // toString // //----------// @Override public String toString () { StringBuilder sb = new StringBuilder("{Cluster#"); sb.append(getId()); sb.append(" interline:").append(getInterline()); sb.append(" size:").append(getSize()); for (Entry<Integer, FilamentLine> entry : lines.entrySet()) { sb.append(" ").append(entry.getValue()); } sb.append("}"); return sb.toString(); } //------// // trim // //------// /** * Remove lines in excess. * * @param count the target line count */ public void trim (int count) { logger.debug("Trim {}", this); // // Determine max true line length in this cluster // int maxTrueLength = 0; // // for (FilamentLine line : lines.values()) { // maxTrueLength = Math.max(maxTrueLength, line.fil.trueLength()); // } // // int minTrueLength = (int) Math.rint( // maxTrueLength * constants.minTrueLength.getValue()); // // // Pruning // for (Iterator<Integer> it = lines.keySet() // .iterator(); it.hasNext();) { // Integer key = it.next(); // FilamentLine line = lines.get(key); // // if (line.fil.trueLength() < minTrueLength) { // it.remove(); // line.fil.setCluster(null, 0); // line.fil.getCombs() // .clear(); // } // } // Pruning while (lines.size() > count) { // Remove the top or bottom line FilamentLine top = lines.get(lines.firstKey()); int topWL = top.fil.trueLength(); FilamentLine bot = lines.get(lines.lastKey()); int botWL = bot.fil.trueLength(); FilamentLine line = null; if (topWL < botWL) { line = top; lines.remove(lines.firstKey()); } else { line = bot; lines.remove(lines.lastKey()); } // House keeping line.fil.setCluster(null, 0); line.fil.getCombs().clear(); } renumberLines(); invalidateCache(); } //---------// // getLine // //---------// private FilamentLine getLine (int pos, LineFilament fil) { FilamentLine line = lines.get(pos); if (line == null) { line = new FilamentLine(fil); lines.put(pos, line); } return line; } //---------// // include // //---------// /** * Include a filament, with all its combs. * * @param pivot the filament to include * @param pivotPos the imposed position within the cluster */ private void include (LineFilament pivot, int pivotPos) { if (logger.isDebugEnabled() || pivot.isVip()) { logger.info("{} include pivot:{} at pos:{}", this, pivot.getId(), pivotPos); if (pivot.isVip()) { setVip(); } } LineFilament ancestor = (LineFilament) pivot.getAncestor(); // Loop on all combs that involve this filament for (FilamentComb comb : pivot.getCombs().values()) { if (comb.isProcessed()) { continue; } comb.setProcessed(true); int deltaPos = pivotPos - comb.getIndex(pivot); logger.debug("{} deltaPos:{}", comb, deltaPos); // Dispatch content of comb to proper lines for (int i = 0; i < comb.getCount(); i++) { LineFilament fil = (LineFilament) comb.getFilament(i). getAncestor(); LineCluster cluster = fil.getCluster(); if (cluster == null) { int pos = i + deltaPos; FilamentLine line = getLine(pos, null); line.add(fil); if (fil.isVip()) { logger.info("Adding {} to {} at pos {}", fil, this, pos); setVip(); } fil.setCluster(this, pos); if (fil != ancestor) { include(fil, pos); // Recursively } } else if (cluster.getAncestor() != this.getAncestor()) { // Need to merge the two clusters include(cluster, (i + deltaPos) - fil.getClusterPos()); } } } } //---------// // include // //---------// /** * Merge another cluster with this one. * * @param that the other cluster * @param deltaPos the delta to apply to that cluster positions */ private void include (LineCluster that, int deltaPos) { if (logger.isDebugEnabled() || isVip() || that.isVip()) { logger.info("Inclusion of {} into {} deltaPos:{}", that, this, deltaPos); if (that.isVip()) { setVip(); } } for (Entry<Integer, FilamentLine> entry : that.lines.entrySet()) { int pos = entry.getKey() + deltaPos; FilamentLine line = entry.getValue(); getLine(pos, null).include(line); } that.parent = this; if (logger.isDebugEnabled()) { logger.debug("Merged:{}", that); logger.debug("Merger:{}", this); } invalidateCache(); } //-----------------// // invalidateCache // //-----------------// private void invalidateCache () { contourBox = null; trueLength = null; } }