package com.xenoage.zong.core.music.beam;
import com.xenoage.utils.annotations.NonNull;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.music.MusicElementType;
import com.xenoage.zong.core.music.WaypointPosition;
import com.xenoage.zong.core.music.chord.Chord;
import com.xenoage.zong.core.music.util.DurationInfo;
import com.xenoage.zong.core.position.MP;
import com.xenoage.zong.core.position.MPContainer;
import com.xenoage.zong.core.position.MPElement;
import lombok.Getter;
import java.util.List;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.collections.CollectionUtils.getFirst;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.math.MathUtils.min;
import static java.lang.Math.max;
/**
* Class for a beam that connects two or more chords.
*
* A beam can be placed within a single staff (the common case) or
* cross two staves (e.g. in a piano score). When crossing two staves,
* the beam belongs to the staff of the first chord.
*
* @author Andreas Wenger
*/
public final class Beam
implements MPElement {
/** Spread of this beam within a system. */
public enum VerticalSpan {
/** Beam within a single staff. */
SingleStaff,
/** Beam crossing two adjacent staves. */
CrossStaff,
/** Other span (not supported). */
Other;
}
/** The waypoints in this beam. */
@NonNull @Getter private List<BeamWaypoint> waypoints;
//cache
private VerticalSpan verticalSpan = null;
private int upperStaffIndex = -1;
private int lowerStaffIndex = -1;
/**
* Creates a new beam consisting of the given waypoints.
* All waypoints must be in the same measure column and must be sorted by beat.
*/
public static Beam beam(List<BeamWaypoint> waypoints) {
Beam beam = new Beam(waypoints);
beam.check();
return beam;
}
/**
* Creates a new beam consisting of the given chords with no subdivisions.
*/
public static Beam beamFromChords(List<Chord> chords) {
Beam ret = beamFromChordsUnchecked(chords);
ret.check();
return ret;
}
/**
* Creates a new beam consisting of the given chords with no subdivisions.
* For testing: No parameter checks!
*/
public static Beam beamFromChordsUnchecked(List<Chord> chords) {
List<BeamWaypoint> waypoints = alist(chords.size());
for (Chord chord : chords) {
waypoints.add(new BeamWaypoint(chord, false));
}
return new Beam(waypoints);
}
private Beam(List<BeamWaypoint> waypoints) {
this.waypoints = waypoints;
}
/**
* Checks the correctness of the beam:
* The beam must have at least one line
* It must have at least 2 chords, must exist in a single measure column
* and the chords must be sorted by beat.
*/
private void check() {
if (getMaxLinesCount() == 0)
throw new IllegalArgumentException("A beam must have at least one line");
if (waypoints.size() < 2)
throw new IllegalArgumentException("At least two chords are needed to create a beam!");
Fraction lastBeat = null;
boolean wasLastChordGrace = false;
int measure = getFirst(waypoints).getChord().getMP().measure;
for (BeamWaypoint wp : waypoints) {
MP mp = wp.getChord().getMP();
//check, if all chords are in the same measure column
if (mp.measure != measure)
throw new IllegalArgumentException("A beam may only span over one measure column");
//check, if chords are sorted by beat.
//for grace notes, the same beat is ok.
if (lastBeat != null) {
int compare = mp.beat.compareTo(lastBeat);
if ((false == wasLastChordGrace && compare <= 0) ||
(wasLastChordGrace && compare < 0))
throw new IllegalArgumentException("Beamed chords must be sorted by beat");
}
lastBeat = mp.beat;
wasLastChordGrace = wp.getChord().isGrace();
}
}
public int size() {
return waypoints.size();
}
public BeamWaypoint getStart() {
return waypoints.get(0);
}
public BeamWaypoint getStop() {
return waypoints.get(waypoints.size() - 1);
}
/**
* Gets the position of the given waypoint.
*/
public WaypointPosition getWaypointPosition(BeamWaypoint wp) {
return getWaypointPosition(wp.getChord());
}
/**
* Gets the position of the given chord: Start, Stop or Continue.
*/
public WaypointPosition getWaypointPosition(Chord chord) {
int index = getWaypointIndex(chord);
if (index == 0)
return WaypointPosition.Start;
else if (index == waypoints.size() - 1)
return WaypointPosition.Stop;
else
return WaypointPosition.Continue;
}
/**
* Gets the index of the given chord within the beam.
*/
public int getWaypointIndex(Chord chord) {
for (int i : range(waypoints))
if (chord == waypoints.get(i).getChord())
return i;
throw new IllegalArgumentException("Given chord is not part of this beam.");
}
/**
* Gets the vertical spanning of this beam.
*/
public VerticalSpan getVerticalSpan() {
if (verticalSpan == null)
computeSpan();
return verticalSpan;
}
/**
* Gets the index of the topmost staff this beam belongs to.
*/
public int getUpperStaffIndex() {
if (upperStaffIndex == -1)
computeSpan();
return upperStaffIndex;
}
/**
* Gets the index of the bottommost staff this beam belongs to.
*/
public int getLowerStaffIndex() {
if (lowerStaffIndex == -1)
computeSpan();
return lowerStaffIndex;
}
/**
* Replaces the given old chord with the given new one.
*/
public void replaceChord(Chord oldChord, Chord newChord) {
for (int i : range(waypoints)) {
if (waypoints.get(i).getChord() == oldChord) {
waypoints.get(i).setChord(newChord);
computeSpan();
return;
}
}
throw new IllegalArgumentException("Given chord is not part of this beam");
}
/**
* Returns true, if a beam lines subdivision ends at the chord
* with the given index.
*/
public boolean isEndOfSubdivision(int chordIndex) {
return waypoints.get(chordIndex).isSubdivision();
}
/**
* Gets the chord with the given index.
*/
public Chord getChord(int chordIndex) {
return waypoints.get(chordIndex).getChord();
}
/**
* Computes the vertical span of this beam.
*/
private void computeSpan() {
int minStaffIndex = Integer.MAX_VALUE;
int maxStaffIndex = Integer.MIN_VALUE;
//check if the beam spans over a single staff or two adjacent staves or more
for (BeamWaypoint waypoint : waypoints) {
Chord chord = waypoint.getChord();
MP mpChord = MP.getMP(chord);
minStaffIndex = Math.min(minStaffIndex, mpChord.staff);
maxStaffIndex = max(maxStaffIndex, mpChord.staff);
}
VerticalSpan verticalSpan = VerticalSpan.Other;
if (maxStaffIndex == minStaffIndex)
verticalSpan = VerticalSpan.SingleStaff;
else if (maxStaffIndex - minStaffIndex == 1)
verticalSpan = VerticalSpan.CrossStaff;
this.verticalSpan = verticalSpan;
this.upperStaffIndex = minStaffIndex;
this.lowerStaffIndex = maxStaffIndex;
}
/**
* Gets the maximum number of beam lines used in the this beam.
*/
public int getMaxLinesCount() {
Fraction minDuration = null;
for (BeamWaypoint waypoint : waypoints)
minDuration = min(minDuration, waypoint.getChord().getDisplayedDuration());
return DurationInfo.getFlagsCount(minDuration);
}
@Override public MusicElementType getMusicElementType() {
return MusicElementType.Beam;
}
/**
* The parent of the beam is defined as the voice of the start of the beam.
*/
@Override public MPContainer getParent() {
return getFirst(waypoints).getChord().getParent();
}
/**
* The MP of the beam is the same as the MP of the first chord in the beam.
*/
@Override public MP getMP() {
return getFirst(waypoints).getChord().getMP();
}
}