//----------------------------------------------------------------------------//
// //
// S l o t //
// //
//----------------------------------------------------------------------------//
// <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.math.InjectionSolver;
import omr.math.Population;
import omr.math.Rational;
import omr.util.Navigable;
import omr.util.TreeNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Class {@code Slot} represents a roughly defined time slot within a
* measure, to gather all chords that start at the same time.
*
* <p>On the diagram shown, slots are indicated by vertical blue lines.</p>
*
* <p>The slot embraces all the staves of its part measure. Perhaps we should
* consider merging slots between parts as well?
*
* <div style="float: right;">
* <img src="doc-files/Slot.png" alt="diagram">
* </div>
*
* @author Hervé Bitteur
*/
public class Slot
implements Comparable<Slot>
{
//~ Static fields/initializers ---------------------------------------------
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(Slot.class);
//~ Instance fields --------------------------------------------------------
//
/** The containing measure. */
@Navigable(false)
private Measure measure;
/** Id unique within the containing measure. */
private final int id;
/** Reference point of the slot. */
private Point refPoint;
/** Chords incoming into this slot, sorted by staff then ordinate. */
private List<Chord> incomings = new ArrayList<>();
/** Time offset since measure start. */
private Rational startTime;
//~ Constructors -----------------------------------------------------------
//
//------//
// Slot //
//------//
/**
* Creates a new Slot object.
*
* @param measure the containing measure
*/
public Slot (Measure measure)
{
this.measure = measure;
id = 1 + measure.getSlots().size();
}
//~ Methods ----------------------------------------------------------------
//
//------------//
// getMeasure //
//------------//
/**
* Report the measure that contains this slot
*
* @return the containing measure
*/
public Measure getMeasure ()
{
return measure;
}
//-----------------//
// dumpSystemSlots //
//-----------------//
public static void dumpSystemSlots (ScoreSystem system)
{
// Dump all measure slots
logger.debug(system.toString());
for (TreeNode node : system.getParts()) {
SystemPart part = (SystemPart) node;
logger.debug(part.toString());
for (TreeNode mn : part.getMeasures()) {
Measure measure = (Measure) mn;
logger.debug(measure.toString());
for (Slot slot : measure.getSlots()) {
logger.debug(slot.toString());
}
}
}
}
//-----------//
// setChords //
//-----------//
public void setChords (Collection<Chord> chords)
{
this.incomings.addAll(chords);
for (Chord chord : chords) {
chord.setSlot(this);
}
// Compute slot refPoint as average of chords centers
Population xPop = new Population();
Population yPop = new Population();
for (Chord chord : chords) {
Point center = chord.getCenter();
xPop.includeValue(center.x);
yPop.includeValue(center.y);
}
refPoint = new Point(
(int) Math.rint(xPop.getMeanValue()),
(int) Math.rint(yPop.getMeanValue()));
}
//-------------//
// buildVoices //
//-------------//
/**
* Compute the various voices in this slot.
*
* @param endingChords the chords that end right at this slot, with their
* voice not available because their group is continuing.
*/
public void buildVoices (List<Chord> endingChords)
{
logger.debug("endingChords={}", endingChords);
logger.debug("incomings={}", incomings);
// Sort chords vertically
Collections.sort(incomings, Chord.byOrdinate);
// Some chords already have the voice assigned
List<Chord> endings = new ArrayList<>(endingChords);
List<Chord> rookies = new ArrayList<>();
for (Chord ch : incomings) {
if (ch.getVoice() != null) {
// Needed to populate the voice slotTable
ch.setVoice(ch.getVoice());
// Remove the ending chord with the same voice
for (Iterator<Chord> it = endings.iterator(); it.hasNext();) {
Chord c = it.next();
if (c.getVoice() == ch.getVoice()) {
it.remove();
break;
}
}
} else {
rookies.add(ch);
}
}
// Nothing left to assign?
if (rookies.isEmpty()) {
return;
}
// Try to map some ending voices to some rookies
if (!endings.isEmpty()) {
InjectionSolver solver = new InjectionSolver(
rookies.size(),
endings.size() + rookies.size(),
new MyDistance(rookies, endings));
int[] links = solver.solve();
for (int i = 0; i < links.length; i++) {
int index = links[i];
// Map new chord to an ending chord?
if (index < endings.size()) {
Voice voice = endings.get(index).getVoice();
logger.debug("Slot#{} Reusing voice#{}",
getId(), voice.getId());
Chord ch = rookies.get(i);
try {
ch.setVoice(voice);
} catch (Exception ex) {
ch.addError("Failed to set voice of chord");
return;
}
}
}
}
// Assign remaining non-mapped chords, using first voice available
assignVoices();
}
//-----------//
// compareTo //
//-----------//
/**
* Compare this slot to another, as needed to insert slots in an
* ordered collection.
*
* @param other another slot
* @return -1, 0 or +1, according to their relative abscissae
*/
@Override
public int compareTo (Slot other)
{
return Integer.compare(getX(), other.getX());
}
//------//
// getX //
//------//
/**
* Report the abscissa of this slot.
*
* @return the slot abscissa (page-based, not measure-based)
*/
public int getX ()
{
return refPoint.x;
}
//-------------------//
// getChordJustAbove //
//-------------------//
/**
* Report the chord which is in staff just above the given point
* in this slot.
*
* @param point the given point
* @return the chord above, or null
*/
public Chord getChordJustAbove (Point point)
{
Chord chordAbove = null;
// Staff at or above point
Staff targetStaff = measure.getPart().getStaffJustAbove(point);
// We look for the chord just above
for (Chord chord : getChords()) {
Point head = chord.getHeadLocation();
if (head != null && head.y < point.y) {
if (chord.getStaff() == targetStaff) {
chordAbove = chord;
}
} else {
break; // Since slot chords are sorted from top to bottom
}
}
return chordAbove;
}
//-------------------//
// getChordJustBelow //
//-------------------//
/**
* Report the chord which is in staff just below the given point
* in this slot.
*
* @param point the given point
* @return the chord below, or null
*/
public Chord getChordJustBelow (Point point)
{
// Staff at or below
Staff targetStaff = measure.getPart().getStaffJustBelow(point);
// We look for the chord just below
for (Chord chord : getChords()) {
Point head = chord.getHeadLocation();
if (head != null && head.y > point.y
&& chord.getStaff() == targetStaff) {
return chord;
}
}
// Not found
return null;
}
//-----------//
// getChords //
//-----------//
/**
* Report the (sorted) collection of chords in this time slot.
*
* @return the collection of chords
*/
public List<Chord> getChords ()
{
return incomings;
}
//-------------------//
// getEmbracedChords //
//-------------------//
/**
* Report the chords whose notes stand in the given vertical range.
*
* @param top upper point of range
* @param bottom lower point of range
* @return the collection of chords, which may be empty
*/
public List<Chord> getEmbracedChords (Point top,
Point bottom)
{
List<Chord> embracedChords = new ArrayList<>();
for (Chord chord : getChords()) {
if (chord.isEmbracedBy(top, bottom)) {
embracedChords.add(chord);
}
}
return embracedChords;
}
//-------//
// getId //
//-------//
/**
* Report the slot Id.
*
* @return the slot id (for debug)
*/
public int getId ()
{
return id;
}
//--------------//
// getStartTime //
//--------------//
/**
* Report the time offset of this slot since the beginning of
* the measure.
*
* @return the time offset of this slot.
*/
public Rational getStartTime ()
{
return startTime;
}
//--------------//
// setStartTime //
//--------------//
/**
* Assign the startTime since the beginning of the measure,
* for all chords in this time slot.
*
* @param startTime time offset since measure start
*/
public void setStartTime (Rational startTime)
{
if (this.startTime == null) {
logger.debug("setStartTime {} for Slot #{}", startTime, getId());
this.startTime = startTime;
// Assign to all chords of this slot first
for (Chord chord : getChords()) {
chord.setStartTime(startTime);
}
// Then, extend this information through the beamed chords if any
for (Chord chord : getChords()) {
BeamGroup group = chord.getBeamGroup();
if (group != null) {
group.computeStartTimes();
}
}
// Update all voices
for (Voice voice : measure.getVoices()) {
voice.updateSlotTable();
}
} else {
if (!this.startTime.equals(startTime)) {
getChords().get(0).addError(
"Reassigning startTime from " + this.startTime + " to "
+ startTime + " in " + this);
}
}
}
//---------------//
// toChordString //
//---------------//
/**
* Report a slot description focused on the chords that start at
* the slot.
*
* @return slot with its incoming chords
*/
public String toChordString ()
{
StringBuilder sb = new StringBuilder();
sb.append("slot#").append(getId());
if (getStartTime() != null) {
sb.append(" start=").append(String.format("%5s", getStartTime()));
}
sb.append(" [");
boolean started = false;
for (Chord chord : getChords()) {
if (started) {
sb.append(",");
}
sb.append(chord);
started = true;
}
sb.append("]");
return sb.toString();
}
//----------//
// toString //
//----------//
@Override
public String toString ()
{
StringBuilder sb = new StringBuilder();
sb.append("{Slot#").append(id);
if (refPoint != null) {
sb.append(" x=").append(getX());
}
if (startTime != null) {
sb.append(" start=").append(startTime);
}
sb.append(" incomings=[");
for (Chord chord : incomings) {
sb.append("#").append(chord.getId());
}
sb.append("]");
sb.append("}");
return sb.toString();
}
//---------------//
// toVoiceString //
//---------------//
/**
* Report a slot description focused on intersected voices.
*
* @return slot with its voices
*/
public String toVoiceString ()
{
StringBuilder sb = new StringBuilder();
sb.append("slot#").append(getId()).append(" start=").append(String.
format("%5s", getStartTime())).append(" [");
SortedMap<Integer, Chord> voiceChords = new TreeMap<>();
for (Chord chord : getChords()) {
voiceChords.put(chord.getVoice().getId(), chord);
}
boolean started = false;
int voiceMax = measure.getVoicesNumber();
for (int iv = 1; iv <= voiceMax; iv++) {
if (started) {
sb.append(", ");
} else {
started = true;
}
Chord chord = voiceChords.get(iv);
if (chord != null) {
sb.append("V").append(chord.getVoice().getId());
sb.append(" Ch#").append(String.format("%02d", chord.getId()));
sb.append(" St").append(chord.getStaff().getId());
sb.append(" Dur=").append(String.format("%5s",
chord.getDuration()));
} else {
sb.append("----------------------");
}
}
sb.append("]");
return sb.toString();
}
//--------------//
// assignVoices //
//--------------//
/**
* Assign available voices to the chords that have yet no voice
* assigned.
*
* @param chords the collection of chords to process for this slot
*/
private void assignVoices ()
{
// Assign remaining non-mapped chords, using first voice available
// with staff continuity whenever possible
for (Chord chord : incomings) {
// Process only the chords that have no voice assigned yet
if (chord.getVoice() == null) {
// Try to reuse an existing voice
for (Voice voice : measure.getVoices()) {
if (voice.isFree(this)) {
// If we have more than one incoming,
// avoid migrating a voice from one staff to another
if (incomings.size() > 1) {
Chord latestVoiceChord = voice.getChordBefore(this);
if (latestVoiceChord != null
&& latestVoiceChord.getStaff() != chord.getStaff()) {
continue;
}
}
chord.setVoice(voice);
break;
}
}
// No compatible voice found, let's create a new one
if (chord.getVoice() == null) {
logger.debug("{} Slot#{} creating voice for Ch#{}",
chord.getContextString(), id, chord.getId());
// Add a new voice
new Voice(chord);
}
}
}
}
//~ Inner Classes ----------------------------------------------------------
//
//------------//
// MyDistance //
//------------//
private static final class MyDistance
implements InjectionSolver.Distance
{
//~ Static fields/initializers -----------------------------------------
private static final int NO_LINK = 20;
private static final int STAFF_DIFF = 40;
private static final int INCOMPATIBLE_VOICES = 10000; // Forbidden
//~ Instance fields ----------------------------------------------------
private final List<Chord> news;
private final List<Chord> olds;
//~ Constructors -------------------------------------------------------
public MyDistance (List<Chord> news,
List<Chord> olds)
{
this.news = news;
this.olds = olds;
}
//~ Methods ------------------------------------------------------------
@Override
public int getDistance (int in,
int ip)
{
// No link to an old chord
if (ip >= olds.size()) {
return NO_LINK;
}
Chord newChord = news.get(in);
Chord oldChord = olds.get(ip);
if ((newChord.getVoice() != null)
&& (oldChord.getVoice() != null)
&& (newChord.getVoice() != oldChord.getVoice())) {
return INCOMPATIBLE_VOICES;
} else if (newChord.getStaff() != oldChord.getStaff()) {
return STAFF_DIFF;
} else {
int dy = Math.abs(
newChord.getHeadLocation().y
- oldChord.getHeadLocation().y) / newChord.getScale().
getInterline();
int dStem = Math.abs(
newChord.getStemDir() - oldChord.getStemDir());
return dy + (2 * dStem);
}
}
}
}