package com.xenoage.zong.musiclayout.spacer.measure; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.utils.math.Fraction._0; import static com.xenoage.zong.musiclayout.spacing.ElementSpacing.empty; import java.util.ArrayList; import java.util.List; import com.xenoage.utils.annotations.MaybeEmpty; import com.xenoage.utils.annotations.MaybeNull; import com.xenoage.utils.math.Fraction; import com.xenoage.zong.core.header.ColumnHeader; import com.xenoage.zong.core.music.ColumnElement; import com.xenoage.zong.core.music.Measure; import com.xenoage.zong.core.music.MeasureElement; import com.xenoage.zong.core.music.clef.Clef; import com.xenoage.zong.core.music.key.Key; import com.xenoage.zong.core.music.key.TraditionalKey; import com.xenoage.zong.core.music.time.TimeSignature; import com.xenoage.zong.core.music.util.BeatE; import com.xenoage.zong.core.music.util.BeatEList; import com.xenoage.zong.musiclayout.layouter.Context; import com.xenoage.zong.musiclayout.notation.Notation; import com.xenoage.zong.musiclayout.notation.Notations; import com.xenoage.zong.musiclayout.settings.LayoutSettings; import com.xenoage.zong.musiclayout.spacing.ElementSpacing; import com.xenoage.zong.musiclayout.spacing.LeadingSpacing; import com.xenoage.zong.musiclayout.spacing.SimpleSpacing; import com.xenoage.zong.musiclayout.spacing.VoiceSpacing; /** * Computes the {@link MeasureElementsSpacing}s for the * {@link MeasureElement}s and {@link ColumnElement}s in * a {@link Measure}, like key signatures or clefs. * * Currently, clefs, key signatures at beat 0 over the * whole column and time signatures over the whole column are supported. * * Because these elements need space, the {@link VoiceSpacing}s * of the voices of this measure must be given. These are modified * to leave enough space for the measure elements. * * The strategy can either create spacings for the elements at * beat 0 or ignore them, if there is already a {@link LeadingSpacing} * for this column. * * @author Andreas Wenger */ public class MeasureElementsSpacer { public static final MeasureElementsSpacer measureElementsSpacer = new MeasureElementsSpacer(); public List<ElementSpacing> compute( Context context, boolean existsLeadingSpacing, List<VoiceSpacing> voiceSpacings, Notations notations) { Measure measure = context.score.getMeasure(context.mp); ColumnHeader columnHeader = context.score.getHeader().getColumnHeader(context.mp.measure); return compute(measure.getClefs(), columnHeader.getKeys(), columnHeader.getTime(), existsLeadingSpacing, voiceSpacings, context.mp.staff, notations, context.settings); } List<ElementSpacing> compute(BeatEList<Clef> clefs, @MaybeEmpty BeatEList<Key> keys, @MaybeNull TimeSignature time, boolean existsLeadingSpacing, List<VoiceSpacing> voiceSpacings, int staff, Notations notations, LayoutSettings layoutSettings) { Key key0 = null; if (keys.size() > 0 && keys.getFirst().beat.equals(_0)) key0 = keys.getFirst().element; if (key0 == null && time == null && (clefs == null || clefs.size() == 0)) { //nothing to do return empty; } ArrayList<ElementSpacing> ret = alist(); float startOffset = layoutSettings.offsetMeasureStart; //key and time //************ boolean isKey = !existsLeadingSpacing && key0 instanceof TraditionalKey; boolean isTime = time != null; if (isKey || isTime) { float currentOffset = startOffset; //key //*** if (isKey) { Notation keyNotation = notations.get(key0, staff); ret.add(new SimpleSpacing(keyNotation, _0, startOffset)); currentOffset += keyNotation.getWidth().getUsedWidth(); } //time //**** if (time != null) { Notation timeNotation = notations.get(time, staff); ret.add(new SimpleSpacing(timeNotation, _0, currentOffset + timeNotation.getWidth().symbolWidth / 2)); currentOffset += timeNotation.getWidth().getUsedWidth(); } //move voice elements, if not enough space before first voice element ElementSpacing leftSE = getFirstElementSpacing(voiceSpacings); if (leftSE != null) { float leftSEx = getLeftX(leftSE); float ES = leftSEx; //existing space float AS = currentOffset - ES; //additional needed space if (AS > 0) { shift(voiceSpacings, AS); startOffset += AS; } } } //clefs //***** //for each measure element ME (with width ME.width) to insert: //find first voice element VE1 before the ME, //and last voice element VE2 at or after the ME (both in any voice!). //the distance between VE1 and VE2 can be used for the ME: // existing space (ES) = (VE2.x - VE1.x) - 2*padding //if this distance is too small (ME.width > ES), additional space is required: // additional space (AS) = ME.width - ES //then, all voice elements at or after VE2.beat are moved AS to the right. //the measure element is placed at ME.x = VE2.x - padding - ME.width/2 // //example sketches: (*: padding, 1: VE1, 2: VE2) // //enough space: //clef: *[clef]* //voice 1: o 2 //voice 2: 1 o // //conflict: //clef: *[clef]* //voice 1: o 2 //voice 2: 1 o // !! //and its solution: move 2 spaces to the right //clef: *[clef]* //voice 1: o 2 //voice 2: 1 o if (clefs != null) { for (BeatE<Clef> ME : clefs) { Fraction MEb = ME.beat; Notation MEnotation = notations.get(ME.element); float MEwidth = MEnotation.getWidth().getWidth(); //if there is a leading spacing, ignore elements at beat 0 if (existsLeadingSpacing && !MEb.isGreater0()) continue; //find VE1 and VE2 for the current element ElementSpacing[] ses = getNearestSpacingElements(MEb, voiceSpacings); ElementSpacing VE1 = ses[0], VE2 = ses[1]; //if VE1 is unknown, use startOffset. if VE2 is unknown, ignore this element float VE1x = (VE1 != null ? getRightX(VE1) : startOffset); if (VE2 == null) continue; float VE2x = getLeftX(VE2); //existing space float ES = VE2x - VE1x - 2 * layoutSettings.spacings.widthDistanceMin; if (ES < MEwidth) { //additional space needed float AS = MEwidth - ES; //move all elements at or after ME.beat VE2x += AS; shiftAfterBeat(voiceSpacings, AS, MEb); } //add measure element float MEx = VE2x - layoutSettings.spacings.widthDistanceMin - MEwidth / 2; ret.add(new SimpleSpacing(MEnotation, ME.beat, MEx)); } } ret.trimToSize(); return ret; } /** * Gets the leftmost {@link ElementSpacing} in the given list of {@link VoiceSpacing}s, * or null there is none. */ private ElementSpacing getFirstElementSpacing(List<VoiceSpacing> vss) { ElementSpacing ret = null; float retLeftX = Float.MAX_VALUE; for (VoiceSpacing vs : vss) { for (ElementSpacing se : vs.elements) { float leftX = getLeftX(se); if (leftX < retLeftX) { retLeftX = leftX; ret = se; break; //no other element after this one in the same voice will be more left } } } return ret; } /** * Gets the nearest two {@link ElementSpacing}s at the * given beat (left [0] and right [1]). */ private ElementSpacing[] getNearestSpacingElements(Fraction beat, List<VoiceSpacing> vss) { ElementSpacing[] ret = { null, null }; float retLeftX = Float.MIN_VALUE; float retRightX = Float.MAX_VALUE; for (VoiceSpacing vs : vss) { for (ElementSpacing se : vs.elements) { int compare = se.beat.compareTo(beat); if (compare < 0) { float leftX = getLeftX(se); if (leftX > retLeftX) { //found nearer left element retLeftX = leftX; ret[0] = se; } } else if (compare >= 0) { float rightX = getRightX(se); if (rightX < retRightX) { //found nearer right element retRightX = rightX; ret[1] = se; } break; //first candidate always matches here } } } return ret; } /** * Gets the leftmost position of the given {@link ElementSpacing} in the given staff. * This is its offset minus the front gap of its {@link Notation}. */ private float getLeftX(ElementSpacing element) { //element and notation may be null, e.g. for last SE in measure Notation notation = element.getNotation(); return element.xIs - (notation != null ? notation.getWidth().frontGap : 0); } /** * Gets the rightmost position of the given {@link ElementSpacing} in the given staff. * This is its offset plus the width of its {@link Notation} (bot not plus its rear gap!). */ private float getRightX(ElementSpacing element) { //element and notation may be null, e.g. for last SE in measure Notation notation = element.getNotation(); return element.xIs + (notation != null ? notation.getWidth().symbolWidth : 0); } /** * Moves all given {@link ElementSpacing}s by the given offset. */ public void shift(List<VoiceSpacing> vss, float offsetIs) { for (VoiceSpacing vs : vss) for (ElementSpacing se : vs.elements) se.xIs += offsetIs; } /** * Moves all given {@link ElementSpacing}s by the given offset, if they are at * or behind the given beat. */ public void shiftAfterBeat(List<VoiceSpacing> vss, float offsetIs, Fraction beat) { for (VoiceSpacing vs : vss) for (ElementSpacing se : vs.elements) if (se.beat.compareTo(beat) >= 0) se.xIs += offsetIs; } }