package com.xenoage.zong.musiclayout.spacer.voice;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.collections.CollectionUtils.getLast;
import static com.xenoage.utils.kernel.Range.range;
import static com.xenoage.utils.kernel.Range.rangeReverse;
import static com.xenoage.utils.math.Fraction.fr;
import java.util.ArrayList;
import java.util.List;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.musiclayout.spacing.BeatOffset;
import com.xenoage.zong.musiclayout.spacing.ElementSpacing;
import com.xenoage.zong.musiclayout.spacing.VoiceSpacing;
/**
* Computes the {@link VoiceSpacing}s for all voices in a measure column,
* based on the individual optimal {@link VoiceSpacing}s and the
* common {@link BeatOffset}s of this measure column.
*
* @author Andreas Wenger
*/
public class AlignedVoicesSpacer {
public static final AlignedVoicesSpacer alignedVoicesSpacer = new AlignedVoicesSpacer();
/**
* Modifies the given {@link VoiceSpacing}, based on the given beat offsets.
*/
public void compute(VoiceSpacing voiceSpacing, List<BeatOffset> beatOffsets) {
List<ElementSpacing> spacingElements = voiceSpacing.elements;
if (spacingElements.size() == 0 || beatOffsets.size() == 0)
return;
//find the given beats, that are also used here
List<BeatOffset> sharedBeats = computeSharedBeats(spacingElements, beatOffsets);
//interpolate positions between the shared beats
float lastGivenBeatPosition = 0;
float lastEndElementPosition = 0;
int firstElement = 0;
int lastElement = -1;
float interlineSpace = voiceSpacing.interlineSpace;
for (int iGivenBeat : range(sharedBeats)) {
//for each given beat: find elements before or at that beat
for (int iElement : range(lastElement + 1, spacingElements.size() - 1)) {
if (spacingElements.get(iElement).beat.compareTo(sharedBeats.get(iGivenBeat).getBeat()) > 0)
break;
lastElement++;
}
if (lastElement == -1)
break;
//compute horizontal positions and distances of the
//given beat offsets and the current voice spacing, from the
//last shared beat up to the current shared beat.
float currentGivenBeatPosition = sharedBeats.get(iGivenBeat).getOffsetMm() / interlineSpace; //we calculate in interline spaces here
float givenBeatsDistance = currentGivenBeatPosition - lastGivenBeatPosition;
float currentEndElementPosition = spacingElements.get(lastElement).xIs;
float elementsDistance = currentEndElementPosition - lastEndElementPosition;
//interpolate the offsets of the current voice spacing
//between the last shared beat and the current shared beat.
//do this in reverse order, because the position of grace notes is dependent
//on the position of the (following) main note
float lastOriginalOffsetIs = getLast(beatOffsets).offsetMm / interlineSpace;
for (int iElement : rangeReverse(lastElement, firstElement)) {
ElementSpacing e = spacingElements.get(iElement);
if (false == e.isGrace()) {
//normal element: interpolate position
float currentElementOffset = e.xIs - lastEndElementPosition;
float newElementOffset;
if (elementsDistance != 0) {
newElementOffset = currentElementOffset / elementsDistance * givenBeatsDistance; //scale offset according to the given beats distance
}
else {
newElementOffset = currentGivenBeatPosition;
}
lastOriginalOffsetIs = e.xIs;
e.xIs = newElementOffset + lastGivenBeatPosition;
}
else {
//grace element: same distance to the main note as before
float distanceBefore = lastOriginalOffsetIs - e.xIs;
lastOriginalOffsetIs = e.xIs;
e.xIs = spacingElements.get(iElement + 1).xIs - distanceBefore;
}
}
//next range up to next shared beat
firstElement = lastElement + 1;
if (firstElement >= spacingElements.size())
break;
lastGivenBeatPosition = currentGivenBeatPosition;
lastEndElementPosition = currentEndElementPosition;
}
}
/**
* Returns the shared beats of the given {@link ElementSpacing}s and {@link BeatOffset}s.
* If there are no beats used by both lists, an empty list is returned.
*/
public List<BeatOffset> computeSharedBeats(List<ElementSpacing> spacingElements, List<BeatOffset> beatOffsets) {
ArrayList<BeatOffset> ret = alist(Math.min(spacingElements.size(), beatOffsets.size()));
int iElement = 0, iBeat = 0;
Fraction lastAddedBeat = fr(-1);
while (iElement < spacingElements.size() && iBeat < beatOffsets.size()) {
Fraction elementBeat = spacingElements.get(iElement).beat;
BeatOffset beatOffset = beatOffsets.get(iBeat);
if (elementBeat.equals(beatOffset.beat)) {
if (beatOffset.beat.equals(lastAddedBeat)) {
//when this beat was already added, replace it. the
//rightmost offset is the offset we need.
ret.set(ret.size() - 1, beatOffset);
}
else {
ret.add(beatOffset);
}
lastAddedBeat = beatOffset.beat;
iElement++;
}
else if (elementBeat.compareTo(beatOffset.beat) > 0) {
iBeat++;
}
else {
iElement++;
}
}
return ret;
}
}