package com.xenoage.zong.musiclayout.spacer.voice;
import static com.xenoage.utils.collections.CList.ilist;
import static com.xenoage.utils.collections.CollectionUtils.alist;
import static com.xenoage.utils.collections.CollectionUtils.llist;
import static com.xenoage.utils.iterators.ReverseIterator.reverseIt;
import static com.xenoage.zong.musiclayout.spacer.element.RestSpacer.restSpacer;
import java.util.LinkedList;
import com.xenoage.utils.math.Fraction;
import com.xenoage.zong.core.music.Voice;
import com.xenoage.zong.core.music.VoiceElement;
import com.xenoage.zong.musiclayout.layouter.Context;
import com.xenoage.zong.musiclayout.notation.ChordNotation;
import com.xenoage.zong.musiclayout.notation.Notation;
import com.xenoage.zong.musiclayout.notation.Notations;
import com.xenoage.zong.musiclayout.notation.RestNotation;
import com.xenoage.zong.musiclayout.settings.LayoutSettings;
import com.xenoage.zong.musiclayout.spacing.BorderSpacing;
import com.xenoage.zong.musiclayout.spacing.ChordSpacing;
import com.xenoage.zong.musiclayout.spacing.ElementSpacing;
import com.xenoage.zong.musiclayout.spacing.ElementWidth;
import com.xenoage.zong.musiclayout.spacing.VoiceSpacing;
/**
* Computes the {@link VoiceSpacing} for a single {@link Voice} regardless of the spacing of other
* voices or staves. Lyrics are ignored.
*
* Since this class only handles {@link VoiceElement}s, the barlines
* at the beginning and the end of the measure, implicit initial clefs,
* time signatures and so on are ignored.
*
* @author Andreas Wenger
*/
public class SingleVoiceSpacer {
public static final SingleVoiceSpacer singleVoiceSpacer = new SingleVoiceSpacer();
public VoiceSpacing compute(Context context, Notations notations) {
Voice voice = context.score.getVoice(context.mp);
float is = context.score.getInterlineSpace(context.mp);
Fraction measureBeats = context.score.getMeasureBeats(context.mp.measure);
int staffLinesCount = context.score.getStaff(context.mp).getLinesCount();
return compute(voice, is, measureBeats, staffLinesCount, notations, context.settings);
}
VoiceSpacing compute(Voice voice, float interlineSpace, Fraction measureBeats,
int staffLinesCount, Notations notations, LayoutSettings layoutSettings) {
LinkedList<ElementSpacing> ret = llist();
//special case: no elements in the measure.
if (voice.getElements().size() == 0) {
return new VoiceSpacing(voice, interlineSpace, alist((ElementSpacing)
new BorderSpacing(Fraction._0, 0), new BorderSpacing(measureBeats,
layoutSettings.spacings.widthMeasureEmpty)));
}
//we compute the spacings in reverse order. this is easier, since grace chords
//use shared space when possible, but are aligned to the right. so we begin
//at position 0, and create the spacings in reverse direction (thus we find negative positions).
//at the end, we shift the voice spacing to the right to be aligned at the left measure border,
//which is position 0.
//last symbol offset:
//real offset where the last element's symbol started
float lastSymbolOffset = 0; //since we do not know the right border yet, we start at 0
//front gap offset:
//offset where the last element (including front gap) started.
float lastFrontGapOffset = lastSymbolOffset;
//last full element offset:
//lastSymbolOffset of the last full (non-grace) element
float lastFullSymbolOffset = lastSymbolOffset;
//at last beat
Fraction curBeat = voice.getFilledBeats();
ret.addFirst(new BorderSpacing(curBeat, lastFrontGapOffset));
//iterate through the elements in reverse order
for (VoiceElement element : reverseIt(voice.getElements())) {
//get the notation
Notation notation = notations.get(element);
if (notation == null)
throw new IllegalStateException("No notation for element " + element);
//get the width of the element (front gap, symbol's width, rear gap, lyric's width)
ElementWidth elementWidth = notation.getWidth();
//add spacing for voice element
float symbolOffset;
boolean grace = !element.getDuration().isGreater0();
if (!grace) {
//full element
//share this rear gap and the front gap of the following
//element + the space of following grace elements, when possible
//(but use at least minimal distance)
symbolOffset = Math.min(lastFrontGapOffset - layoutSettings.spacings.widthDistanceMin,
lastFullSymbolOffset - elementWidth.rearGap) - elementWidth.symbolWidth;
lastFullSymbolOffset = symbolOffset;
//update beat cursor
curBeat = curBeat.sub(element.getDuration());
}
else {
//grace element
//share this rear gap and the front gap of the following element, when possible
symbolOffset = Math.min(lastFrontGapOffset, lastSymbolOffset - elementWidth.rearGap) -
elementWidth.symbolWidth;
}
ElementSpacing elementSpacing = null;
if (notation instanceof RestNotation) {
//rest spacing
elementSpacing = restSpacer.compute((RestNotation) notation, curBeat,
symbolOffset, staffLinesCount);
}
else {
//chord spacing
elementSpacing = new ChordSpacing((ChordNotation) notation, curBeat, symbolOffset);
}
ret.addFirst(elementSpacing);
lastFrontGapOffset = symbolOffset - elementWidth.frontGap;
lastSymbolOffset = symbolOffset;
}
//shift spacings to the right
float shift = (-lastFrontGapOffset) + layoutSettings.offsetMeasureStart;
for (ElementSpacing e : ret)
e.xIs += shift;
return new VoiceSpacing(voice, interlineSpace, ilist(ret));
}
}