//----------------------------------------------------------------------------//
// //
// R u n s T a b l e //
// //
//----------------------------------------------------------------------------//
// <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.run;
import omr.selection.LocationEvent;
import omr.selection.MouseMovement;
import omr.selection.RunEvent;
import omr.selection.SelectionHint;
import omr.selection.SelectionService;
import omr.util.Predicate;
import org.bushe.swing.event.EventSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Class {@code RunsTable} handles a rectangular assembly of oriented
* runs.
*
* @author Hervé Bitteur
*/
public class RunsTable
implements Cloneable,
PixelSource,
Oriented,
EventSubscriber<LocationEvent>
{
//~ Static fields/initializers ---------------------------------------------
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(RunsTable.class);
/** Events that can be published on the table run service */
public static final Class<?>[] eventsWritten = new Class<?>[]{RunEvent.class};
/** Events observed on location service */
public static final Class<?>[] eventsRead = new Class<?>[]{LocationEvent.class};
//~ Instance fields --------------------------------------------------------
/** (Debugging) name of this runs table */
private final String name;
/** Orientation, the same for this table and all contained runs */
private final Orientation orientation;
/** Absolute dimension of the table */
private final Dimension dimension;
/** List of Runs found in each row. This is a list of lists of Runs */
private final List<List<Run>> runs;
/** Hosted event service for UI events related to this table (Runs) */
private final SelectionService runService;
//~ Constructors -----------------------------------------------------------
//-----------//
// RunsTable //
//-----------//
/**
* Creates a new RunsTable object.
*
* @param name name for debugging
* @param orientation orientation of each run
* @param dimension absolute dimensions of the table (width is horizontal,
* height is vertical)
*/
public RunsTable (String name,
Orientation orientation,
Dimension dimension)
{
this.name = name;
this.orientation = orientation;
this.dimension = dimension;
runService = new SelectionService(name, eventsWritten);
// Allocate the runs, according to orientation
Rectangle rect = orientation.oriented(
new Rectangle(0, 0, dimension.width, dimension.height));
// Prepare the collections of runs, one collection per pos value
runs = new ArrayList<>(rect.height);
for (int i = 0; i < rect.height; i++) {
runs.add(new ArrayList<Run>());
}
}
//~ Methods ----------------------------------------------------------------
//------//
// copy //
//------//
/**
* Make a copy of the table, but sharing the run instances
*
* @return another table on the same run instances
*/
public RunsTable copy ()
{
return copy(name + "(copy)");
}
//-------//
// copy //
//-------//
/**
* Make a copy of the table, but sharing the run instances
*
* @param name a new name for the copy
* @return another table on the same run instances
*/
public RunsTable copy (String name)
{
RunsTable clone = new RunsTable(name, orientation, dimension);
for (int i = 0; i < getSize(); i++) {
List<Run> seq = getSequence(i);
List<Run> cloneSeq = clone.getSequence(i);
for (Run run : seq) {
cloneSeq.add(run);
}
}
return clone;
}
//--------//
// dumpOf //
//--------//
/**
* Report the image of the runs table.
*/
public String dumpOf ()
{
StringBuilder sb = new StringBuilder();
sb.append(String.format("%s%n", this));
// Prepare output buffer
PixelsBuffer buffer = getBuffer();
// Print the buffer
sb.append('+');
for (int c = 0; c < dimension.width; c++) {
sb.append('=');
}
sb.append(String.format("+%n"));
for (int row = 0; row < dimension.height; row++) {
sb.append('|');
for (int col = 0; col < buffer.getWidth(); col++) {
sb.append((buffer.getPixel(col, row) == BACKGROUND) ? '-' : 'X');
}
sb.append(String.format("|%n"));
}
sb.append('+');
for (int c = 0; c < dimension.width; c++) {
sb.append('=');
}
sb.append(String.format("+%n"));
return sb.toString();
}
//----------//
// getPixel //
//----------//
/**
* {@inheritDoc}
*
* <br><b>Beware</b>, this implementation is not efficient enough
* for bulk operations.
* For such needs, a much more efficient way is to first
* retrieve a full buffer, via {@link #getBuffer()} method, then use this
* temporary buffer as the {@link PixelSource} instead of this table.
*
* @param x absolute abscissa
* @param y absolute ordinate
* @return the pixel gray level
*/
@Override
public final int getPixel (int x,
int y)
{
Run run = getRunAt(x, y);
return (run != null) ? run.getLevel() : BACKGROUND;
}
//----------//
// getRunAt //
//----------//
/**
* Report the run found at given coordinates, if any.
*
* @param x absolute abscissa
* @param y absolute ordinate
* @return the run found, or null otherwise
*/
public final Run getRunAt (int x,
int y)
{
Point oPt = orientation.oriented(new Point(x, y));
// Protection
if ((oPt.y < 0) || (oPt.y >= runs.size())) {
return null;
}
List<Run> seq = getSequence(oPt.y);
for (Run run : seq) {
if (run.getStart() > oPt.x) {
return null;
}
if (run.getStop() >= oPt.x) {
return run;
}
}
return null;
}
//-----------//
// getBuffer //
//-----------//
/**
* Fill a rectangular buffer with the runs
*
* @return the filled buffer
*/
public PixelsBuffer getBuffer ()
{
// Prepare output buffer
PixelsBuffer buffer = new PixelsBuffer(dimension);
switch (orientation) {
case HORIZONTAL:
for (int row = 0; row < getSize(); row++) {
List<Run> seq = getSequence(row);
for (Run run : seq) {
for (int c = run.getStart(); c <= run.getStop(); c++) {
buffer.setPixel(c, row, (char) 0);
}
}
}
break;
case VERTICAL:
for (int row = 0; row < getSize(); row++) {
List<Run> seq = getSequence(row);
for (Run run : seq) {
for (int col = run.getStart(); col <= run.getStop();
col++) {
buffer.setPixel(row, col, (char) 0);
}
}
}
break;
}
return buffer;
}
//-------------//
// getSequence //
//-------------//
/**
* Report the sequence of runs at a given index
*
* @param index the desired index
* @return the MODIFIABLE sequence of rows
*/
public final List<Run> getSequence (int index)
{
return runs.get(index);
}
//---------//
// getSize //
//---------//
/**
* Report the number of sequences of runs in the table
*
* @return the table size (in terms of sequences)
*/
public final int getSize ()
{
return runs.size();
}
//--------------//
// getDimension //
//--------------//
/**
* Report the absolute dimension of the table, width along x axis
* and height along the y axis.
*
* @return the absolute dimension
*/
public Dimension getDimension ()
{
return new Dimension(dimension);
}
//-----------//
// getHeight //
//-----------//
@Override
public int getHeight ()
{
return dimension.height;
}
//---------//
// getName //
//---------//
/**
* @return the name
*/
public String getName ()
{
return name;
}
//----------------//
// getOrientation //
//----------------//
/**
* @return the orientation of the runs
*/
@Override
public Orientation getOrientation ()
{
return orientation;
}
//-------------//
// getRunCount //
//-------------//
/**
* Count and return the total number of runs in this table
*
* @return the run count
*/
public int getRunCount ()
{
int runCount = 0;
for (List<Run> seq : runs) {
for (Run run : seq) {
runCount += run.getLength();
}
}
return runCount;
}
//-------------//
// getRunStats //
//-------------//
/**
* print tabular information about runs that interest us
*
* @return string of breakdown by row
*/
public void printRunStats(int lowLimitRunOfInterest) {
int[] stats = new int[dimension.height];
int seqPos = 0;
String output = "\nRuns of => " + lowLimitRunOfInterest + "\n"
+ "Delta Y\tY\tX\tLength\n";
int lastSeqPos = 0;
for (List<Run> seq : runs) {
seqPos++;
for (Run run : seq) {
if (run.getLength() > lowLimitRunOfInterest - 1){
int deltaY = seqPos - lastSeqPos;
lastSeqPos = seqPos;
output += deltaY + "\t" + seqPos
+ "\t" + run.getStart() + "\t"
+ run.getLength() + "\n";
}
}
}
output += "\n";
logger.info(output);
}
//-------------//
// getRunLengthStats //
//-------------//
/**
* Count and return the occurrences of various run lengths
*
* @return string of breakdown by row
*/
public void printRunLengthStats(int lowLimitRunOfInterest) {
int[] stats = new int[dimension.height];
String output = "";
for (List<Run> seq : runs) {
for (Run run : seq) {
stats[run.getLength()]++;
}
}
for (int i = 0; i < stats.length; i++){
if ( i > lowLimitRunOfInterest - 1 && stats[i] > 0){
output += i + "\t" + stats[i] + "\n";
}
}
logger.info(output);
}
//-------------//
// getRunCount //
//-------------//
/**
* Count and return the total number of runs in this table AND
* break down my row
*
* @return string of breakdown by row
*/
public String getRunDispersion() {
String output = "row#\tCount\tTotal Pixels\tAverage"
+ "\tLongest Run\tShortest Run"
+ "\tStart\tEnd\tDiff\n";
int seqCount = 0;
for (List<Run> seq : runs) {
int runCount = 0;
int totalPixels = 0;
int maxRun = 0;
int minRun = 0;
int startRuns = 0;
int endRuns = 0;
seqCount++;
for (Run run : seq) {
runCount++;
if (startRuns == 0){
// the first run may be the leftmost.
startRuns = run.getStart();
} else {
// something later may be more leftmost
if (run.getStart() < startRuns){
startRuns = run.getStart();
}
}
if (run.getStop() > endRuns) {
endRuns = run.getStop();
}
totalPixels += run.getLength();
if (run.getLength() > maxRun){
maxRun = run.getLength();
}
// if we only have 1 run, min will remain at 0'
// Only with 2 or more unequal runs will minLength change
if (run.getLength() < maxRun){
minRun = run.getLength();
}
}
if (runCount > 0){
double avgLength = ((double) totalPixels)/runCount;
output += seqCount + "\t" + runCount
+ "\t" + totalPixels
+ "\t" + String.valueOf(avgLength)
+ "\t" + maxRun
+ "\t" + minRun
+ "\t" + startRuns
+ "\t" + endRuns
+ "\t" + (endRuns - startRuns) + "\n";
}
}
return output + "\n";
}
//---------------//
// getRunService //
//---------------//
/**
* Report the table run selection service
*
* @return the run selection service
*/
public SelectionService getRunService ()
{
return runService;
}
//----------//
// getWidth //
//----------//
@Override
public int getWidth ()
{
return dimension.width;
}
//---------//
// include //
//---------//
/**
* Include the content of the provided table into this one
*
* @param that the table of runs to include into this one
*/
public void include (RunsTable that)
{
if (that == null) {
throw new IllegalArgumentException(
"Cannot include a null runsTable");
}
if (that.orientation != orientation) {
throw new IllegalArgumentException(
"Cannot include a runsTable of different orientation");
}
if (!that.dimension.equals(dimension)) {
throw new IllegalArgumentException(
"Cannot include a runsTable of different dimension");
}
for (int row = 0; row < getSize(); row++) {
List<Run> thisSeq = this.getSequence(row);
List<Run> thatSeq = that.getSequence(row);
for (Run thatRun : thatSeq) {
int start = thatRun.getStart();
int iRun = 0;
for (; iRun < thisSeq.size(); iRun++) {
Run thisRun = thisSeq.get(iRun);
if (thisRun.getStart() > start) {
break;
}
}
thisSeq.add(iRun, thatRun);
}
}
}
//-------------//
// isIdentical //
//-------------//
/**
* Field by field comparison (TODO: used by unit tests only!)
*
* @param that the other RunsTable to compare with
* @return true if identical
*/
public boolean isIdentical (RunsTable that)
{
// Check null entities
if (that == null) {
return false;
}
if ((this.orientation == that.orientation)
&& this.dimension.equals(that.dimension)) {
// Check runs
for (int row = 0; row < getSize(); row++) {
List<Run> thisSeq = getSequence(row);
List<Run> thatSeq = that.getSequence(row);
if (thisSeq.size() != thatSeq.size()) {
return false;
}
for (int iRun = 0; iRun < thisSeq.size(); iRun++) {
Run thisRun = thisSeq.get(iRun);
Run thatRun = thatSeq.get(iRun);
if (!thisRun.isIdentical(thatRun)) {
return false;
}
}
}
return true;
} else {
return false;
}
}
//-----------//
// lookupRun //
//-----------//
/**
* Given an absolute point, retrieve the containing run if any
*
* @param point coordinates of the given point
* @return the run found, or null otherwise
*/
public Run lookupRun (Point point)
{
Point oPt = orientation.oriented(point);
if ((oPt.y < 0) || (oPt.y >= getSize())) {
return null;
}
for (Run run : getSequence(oPt.y)) {
if (run.getStart() > oPt.x) {
return null;
}
if (run.getStop() >= oPt.x) {
return run;
}
}
return null;
}
//---------//
// onEvent //
//---------//
/**
* Interest on Location => Run
*
* @param locationEvent the interesting event
*/
@Override
public void onEvent (LocationEvent locationEvent)
{
try {
// Ignore RELEASING
if (locationEvent.movement == MouseMovement.RELEASING) {
return;
}
logger.debug("RunsTable {}: {}", name, locationEvent);
if (locationEvent instanceof LocationEvent) {
// Location => Run
handleEvent(locationEvent);
}
} catch (Exception ex) {
logger.warn(getClass().getName() + " onEvent error", ex);
}
}
//-------//
// purge //
//-------//
/**
* Purge a runs table of all runs that match the provided predicate
*
* @param predicate the filter to detect runs to remove
* @return this runs table, to allow easy chaining
*/
public RunsTable purge (Predicate<Run> predicate)
{
return purge(predicate, null);
}
//-------//
// purge //
//-------//
/**
* Purge a runs table of all runs that match the provided predicate, and
* populate the provided 'removed' table with the removed runs.
*
* @param predicate the filter to detect runs to remove
* @param removed a table to be filled, if not null, with purged runs
* @return this runs table, to allow easy chaining
*/
public RunsTable purge (Predicate<Run> predicate,
RunsTable removed)
{
// Check parameters
if (removed != null) {
if (removed.orientation != orientation) {
throw new IllegalArgumentException(
"'removed' table is of different orientation");
}
if (!removed.dimension.equals(dimension)) {
throw new IllegalArgumentException(
"'removed' table is of different dimension");
}
}
for (int i = 0; i < getSize(); i++) {
List<Run> seq = getSequence(i);
for (Iterator<Run> it = seq.iterator(); it.hasNext();) {
Run run = it.next();
if (predicate.check(run)) {
it.remove();
if (removed != null) {
removed.getSequence(i).add(run);
}
}
}
}
return this;
}
//-----------//
// removeRun //
//-----------//
/**
* Remove the provided run at indicated position
*
* @param pos the position where run is to be found
* @param run the run to remove
*/
public void removeRun (int pos,
Run run)
{
List<Run> seq = getSequence(pos);
if (!seq.remove(run)) {
throw new RuntimeException(
this + " Cannot find " + run + " at pos " + pos);
}
}
//--------------------//
// setLocationService //
//--------------------//
public void setLocationService (SelectionService locationService)
{
for (Class<?> eventClass : eventsRead) {
locationService.subscribeStrongly(eventClass, this);
}
}
//--------------------//
// cutLocationService //
//--------------------//
public void cutLocationService (SelectionService locationService)
{
for (Class<?> eventClass : eventsRead) {
locationService.unsubscribe(eventClass, this);
}
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder("{");
sb.append(getClass().getSimpleName());
sb.append(" ").append(name);
sb.append(" ").append(orientation);
sb.append(" ").append(dimension.width).append("x").append(
dimension.height);
// Debug
if (false) {
int count = 0;
for (List<Run> seq : runs) {
count += seq.size();
}
sb.append(" count:").append(count);
}
sb.append("}");
return sb.toString();
}
//-------------//
// handleEvent //
//-------------//
/**
* Interest in location => Run
*
* @param location
*/
private void handleEvent (LocationEvent locationEvent)
{
Rectangle rect = locationEvent.getData();
if (rect == null) {
return;
}
SelectionHint hint = locationEvent.hint;
MouseMovement movement = locationEvent.movement;
if (!hint.isLocation() && !hint.isContext()) {
return;
}
if ((rect.width == 0) && (rect.height == 0)) {
Point pt = rect.getLocation();
// Publish Run information
Run run = getRunAt(pt.x, pt.y);
runService.publish(new RunEvent(this, hint, movement, run));
}
}
}