package com.xenoage.zong.musiclayout.spacer.system; import com.xenoage.utils.Optional; import com.xenoage.utils.math.geom.Size2f; import com.xenoage.zong.core.Score; import com.xenoage.zong.core.format.Break; import com.xenoage.zong.core.format.ScoreFormat; import com.xenoage.zong.core.format.SystemLayout; import com.xenoage.zong.core.header.ScoreHeader; import com.xenoage.zong.core.music.layout.SystemBreak; import com.xenoage.zong.core.position.MP; import com.xenoage.zong.musiclayout.layouter.Context; import com.xenoage.zong.musiclayout.notation.Notations; import com.xenoage.zong.musiclayout.spacing.ColumnSpacing; import com.xenoage.zong.musiclayout.spacing.LeadingSpacing; import com.xenoage.zong.musiclayout.spacing.StavesSpacing; import com.xenoage.zong.musiclayout.spacing.SystemSpacing; import java.util.List; import static com.xenoage.utils.Optional.absent; import static com.xenoage.utils.Optional.of; import static com.xenoage.utils.collections.CollectionUtils.alist; import static com.xenoage.zong.musiclayout.spacer.measure.ColumnSpacer.columnSpacer; import static com.xenoage.zong.musiclayout.spacer.system.StavesSpacer.stavesSpacer; /** * Arranges a list of measure columns into a {@link SystemSpacing}. * * The systems will not be stretched to their full possible width. * This can be done in a later layouting step. However, a {@link LeadingSpacing} * is added at the beginning of the system. * * This strategy also regards forced and prohibited system breaks * and custom system margins (left, right) and staff margins. * * @author Andreas Wenger */ public class SystemSpacer { public static final SystemSpacer systemSpacer = new SystemSpacer(); /** * Arranges an optimum number of measures columns in a system, beginning at the given measure, * if possible. * @param context the context of the layouter, with the {@link MP} set to the start measure * @param usableSizeMm the usable size within the score frame in mm * @param offsetYMm the vertical offset of the system in mm * @param systemIndex the global system index (over all frames) * @param isFirst true, iff this system is the first one in a score frame * @param isAdditional true, iff this system is created for an additional frame, which is created * for a complete score layout, but is not part of the defined layout * @param measureColumnSpacings a list of all measure column spacings without leading spacings * @param notations the notations of the elements, needed when a column has to be recomputed * because of a leading spacing */ public Optional<SystemSpacing> compute(Context context, Size2f usableSizeMm, float offsetYMm, int systemIndex, boolean isFirst, boolean isAdditional, List<ColumnSpacing> measureColumnSpacings, Notations notations) { int startMeasure = context.mp.measure; //test if there is enough height for the system Score score = context.score; ScoreFormat scoreFormat = score.getFormat(); ScoreHeader scoreHeader = score.getHeader(); //compute spacing of staves StavesSpacing stavesSpacing = stavesSpacer.compute(score, systemIndex); //enough space? if (offsetYMm + stavesSpacing.getTotalHeightMm() > usableSizeMm.height) { //when the system is the first one in an additional frame, we force it into the frame if (false == (isFirst && isAdditional)) return absent(); //not enough space } //compute the usable width for the system float systemLeftMarginMm = getLeftMarginMm(systemIndex, scoreFormat, scoreHeader); float systemRightMarginMm = getRightMarginMm(systemIndex, scoreFormat, scoreHeader); float usableWidthMm = usableSizeMm.width - systemLeftMarginMm - systemRightMarginMm; //append system measure-by-measure, until there is no space any more //or until there are no measures left int measuresCount = score.getMeasuresCount(); List<ColumnSpacing> system = alist(); float usedWidthMm = 0; int iMeasure; while (startMeasure + system.size() < measuresCount) { iMeasure = startMeasure + system.size(); //decide if to add a leading spacing to the current measure or not boolean firstMeasure = system.size() == 0; ColumnSpacing column; if (firstMeasure) { //first measure within this system: add leading elements (clef, time sig.) column = columnSpacer.compute(context, true /* leading! */, notations); } else { //otherwise: use the precomputed spacing (without leading spacing) column = measureColumnSpacings.get(iMeasure); } //try to add this measure to the current system. if there is no space left for //it, although it is the only measure in this system, force it into the system when //this system is created for a complete score layout if (false == canAppend(column, iMeasure, usableWidthMm, usedWidthMm, scoreHeader, firstMeasure)) { if (system.size() == 0 && isAdditional) { //force the single measure in this system usedWidthMm += column.getWidthMm(); system.add(column); } else { break; //no more space for another measure } } else { usedWidthMm += column.getWidthMm(); system.add(column); } } //we are finished if (system.size() == 0) { return absent(); //not enough space for the system on this area } else { SystemSpacing ret = new SystemSpacing( system, systemLeftMarginMm, systemRightMarginMm, usedWidthMm, stavesSpacing, offsetYMm); return of(ret); } } /** * Returns true, if the given measure can be appended to the currently computed system, * otherwise false. * It is not tested if there is enough vertical space, this must be done before. */ public boolean canAppend(ColumnSpacing column, int measureIndex, float usableWidthMm, float usedWidthMm, ScoreHeader scoreHeader, boolean isFirstMeasure) { //if a line break is forced, do it (but one measure is always allowed) Break br = scoreHeader.getColumnHeader(measureIndex).getMeasureBreak(); if (br != null && br.getSystemBreak() == SystemBreak.NewSystem && false == isFirstMeasure) return false; //if a line break is prohibited, force the measure to be within this system if (br != null && br.getSystemBreak() == SystemBreak.NoNewSystem) return true; //enough horizontal space? float remainingWidthMm = usableWidthMm - usedWidthMm; if (remainingWidthMm < column.getWidthMm()) return false; //ok, append the measure return true; } /** * Returns the left margin of the given system (global index) in mm. */ private float getLeftMarginMm(int systemIndex, ScoreFormat scoreFormat, ScoreHeader scoreHeader) { SystemLayout systemLayout = scoreHeader.getSystemLayout(systemIndex); if (systemLayout != null) { //use custom system margin return systemLayout.getMarginLeft(); } else { //use default system margin return scoreFormat.getSystemLayout().getMarginLeft(); } } /** * Returns the right margin of the given system (global index) in mm. */ private float getRightMarginMm(int systemIndex, ScoreFormat scoreFormat, ScoreHeader scoreHeader) { SystemLayout systemLayout = scoreHeader.getSystemLayout(systemIndex); if (systemLayout != null) { //use custom system margin return systemLayout.getMarginRight(); } else { //use default system margin return scoreFormat.getSystemLayout().getMarginRight(); } } }