package com.xenoage.zong.musiclayout.spacing;
import com.xenoage.utils.annotations.Optimized;
import com.xenoage.utils.kernel.Range;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.Time;
import com.xenoage.zong.musiclayout.SLP;
import lombok.Getter;
import java.util.List;
import static com.xenoage.utils.NullUtils.notNull;
import static com.xenoage.utils.annotations.Optimized.Reason.Performance;
import static com.xenoage.utils.collections.CollectionUtils.getFirst;
import static com.xenoage.utils.collections.CollectionUtils.getLast;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.zong.core.position.MP.*;
/**
* The horizontal and vertical spacing of a system.
*
* It contains the indices of the first and last measure of the system, the widths of all
* measure columns, the width of the system (which may be longer than the width used
* by the measures), the distances between the staffStampings and the vertical offset of the system.
*
* @author Andreas Wenger
*/
@Getter
public class SystemSpacing {
/** The list of the spacings of the measure columns in this system. */
public List<ColumnSpacing> columns;
/** The left margin of the system in mm. */
public float marginLeftMm;
/** The right margin of the system in mm. */
public float marginRightMm;
/** The width of the system (without the horizontal offset).
* It may be longer than the used width, e.g. to create empty staves.
* To get the used width, call {@link #getUsedWidth()}. */
public float widthMm;
/** The vertical spacing of the staves. */
public StavesSpacing staves;
/** The vertical offset of the system in mm, relative to the top. */
public float offsetYMm;
/** Backward reference to the frame. */
public FrameSpacing parentFrame = null;
//the horizontal offsets of the columns
@Optimized(Performance) private float[] cacheColumnsXMm;
public SystemSpacing(List<ColumnSpacing> columnSpacings, float marginLeftMm, float marginRightMm,
float widthMm, StavesSpacing staves, float offsetYMm) {
this.columns = columnSpacings;
this.marginLeftMm = marginLeftMm;
this.marginRightMm = marginRightMm;
this.widthMm = widthMm;
this.staves = staves;
this.offsetYMm = offsetYMm;
onColumnsWidthChange();
//set backward references
for (ColumnSpacing column : columnSpacings)
column.parentSystem = this;
}
/**
* Gets the height of the staff with the given index.
*/
public float getStaffHeightMm(int staff) {
return staves.getStaffHeightMm(staff);
}
/**
* Gets the distance between the previous and the given staff.
*/
public float getStaffDistanceMm(int staff) {
return staves.getStaffDistanceMm(staff);
}
/**
* Gets the total height of this system in mm.
*/
public float getHeightMm() {
return staves.getTotalHeightMm();
}
/**
* Gets the used width of the system.
*/
public float getUsedWidth() {
float usedWidth = 0;
for (ColumnSpacing mcs : columns)
usedWidth += mcs.getWidthMm();
return usedWidth;
}
public int getStartMeasure() {
return getFirst(columns).measureIndex;
}
public int getEndMeasure() {
return getLast(columns).measureIndex;
}
public boolean containsMeasure(int scoreMeasure) {
return getStartMeasure() <= scoreMeasure && scoreMeasure <= getEndMeasure();
}
public Range getMeasures() {
return range(getStartMeasure(), getEndMeasure());
}
public ColumnSpacing getColumn(int scoreMeasure) {
return columns.get(scoreMeasure - getStartMeasure());
}
public int getSystemIndexInFrame() {
return parentFrame.systems.indexOf(this);
}
/**
* Gets the start position in mm of the measure with the given global index
* relative to the beginning of the system.
*/
public float getMeasureStartMm(int scoreMeasure) {
float x = 0;
for (int iMeasure : range(scoreMeasure - getStartMeasure()))
x += columns.get(iMeasure).getWidthMm();
return x;
}
/**
* Gets the end position of the leading spacing in mm of the measure with the given
* global index, relative to the beginning of the system.
* If there is no leading spacing, this value is equal to {@link #getMeasureStartMm(int)}
*/
public float getMeasureStartAfterLeadingMm(int scoreMeasure) {
int systemMeasure = scoreMeasure - getStartMeasure();
return getMeasureStartMm(scoreMeasure) + columns.get(systemMeasure).getLeadingWidthMm();
}
/**
* Gets the end position in mm of the measure with the given global index
* relative to the beginning of the system.
*/
public float getMeasureEndMm(int scoreMeasure) {
int systemMeasure = scoreMeasure - getStartMeasure();
return getMeasureStartMm(scoreMeasure) + columns.get(systemMeasure).getWidthMm();
}
/**
* Gets the {@link MP} at the given horizontal position in mm.
* If the given staff is {@link MP#unknown}, all beats of the column
* are used, otherwise only the beats used by the given staff.
*
* If it is between two beats (which will be true almost ever), the
* the right mark is selected (like it is usual e.g. in text
* processing applications). If it is behind all known beats of the
* hit measure, the last known beat is returned.
*
* If it is not within the boundaries of a measure, {@link MP#unknownMp} is returned.
*/
public MP getMpAt(float xMm, int staff) {
//find the measure
int measureIndex = getSystemMeasureIndexAt(xMm);
float xMmInMeasure = xMm - getMeasureStartMm(measureIndex);
//when measure was not found, return null
if (measureIndex == unknown)
return unknownMp;
//get the beat at the given position
Fraction beat = columns.get(measureIndex).getBeatAt(xMmInMeasure, staff);
return atBeat(staff, measureIndex, unknown, beat);
}
/**
* Gets the horizontal position in mm, relative to the beginning of the staff,
* of the given time.
* If the given beat is after the last beat, the offset of the last beat is returned.
*/
public float getXMmAt(Time time) {
float measureXMm = getMeasureStartMm(time.measure);
float elementXMm = columns.get(time.measure - getStartMeasure()).getXMmAt(time.beat);
return measureXMm + elementXMm;
}
/**
* Gets the interline space of the staff with the given index.
*/
public float getInterlineSpace(int staff) {
return notNull(staves.getStaves().get(staff).getInterlineSpace(), staves.getDefaultIs());
}
/**
* Gets the system-relative index of the measure at the given position in mm,
* or {@link MP#unknown} if there is none.
*/
private int getSystemMeasureIndexAt(float xMm) {
if (xMm < 0)
return unknown;
float x = 0;
for (int iMeasure : range(columns)) {
x += columns.get(iMeasure).getWidthMm();
if (xMm < x)
return iMeasure;
}
return unknown;
}
/**
* Gets the vertical offset of the given staff in mm in frame space.
*/
public float getStaffYOffsetMm(int staff) {
return offsetYMm + staves.getStaffYOffsetMm(staff);
}
/**
* Computes and returns the y-coordinate in mm in frame space
* of an object on the given {@link SLP}.
*/
public float getYMm(SLP slp) {
return offsetYMm + staves.getYMm(slp);
}
/**
* Computes and returns the y-coordinate of an object in the given staff
* at the given vertical position in mm in frame space as a line position.
*/
public float getLp(int staff, float mm) {
return staves.getLp(staff, mm - offsetYMm);
}
/**
* Call this method when the width of a column has been changed.
*/
public void onColumnsWidthChange() {
//recompute cache
if (cacheColumnsXMm == null || cacheColumnsXMm.length != columns.size())
cacheColumnsXMm = new float[columns.size()];
float xMm = 0;
for (int measure : range(columns)) {
ColumnSpacing column = columns.get(measure);
cacheColumnsXMm[measure] = xMm;
xMm += column.getWidthMm();
}
}
/**
* Gets the horizontal offset in mm of the measure with the given global index.
*/
public float getColumnXMm(int scoreMeasure) {
return cacheColumnsXMm[scoreMeasure - getStartMeasure()];
}
}