package com.xenoage.zong.musiclayout.spacing;
import com.xenoage.utils.annotations.MaybeNull;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.musiclayout.notation.Notation;
import lombok.Getter;
import java.util.List;
import static com.xenoage.utils.collections.CollectionUtils.getLast;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.zong.core.position.MP.unknown;
/**
* The horizontal spacing for one measure column.
*
* It contains a list of the offsets of each multiused
* beat in the measure, that is beat 0,
* the beats that are used by more than one voice
* (or by one voice, if it is the only one), and
* the beat at the end of the measure.
*
* It also contains the spacings of all elements in
* the measures and its voices.
*
* The units are measured in mm, since the staves may
* have different heights.
*
* @author Andreas Wenger
*/
@Getter
public class ColumnSpacing {
/** The global measure index. */
public int measureIndex;
/** The list of measure spacings.
* Eeach staff of the measure has its own spacing. */
public List<MeasureSpacing> measures;
/** The offsets of the relevant beats (notes and rests) of this measure in mm. */
public List<BeatOffset> beatOffsets;
/** The barline offsets of this measure in mm.
* At least the position of the start barline and the end barline
* is available, and mid-measure barlines may also be included. */
public List<BeatOffset> barlineOffsets;
/** Backward reference to the system. */
public SystemSpacing parentSystem = null;
public ColumnSpacing(int measureIndex, List<MeasureSpacing> measureSpacings,
List<BeatOffset> beatOffsets, List<BeatOffset> barlineOffsets) {
this.measureIndex = measureIndex;
this.measures = measureSpacings;
this.beatOffsets = beatOffsets;
this.barlineOffsets = barlineOffsets;
//set backward references
for (MeasureSpacing measure : measureSpacings)
measure.column = this;
}
/**
* Gets the width of the measure in mm.
* This is the width of the leading spacing plus the width of the voices.
*/
public float getWidthMm() {
return getLeadingWidthMm() + getVoicesWidthMm();
}
/**
* Gets the width of the leading spacing in mm.
* If there is no leading spacing, 0 is returned. TODO
*/
public float getLeadingWidthMm() {
float ret = 0; //TODO
//find the maximum width of the leading spacings
//of each staff
for (int iStaff : range(measures)) {
MeasureSpacing measure = measures.get(iStaff);
LeadingSpacing leading = measure.leading;
if (leading != null) {
float width = leading.widthIs * measure.interlineSpace;
if (width > ret)
ret = width;
}
}
return ret;
}
/**
* Gets the width of the voices space in mm.
*/
public float getVoicesWidthMm() {
return barlineOffsets.get(barlineOffsets.size() - 1).offsetMm;
}
/**
* Gets the offset for the given beat, or null if unknown.
*/
public BeatOffset getBeatOffset(Fraction beat) {
for (BeatOffset beatOffset : beatOffsets) {
if (beatOffset.getBeat().equals(beat))
return beatOffset;
}
return null;
}
/**
* Gets the offset in mm of the barline at the given beat, or 0 if unknown.
*/
public float getBarlineOffsetMm(Fraction beat) {
for (BeatOffset bo : barlineOffsets) {
if (bo.getBeat().equals(beat))
return bo.getOffsetMm();
}
return 0;
}
/**
* Gets the VoiceSpacing at the given staff and voice.
*/
public VoiceSpacing getVoice(int staff, int voice) {
return measures.get(staff).voices.get(voice);
}
/**
* Convenience method to get the {@link ElementSpacing} of the given
* {@link VoiceElement} {@link Notation} in this column.
*/
public ElementSpacing getElement(Notation notation) {
MP mp = notation.getMp();
VoiceSpacing voice = getVoice(mp.staff, mp.voice);
return voice.getElement((VoiceElement) notation.getElement());
}
/**
* Gets the beat at the given horizontal position in mm,
* relative to the beginning of the measure.
*
* If it is between two beats (which will be true almost ever), the
* the beat nearest to the position is selected (like it is usual e.g. in text
* processing applications). If it is behind all known beats,
* the last known beat is returned.
*/
public Fraction getBeatAt(float xMm) {
//find beat and return it
BeatOffset last = null;
for (BeatOffset bo : beatOffsets) {
if (xMm <= bo.offsetMm)
return getBeatAt(xMm, last, bo);
last = bo;
}
//return last beat
return getLast(beatOffsets).beat;
}
/**
* Like {@link #getBeatAt(float)}, but only for a single staff,
* i.e. unused beats in this staff are ignored.
*
* When the staff is {@link MP#unknown}, this method works like
* {@link #getBeatAt(float)}.
*/
public Fraction getBeatAt(float xMm, int staff) {
if (staff == unknown)
return getBeatAt(xMm);
//find beat and return it
MeasureSpacing measure = measures.get(staff);
BeatOffset last = null;
for (Fraction beat : measure.usedBeats) {
BeatOffset bo = getBeatOffset(beat);
if (xMm <= bo.offsetMm)
return getBeatAt(xMm, last, bo);
last = bo;
}
//return last beat
return getLast(beatOffsets).beat;
}
/**
* Returns the beat at the given position from the candidate,
* which position is nearer to the given position.
*/
private Fraction getBeatAt(float xMm, @MaybeNull BeatOffset leftCandidate, BeatOffset rightCandidate) {
if (leftCandidate == null)
return rightCandidate.beat;
//check previous or given candidate
boolean right = (xMm >= (leftCandidate.offsetMm + rightCandidate.offsetMm) / 2);
return (right ? rightCandidate : leftCandidate).beat;
}
/**
* Gets the horizontal position in mm, relative to the beginning of the measure,
* of the given beat.
* If the given beat is after the last beat, the offset of the last beat is returned.
*/
public float getXMmAt(Fraction beat) {
//find offset and return it
for (BeatOffset bo : beatOffsets) {
if (beat.compareTo(bo.beat) <= 0)
return bo.offsetMm;
}
//return last offset
return getLast(beatOffsets).offsetMm;
}
}