opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/MusicalScore/Graphical/MusicSheetCalculator.ts

Summary

Maintainability
F
2 mos
Test Coverage
import { GraphicalStaffEntry } from "./GraphicalStaffEntry";
import { StaffLine } from "./StaffLine";
import { GraphicalMusicSheet } from "./GraphicalMusicSheet";
import { EngravingRules } from "./EngravingRules";
import { Tie } from "../VoiceData/Tie";
import { Fraction } from "../../Common/DataObjects/Fraction";
import { Note } from "../VoiceData/Note";
import { MusicSheet } from "../MusicSheet";
import { GraphicalMeasure } from "./GraphicalMeasure";
import {ClefInstruction, ClefEnum} from "../VoiceData/Instructions/ClefInstruction";
import { LyricWord } from "../VoiceData/Lyrics/LyricsWord";
import { SourceMeasure } from "../VoiceData/SourceMeasure";
import { GraphicalMusicPage } from "./GraphicalMusicPage";
import { GraphicalNote } from "./GraphicalNote";
import { Beam } from "../VoiceData/Beam";
import { OctaveEnum } from "../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
import { VoiceEntry, StemDirectionType } from "../VoiceData/VoiceEntry";
import { OrnamentContainer } from "../VoiceData/OrnamentContainer";
import { Articulation } from "../VoiceData/Articulation";
import { Tuplet } from "../VoiceData/Tuplet";
import { MusicSystem } from "./MusicSystem";
import { GraphicalTie } from "./GraphicalTie";
import { RepetitionInstruction } from "../VoiceData/Instructions/RepetitionInstruction";
import { MultiExpression, MultiExpressionEntry } from "../VoiceData/Expressions/MultiExpression";
import { StaffEntryLink } from "../VoiceData/StaffEntryLink";
import { MusicSystemBuilder } from "./MusicSystemBuilder";
import { MultiTempoExpression } from "../VoiceData/Expressions/MultiTempoExpression";
import { Repetition } from "../MusicSource/Repetition";
import { PointF2D } from "../../Common/DataObjects/PointF2D";
import { SourceStaffEntry } from "../VoiceData/SourceStaffEntry";
import { BoundingBox } from "./BoundingBox";
import { Instrument } from "../Instrument";
import { GraphicalLabel } from "./GraphicalLabel";
import { TextAlignmentEnum } from "../../Common/Enums/TextAlignment";
import { VerticalGraphicalStaffEntryContainer } from "./VerticalGraphicalStaffEntryContainer";
import { KeyInstruction } from "../VoiceData/Instructions/KeyInstruction";
import { AbstractNotationInstruction } from "../VoiceData/Instructions/AbstractNotationInstruction";
import { TechnicalInstruction, TechnicalInstructionType } from "../VoiceData/Instructions/TechnicalInstruction";
import { Pitch } from "../../Common/DataObjects/Pitch";
import { LinkedVoice } from "../VoiceData/LinkedVoice";
import { ColDirEnum } from "./BoundingBox";
import { IGraphicalSymbolFactory } from "../Interfaces/IGraphicalSymbolFactory";
import { ITextMeasurer } from "../Interfaces/ITextMeasurer";
import { ITransposeCalculator } from "../Interfaces/ITransposeCalculator";
import { OctaveShiftParams } from "./OctaveShiftParams";
import { AccidentalCalculator } from "./AccidentalCalculator";
import { MidiInstrument } from "../VoiceData/Instructions/ClefInstruction";
import { Staff } from "../VoiceData/Staff";
import { OctaveShift } from "../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
import log from "loglevel";
import { Dictionary } from "typescript-collections";
import { GraphicalLyricEntry } from "./GraphicalLyricEntry";
import { GraphicalLyricWord } from "./GraphicalLyricWord";
import { GraphicalLine } from "./GraphicalLine";
import { Label } from "../Label";
import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
import { VerticalSourceStaffEntryContainer } from "../VoiceData/VerticalSourceStaffEntryContainer";
import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
import { AbstractGraphicalInstruction } from "./AbstractGraphicalInstruction";
import { GraphicalInstantaneousTempoExpression } from "./GraphicalInstantaneousTempoExpression";
import { InstantaneousTempoExpression, TempoEnum } from "../VoiceData/Expressions/InstantaneousTempoExpression";
import { ContinuousTempoExpression } from "../VoiceData/Expressions/ContinuousExpressions/ContinuousTempoExpression";
import { FontStyles } from "../../Common/Enums/FontStyles";
import { AbstractTempoExpression } from "../VoiceData/Expressions/AbstractTempoExpression";
import { GraphicalInstantaneousDynamicExpression } from "./GraphicalInstantaneousDynamicExpression";
import { ContDynamicEnum } from "../VoiceData/Expressions/ContinuousExpressions/ContinuousDynamicExpression";
import { GraphicalContinuousDynamicExpression } from "./GraphicalContinuousDynamicExpression";
import { FillEmptyMeasuresWithWholeRests } from "../../OpenSheetMusicDisplay/OSMDOptions";
import { IStafflineNoteCalculator } from "../Interfaces/IStafflineNoteCalculator";
import { GraphicalUnknownExpression } from "./GraphicalUnknownExpression";
import { GraphicalChordSymbolContainer } from "./GraphicalChordSymbolContainer";
import { LyricsEntry } from "../VoiceData/Lyrics/LyricsEntry";
import { Voice } from "../VoiceData/Voice";
import { TabNote } from "../VoiceData/TabNote";

/**
 * Class used to do all the calculations in a MusicSheet, which in the end populates a GraphicalMusicSheet.
 */
export abstract class MusicSheetCalculator {
    public static symbolFactory: IGraphicalSymbolFactory;
    public static transposeCalculator: ITransposeCalculator;
    public static stafflineNoteCalculator: IStafflineNoteCalculator;
    protected static textMeasurer: ITextMeasurer;

    protected staffEntriesWithGraphicalTies: GraphicalStaffEntry[] = [];
    protected staffEntriesWithOrnaments: GraphicalStaffEntry[] = [];
    protected staffEntriesWithChordSymbols: GraphicalStaffEntry[] = [];
    protected staffLinesWithLyricWords: StaffLine[] = [];

    protected graphicalLyricWords: GraphicalLyricWord[] = [];

    protected graphicalMusicSheet: GraphicalMusicSheet;
    protected rules: EngravingRules;
    protected musicSystems: MusicSystem[];

    private abstractNotImplementedErrorMessage: string = "abstract, not implemented";

    public static get TextMeasurer(): ITextMeasurer {
        return MusicSheetCalculator.textMeasurer;
    }

    public static set TextMeasurer(value: ITextMeasurer) {
        MusicSheetCalculator.textMeasurer = value;
    }

    protected get leadSheet(): boolean {
        return this.graphicalMusicSheet.LeadSheet;
    }

    protected static setMeasuresMinStaffEntriesWidth(measures: GraphicalMeasure[], minimumStaffEntriesWidth: number): void {
        for (let idx: number = 0, len: number = measures.length; idx < len; ++idx) {
            const measure: GraphicalMeasure = measures[idx];
            if (measure) {
                measure.minimumStaffEntriesWidth = minimumStaffEntriesWidth;
            }
        }
    }

    public initialize(graphicalMusicSheet: GraphicalMusicSheet): void {
        this.graphicalMusicSheet = graphicalMusicSheet;
        this.rules = graphicalMusicSheet.ParentMusicSheet.Rules;
        this.rules.clearMusicSheetObjects();
        this.prepareGraphicalMusicSheet();
        //this.calculate();
    }

    /**
     * Build the 2D [[GraphicalMeasure]] list needed for the [[MusicSheetCalculator]].
     * Internally it creates [[GraphicalMeasure]]s, [[GraphicalStaffEntry]]'s and [[GraphicalNote]]s.
     */
    public prepareGraphicalMusicSheet(): void {
        // Clear the stored system images dict - all systems have to be redrawn.
        // Not necessary now. TODO Check
        // this.graphicalMusicSheet.SystemImages.length = 0;
        const musicSheet: MusicSheet = this.graphicalMusicSheet.ParentMusicSheet;

        this.staffEntriesWithGraphicalTies = [];
        this.staffEntriesWithOrnaments = [];
        this.staffEntriesWithChordSymbols = [];
        this.staffLinesWithLyricWords = [];
        // this.staffLinesWithGraphicalExpressions = [];

        this.graphicalMusicSheet.Initialize();
        const measureList: GraphicalMeasure[][] = this.graphicalMusicSheet.MeasureList;

        // one AccidentalCalculator for each Staff (regardless of Instrument)
        const accidentalCalculators: AccidentalCalculator[] = this.createAccidentalCalculators();

        // List of Active ClefInstructions
        const activeClefs: ClefInstruction[] = this.graphicalMusicSheet.initializeActiveClefs();

        // LyricWord - GraphicalLyricWord Lists
        const lyricWords: LyricWord[] = [];

        const completeNumberOfStaves: number = musicSheet.getCompleteNumberOfStaves();

        // Octave Shifts List
        const openOctaveShifts: OctaveShiftParams[] = [];

        // TieList - timestampsArray
        for (let i: number = 0; i < completeNumberOfStaves; i++) {
            openOctaveShifts.push(undefined);
        }

        // go through all SourceMeasures (taking into account normal SourceMusicParts and Repetitions)
        for (let idx: number = 0, len: number = musicSheet.SourceMeasures.length; idx < len; ++idx) {
            const sourceMeasure: SourceMeasure = musicSheet.SourceMeasures[idx];
            const graphicalMeasures: GraphicalMeasure[] = this.createGraphicalMeasuresForSourceMeasure(
                sourceMeasure,
                accidentalCalculators,
                lyricWords,
                openOctaveShifts,
                activeClefs
            );
            measureList.push(graphicalMeasures);
            if (sourceMeasure.multipleRestMeasures > 0 && this.rules.RenderMultipleRestMeasures) {
                // multiRest given in XML, skip the next measures included
                sourceMeasure.isReducedToMultiRest = true;
                sourceMeasure.multipleRestMeasureNumber = 1;
                const measuresToSkip: number = sourceMeasure.multipleRestMeasures - 1;
                // console.log(`skipping ${measuresToSkip} measures for measure #${sourceMeasure.MeasureNumber}.`);
                idx += measuresToSkip;
                for (let idx2: number = 1; idx2 <= measuresToSkip; idx2++) {
                    const nextMeasureIndex: number = musicSheet.SourceMeasures.indexOf(sourceMeasure) + idx2;
                    // note that if there are pickup measures in the sheet, the measure index is not MeasureNumber - 1.
                    //   (if first measure in the sheet is a pickup measure, its index and measure number will be 0)
                    if (nextMeasureIndex >= musicSheet.SourceMeasures.length) {
                        break; // shouldn't happen, but for safety.
                    }
                    const nextSourceMeasure: SourceMeasure = musicSheet.SourceMeasures[nextMeasureIndex];
                    // TODO handle the case that a measure after the first multiple rest measure can't be reduced
                    nextSourceMeasure.multipleRestMeasureNumber = idx2 + 1;
                    nextSourceMeasure.isReducedToMultiRest = true;
                    measureList.push([undefined]);
                    // TODO we could push an object here or push nothing entirely,
                    //   but then the index doesn't correspond to measure numbers anymore.
                }
            }
        }

        if (this.rules.AutoGenerateMultipleRestMeasuresFromRestMeasures && this.rules.RenderMultipleRestMeasures) {
            //track number of multirests
            let beginMultiRestMeasure: SourceMeasure = undefined;
            let multiRestCount: number = 0;
            //go through all source measures again. Need to calc auto-multi-rests
            for (let idx: number = 0, len: number = musicSheet.SourceMeasures.length; idx < len; ++idx) {
                const sourceMeasure: SourceMeasure = musicSheet.SourceMeasures[idx];
                // console.log(sourceMeasure.MeasureNumber + " can be reduced: " + sourceMeasure.canBeReducedToMultiRest());
                if (!sourceMeasure.isReducedToMultiRest && sourceMeasure.canBeReducedToMultiRest()) {
                    //we've already been initialized, we are in the midst of a multirest sequence
                    if (multiRestCount > 0) {
                        beginMultiRestMeasure.isReducedToMultiRest = true;
                        beginMultiRestMeasure.multipleRestMeasureNumber = 1;
                        multiRestCount++;
                        sourceMeasure.multipleRestMeasureNumber = multiRestCount;
                        sourceMeasure.isReducedToMultiRest = true;
                        //clear out these measures. We know now that we are in multirest mode
                        for (let idx2: number = 0; idx2 < measureList[idx].length; idx2++) {
                            measureList[idx][idx2] = undefined;
                        }
                    } else { //else this is the (potential) beginning
                        beginMultiRestMeasure = sourceMeasure;
                        multiRestCount = 1;
                    }
                } else { //not multirest measure
                    if (multiRestCount > 1) { //Actual multirest sequence just happened. Process
                        beginMultiRestMeasure.multipleRestMeasures = multiRestCount;
                        //regen graphical measures for this source measure
                        const graphicalMeasures: GraphicalMeasure[] = this.createGraphicalMeasuresForSourceMeasure(
                            beginMultiRestMeasure,
                            accidentalCalculators,
                            lyricWords,
                            openOctaveShifts,
                            activeClefs
                        );
                        measureList[beginMultiRestMeasure.measureListIndex] = graphicalMeasures;
                        multiRestCount = 0;
                        beginMultiRestMeasure = undefined;
                    } else { //had a potential multirest sequence, but didn't pan out. only one measure was rests
                        multiRestCount = 0;
                        beginMultiRestMeasure = undefined;
                    }
                }
            }
            //If we reached the end of the sheet and have pending multirest measure, process
            if (multiRestCount > 1) {
                beginMultiRestMeasure.multipleRestMeasures = multiRestCount;
                beginMultiRestMeasure.isReducedToMultiRest = true;
                //regen graphical measures for this source measure
                const graphicalMeasures: GraphicalMeasure[] = this.createGraphicalMeasuresForSourceMeasure(
                    beginMultiRestMeasure,
                    accidentalCalculators,
                    lyricWords,
                    openOctaveShifts,
                    activeClefs
                );
                measureList[beginMultiRestMeasure.measureListIndex] = graphicalMeasures;
                multiRestCount = 0;
                beginMultiRestMeasure = undefined;
            }
        }

        const staffIsPercussionArray: Array<boolean> =
                        activeClefs.map(clef => (clef.ClefType === ClefEnum.percussion));

        this.handleStaffEntries(staffIsPercussionArray);
        this.calculateVerticalContainersList();
        this.setIndicesToVerticalGraphicalContainers();
    }

    /**
     * The main method for the Calculator.
     */
    public calculate(): void {
        this.musicSystems = [];

        this.clearSystemsAndMeasures();

        // delete graphicalObjects (currently: ties) that will be recalculated, newly create GraphicalObjects streching over a single StaffEntry
        this.clearRecreatedObjects();

        // this.graphicalMusicSheet.initializeActiveClefs(); // could have been changed since last render?

        this.createGraphicalTies();

        // calculate SheetLabelBoundingBoxes
        this.calculateSheetLabelBoundingBoxes();
        this.calculateXLayout(this.graphicalMusicSheet, this.maxInstrNameLabelLength());

        // create List<MusicPage>
        this.graphicalMusicSheet.MusicPages.length = 0;

        // create new MusicSystems and StaffLines (as many as necessary) and populate them with Measures from measureList
        this.calculateMusicSystems();

        // Add some white space at the end of the piece:
        //this.graphicalMusicSheet.MusicPages[0].PositionAndShape.BorderMarginBottom += 9;

        // transform Relative to Absolute Positions
        //This is called for each measure in calculate music systems (calculateLines -> calculateSkyBottomLines)
        GraphicalMusicSheet.transformRelativeToAbsolutePosition(this.graphicalMusicSheet);
    }

    public calculateXLayout(graphicalMusicSheet: GraphicalMusicSheet, maxInstrNameLabelLength: number): void {
        // for each inner List in big Measure List calculate new Positions for the StaffEntries
        // and adjust Measures sizes
        // calculate max measure length for maximum zoom in.

        // let minLength: number = 0; // currently unused
        // const maxInstructionsLength: number = this.rules.MaxInstructionsConstValue;
        if (this.graphicalMusicSheet.MeasureList.length > 0) {
            /** list of vertically ordered measures belonging to one bar */
            // let measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[0];
            // let minimumStaffEntriesWidth: number = this.calculateMeasureXLayout(measures);
            // minimumStaffEntriesWidth = this.calculateMeasureWidthFromStaffEntries(measures, minimumStaffEntriesWidth);
            // MusicSheetCalculator.setMeasuresMinStaffEntriesWidth(measures, minimumStaffEntriesWidth);
            // minLength = minimumStaffEntriesWidth * 1.2 + maxInstrNameLabelLength + maxInstructionsLength;
            let maxWidth: number = 0;
            let measures: GraphicalMeasure[];
            let measureWidthFactor: number = 1;
            for (let i: number = 0; i < this.graphicalMusicSheet.MeasureList.length; i++) {
                measures = this.graphicalMusicSheet.MeasureList[i];
                let minimumStaffEntriesWidth: number = this.calculateMeasureXLayout(measures);
                minimumStaffEntriesWidth = this.calculateMeasureWidthFromStaffEntries(measures, minimumStaffEntriesWidth);
                if (minimumStaffEntriesWidth > maxWidth) {
                    maxWidth = minimumStaffEntriesWidth;
                }
                const globalWidthFactor: number = this.graphicalMusicSheet.ParentMusicSheet.MeasureWidthFactor;
                for (const verticalMeasure of measures) {
                    if (verticalMeasure?.parentSourceMeasure.WidthFactor) { // some of these GraphicalMeasures might be undefined (multi-rest)
                        measureWidthFactor = verticalMeasure.parentSourceMeasure.WidthFactor;
                        break;
                    }
                }
                minimumStaffEntriesWidth *= globalWidthFactor * measureWidthFactor;
                //console.log(`min width for measure ${measures[0].MeasureNumber}: ${minimumStaffEntriesWidth}`);
                MusicSheetCalculator.setMeasuresMinStaffEntriesWidth(measures, minimumStaffEntriesWidth);
                // minLength = Math.max(minLength, minimumStaffEntriesWidth * 1.2 + maxInstructionsLength);
            }
            if (this.rules.FixedMeasureWidth) {
                // experimental: use the same measure width for all measures
                //   here we take the maximum measure width for now,
                //   otherwise Vexflow's layout can get completely messed up and place everything on top of each other,
                //   if it gets less width than it says it needs as a minimum for a measure. (formatter.preCalculateMinTotalWidth)
                let targetWidth: number = maxWidth;
                if (this.rules.FixedMeasureWidthFixedValue) {
                    targetWidth = this.rules.FixedMeasureWidthFixedValue;
                }
                for (let i: number = 0; i < this.graphicalMusicSheet.MeasureList.length; i++) {
                    measures = this.graphicalMusicSheet.MeasureList[i];
                    if (!this.rules.FixedMeasureWidthUseForPickupMeasures && measures[0]?.parentSourceMeasure.ImplicitMeasure) {
                        // note that measures[0] is undefined for multi-measure rests
                        continue;
                    }
                    MusicSheetCalculator.setMeasuresMinStaffEntriesWidth(measures, targetWidth);
                }
            }
        }
        // this.graphicalMusicSheet.MinAllowedSystemWidth = minLength; // currently unused
    }

    public calculateMeasureWidthFromStaffEntries(measuresVertical: GraphicalMeasure[], oldMinimumStaffEntriesWidth: number): number {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected formatMeasures(): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Calculates the x layout of the staff entries within the staff measures belonging to one source measure.
     * All staff entries are x-aligned throughout all the measures.
     * @param measures - The minimum required x width of the source measure
     */
    protected calculateMeasureXLayout(measures: GraphicalMeasure[]): number {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Called for every source measure when generating the list of staff measures for it.
     */
    protected initGraphicalMeasuresCreation(): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected handleBeam(graphicalNote: GraphicalNote, beam: Beam, openBeams: Beam[]): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Check if the tied graphical note belongs to any beams or tuplets and react accordingly.
     * @param tiedGraphicalNote
     * @param beams
     * @param activeClef
     * @param octaveShiftValue
     * @param graphicalStaffEntry
     * @param duration
     * @param openTie
     * @param isLastTieNote
     */
    protected handleTiedGraphicalNote(tiedGraphicalNote: GraphicalNote, beams: Beam[], activeClef: ClefInstruction,
                                      octaveShiftValue: OctaveEnum, graphicalStaffEntry: GraphicalStaffEntry, duration: Fraction,
                                      openTie: Tie, isLastTieNote: boolean): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected handleVoiceEntryLyrics(voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry,
                                     openLyricWords: LyricWord[]): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected handleVoiceEntryOrnaments(ornamentContainer: OrnamentContainer, voiceEntry: VoiceEntry,
                                        graphicalStaffEntry: GraphicalStaffEntry): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected handleVoiceEntryArticulations(articulations: Articulation[],
                                            voiceEntry: VoiceEntry,
                                            staffEntry: GraphicalStaffEntry): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Adds a technical instruction at the given staff entry.
     * @param technicalInstructions
     * @param voiceEntry
     * @param staffEntry
     */
    protected handleVoiceEntryTechnicalInstructions(technicalInstructions: TechnicalInstruction[],
                                                    voiceEntry: VoiceEntry, staffEntry: GraphicalStaffEntry): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }


    protected handleTuplet(graphicalNote: GraphicalNote, tuplet: Tuplet, openTuplets: Tuplet[]): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected layoutVoiceEntry(voiceEntry: VoiceEntry, graphicalNotes: GraphicalNote[],
                               graphicalStaffEntry: GraphicalStaffEntry, hasPitchedNote: boolean): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected layoutStaffEntry(graphicalStaffEntry: GraphicalStaffEntry): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected createGraphicalTie(tie: Tie, startGse: GraphicalStaffEntry, endGse: GraphicalStaffEntry, startNote: GraphicalNote,
                                 endNote: GraphicalNote): GraphicalTie {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected updateStaffLineBorders(staffLine: StaffLine): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Iterate through all Measures and calculates the MeasureNumberLabels.
     * @param musicSystem
     */
    protected calculateMeasureNumberPlacement(musicSystem: MusicSystem): void {
        const staffLine: StaffLine = musicSystem.StaffLines[0];
        if (!staffLine || !staffLine.Measures[0]) {
            log.warn("calculateMeasureNumberPlacement: measure undefined for system.Id " + musicSystem.Id);
            return; // TODO apparently happens in script sometimes (mp #70)
        }
        let previousMeasureNumber: number = staffLine.Measures[0].MeasureNumber;
        let labelOffsetX: number = 0;
        for (let i: number = 0; i < staffLine.Measures.length; i++) {
            const measure: GraphicalMeasure = staffLine.Measures[i];
            let skip: boolean = this.rules.RenderMeasureNumbersOnlyAtSystemStart && i > 1;
            if (i === 1 && staffLine.Measures[0].parentSourceMeasure.ImplicitMeasure) {
                skip = false; // if the first measure (i=0) is a pickup measure, we shouldn't skip measure number 1 (i=1)
            }
            if (skip) {
                return; // no more measures number labels need to be rendered for this system, so we can just return instead of continue.
            }
            if (measure.MeasureNumber === 0 || measure.MeasureNumber === 1) {
                previousMeasureNumber = measure.MeasureNumber;
                // for the first measure, this label still needs to be created. Afterwards, this variable will hold the previous label's measure number.
            }
            if (measure !== staffLine.Measures[0] && this.rules.MeasureNumberLabelXOffset) {
                labelOffsetX = this.rules.MeasureNumberLabelXOffset;
            } else {
                labelOffsetX = 0; // don't offset label for first measure in staffline
            }

            const isFirstMeasureAndNotPrintedOne: boolean = this.rules.UseXMLMeasureNumbers &&
                measure.MeasureNumber === 1 && measure.parentSourceMeasure.getPrintedMeasureNumber() !== 1;
            if ((measure.MeasureNumber === previousMeasureNumber ||
                measure.MeasureNumber >= previousMeasureNumber + this.rules.MeasureNumberLabelOffset) &&
                !measure.parentSourceMeasure.ImplicitMeasure ||
                isFirstMeasureAndNotPrintedOne) {
                if (measure.MeasureNumber !== 1 ||
                    (measure.MeasureNumber === 1 && measure !== staffLine.Measures[0]) ||
                    isFirstMeasureAndNotPrintedOne
                    ) {
                    this.calculateSingleMeasureNumberPlacement(measure, staffLine, musicSystem, labelOffsetX);
                }
                previousMeasureNumber = measure.MeasureNumber;
            }
        }
    }

    /// <summary>
    /// This method calculates a single MeasureNumberLabel and adds it to the graphical label list of the music system
    /// </summary>
    /// <param name="measure"></param>
    /// <param name="staffLine"></param>
    /// <param name="musicSystem"></param>
    private calculateSingleMeasureNumberPlacement(measure: GraphicalMeasure, staffLine: StaffLine, musicSystem: MusicSystem,
                                                  labelOffsetX: number = 0): void {
        const labelNumber: string = measure.parentSourceMeasure.getPrintedMeasureNumber().toString();
        const label: Label = new Label(labelNumber);
        // maybe give rules as argument instead of just setting fontStyle and maybe other settings manually afterwards
        const graphicalLabel: GraphicalLabel = new GraphicalLabel(label, this.rules.MeasureNumberLabelHeight,
                                                                  TextAlignmentEnum.LeftBottom, this.rules);

        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;

        // calculate LabelBoundingBox and set PSI parent
        graphicalLabel.setLabelPositionAndShapeBorders();
        graphicalLabel.PositionAndShape.Parent = musicSystem.PositionAndShape;

        // calculate relative Position
        const relativeX: number = staffLine.PositionAndShape.RelativePosition.x +
            measure.PositionAndShape.RelativePosition.x - graphicalLabel.PositionAndShape.BorderMarginLeft +
            labelOffsetX;
        let relativeY: number;

        // and the corresponding SkyLine indices
        let start: number = relativeX;
        let end: number = relativeX - graphicalLabel.PositionAndShape.BorderLeft + graphicalLabel.PositionAndShape.BorderRight;

        start -= staffLine.PositionAndShape.RelativePosition.x;
        end -= staffLine.PositionAndShape.RelativePosition.x;

        // correct for hypersensitive collision checks, notes having skyline extend too far to left and right
        const startCollisionCheck: number = start + 0.5;
        const endCollisionCheck: number = end - 0.5;

        // get the minimum corresponding SkyLine value
        const skyLineMinValue: number = skyBottomLineCalculator.getSkyLineMinInRange(startCollisionCheck, endCollisionCheck);

        if (measure === staffLine.Measures[0]) {
            // must take into account possible MusicSystem Brackets
            let minBracketTopBorder: number = 0;
            if (musicSystem.GroupBrackets.length > 0) {
                for (const groupBracket of musicSystem.GroupBrackets) {
                    minBracketTopBorder = Math.min(minBracketTopBorder, groupBracket.PositionAndShape.BorderTop);
                }
            } else if (measure.ParentStaff.ParentInstrument.Parent) { // Parent InstrumentalGroup
                // note that GroupBracket creation is currently done after measure number creation, so we have to check it indirectly.
                minBracketTopBorder = -1;
            }
            relativeY = Math.min(skyLineMinValue, minBracketTopBorder);
        } else {
            relativeY = skyLineMinValue;
        }

        relativeY = Math.min(0, relativeY);

        graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(relativeX, relativeY);
        musicSystem.MeasureNumberLabels.push(graphicalLabel);
    }
    //So we can apply slurs first, then do these
    private calculateMeasureNumberSkyline(musicSystem: MusicSystem): void {
        const staffLine: StaffLine = musicSystem.StaffLines[0];
        for(const measureNumberLabel of musicSystem.MeasureNumberLabels) {
            // and the corresponding SkyLine indices
            let start: number = measureNumberLabel.PositionAndShape.RelativePosition.x;
            let end: number = start - measureNumberLabel.PositionAndShape.BorderLeft + measureNumberLabel.PositionAndShape.BorderRight;
            start -= staffLine.PositionAndShape.RelativePosition.x;
            end -= staffLine.PositionAndShape.RelativePosition.x;
            staffLine.SkyBottomLineCalculator.updateSkyLineInRange(start, end,
                measureNumberLabel.PositionAndShape.RelativePosition.y + measureNumberLabel.PositionAndShape.BorderMarginTop);
        }
    }

    /**
     * Calculate the shape (Bézier curve) for this tie.
     * @param tie
     * @param tieIsAtSystemBreak
     */
    protected layoutGraphicalTie(tie: GraphicalTie, tieIsAtSystemBreak: boolean, isTab: boolean): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Calculate the Lyrics YPositions for a single [[StaffLine]].
     * @param staffLine
     * @param lyricVersesNumber
     */
    protected calculateSingleStaffLineLyricsPosition(staffLine: StaffLine, lyricVersesNumber: string[]): GraphicalStaffEntry[] {
        let numberOfVerses: number = 0;
        let lyricsStartYPosition: number = this.rules.StaffHeight; // Add offset to prevent collision
        const relevantVerseNumbers: Map<string, boolean> = new Map<string, boolean>();
        const lyricsStaffEntriesList: GraphicalStaffEntry[] = [];
        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;

        // first find maximum Ycoordinate for the whole StaffLine
        let len: number = staffLine.Measures.length;
        for (let idx: number = 0; idx < len; ++idx) {
            const measure: GraphicalMeasure = staffLine.Measures[idx];
            const measureRelativePosition: PointF2D = measure.PositionAndShape.RelativePosition;
            const len2: number = measure.staffEntries.length;
            for (let idx2: number = 0; idx2 < len2; ++idx2) {
                const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx2];

                // Collect relevant verse numbers
                const len3: number = staffEntry.LyricsEntries.length;
                for (let idx3: number = 0; idx3 < len3; ++idx3) {
                    const lyricsEntry: LyricsEntry = staffEntry.LyricsEntries[idx3].LyricsEntry;
                    relevantVerseNumbers[lyricsEntry.VerseNumber] = lyricsEntry.IsChorus;
                }

                if (len3 > 0) {
                    lyricsStaffEntriesList.push(staffEntry);
                    numberOfVerses = Math.max(numberOfVerses, staffEntry.LyricsEntries.length);

                    // Position of Staffentry relative to StaffLine
                    const staffEntryPositionX: number = staffEntry.PositionAndShape.RelativePosition.x +
                        measureRelativePosition.x;

                    let minMarginLeft: number = Number.MAX_VALUE;
                    let maxMarginRight: number = Number.MIN_VALUE;

                    // if more than one LyricEntry in StaffEntry, find minMarginLeft, maxMarginRight of all corresponding Labels
                    for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
                        const lyricsEntryLabel: GraphicalLabel = staffEntry.LyricsEntries[i].GraphicalLabel;
                        minMarginLeft = Math.min(minMarginLeft, staffEntryPositionX + lyricsEntryLabel.PositionAndShape.BorderMarginLeft);
                        maxMarginRight = Math.max(maxMarginRight, staffEntryPositionX + lyricsEntryLabel.PositionAndShape.BorderMarginRight);
                    }

                    // check BottomLine in this range and take the maximum between the two values
                    const bottomLineMax: number = skyBottomLineCalculator.getBottomLineMaxInRange(minMarginLeft, maxMarginRight);
                    lyricsStartYPosition = Math.max(lyricsStartYPosition, bottomLineMax + this.rules.LyricsYMarginToBottomLine);
                }
            }
        }

        let maxPosition: number = 0;
        // iterate again through the Staffentries with LyricEntries
        len = lyricsStaffEntriesList.length;
        for (const staffEntry of lyricsStaffEntriesList) {

            // Filter verse numbers
            const filteredLyricVersesNumber: string[] = [];
            let isChorus: boolean = true;
            for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
                isChorus &&= staffEntry.LyricsEntries[i].LyricsEntry.IsChorus;
            }
            for (const lyricVerseNumber of lyricVersesNumber){
                if (relevantVerseNumbers[lyricVerseNumber] === isChorus) {
                    filteredLyricVersesNumber.push(lyricVerseNumber);
                }
            }

            // set LyricEntryLabel RelativePosition
            for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
                const lyricEntry: GraphicalLyricEntry = staffEntry.LyricsEntries[i];
                const lyricsEntryLabel: GraphicalLabel = lyricEntry.GraphicalLabel;

                // read the verseNumber and get index of this number in the sorted LyricVerseNumbersList of Instrument
                // eg verseNumbers: 2,3,4,6 => 1,2,3,4
                const verseNumber: string = lyricEntry.LyricsEntry.VerseNumber;
                const sortedLyricVerseNumberIndex: number = filteredLyricVersesNumber.indexOf(verseNumber);
                const firstPosition: number = lyricsStartYPosition + this.rules.LyricsHeight + this.rules.VerticalBetweenLyricsDistance +
                    this.rules.LyricsYOffsetToStaffHeight;

                // Y-position calculated according to aforementioned mapping
                const position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * sortedLyricVerseNumberIndex;
                // TODO not sure what this leadsheet lyrics positioning was supposed to be, but it seems to ALWAYS put the lyrics inside the stafflines now.
                // if (this.leadSheet) {
                //     position = 3.4 + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
                // }
                const previousRelativeX: number = lyricsEntryLabel.PositionAndShape.RelativePosition.x;
                lyricsEntryLabel.PositionAndShape.RelativePosition = new PointF2D(previousRelativeX, position);
                lyricsEntryLabel.Label.fontStyle = lyricEntry.LyricsEntry.FontStyle;
                maxPosition = Math.max(maxPosition, position);
            }
        }

        // update BottomLine (on the whole StaffLine's length)
        if (lyricsStaffEntriesList.length > 0) {
            const endX: number = staffLine.PositionAndShape.Size.width;
            let startX: number = lyricsStaffEntriesList[0].PositionAndShape.RelativePosition.x +
                lyricsStaffEntriesList[0].PositionAndShape.BorderMarginLeft +
                lyricsStaffEntriesList[0].parentMeasure.PositionAndShape.RelativePosition.x;
            startX = startX > endX ? endX : startX;
            skyBottomLineCalculator.updateBottomLineInRange(startX, endX, maxPosition);
        }
        return lyricsStaffEntriesList;
    }

    /**
     * calculates the dashes of lyric words and the extending underscore lines of syllables sung on more than one note.
     * @param lyricsStaffEntries
     */
    protected calculateLyricsExtendsAndDashes(lyricsStaffEntries: GraphicalStaffEntry[]): void {
        // iterate again to create now the extend lines and dashes for words
        for (let idx: number = 0, len: number = lyricsStaffEntries.length; idx < len; ++idx) {
            const staffEntry: GraphicalStaffEntry = lyricsStaffEntries[idx];
            // set LyricEntryLabel RelativePosition
            for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
                const lyricEntry: GraphicalLyricEntry = staffEntry.LyricsEntries[i];
                // calculate LyricWord's Dashes and underscoreLine
                if (lyricEntry.ParentLyricWord &&
                    lyricEntry.ParentLyricWord.GraphicalLyricsEntries[lyricEntry.ParentLyricWord.GraphicalLyricsEntries.length - 1] !== lyricEntry) {
                    this.calculateSingleLyricWord(lyricEntry);
                }
                // calculate the underscore line extend if needed
                if (lyricEntry.LyricsEntry.extend) {
                    this.calculateLyricExtend(lyricEntry);
                }
            }
        }
    }

    /**
     * Calculate a single OctaveShift for a [[MultiExpression]].
     * @param sourceMeasure
     * @param multiExpression
     * @param measureIndex
     * @param staffIndex
     */
    protected calculateSingleOctaveShift(sourceMeasure: SourceMeasure, multiExpression: MultiExpression,
                                         measureIndex: number, staffIndex: number): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Calculate a single Pedal for a [[MultiExpression]].
     * @param sourceMeasure
     * @param multiExpression
     * @param measureIndex
     * @param staffIndex
     */
    protected abstract calculateSinglePedal(sourceMeasure: SourceMeasure, multiExpression: MultiExpression,
        measureIndex: number, staffIndex: number): void;

    /**
     * Calculate all the textual [[RepetitionInstruction]]s (e.g. dal segno) for a single [[SourceMeasure]].
     * @param repetitionInstruction
     * @param measureIndex
     */
    protected calculateWordRepetitionInstruction(repetitionInstruction: RepetitionInstruction,
                                                 measureIndex: number): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    /**
     * Calculate all the Mood and Unknown Expressions for a single [[MultiExpression]].
     * @param multiExpression
     * @param measureIndex
     * @param staffIndex
     */
    protected calculateMoodAndUnknownExpression(multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
        // calculate absolute Timestamp
        const absoluteTimestamp: Fraction = multiExpression.AbsoluteTimestamp;
        const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[measureIndex];
        let relative: PointF2D = new PointF2D();

        const defaultYXml: number = multiExpression.UnknownList[0]?.defaultYXml;
        if ((multiExpression.MoodList.length > 0) || (multiExpression.UnknownList.length > 0)) {
        let combinedExprString: string  = "";
        for (let idx: number = 0, len: number = multiExpression.EntriesList.length; idx < len; ++idx) {
            const entry: MultiExpressionEntry = multiExpression.EntriesList[idx];
            if (entry.prefix !== "") {
                if (combinedExprString === "") {
                    combinedExprString += entry.prefix;
                } else {
                    combinedExprString += " " + entry.prefix;
                }
            }
            if (combinedExprString === "") {
                combinedExprString += entry.label;
            } else {
                combinedExprString += " " + entry.label;
            }
        }
        const staffLine: StaffLine = measures[staffIndex].ParentStaffLine;
        if (!staffLine) {
            log.debug("MusicSheetCalculator.calculateMoodAndUnknownExpression: staffLine undefined. Returning.");
            return;
        }
        relative = this.getRelativePositionInStaffLineFromTimestamp(absoluteTimestamp, staffIndex, staffLine, staffLine?.isPartOfMultiStaffInstrument());

        if (Math.abs(relative.x - 0) < 0.0001) {
            relative.x = measures[staffIndex].beginInstructionsWidth + this.rules.RhythmRightMargin;
        }

        const fontHeight: number = this.rules.UnknownTextHeight;
        const placement: PlacementEnum = multiExpression.getPlacementOfFirstEntry();
        const graphLabel: GraphicalLabel  = this.calculateLabel(staffLine,
                                                                relative, combinedExprString,
                                                                multiExpression.getFontstyleOfFirstEntry(),
                                                                placement,
                                                                fontHeight);
        if (this.rules.PlaceWordsInsideStafflineFromXml) {
            if (defaultYXml < 0 && defaultYXml > -50) { // within staffline
                let newY: number = defaultYXml / 10; // OSMD units
                newY += this.rules.PlaceWordsInsideStafflineYOffset;
                graphLabel.PositionAndShape.RelativePosition.y = newY;
            }
        }

        const gue: GraphicalUnknownExpression = new GraphicalUnknownExpression(
            staffLine, graphLabel, placement, measures[staffIndex]?.parentSourceMeasure, multiExpression);
        //    multiExpression); // TODO would be nice to hand over and save reference to original expression,
        //                         but MultiExpression is not an AbstractExpression.
        staffLine.AbstractExpressions.push(gue);
        }
    }

    /**
     * Delete all Objects that must be recalculated.
     * If graphicalMusicSheet.reCalculate has been called, then this method will be called to reset or remove all flexible
     * graphical music symbols (e.g. Ornaments, Lyrics, Slurs) graphicalMusicSheet will have MusicPages, they will have MusicSystems etc...
     */
    protected clearRecreatedObjects(): void {
        // Clear StaffEntries with GraphicalTies
        for (let idx: number = 0, len: number = this.staffEntriesWithGraphicalTies.length; idx < len; ++idx) {
            const staffEntriesWithGraphicalTie: GraphicalStaffEntry = this.staffEntriesWithGraphicalTies[idx];
            staffEntriesWithGraphicalTie.GraphicalTies.length = 0;
        }
        this.staffEntriesWithGraphicalTies.length = 0;
        return;
    }

    /**
     * This method handles a [[StaffEntryLink]].
     * @param graphicalStaffEntry
     * @param staffEntryLinks
     */
    protected handleStaffEntryLink(graphicalStaffEntry: GraphicalStaffEntry,
                                   staffEntryLinks: StaffEntryLink[]): void {
        log.debug("handleStaffEntryLink not implemented");
    }

    /**
     * Store the newly computed [[Measure]]s in newly created [[MusicSystem]]s.
     */
    protected calculateMusicSystems(): void {
        if (!this.graphicalMusicSheet.MeasureList) {
            return;
        }

        const allMeasures: GraphicalMeasure[][] = this.graphicalMusicSheet.MeasureList;
        if (!allMeasures) {
            return;
        }
        if (this.rules.MinMeasureToDrawIndex > allMeasures.length - 1) {
            log.debug("minimum measure to draw index out of range. resetting min measure index to limit.");
            this.rules.MinMeasureToDrawIndex = allMeasures.length - 1;
        }

        // visible 2D-MeasureList
        const visibleMeasureList: GraphicalMeasure[][] = [];
        for (let idx: number = this.rules.MinMeasureToDrawIndex, len: number = allMeasures.length;
            idx < len && idx <= this.rules.MaxMeasureToDrawIndex; ++idx) {
            const graphicalMeasures: GraphicalMeasure[] = allMeasures[idx];
            const visiblegraphicalMeasures: GraphicalMeasure[] = [];
            for (let idx2: number = 0, len2: number = graphicalMeasures.length; idx2 < len2; ++idx2) {
                const graphicalMeasure: GraphicalMeasure = allMeasures[idx][idx2];

                if (graphicalMeasure?.isVisible()) {
                    visiblegraphicalMeasures.push(graphicalMeasure);

                    if (this.rules.ColoringEnabled) {
                        // (re-)color notes
                        for (const staffEntry of graphicalMeasure.staffEntries) {
                            for (const gve of staffEntry.graphicalVoiceEntries) {
                                gve.color();
                            }
                        }
                    }
                }
            }
            visibleMeasureList.push(visiblegraphicalMeasures);
        }

        // find out how many StaffLine Instances we need
        let numberOfStaffLines: number = 0;

        for (let idx: number = 0, len: number = visibleMeasureList.length; idx < len; ++idx) {
            const gmlist: GraphicalMeasure[] = visibleMeasureList[idx];
            numberOfStaffLines = Math.max(gmlist.length, numberOfStaffLines);

            break;
        }
        if (numberOfStaffLines === 0) {
            return;
        }


        // build the MusicSystems (and StaffLines)
        const musicSystemBuilder: MusicSystemBuilder = new MusicSystemBuilder();
        musicSystemBuilder.initialize(this.graphicalMusicSheet, visibleMeasureList, numberOfStaffLines);
        this.musicSystems = musicSystemBuilder.buildMusicSystems();

        this.formatMeasures();

        // check for Measures with only WholeRestNotes and correct their X-Position (middle of Measure)
        // this.checkMeasuresForWholeRestNotes(); // this currently does nothing
        if (!this.leadSheet) {
            // calculate Beam Placement
            // this.calculateBeams(); // does nothing for now, because layoutBeams() is an empty method
            // possible Displacement of RestNotes
            this.optimizeRestPlacement();
            // possible Displacement of RestNotes
            this.calculateStaffEntryArticulationMarks();
            if (this.rules.RenderSlurs) { // technically we should separate slurs and ties, but shouldn't be relevant for now
                // calculate Ties
                this.calculateTieCurves();
            }
        }
        // calculate Sky- and BottomLine
        // will have reasonable values only between ObjectsBorders (eg StaffEntries)
        this.calculateSkyBottomLines();
        // calculate TupletsNumbers
        this.calculateTupletNumbers();

        // calculate MeasureNumbers
        if (this.rules.RenderMeasureNumbers) {
            for (let idx: number = 0, len: number = this.musicSystems.length; idx < len; ++idx) {
                const musicSystem: MusicSystem = this.musicSystems[idx];
                this.calculateMeasureNumberPlacement(musicSystem);
            }
        }
        if (this.rules.RenderFingerings) {
            this.calculateFingerings(); // if this is done after slurs, fingerings can be on top of slurs
        }
        // calculate Slurs
        if (!this.leadSheet && this.rules.RenderSlurs) {
            this.calculateSlurs();
        }
        this.calculateGlissandi();
        //Calculate measure number skyline AFTER slurs
        if (this.rules.RenderMeasureNumbers) {
            for (let idx: number = 0, len: number = this.musicSystems.length; idx < len; ++idx) {
                const musicSystem: MusicSystem = this.musicSystems[idx];
                this.calculateMeasureNumberSkyline(musicSystem);
            }
        }
        // calculate StaffEntry Ornaments
        // (must come after Slurs)
        if (!this.leadSheet) {
            this.calculateOrnaments();
        }
        // calculate StaffEntry ChordSymbols
        this.calculateChordSymbols();
        if (!this.leadSheet) {
            // calculate all Instantaneous/Continuous Dynamics Expressions
            this.calculateDynamicExpressions();
            // calculate all Mood and Unknown Expression
            this.calculateMoodAndUnknownExpressions();
            // Calculate the alignment of close expressions
            this.calculateExpressionAlignements();
            // calculate all OctaveShifts
            this.calculateOctaveShifts();
            if (this.rules.RenderPedals) {
                // calculate all Pedal Expressions
                this.calculatePedals();
            }
            // calcualte RepetitionInstructions (Dal Segno, Coda, etc)
            this.calculateWordRepetitionInstructions();
        }
        // calculate endings last, so they appear above measure numbers
        this.calculateRepetitionEndings();
        // calcualte all Tempo Expressions
        if (!this.leadSheet) {
            this.calculateTempoExpressions();
        }
        this.calculateRehearsalMarks();

        // calculate all LyricWords Positions
        this.calculateLyricsPosition();

        // update all StaffLine's Borders
        // create temporary Object, just to call the methods (in order to avoid declaring them static)
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const musicSystem: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
                this.updateStaffLineBorders(staffLine);
            }
        }

        // calculate Y-spacing -> MusicPages are created here
        musicSystemBuilder.calculateSystemYLayout();
        // calculate Comments for each Staffline
        this.calculateComments();
        // calculate marked Areas for Systems
        this.calculateMarkedAreas();

        // the following must be done after Y-spacing, when the MusicSystems's final Dimensions are set
        // set the final yPositions of Objects such as SystemLabels and SystemLinesContainers,
        // create all System Lines, Brackets and MeasureNumbers (for all systems and for all pages)
        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
                const isFirstSystem: boolean = idx === 0 && idx2 === 0;
                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                musicSystem.setMusicSystemLabelsYPosition();
                if (!this.leadSheet) {
                    musicSystem.setYPositionsToVerticalLineObjectsAndCreateLines(this.rules);
                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin, isFirstSystem);
                    musicSystem.createInstrumentBrackets(this.graphicalMusicSheet.ParentMusicSheet.Instruments, this.rules.StaffHeight);
                    musicSystem.createGroupBrackets(this.graphicalMusicSheet.ParentMusicSheet.InstrumentalGroups, this.rules.StaffHeight, 0);
                    musicSystem.alignBeginInstructions();
                } else if (musicSystem === musicSystem.Parent.MusicSystems[0]) {
                    musicSystem.createSystemLeftLine(this.rules.SystemThinLineWidth, this.rules.SystemLabelsRightMargin, isFirstSystem);
                }
                musicSystem.calculateBorders(this.rules);
            }
            const distance: number = graphicalMusicPage.MusicSystems[0].PositionAndShape.BorderTop;
            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                // let newPosition: PointF2D = new PointF2D(musicSystem.PositionAndShape.RelativePosition.x,
                // musicSystem.PositionAndShape.RelativePosition.y - distance);
                musicSystem.PositionAndShape.RelativePosition =
                    new PointF2D(musicSystem.PositionAndShape.RelativePosition.x, musicSystem.PositionAndShape.RelativePosition.y - distance);
            }
            // add ActivitySymbolClickArea - currently unused, extends boundingbox of MusicSystem unnecessarily -> PageRightMargin 0 impossible
            // for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
            //     const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
            //     for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
            //         const staffLine: StaffLine = musicSystem.StaffLines[idx3];
            //         staffLine.addActivitySymbolClickArea();
            //     }
            // }

            // calculate TopBottom Borders for all elements recursively
            //   necessary for composer label (page labels) for high notes in first system
            graphicalMusicPage.PositionAndShape.calculateTopBottomBorders();
            // TODO how much performance does this cost? can we reduce the amount of calculations, e.g. only checking top?

            // calculate all Labels's Positions for the first Page
            if (graphicalMusicPage === this.graphicalMusicSheet.MusicPages[0]) {
                this.calculatePageLabels(graphicalMusicPage);
            }

            // calculate TopBottom Borders for all elements recursively
            graphicalMusicPage.PositionAndShape.calculateTopBottomBorders(); // this is where top bottom borders were originally calculated (only once)
        }
    }

    protected calculateMarkedAreas(): void {
        //log.debug("calculateMarkedAreas not implemented");
        return;
    }

    protected calculateComments(): void {
        //log.debug("calculateComments not implemented");
        return;
    }

    protected calculateChordSymbols(): void {
        for (const musicSystem of this.musicSystems) {
            for (const staffLine of musicSystem.StaffLines) {
                const skybottomcalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
                let minimumOffset: number = Number.MAX_SAFE_INTEGER; // only calculated if option set
                if (this.rules.ChordSymbolYAlignment && this.rules.ChordSymbolYAlignmentScope === "staffline") {
                    // get the max y position of all chord symbols in the staffline in advance
                    const alignmentScopedStaffEntries: GraphicalStaffEntry[] = [];
                    for (const measure of staffLine.Measures) {
                        alignmentScopedStaffEntries.push(...measure.staffEntries);
                    }
                    minimumOffset = this.calculateAlignedChordSymbolsOffset(alignmentScopedStaffEntries, skybottomcalculator);
                }
                for (let measureStafflineIndex: number = 0; measureStafflineIndex < staffLine.Measures.length; measureStafflineIndex++) {
                    const measure: GraphicalMeasure = staffLine.Measures[measureStafflineIndex];
                    if (this.rules.ChordSymbolYAlignment && this.rules.ChordSymbolYAlignmentScope === "measure") {
                        minimumOffset = this.calculateAlignedChordSymbolsOffset(measure.staffEntries, skybottomcalculator);
                    }
                    let previousChordContainer: GraphicalChordSymbolContainer;
                    for (const staffEntry of measure.staffEntries) {
                        if (!staffEntry.graphicalChordContainers || staffEntry.graphicalChordContainers.length === 0) {
                            continue;
                        }
                        for (let i: number = 0; i < staffEntry.graphicalChordContainers.length; i++) {
                            const graphicalChordContainer: GraphicalChordSymbolContainer = staffEntry.graphicalChordContainers[i];
                            // check for chord not over a note
                            if (staffEntry.graphicalVoiceEntries.length === 0 && staffEntry.relInMeasureTimestamp.RealValue > 0) {
                                // re-position (second chord symbol on whole measure rest)
                                let firstNoteStartX: number = 0;
                                if (measure.staffEntries[0].relInMeasureTimestamp.RealValue === 0) {
                                    firstNoteStartX = measure.staffEntries[0].PositionAndShape.RelativePosition.x;
                                    if (measure.MeasureNumber === 1) {
                                        firstNoteStartX += this.rules.ChordSymbolWholeMeasureRestXOffsetMeasure1;
                                        // shift second chord same way as first chord
                                    }
                                }
                                const measureEndX: number = measure.PositionAndShape.Size.width - measure.endInstructionsWidth;
                                const proportionInMeasure: number = staffEntry.relInMeasureTimestamp.RealValue / measure.parentSourceMeasure.Duration.RealValue;
                                let newStartX: number = firstNoteStartX + (measureEndX - firstNoteStartX) * proportionInMeasure +
                                    graphicalChordContainer.PositionAndShape.BorderMarginLeft; // negative -> shift a bit left to where it starts visually
                                if (previousChordContainer) {
                                    // prevent overlap to previous chord symbol
                                    newStartX = Math.max(newStartX, previousChordContainer.PositionAndShape.RelativePosition.x +
                                        previousChordContainer.GraphicalLabel.PositionAndShape.Size.width +
                                        this.rules.ChordSymbolXSpacing);
                                }
                                graphicalChordContainer.PositionAndShape.RelativePosition.x = newStartX;
                                graphicalChordContainer.PositionAndShape.Parent = measure.staffEntries[0].PositionAndShape.Parent;
                                // TODO it would be more clean to set the staffEntry relative position instead of the container's,
                                //   so that the staff entry also gets a valid position (and not relative 0),
                                //   but this is tricky with elongationFactor, skyline etc, would need some adjustments
                                // // graphicalChordContainer.PositionAndShape.Parent = measure.staffEntries[0].PositionAndShape.Parent; // not here
                                // //   don't switch parent from StaffEntry if setting staffEntry.x
                                // staffEntry.PositionAndShape.RelativePosition.x = newStartX;
                                // staffEntry.PositionAndShape.calculateAbsolutePosition();
                            }
                            const gps: BoundingBox = graphicalChordContainer.PositionAndShape;
                            const parentBbox: BoundingBox = gps.Parent; // usually the staffEntry (bbox), but sometimes measure (for whole measure rests)
                            if (parentBbox.DataObject instanceof GraphicalMeasure) {
                                if (staffEntry.relInMeasureTimestamp.RealValue === 0) {
                                    gps.RelativePosition.x = Math.max(measure.beginInstructionsWidth, gps.RelativePosition.x);
                                    // beginInstructionsWidth wasn't set correctly before this
                                    if (measure.MeasureNumber === 1 && gps.RelativePosition.x > 3) {
                                        gps.RelativePosition.x += this.rules.ChordSymbolWholeMeasureRestXOffsetMeasure1;
                                    }
                                }
                            }
                            // check if there already exists a vertical staffentry with the same relative timestamp,
                            //   use its relativePosition (= x-align chord symbols to vertical staffentries in other measures)
                            if (staffEntry.PositionAndShape.RelativePosition.x === 0) {
                                const verticalMeasures: GraphicalMeasure[] = musicSystem.GraphicalMeasures[measureStafflineIndex];
                                for (const verticalMeasure of verticalMeasures) {
                                    let positionFound: boolean = false;
                                    for (const verticalSe of verticalMeasure.staffEntries) {
                                        if (verticalSe.relInMeasureTimestamp === staffEntry.relInMeasureTimestamp &&
                                            verticalSe.PositionAndShape.RelativePosition.x !== 0) {
                                            gps.RelativePosition.x = verticalSe.PositionAndShape.RelativePosition.x;
                                            positionFound = true;
                                            break;
                                        }
                                    }
                                    if (positionFound) {
                                        break;
                                    }
                                }
                            }
                            const start: number = gps.BorderMarginLeft + parentBbox.AbsolutePosition.x + gps.RelativePosition.x;
                            const end: number = gps.BorderMarginRight + parentBbox.AbsolutePosition.x + gps.RelativePosition.x;
                            if (!this.rules.ChordSymbolYAlignment || minimumOffset > 0) {
                                //minimumOffset = this.calculateAlignedChordSymbolsOffset([staffEntry], skybottomcalculator);
                                minimumOffset = skybottomcalculator.getSkyLineMinInRange(start, end); // same as above, less code executed
                            }
                            let yShift: number = 0;
                            if (i === 0) {
                                yShift += this.rules.ChordSymbolYOffset;
                                yShift += 0.1; // above is a bit closer to the notes than below ones for some reason
                            } else {
                                yShift += this.rules.ChordSymbolYPadding;
                            }
                            yShift *= -1;
                            const gLabel: GraphicalLabel = graphicalChordContainer.GraphicalLabel;
                            gLabel.PositionAndShape.RelativePosition.y = minimumOffset + yShift;
                            gLabel.setLabelPositionAndShapeBorders();
                            gLabel.PositionAndShape.calculateBoundingBox();
                            skybottomcalculator.updateSkyLineInRange(start, end, minimumOffset + gLabel.PositionAndShape.BorderMarginTop);
                            previousChordContainer = graphicalChordContainer;
                        }
                    }
                }
            }
        }
    }

    protected calculateAlignedChordSymbolsOffset(staffEntries: GraphicalStaffEntry[], sbc: SkyBottomLineCalculator): number {
        let minimumOffset: number = Number.MAX_SAFE_INTEGER;
        for (const staffEntry of staffEntries) {
            for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
                const gps: BoundingBox = graphicalChordContainer.PositionAndShape;
                const parentBbox: BoundingBox = gps.Parent; // usually the staffEntry (bbox), but sometimes measure (for whole measure rests)
                let start: number = gps.BorderMarginLeft + parentBbox.AbsolutePosition.x;
                let end: number = gps.BorderMarginRight + parentBbox.AbsolutePosition.x;
                if (parentBbox.DataObject instanceof GraphicalMeasure) {
                    start += (parentBbox.DataObject as GraphicalMeasure).beginInstructionsWidth;
                    end += (parentBbox.DataObject as GraphicalMeasure).beginInstructionsWidth;
                }
                minimumOffset = Math.min(minimumOffset, sbc.getSkyLineMinInRange(start, end));
            }
        }
        return minimumOffset;
    }

    /**
     * Do layout on staff measures which only consist of a full rest.
     * @param rest
     * @param gse
     * @param measure
     */
    protected layoutMeasureWithWholeRest(rest: GraphicalNote, gse: GraphicalStaffEntry,
                                         measure: GraphicalMeasure): void {
        return;
    }

    protected layoutBeams(staffEntry: GraphicalStaffEntry): void {
        return;
    }

    protected layoutArticulationMarks(articulations: Articulation[], voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry): void {
        return;
    }

    protected layoutOrnament(ornaments: OrnamentContainer, voiceEntry: VoiceEntry,
                             graphicalStaffEntry: GraphicalStaffEntry): void {
        return;
    }

    protected calculateRestNotePlacementWithinGraphicalBeam(graphicalStaffEntry: GraphicalStaffEntry,
                                                            restNote: GraphicalNote,
                                                            previousNote: GraphicalNote,
                                                            nextStaffEntry: GraphicalStaffEntry,
                                                            nextNote: GraphicalNote): void {
        return;
    }

    protected calculateTupletNumbers(): void {
        if (!this.rules.TupletNumberLimitConsecutiveRepetitions) {
            return;
        }
        let currentTupletNumber: number = -1;
        let currentTypeLength: Fraction = undefined;
        let consecutiveTupletCount: number = 0;
        let currentTuplet: Tuplet = undefined;
        let skipTuplet: Tuplet = undefined; // if set, ignore (further) handling of this tuplet
        const disabledPerVoice: Object = {};
        for (const instrument of this.graphicalMusicSheet.ParentMusicSheet.Instruments) {
            for (const voice of instrument.Voices) {
                consecutiveTupletCount = 0; // reset for next voice
                disabledPerVoice[voice.VoiceId] = {};
                for (const ve of voice.VoiceEntries) {
                    if (ve.Notes.length > 0) {
                        const firstNote: Note = ve.Notes[0];
                        if (!firstNote.NoteTuplet ||
                            firstNote.NoteTuplet.shouldBeBracketed(
                                this.rules.TupletsBracketedUseXMLValue,
                                this.rules.TupletsBracketed,
                                this.rules.TripletsBracketed
                            )
                        ) {
                            // don't disable tuplet numbers under these conditions, reset consecutive tuplet count
                            currentTupletNumber = -1;
                            consecutiveTupletCount = 0;
                            currentTuplet = undefined;
                            currentTypeLength = undefined;
                            continue;
                        }
                        if (firstNote.NoteTuplet === skipTuplet) {
                            continue;
                        }
                        let typeLength: Fraction = firstNote.TypeLength;
                        if (!typeLength) {
                            // shouldn't happen, now that rest notes have TypeLength set too, see VoiceGenerator.addRestNote(), addSingleNote()
                            //   see test_tuplets_starting_with_rests_layout.mxl (first measure bass)
                            log.warn("note missing TypeLength");
                            typeLength = firstNote.NoteTuplet.Fractions[0];
                        }
                        if (firstNote.NoteTuplet !== currentTuplet) {
                            if (disabledPerVoice[voice.VoiceId][firstNote.NoteTuplet.TupletLabelNumber]) {
                                if (disabledPerVoice[voice.VoiceId][firstNote.NoteTuplet.TupletLabelNumber][typeLength.RealValue]) {
                                    firstNote.NoteTuplet.RenderTupletNumber = false;
                                    skipTuplet = firstNote.NoteTuplet;
                                    continue;
                                }
                            }
                        }
                        if (firstNote.NoteTuplet.TupletLabelNumber !== currentTupletNumber ||
                            !typeLength.Equals(currentTypeLength) ||
                            firstNote.NoteTuplet.Bracket) {
                            currentTupletNumber = firstNote.NoteTuplet.TupletLabelNumber;
                            currentTypeLength = typeLength;
                            consecutiveTupletCount = 0;
                        }
                        currentTuplet = firstNote.NoteTuplet;
                        consecutiveTupletCount++;
                        if (consecutiveTupletCount <= this.rules.TupletNumberMaxConsecutiveRepetitions) {
                            firstNote.NoteTuplet.RenderTupletNumber = true; // need to re-activate after re-render when it was set to false
                        }
                        if (consecutiveTupletCount > this.rules.TupletNumberMaxConsecutiveRepetitions) {
                            firstNote.NoteTuplet.RenderTupletNumber = false;
                            if (this.rules.TupletNumberAlwaysDisableAfterFirstMax) {
                                if (!disabledPerVoice[voice.VoiceId][currentTupletNumber]) {
                                    disabledPerVoice[voice.VoiceId][currentTupletNumber] = {};
                                }
                                disabledPerVoice[voice.VoiceId][currentTupletNumber][typeLength.RealValue] = true;
                            }
                        }
                        skipTuplet = currentTuplet;
                    }
                }
            }
        }
        return;
    }

    protected calculateSlurs(): void {
        return;
    }

    protected calculateGlissandi(): void {
        return;
    }

    protected calculateDynamicExpressionsForMultiExpression(multiExpression: MultiExpression, measureIndex: number, staffIndex: number): void {
        return;
    }


    /**
     * This method calculates the RelativePosition of a single verbal GraphicalContinuousDynamic.
     * @param graphicalContinuousDynamic Graphical continous dynamic to be calculated
     * @param startPosInStaffline Starting point in staff line
     */
    protected calculateGraphicalVerbalContinuousDynamic(graphicalContinuousDynamic: GraphicalContinuousDynamicExpression,
                                                        startPosInStaffline: PointF2D): void {
        // if ContinuousDynamicExpression is given from words
        const graphLabel: GraphicalLabel = graphicalContinuousDynamic.Label;
        const left: number = startPosInStaffline.x + graphLabel.PositionAndShape.BorderMarginLeft;
        const right: number = startPosInStaffline.x + graphLabel.PositionAndShape.BorderMarginRight;
        // placement always below the currentStaffLine, with the exception of Voice Instrument (-> above)
        const placement: PlacementEnum = graphicalContinuousDynamic.ContinuousDynamic.Placement;
        const staffLine: StaffLine = graphicalContinuousDynamic.ParentStaffLine;
        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;

        let drawingHeight: number;
        if (placement === PlacementEnum.Below) {
            drawingHeight = skyBottomLineCalculator.getBottomLineMaxInRange(left, right);    // Bottom line
            graphLabel.PositionAndShape.RelativePosition = new PointF2D(startPosInStaffline.x, drawingHeight - graphLabel.PositionAndShape.BorderMarginTop);
        } else {
            drawingHeight = skyBottomLineCalculator.getSkyLineMinInRange(left, right);
            graphLabel.PositionAndShape.RelativePosition = new PointF2D(startPosInStaffline.x, drawingHeight - graphLabel.PositionAndShape.BorderMarginBottom);
        }
    }

   /**
    * This method calculates the RelativePosition of a single GraphicalContinuousDynamic.
    * @param graphicalContinuousDynamic Graphical continous dynamic to be calculated
    * @param startPosInStaffline Starting point in staff line
    */
    public calculateGraphicalContinuousDynamic(graphicalContinuousDynamic: GraphicalContinuousDynamicExpression, startPosInStaffline: PointF2D): void {
        const isSoftAccent: boolean = graphicalContinuousDynamic.IsSoftAccent;
        const staffIndex: number = graphicalContinuousDynamic.ParentStaffLine.ParentStaff.idInMusicSheet;
        // TODO: Previously the staffIndex was passed down. BUT you can (and this function actually does this) get it from
        // the musicSystem OR from the ParentStaffLine. Is this the same index?
        // const staffIndex: number = musicSystem.StaffLines.indexOf(staffLine);

        // We know we have an end measure because otherwise we won't be called
        const endMeasure: GraphicalMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(
            graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.SourceMeasureParent, staffIndex);
        if (!endMeasure) {
            log.warn("MusicSheetCalculator.calculateGraphicalContinuousDynamic: No endMeasure found");
            return;
        }

        graphicalContinuousDynamic.EndMeasure = endMeasure;
        const staffLine: StaffLine = graphicalContinuousDynamic.ParentStaffLine;
        const endStaffLine: StaffLine = endMeasure.ParentStaffLine;

        // check if Expression spreads over the same StaffLine or not
        const sameStaffLine: boolean = endStaffLine && staffLine === endStaffLine;

        let isPartOfMultiStaffInstrument: boolean = false;
        if (endStaffLine) { // unfortunately we can't do something like (endStaffLine?.check() || staffLine?.check()) in this typescript version
            isPartOfMultiStaffInstrument = endStaffLine?.isPartOfMultiStaffInstrument();
        } else if (staffLine) {
            isPartOfMultiStaffInstrument = staffLine?.isPartOfMultiStaffInstrument();
        }

        const endAbsoluteTimestamp: Fraction = Fraction.createFromFraction(graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.AbsoluteTimestamp);
        const container: VerticalGraphicalStaffEntryContainer = this.graphicalMusicSheet.GetVerticalContainerFromTimestamp(endAbsoluteTimestamp);
        const parentMeasure: GraphicalMeasure = container.getFirstNonNullStaffEntry().parentMeasure;
        const endOfMeasure: number = parentMeasure.PositionAndShape.AbsolutePosition.x + parentMeasure.PositionAndShape.BorderRight;
        let maxNoteLength: Fraction = new Fraction(0, 0, 0);
        for (const staffEntry of container.StaffEntries) {
            const currentMaxLength: Fraction = staffEntry?.sourceStaffEntry?.calculateMaxNoteLength(false);
            if ( currentMaxLength?.gt(maxNoteLength) ) {
                maxNoteLength = currentMaxLength;
            }
        }
        const useStaffEntryBorderLeft: boolean = !isSoftAccent &&
            graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo;
        const endPosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
            endAbsoluteTimestamp, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0,
            useStaffEntryBorderLeft);

        const beginOfNextNote: Fraction = Fraction.plus(endAbsoluteTimestamp, maxNoteLength);
        const placementFraction: Fraction = beginOfNextNote.clone();
        const endOffsetFraction: Fraction = graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.EndOffsetFraction;
        if (endOffsetFraction && this.rules.UseEndOffsetForExpressions) {
            placementFraction.Add(graphicalContinuousDynamic.ContinuousDynamic.EndMultiExpression.EndOffsetFraction);
        }
        // TODO for the last note of the piece (wedge ending after last note), this timestamp is incorrect, being after the last note
        //   but there's a workaround in getRelativePositionInStaffLineFromTimestamp() via the variable endAfterRightStaffEntry
        const nextNotePosInStaffLine: PointF2D = this.getRelativePositionInStaffLineFromTimestamp(
            placementFraction, staffIndex, endStaffLine, isPartOfMultiStaffInstrument, 0,
            graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.diminuendo);
        const wedgePadding: number = this.rules.SoftAccentWedgePadding;
        const staffEntryWidth: number = container.getFirstNonNullStaffEntry().PositionAndShape.Size.width; // staff entry widths for whole notes is too long
        const sizeFactor: number = this.rules.SoftAccentSizeFactor;
        //const standardWidth: number = 2;

        //If the next note position is not on the next staffline
        //extend close to the next note
        if (isSoftAccent) {
            //startPosInStaffline.x -= 1;
            startPosInStaffline.x -= staffEntryWidth / 2 * sizeFactor + wedgePadding;
            endPosInStaffLine.x = startPosInStaffline.x + staffEntryWidth / 2 * sizeFactor;
        } else if (nextNotePosInStaffLine.x > endPosInStaffLine.x && nextNotePosInStaffLine.x < endOfMeasure) {
            endPosInStaffLine.x += (nextNotePosInStaffLine.x - endPosInStaffLine.x) / this.rules.WedgeEndDistanceBetweenTimestampsFactor;
        } else { //Otherwise extend to the end of the measure
            endPosInStaffLine.x = endOfMeasure - this.rules.WedgeHorizontalMargin;
        }

        const startCollideBox: BoundingBox =
            this.dynamicExpressionMap.get(graphicalContinuousDynamic.ContinuousDynamic.StartMultiExpression.AbsoluteTimestamp.RealValue);
        if (startCollideBox) {
            if ((startCollideBox.DataObject as any).ParentStaffLine === staffLine) {
                // TODO the dynamicExpressionMap doesn't distinguish between staffLines, so we may react to a different staffline otherwise
                //   so the more fundamental solution would be to fix dynamicExpressionMap mapping across stafflines.
                startPosInStaffline.x = startCollideBox.RelativePosition.x + this.rules.WedgeHorizontalMargin;
                startPosInStaffline.x += startCollideBox.BorderMarginRight;
            }
        }
        //currentMusicSystem and currentStaffLine
        const musicSystem: MusicSystem = staffLine.ParentMusicSystem;
        const currentStaffLineIndex: number = musicSystem.StaffLines.indexOf(staffLine);
        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
        // let expressionIndex: number;

        // placement always below the currentStaffLine, with the exception of Voice Instrument (-> above)
        const placement: PlacementEnum = graphicalContinuousDynamic.ContinuousDynamic.Placement;

        // if ContinuousDynamicExpression is given from wedge
        let endGraphicalContinuousDynamic: GraphicalContinuousDynamicExpression = undefined;

        // last length check
        if (sameStaffLine && endPosInStaffLine.x - startPosInStaffline.x < this.rules.WedgeMinLength && !isSoftAccent) {
            endPosInStaffLine.x = startPosInStaffline.x + this.rules.WedgeMinLength;
        }

        // First staff wedge always starts at the given position and the last and inbetween wedges always start at the begin of measure
        //   TODO: rename upper / lower to first / last, now that we can have inbetween wedges, though this creates a huge diff, and this should be clear now.
        const upperStartX: number = startPosInStaffline.x;
        let lowerStartX: number = endStaffLine.Measures[0].beginInstructionsWidth - this.rules.WedgeHorizontalMargin - 2;
        //TODO fix this when a range of measures to draw is given that doesn't include all the dynamic's measures (e.g. for crescendo)
        let upperEndX: number = 0;
        let lowerEndX: number = 0;

        /** Wedges between first and last staffline, in case we span more than 2 stafflines. */
        const inbetweenWedges: GraphicalContinuousDynamicExpression[] = [];
        if (!sameStaffLine) {
            // add wedge in all stafflines between (including) start and end measure
            upperEndX = staffLine.PositionAndShape.Size.width;
            lowerEndX = endPosInStaffLine.x;

            // get all stafflines between start measure and end measure, and add wedges for them.
            //   This would be less lines of code if there was already a list of stafflines for the sheet.
            const stafflinesCovered: StaffLine[] = [staffLine, endStaffLine]; // start and end staffline already get a wedge
            const startMeasure: GraphicalMeasure = graphicalContinuousDynamic.StartMeasure;
            let nextMeasure: GraphicalMeasure = startMeasure;
            let iterations: number = 0; // safety measure against infinite loop
            let sourceMeasureIndex: number = startMeasure.parentSourceMeasure.measureListIndex;
            while (nextMeasure !== endMeasure && iterations < 1000) {
                const nextSourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[sourceMeasureIndex];
                const potentialNextMeasure: GraphicalMeasure = this.graphicalMusicSheet.getGraphicalMeasureFromSourceMeasureAndIndex(
                    nextSourceMeasure, staffIndex
                );
                if (potentialNextMeasure) {
                    nextMeasure = potentialNextMeasure;
                    const nextStaffline: StaffLine = nextMeasure.ParentStaffLine;
                    if (!stafflinesCovered.includes(nextStaffline)) {
                        stafflinesCovered.push(nextStaffline);
                        const newWedge: GraphicalContinuousDynamicExpression =
                            new GraphicalContinuousDynamicExpression(
                                graphicalContinuousDynamic.ContinuousDynamic,
                                nextStaffline,
                                nextStaffline.Measures[0].parentSourceMeasure
                            );
                        newWedge.IsSplittedPart = true;
                        inbetweenWedges.push(newWedge);
                    }
                }
                sourceMeasureIndex++;
                iterations++;
            }

            // last wedge at endMeasure
            endGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
                graphicalContinuousDynamic.ContinuousDynamic, endStaffLine, endMeasure.parentSourceMeasure);
            endGraphicalContinuousDynamic.IsSplittedPart = true;
            graphicalContinuousDynamic.IsSplittedPart = true;
        } else {
            upperEndX = endPosInStaffLine.x;
        }
        if (isSoftAccent) {
            // secondGraphicalContinuousDynamic = new GraphicalContinuousDynamicExpression(
            //     graphicalContinuousDynamic.ContinuousDynamic,
            //     graphicalContinuousDynamic.ParentStaffLine,
            //     graphicalContinuousDynamic.StartMeasure.parentSourceMeasure
            // );
            // secondGraphicalContinuousDynamic.StartIsEnd = true;
            // doesn't work well with secondGraphicalDynamic, positions/rendering messed up
            lowerStartX = endPosInStaffLine.x + wedgePadding;
            lowerEndX = lowerStartX + staffEntryWidth / 2 * sizeFactor;
        }

        // the Height of the Expression's placement
        let idealY: number = 0;
        let endIdealY: number = 0;

        if (placement === PlacementEnum.Below) {
            // can be a single Staff Instrument or an Instrument with 2 Staves
            let nextStaffLineIndex: number = 0;
            if (currentStaffLineIndex < musicSystem.StaffLines.length - 1) {
                nextStaffLineIndex = currentStaffLineIndex + 1;
            }

            // check, maybe currentStaffLine is the last of the MusicSystem (and it has a ContinuousDynamicExpression with placement below)
            if (nextStaffLineIndex > currentStaffLineIndex) {
                // currentStaffLine isn't the last of the MusicSystem
                const nextStaffLine: StaffLine = musicSystem.StaffLines[nextStaffLineIndex];

                const distanceBetweenStaffLines: number = nextStaffLine.PositionAndShape.RelativePosition.y -
                    staffLine.PositionAndShape.RelativePosition.y -
                    this.rules.StaffHeight;

                // ideal Height is exactly between the two StaffLines
                idealY = this.rules.StaffHeight + distanceBetweenStaffLines / 2;
            } else {
                // currentStaffLine is the MusicSystem's last
                idealY = this.rules.WedgePlacementBelowY;
            }

            // must consider the upperWedge starting/ending tip for the comparison with the BottomLine
            idealY -= this.rules.WedgeOpeningLength / 2;
            if (!sameStaffLine) {
                // Set the value for the splitted y position to the ideal position before we check and modify it with
                // the skybottom calculator detection
                endIdealY = idealY;
            }
            // must check BottomLine for possible collisions within the Length of the Expression
            // find the corresponding max value for the given Length
            let maxBottomLineValueForExpressionLength: number = skyBottomLineCalculator.getBottomLineMaxInRange(upperStartX, upperEndX);

            // if collisions, then set the Height accordingly
            if (maxBottomLineValueForExpressionLength > idealY) {
                idealY = maxBottomLineValueForExpressionLength;
            }

            // special case - wedge must be drawn within the boundaries of a crossedBeam
            const withinCrossedBeam: boolean = false;

            if (currentStaffLineIndex < musicSystem.StaffLines.length - 1) {
                // find GraphicalStaffEntries closest to wedge's xPositions
                const closestToEndStaffEntry: GraphicalStaffEntry = staffLine.findClosestStaffEntry(upperEndX);
                const closestToStartStaffEntry: GraphicalStaffEntry = staffLine.findClosestStaffEntry(upperStartX);

                if (closestToStartStaffEntry && closestToEndStaffEntry) {
                    // must check both StaffLines
                    const startVerticalContainer: VerticalGraphicalStaffEntryContainer = closestToStartStaffEntry.parentVerticalContainer;
                    // const endVerticalContainer: VerticalGraphicalStaffEntryContainer = closestToEndStaffEntry.parentVerticalContainer;
                    if (startVerticalContainer) {
                        // TODO: Needs to be implemented?
                        // withinCrossedBeam = areStaffEntriesWithinCrossedBeam(startVerticalContainer,
                        // endVerticalContainer, currentStaffLineIndex, nextStaffLineIndex);
                    }

                    if (withinCrossedBeam) {
                        const nextStaffLine: StaffLine = musicSystem.StaffLines[nextStaffLineIndex];
                        const nextStaffLineMinSkyLineValue: number = nextStaffLine.SkyBottomLineCalculator.getSkyLineMinInRange(upperStartX, upperEndX);
                        const distanceBetweenStaffLines: number = nextStaffLine.PositionAndShape.RelativePosition.y -
                            staffLine.PositionAndShape.RelativePosition.y;
                        const relativeSkyLineHeight: number = distanceBetweenStaffLines + nextStaffLineMinSkyLineValue;

                        if (relativeSkyLineHeight - this.rules.WedgeOpeningLength > this.rules.StaffHeight) {
                            idealY = relativeSkyLineHeight - this.rules.WedgeVerticalMargin;
                        } else {
                            idealY = this.rules.StaffHeight + this.rules.WedgeOpeningLength;
                        }

                        graphicalContinuousDynamic.NotToBeRemoved = true;
                    }
                }
            }

            // do the same in case of a Wedge ending at another StaffLine
            if (!sameStaffLine) {
                maxBottomLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(lowerStartX, lowerEndX);

                if (maxBottomLineValueForExpressionLength > endIdealY) {
                    endIdealY = maxBottomLineValueForExpressionLength;
                }

                endIdealY += this.rules.WedgeOpeningLength / 2;
                endIdealY += this.rules.WedgeVerticalMargin;
            }

            if (!withinCrossedBeam) {
                idealY += this.rules.WedgeOpeningLength / 2;
                idealY += this.rules.WedgeVerticalMargin;
            }

        } else if (placement === PlacementEnum.Above) {
            // single Staff Instrument (eg Voice)
            if (staffLine.ParentStaff.ParentInstrument.Staves.length === 1) {
                // single Staff Voice Instrument
                idealY = this.rules.WedgePlacementAboveY;
            } else {
                // Staff = not the first Staff of a 2-staved Instrument
                let previousStaffLineIndex: number = 0;
                if (currentStaffLineIndex > 0) {
                    previousStaffLineIndex = currentStaffLineIndex - 1;
                }

                const previousStaffLine: StaffLine = musicSystem.StaffLines[previousStaffLineIndex];
                const distanceBetweenStaffLines: number = staffLine.PositionAndShape.RelativePosition.y -
                    previousStaffLine.PositionAndShape.RelativePosition.y -
                    this.rules.StaffHeight;

                // ideal Height is exactly between the two StaffLines
                idealY = -distanceBetweenStaffLines / 2;
            }

            // must consider the upperWedge starting/ending tip for the comparison with the SkyLine
            idealY += this.rules.WedgeOpeningLength / 2;
            if (!sameStaffLine) {
                endIdealY = idealY;
            }

            // must check SkyLine for possible collisions within the Length of the Expression
            // find the corresponding min value for the given Length
            let minSkyLineValueForExpressionLength: number = skyBottomLineCalculator.getSkyLineMinInRange(upperStartX, upperEndX);

            // if collisions, then set the Height accordingly
            if (minSkyLineValueForExpressionLength < idealY) {
                idealY = minSkyLineValueForExpressionLength;
            }
            const withinCrossedBeam: boolean = false;

            // special case - wedge must be drawn within the boundaries of a crossedBeam
            if (staffLine.ParentStaff.ParentInstrument.Staves.length > 1 && currentStaffLineIndex > 0) {
                // find GraphicalStaffEntries closest to wedge's xPositions
                const closestToStartStaffEntry: GraphicalStaffEntry = staffLine.findClosestStaffEntry(upperStartX);
                const closestToEndStaffEntry: GraphicalStaffEntry = staffLine.findClosestStaffEntry(upperEndX);

                if (closestToStartStaffEntry && closestToEndStaffEntry) {
                    // must check both StaffLines
                    const startVerticalContainer: VerticalGraphicalStaffEntryContainer = closestToStartStaffEntry.parentVerticalContainer;
                    // const endVerticalContainer: VerticalGraphicalStaffEntryContainer = closestToEndStaffEntry.parentVerticalContainer;
                    const formerStaffLineIndex: number = currentStaffLineIndex - 1;
                    if (startVerticalContainer) {
                        // withinCrossedBeam = this.areStaffEntriesWithinCrossedBeam(startVerticalContainer,
                        // endVerticalContainer, currentStaffLineIndex, formerStaffLineIndex);
                    }

                    if (withinCrossedBeam) {
                        const formerStaffLine: StaffLine = musicSystem.StaffLines[formerStaffLineIndex];
                        const formerStaffLineMaxBottomLineValue: number = formerStaffLine.SkyBottomLineCalculator.
                                                                          getBottomLineMaxInRange(upperStartX, upperEndX);
                        const distanceBetweenStaffLines: number = staffLine.PositionAndShape.RelativePosition.y -
                            formerStaffLine.PositionAndShape.RelativePosition.y;
                        const relativeSkyLineHeight: number = distanceBetweenStaffLines - formerStaffLineMaxBottomLineValue;
                        idealY = (relativeSkyLineHeight - this.rules.StaffHeight) / 2 + this.rules.StaffHeight;
                    }
                }
            }

            // do the same in case of a Wedge ending at another StaffLine
            if (!sameStaffLine) {
                minSkyLineValueForExpressionLength = endStaffLine.SkyBottomLineCalculator.getSkyLineMinInRange(lowerStartX, lowerEndX);

                if (minSkyLineValueForExpressionLength < endIdealY) {
                    endIdealY = minSkyLineValueForExpressionLength;
                }

                endIdealY -= this.rules.WedgeOpeningLength / 2;
            }

            if (!withinCrossedBeam) {
                idealY -= this.rules.WedgeOpeningLength / 2;
                idealY -= this.rules.WedgeVerticalMargin;
            }
            if (!sameStaffLine) {
                endIdealY -= this.rules.WedgeVerticalMargin;
            }
        }

        // now we have the correct placement Height for the Expression
        // the idealY is calculated relative to the currentStaffLine

        graphicalContinuousDynamic.Lines.clear();
        // create wedges (crescendo / decrescendo lines)
        if (isSoftAccent) {
            graphicalContinuousDynamic.createFirstHalfCrescendoLines(upperStartX, upperEndX, idealY);
            graphicalContinuousDynamic.createSecondHalfDiminuendoLines(lowerStartX, lowerEndX, idealY);
            graphicalContinuousDynamic.calcPsi();
        } else if (sameStaffLine && !isSoftAccent) {
            // either create crescendo or decrescendo lines, same principle / parameters.
            graphicalContinuousDynamic.createLines(upperStartX, upperEndX, idealY);
            graphicalContinuousDynamic.calcPsi();
        } else {
            // two+ different Wedges
            // first wedge
            graphicalContinuousDynamic.createFirstHalfLines(upperStartX, upperEndX, idealY);
            graphicalContinuousDynamic.calcPsi();

            // inbetween wedges
            for (let i: number = 0; i < inbetweenWedges.length; i++) {
                const inbetweenWedge: GraphicalContinuousDynamicExpression = inbetweenWedges[i];
                const inbetweenStaffline: StaffLine = inbetweenWedge.ParentStaffLine;
                let betweenIdealY: number = endIdealY;

                if (placement === PlacementEnum.Below) {
                    const maxBottomLineValueForExpressionLength: number =
                    endStaffLine.SkyBottomLineCalculator.getBottomLineMaxInRange(lowerStartX, upperEndX);
                    if (maxBottomLineValueForExpressionLength > betweenIdealY) {
                        betweenIdealY = maxBottomLineValueForExpressionLength;
                    }
                    betweenIdealY += this.rules.WedgeOpeningLength / 2;
                    betweenIdealY += this.rules.WedgeVerticalMargin;
                } else if (placement === PlacementEnum.Above) {
                    const minSkyLineValueForExpressionLength: number =
                        inbetweenStaffline.SkyBottomLineCalculator.getSkyLineMinInRange(lowerStartX, lowerEndX);
                    if (minSkyLineValueForExpressionLength < endIdealY) {
                        betweenIdealY = minSkyLineValueForExpressionLength;
                    }
                    betweenIdealY -= this.rules.WedgeOpeningLength / 2;
                }

                if (graphicalContinuousDynamic.ContinuousDynamic.DynamicType === ContDynamicEnum.crescendo) {
                    inbetweenWedge.createSecondHalfCrescendoLines(0, inbetweenStaffline.PositionAndShape.Size.width, betweenIdealY);
                    // for crescendo, we want the same look as on the last staffline: not starting with an intersection / starting wedge
                } else {
                    inbetweenWedge.createFirstHalfDiminuendoLines(0, inbetweenStaffline.PositionAndShape.Size.width, betweenIdealY);
                    // for diminuendo, we want the same look as on the first staffline: not ending in an intersection / looking finished
                }
                inbetweenWedge.calcPsi();
            }

            // last wedge
            endGraphicalContinuousDynamic.createSecondHalfLines(lowerStartX, lowerEndX, endIdealY);
            endGraphicalContinuousDynamic.calcPsi();
        }
        this.dynamicExpressionMap.set(endAbsoluteTimestamp.RealValue, graphicalContinuousDynamic.PositionAndShape);
    }

    /**
     * This method calculates the RelativePosition of a single GraphicalInstantaneousDynamicExpression.
     * @param graphicalInstantaneousDynamic Dynamic expression to be calculated
     * @param startPosInStaffline Starting point in staff line
     */
    protected calculateGraphicalInstantaneousDynamicExpression(graphicalInstantaneousDynamic: GraphicalInstantaneousDynamicExpression,
                                                               startPosInStaffline: PointF2D, timestamp: Fraction): void {
        // get Margin Dimensions
        const staffLine: StaffLine = graphicalInstantaneousDynamic.ParentStaffLine;
        if (!staffLine) {
            return; // TODO can happen when drawing range modified (osmd.setOptions({drawFromMeasureNumber...}))
        }

        const left: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginLeft;
        const right: number = startPosInStaffline.x + graphicalInstantaneousDynamic.PositionAndShape.BorderMarginRight;
        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
        let yPosition: number = 0;

        // calculate yPosition according to Placement
        if (graphicalInstantaneousDynamic.Placement === PlacementEnum.Above) {
            const skyLineValue: number = skyBottomLineCalculator.getSkyLineMinInRange(left, right);

            // if StaffLine part of multiStaff Instrument and not the first one, ideal yPosition middle of distance between Staves
            if (staffLine.isPartOfMultiStaffInstrument() && staffLine.ParentStaff !== staffLine.ParentStaff.ParentInstrument.Staves[0]) {
                const formerStaffLine: StaffLine = staffLine.ParentMusicSystem.StaffLines[staffLine.ParentMusicSystem.StaffLines.indexOf(staffLine) - 1];
                const difference: number = staffLine.PositionAndShape.RelativePosition.y -
                    formerStaffLine.PositionAndShape.RelativePosition.y - this.rules.StaffHeight;

                // take always into account the size of the Dynamic
                if (skyLineValue > -difference / 2) {
                    yPosition = -difference / 2;
                } else {
                    yPosition = skyLineValue - graphicalInstantaneousDynamic.PositionAndShape.BorderMarginBottom;
                }
            } else {
                yPosition = skyLineValue - graphicalInstantaneousDynamic.PositionAndShape.BorderMarginBottom;
            }

            graphicalInstantaneousDynamic.PositionAndShape.RelativePosition = new PointF2D(startPosInStaffline.x, yPosition);
        } else if (graphicalInstantaneousDynamic.Placement === PlacementEnum.Below) {
            const bottomLineValue: number = skyBottomLineCalculator.getBottomLineMaxInRange(left, right);
            // if StaffLine part of multiStaff Instrument and not the last one, ideal yPosition middle of distance between Staves
            const lastStaff: Staff = staffLine.ParentStaff.ParentInstrument.Staves[staffLine.ParentStaff.ParentInstrument.Staves.length - 1];
            if (staffLine.isPartOfMultiStaffInstrument() && staffLine.ParentStaff !== lastStaff) {
                const nextStaffLine: StaffLine = staffLine.ParentMusicSystem.StaffLines[staffLine.ParentMusicSystem.StaffLines.indexOf(staffLine) + 1];
                const difference: number = nextStaffLine.PositionAndShape.RelativePosition.y -
                    staffLine.PositionAndShape.RelativePosition.y - this.rules.StaffHeight;
                const border: number = graphicalInstantaneousDynamic.PositionAndShape.BorderMarginBottom;

                // take always into account the size of the Dynamic
                if (bottomLineValue + border < this.rules.StaffHeight + difference / 2) {
                    yPosition = this.rules.StaffHeight + difference / 2;
                } else {
                    yPosition = bottomLineValue - graphicalInstantaneousDynamic.PositionAndShape.BorderMarginTop;
                }
            } else {
                yPosition = bottomLineValue - graphicalInstantaneousDynamic.PositionAndShape.BorderMarginTop;
            }

            graphicalInstantaneousDynamic.PositionAndShape.RelativePosition = new PointF2D(startPosInStaffline.x, yPosition);
        }
        graphicalInstantaneousDynamic.updateSkyBottomLine();
    }

    protected calcGraphicalRepetitionEndingsRecursively(repetition: Repetition): void {
        return;
    }

    /**
     * Calculate a single GraphicalRepetition.
     * @param start
     * @param end
     * @param numberText
     * @param offset
     * @param leftOpen
     * @param rightOpen
     */
    protected layoutSingleRepetitionEnding(start: GraphicalMeasure, end: GraphicalMeasure, numberText: string,
                                           offset: number, leftOpen: boolean, rightOpen: boolean): void {
        return;
    }

    protected calculateLabel(staffLine: StaffLine,
                             relative: PointF2D,
                             combinedString: string,
                             style: FontStyles,
                             placement: PlacementEnum,
                             fontHeight: number,
                             textAlignment: TextAlignmentEnum = TextAlignmentEnum.CenterBottom,
                             yPadding: number = 0): GraphicalLabel {
        const label: Label = new Label(combinedString, textAlignment);
        label.fontStyle = style;
        label.fontHeight = fontHeight;

        // TODO_RR: TextHeight from first Entry
        const graphLabel: GraphicalLabel = new GraphicalLabel(label, fontHeight, label.textAlignment, this.rules, staffLine.PositionAndShape);
        const marginFactor: number = 1.1;

        if (placement === PlacementEnum.Below) {
            graphLabel.Label.textAlignment = TextAlignmentEnum.LeftTop;
        }

        graphLabel.setLabelPositionAndShapeBorders();
        graphLabel.PositionAndShape.BorderMarginBottom *= marginFactor;
        graphLabel.PositionAndShape.BorderMarginTop *= marginFactor;
        graphLabel.PositionAndShape.BorderMarginLeft *= marginFactor;
        graphLabel.PositionAndShape.BorderMarginRight *= marginFactor;

        let left: number = relative.x + graphLabel.PositionAndShape.BorderMarginLeft;
        let right: number = relative.x + graphLabel.PositionAndShape.BorderMarginRight;

        // check if GraphicalLabel exceeds the StaffLine's borders.
        if (right > staffLine.PositionAndShape.Size.width) {
            right = staffLine.PositionAndShape.Size.width - this.rules.MeasureRightMargin;
            left = right - graphLabel.PositionAndShape.MarginSize.width;
            relative.x = left - graphLabel.PositionAndShape.BorderMarginLeft;
        }

        // find allowed position (where the Label can be positioned) from Sky- BottomLine
        let drawingHeight: number;
        const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
        if (placement === PlacementEnum.Below) {
            drawingHeight = skyBottomLineCalculator.getBottomLineMaxInRange(left, right) + yPadding;
        } else {
            drawingHeight = skyBottomLineCalculator.getSkyLineMinInRange(left, right) - yPadding;
        }

        // set RelativePosition
        graphLabel.PositionAndShape.RelativePosition = new PointF2D(relative.x, drawingHeight);

        // update Sky- BottomLine
        if (placement === PlacementEnum.Below) {
            skyBottomLineCalculator.updateBottomLineInRange(left, right, graphLabel.PositionAndShape.BorderMarginBottom + drawingHeight);
        } else {
            skyBottomLineCalculator.updateSkyLineInRange(left, right, graphLabel.PositionAndShape.BorderMarginTop + drawingHeight);
        }
        return graphLabel;
    }

    protected calculateTempoExpressionsForMultiTempoExpression(sourceMeasure: SourceMeasure, multiTempoExpression: MultiTempoExpression,
                                                               measureIndex: number): void {
        // calculate absolute Timestamp
        const absoluteTimestamp: Fraction = Fraction.plus(sourceMeasure.AbsoluteTimestamp, multiTempoExpression.Timestamp);
        const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[measureIndex];
        let relative: PointF2D = new PointF2D();

        if (multiTempoExpression.ContinuousTempo || multiTempoExpression.InstantaneousTempo) {
            // TempoExpressions always on the first visible System's StaffLine // TODO is it though?
            if (this.rules.MinMeasureToDrawIndex > 0) {
                return; // assuming that the tempo is always in measure 1 (idx 0), adding the expression causes issues when we don't draw measure 1
            }
            if (!measures[0]) {
                return;
            }
            let staffLine: StaffLine = measures[0].ParentStaffLine;
            let firstVisibleMeasureX: number = measures[0].PositionAndShape.RelativePosition.x;
            let verticalIndex: number = 0;
            for (let j: number = 0; j < measures.length; j++) {
                if (!measures[j].ParentStaffLine || measures[j].ParentStaffLine.Measures.length === 0) {
                    continue;
                }

                if (measures[j].ParentStaffLine.Measures.length > 0) {
                    staffLine = measures[j].ParentStaffLine;
                    firstVisibleMeasureX = measures[j].PositionAndShape.RelativePosition.x;
                    verticalIndex = j;
                    break;
                }
            }
            relative = this.getRelativePositionInStaffLineFromTimestamp(absoluteTimestamp,
                                                                        verticalIndex,
                                                                        staffLine,
                                                                        staffLine.isPartOfMultiStaffInstrument(),
                                                                        firstVisibleMeasureX);

            // also placement Above
            if (multiTempoExpression.EntriesList.length > 0 &&
                multiTempoExpression.EntriesList[0].Expression instanceof InstantaneousTempoExpression) {
                const instantaniousTempo: InstantaneousTempoExpression = (multiTempoExpression.EntriesList[0].Expression as InstantaneousTempoExpression);
                instantaniousTempo.Placement = PlacementEnum.Above;

                // if an InstantaniousTempoExpression exists at the very beginning then
                // check if expression is positioned at first ever StaffEntry and
                // check if MusicSystem is first MusicSystem
                if (staffLine.Measures[0].staffEntries.length > 0 &&
                    Math.abs(relative.x - staffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x) === 0 &&
                    staffLine.ParentMusicSystem === this.musicSystems[0]) {
                    const firstInstructionEntry: GraphicalStaffEntry = staffLine.Measures[0].FirstInstructionStaffEntry;
                    if (firstInstructionEntry) {
                        const lastInstruction: AbstractGraphicalInstruction = firstInstructionEntry.GraphicalInstructions.last();
                        relative.x = lastInstruction.PositionAndShape.RelativePosition.x;
                    }
                    if (this.rules.CompactMode) {
                        relative.x = staffLine.PositionAndShape.RelativePosition.x +
                            staffLine.Measures[0].PositionAndShape.RelativePosition.x;
                    }
                }
            }

            // const addAtLastList: GraphicalObject[] = [];
            for (const entry of multiTempoExpression.EntriesList) {
                let textAlignment: TextAlignmentEnum = TextAlignmentEnum.CenterBottom;
                if (this.rules.CompactMode) {
                    textAlignment = TextAlignmentEnum.LeftBottom;
                }
                const graphLabel: GraphicalLabel = this.calculateLabel(staffLine,
                                                                       relative,
                                                                       entry.label,
                                                                       multiTempoExpression.getFontstyleOfFirstEntry(),
                                                                       entry.Expression.Placement,
                                                                       this.rules.UnknownTextHeight,
                                                                       textAlignment,
                                                                       this.rules.TempoYSpacing);
                if (entry.Expression.ColorXML && this.rules.ExpressionsUseXMLColor) {
                    graphLabel.ColorXML = entry.Expression.ColorXML;
                }

                if (entry.Expression instanceof InstantaneousTempoExpression) {
                    //already added?
                    for (const expr of staffLine.AbstractExpressions) {
                        if (expr instanceof GraphicalInstantaneousTempoExpression &&
                            (expr.SourceExpression as AbstractTempoExpression).Label === entry.Expression.Label) {
                            //already added
                            continue;
                        }
                    }

                    const graphicalTempoExpr: GraphicalInstantaneousTempoExpression = new GraphicalInstantaneousTempoExpression(entry.Expression, graphLabel);
                    if (!graphicalTempoExpr.ParentStaffLine) {
                        log.warn("Adding staffline didn't work");
                        // I am actually fooling the linter here and use the created object. This method needs refactoring,
                        // all graphical expression creations should be in one place and have basic stuff like labels, lines, ...
                        // in their constructor
                    }
                    // in case of metronome mark:
                    if (this.rules.MetronomeMarksDrawn) {
                        if ((entry.Expression as InstantaneousTempoExpression).Enum === TempoEnum.metronomeMark) {
                            this.createMetronomeMark((entry.Expression as InstantaneousTempoExpression));
                            continue;
                        }
                    }
                } else if (entry.Expression instanceof ContinuousTempoExpression) {
                    for (const expr of staffLine.AbstractExpressions) {
                        if (expr instanceof GraphicalInstantaneousTempoExpression &&
                        (expr.SourceExpression as AbstractTempoExpression).Label === entry.Expression.Label) {
                            continue; // already added
                        }
                    }
                    // TODO maybe create GraphicalContinuousTempoExpression class,
                    //   though the ContinuousTempoExpressions we have currently behave the same graphically (accelerando, ritardando, etc).
                    //   The behavior difference rather affects playback (e.g. ritardando, which gradually changes tempo)
                    staffLine.AbstractExpressions.push(new GraphicalInstantaneousTempoExpression(entry.Expression, graphLabel));
                }
            }
        }
    }

    protected createMetronomeMark(metronomeExpression: InstantaneousTempoExpression): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    protected graphicalMeasureCreatedCalculations(measure: GraphicalMeasure): void {
        return;
    }

    protected clearSystemsAndMeasures(): void {
        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
                    for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
                        const graphicalMeasure: GraphicalMeasure = staffLine.Measures[idx4];
                        if (graphicalMeasure.FirstInstructionStaffEntry) {
                            const index: number = graphicalMeasure.PositionAndShape.ChildElements.indexOf(
                                graphicalMeasure.FirstInstructionStaffEntry.PositionAndShape
                            );
                            if (index > -1) {
                                graphicalMeasure.PositionAndShape.ChildElements.splice(index, 1);
                            }
                            graphicalMeasure.FirstInstructionStaffEntry = undefined;
                            graphicalMeasure.beginInstructionsWidth = 0.0;
                        }
                        if (graphicalMeasure.LastInstructionStaffEntry) {
                            const index: number = graphicalMeasure.PositionAndShape.ChildElements.indexOf(
                                graphicalMeasure.LastInstructionStaffEntry.PositionAndShape
                            );
                            if (index > -1) {
                                graphicalMeasure.PositionAndShape.ChildElements.splice(index, 1);
                            }
                            graphicalMeasure.LastInstructionStaffEntry = undefined;
                            graphicalMeasure.endInstructionsWidth = 0.0;
                        }
                    }
                    staffLine.Measures = [];
                    staffLine.PositionAndShape.ChildElements = [];
                }
                musicSystem.StaffLines.length = 0;
                musicSystem.PositionAndShape.ChildElements = [];
            }
            graphicalMusicPage.MusicSystems = [];
            graphicalMusicPage.PositionAndShape.ChildElements = [];
        }
        this.graphicalMusicSheet.MusicPages = [];
    }

    protected handleVoiceEntry(voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry,
                               accidentalCalculator: AccidentalCalculator, openLyricWords: LyricWord[],
                               activeClef: ClefInstruction,
                               openTuplets: Tuplet[], openBeams: Beam[],
                               octaveShiftValue: OctaveEnum, staffIndex: number,
                               linkedNotes: Note[] = undefined,
                               sourceStaffEntry: SourceStaffEntry = undefined): OctaveEnum {
        if (voiceEntry.StemDirectionXml !== StemDirectionType.Undefined &&
            this.rules.SetWantedStemDirectionByXml &&
            voiceEntry.StemDirectionXml !== undefined) {
                voiceEntry.WantedStemDirection = voiceEntry.StemDirectionXml;
        } else {
            this.calculateStemDirectionFromVoices(voiceEntry);
        }
        // if GraphicalStaffEntry has been created earlier (because of Tie), then the GraphicalNotesLists have also been created
        const gve: GraphicalVoiceEntry = graphicalStaffEntry.findOrCreateGraphicalVoiceEntry(voiceEntry);
        gve.octaveShiftValue = octaveShiftValue;
        // check for Tabs:
        const tabStaffEntry: GraphicalStaffEntry = graphicalStaffEntry.tabStaffEntry;
        let graphicalTabVoiceEntry: GraphicalVoiceEntry;
        if (tabStaffEntry) {
            graphicalTabVoiceEntry = tabStaffEntry.findOrCreateGraphicalVoiceEntry(voiceEntry);
        }

        for (let idx: number = 0, len: number = voiceEntry.Notes.length; idx < len; ++idx) {
            const note: Note = voiceEntry.Notes[idx];
            if (!note) {
                continue;
            }
            if (sourceStaffEntry !== undefined && sourceStaffEntry.Link !== undefined && linkedNotes !== undefined && linkedNotes.indexOf(note) > -1) {
                continue;
            }
            let graphicalNote: GraphicalNote;
            if (voiceEntry.IsGrace) {
                graphicalNote = MusicSheetCalculator.symbolFactory.createGraceNote(note, gve, activeClef, this.rules, octaveShiftValue);
            } else {
                graphicalNote = MusicSheetCalculator.symbolFactory.createNote(note, gve, activeClef, octaveShiftValue, this.rules, undefined);
                MusicSheetCalculator.stafflineNoteCalculator.trackNote(graphicalNote);
            }
            if (note.Pitch) {
                this.checkNoteForAccidental(graphicalNote, accidentalCalculator, activeClef, octaveShiftValue);
            }
            this.resetYPositionForLeadSheet(graphicalNote.PositionAndShape);
            graphicalStaffEntry.addGraphicalNoteToListAtCorrectYPosition(gve, graphicalNote);
            graphicalNote.PositionAndShape.calculateBoundingBox();
            if (!this.leadSheet) {
                if (note.NoteBeam !== undefined && note.PrintObject) {
                    if (!(note instanceof TabNote) || this.rules.TabBeamsRendered) {
                        this.handleBeam(graphicalNote, note.NoteBeam, openBeams);
                    }
                }
                if (note.NoteTuplet !== undefined && note.PrintObject) {
                    this.handleTuplet(graphicalNote, note.NoteTuplet, openTuplets);
                }
            }

            // handle TabNotes:
            if (graphicalTabVoiceEntry) {
                // notes should be either TabNotes or RestNotes -> add all:
                const graphicalTabNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(
                    note,
                    graphicalTabVoiceEntry,
                    activeClef,
                    octaveShiftValue,
                    this.rules,
                    undefined);
                tabStaffEntry.addGraphicalNoteToListAtCorrectYPosition(graphicalTabVoiceEntry, graphicalTabNote);
                graphicalTabNote.PositionAndShape.calculateBoundingBox();

                if (!this.leadSheet) {
                    if (note.NoteTuplet) {
                        this.handleTuplet(graphicalTabNote, note.NoteTuplet, openTuplets);
                    }
                }
            }
        }
        if (voiceEntry.Articulations.length > 0) {
            this.handleVoiceEntryArticulations(voiceEntry.Articulations, voiceEntry, graphicalStaffEntry);
        }
        if (voiceEntry.TechnicalInstructions.length > 0) {
            this.handleVoiceEntryTechnicalInstructions(voiceEntry.TechnicalInstructions, voiceEntry, graphicalStaffEntry);
        }
        if (voiceEntry.LyricsEntries.size() > 0) {
            this.handleVoiceEntryLyrics(voiceEntry, graphicalStaffEntry, openLyricWords);
        }
        if (voiceEntry.OrnamentContainer) {
            this.handleVoiceEntryOrnaments(voiceEntry.OrnamentContainer, voiceEntry, graphicalStaffEntry);
        }
        return octaveShiftValue;
    }

    protected resetYPositionForLeadSheet(psi: BoundingBox): void {
        if (this.leadSheet) {
            psi.RelativePosition = new PointF2D(psi.RelativePosition.x, 0.0);
        }
    }

    protected layoutVoiceEntries(graphicalStaffEntry: GraphicalStaffEntry, staffIndex: number): void {
        graphicalStaffEntry.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
        if (!this.leadSheet) {
            for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
                const graphicalNotes: GraphicalNote[] = gve.notes;
                if (graphicalNotes.length === 0) {
                    continue;
                }
                const voiceEntry: VoiceEntry = graphicalNotes[0].sourceNote.ParentVoiceEntry;
                const hasPitchedNote: boolean = graphicalNotes[0].sourceNote.Pitch !== undefined;
                this.layoutVoiceEntry(voiceEntry, graphicalNotes, graphicalStaffEntry, hasPitchedNote);
            }
        }
    }

    protected maxInstrNameLabelLength(): number {
        let maxLabelLength: number = 0.0;
        for (const instrument of this.graphicalMusicSheet.ParentMusicSheet.Instruments) {
            if (instrument.NameLabel?.print && instrument.Voices.length > 0 && instrument.Voices[0].Visible) {
                let renderedLabel: Label = instrument.NameLabel;
                if (!this.rules.RenderPartNames) {
                    renderedLabel = new Label("", renderedLabel.textAlignment, renderedLabel.font);
                }
                const graphicalLabel: GraphicalLabel = new GraphicalLabel(
                    renderedLabel, this.rules.InstrumentLabelTextHeight, TextAlignmentEnum.LeftCenter, this.rules);
                graphicalLabel.setLabelPositionAndShapeBorders();
                maxLabelLength = Math.max(maxLabelLength, graphicalLabel.PositionAndShape.MarginSize.width);
            }
        }
        if (!this.rules.RenderPartNames) {
            return 0;
        }
        return maxLabelLength;
    }

    protected calculateSheetLabelBoundingBoxes(): void {
        const musicSheet: MusicSheet = this.graphicalMusicSheet.ParentMusicSheet;
        const defaultColorTitle: string = this.rules.DefaultColorTitle; // can be undefined => black
        if (musicSheet.Title !== undefined && this.rules.RenderTitle) {
            const title: GraphicalLabel = new GraphicalLabel(musicSheet.Title, this.rules.SheetTitleHeight, TextAlignmentEnum.CenterBottom, this.rules);
            title.Label.IsCreditLabel = true;
            title.Label.colorDefault = defaultColorTitle;
            this.graphicalMusicSheet.Title = title;
            title.setLabelPositionAndShapeBorders();
        } else if (!this.rules.RenderTitle) {
            this.graphicalMusicSheet.Title = undefined; // clear label if rendering it was disabled after last render
        }
        if (musicSheet.Subtitle !== undefined && this.rules.RenderSubtitle) {
            const subtitle: GraphicalLabel = new GraphicalLabel(
                musicSheet.Subtitle, this.rules.SheetSubtitleHeight, TextAlignmentEnum.CenterCenter, this.rules);
            subtitle.Label.IsCreditLabel = true;
            subtitle.Label.colorDefault = defaultColorTitle;
            this.graphicalMusicSheet.Subtitle = subtitle;
            subtitle.setLabelPositionAndShapeBorders();
        } else if (!this.rules.RenderSubtitle) {
            this.graphicalMusicSheet.Subtitle = undefined;
        }
        if (musicSheet.Composer !== undefined && this.rules.RenderComposer) {
            const composer: GraphicalLabel = new GraphicalLabel(
                musicSheet.Composer, this.rules.SheetComposerHeight, TextAlignmentEnum.RightCenter, this.rules);
            composer.Label.IsCreditLabel = true;
            composer.Label.colorDefault = defaultColorTitle;
            this.graphicalMusicSheet.Composer = composer;
            composer.setLabelPositionAndShapeBorders();
        } else if (!this.rules.RenderComposer) {
            this.graphicalMusicSheet.Composer = undefined;
        }
        if (musicSheet.Lyricist !== undefined && this.rules.RenderLyricist) {
            const lyricist: GraphicalLabel = new GraphicalLabel(
                musicSheet.Lyricist, this.rules.SheetAuthorHeight, TextAlignmentEnum.LeftCenter, this.rules);
            lyricist.Label.IsCreditLabel = true;
            lyricist.Label.colorDefault = defaultColorTitle;
            this.graphicalMusicSheet.Lyricist = lyricist;
            lyricist.setLabelPositionAndShapeBorders();
        } else if (!this.rules.RenderLyricist) {
            this.graphicalMusicSheet.Lyricist = undefined;
        }
        if (musicSheet.Copyright !== undefined && this.rules.RenderCopyright) {
            const copyright: GraphicalLabel = new GraphicalLabel(
                musicSheet.Copyright, this.rules.SheetCopyrightHeight, TextAlignmentEnum.CenterBottom, this.rules);
                copyright.Label.IsCreditLabel = true;
                copyright.Label.colorDefault = defaultColorTitle;
            this.graphicalMusicSheet.Copyright = copyright;
            copyright.setLabelPositionAndShapeBorders();
        } else if (!this.rules.RenderCopyright) {
            this.graphicalMusicSheet.Copyright = undefined;
        }
    }

    protected checkMeasuresForWholeRestNotes(): void {
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const musicSystem: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
                for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
                    const measure: GraphicalMeasure = staffLine.Measures[idx4];
                    if (measure.staffEntries.length === 1) {
                        const gse: GraphicalStaffEntry = measure.staffEntries[0];
                        if (gse.graphicalVoiceEntries.length > 0 && gse.graphicalVoiceEntries[0].notes.length === 1) {
                            const graphicalNote: GraphicalNote = gse.graphicalVoiceEntries[0].notes[0];
                            if (!graphicalNote.sourceNote.Pitch && (new Fraction(1, 2)).lt(graphicalNote.sourceNote.Length)) {
                                this.layoutMeasureWithWholeRest(graphicalNote, gse, measure);
                            }
                        }
                    }
                }
            }
        }
    }

    protected optimizeRestNotePlacement(graphicalStaffEntry: GraphicalStaffEntry, measure: GraphicalMeasure): void {
        if (graphicalStaffEntry.graphicalVoiceEntries.length === 0) {
            return;
        }
        const voice1Notes: GraphicalNote[] = graphicalStaffEntry.graphicalVoiceEntries[0].notes;
        if (voice1Notes.length === 0) {
            return;
        }
        const voice1Note1: GraphicalNote = voice1Notes[0];
        const voice1Note1IsRest: boolean = voice1Note1.sourceNote.isRest();
        if (graphicalStaffEntry.graphicalVoiceEntries.length === 2) {
            let voice2Note1IsRest: boolean = false;
            const voice2Notes: GraphicalNote[] = graphicalStaffEntry.graphicalVoiceEntries[1].notes;
            if (voice2Notes.length > 0) {
                const voice2Note1: GraphicalNote = voice2Notes[0];
                voice2Note1IsRest = voice2Note1.sourceNote.isRest();
            }
            if (voice1Note1IsRest && voice2Note1IsRest) {
                this.calculateTwoRestNotesPlacementWithCollisionDetection(graphicalStaffEntry);
            } else if (voice1Note1IsRest || voice2Note1IsRest) {
                this.calculateRestNotePlacementWithCollisionDetectionFromGraphicalNote(graphicalStaffEntry);
            }
        } else if (voice1Note1IsRest && graphicalStaffEntry !== measure.staffEntries[0] &&
            graphicalStaffEntry !== measure.staffEntries[measure.staffEntries.length - 1]) {
            const staffEntryIndex: number = measure.staffEntries.indexOf(graphicalStaffEntry);
            const previousStaffEntry: GraphicalStaffEntry = measure.staffEntries[staffEntryIndex - 1];
            const nextStaffEntry: GraphicalStaffEntry = measure.staffEntries[staffEntryIndex + 1];
            if (previousStaffEntry.graphicalVoiceEntries.length === 1) {
                const previousNote: GraphicalNote = previousStaffEntry.graphicalVoiceEntries[0].notes[0];
                if (previousNote.sourceNote.NoteBeam !== undefined && nextStaffEntry.graphicalVoiceEntries.length === 1) {
                    const nextNote: GraphicalNote = nextStaffEntry.graphicalVoiceEntries[0].notes[0];
                    if (nextNote.sourceNote.NoteBeam !== undefined && previousNote.sourceNote.NoteBeam === nextNote.sourceNote.NoteBeam) {
                        this.calculateRestNotePlacementWithinGraphicalBeam(
                            graphicalStaffEntry, voice1Note1, previousNote,
                            nextStaffEntry, nextNote
                        );
                        graphicalStaffEntry.PositionAndShape.calculateBoundingBox();
                    }
                }
            }
        }
    }

    protected getRelativePositionInStaffLineFromTimestamp(
        timestamp: Fraction, verticalIndex: number, staffLine: StaffLine,
        multiStaffInstrument: boolean, firstVisibleMeasureRelativeX: number = 0.0,
        useLeftStaffEntryBorder: boolean = false
    ): PointF2D {
        let relative: PointF2D = new PointF2D();
        let leftStaffEntry: GraphicalStaffEntry = undefined;
        let rightStaffEntry: GraphicalStaffEntry = undefined;
        const numEntries: number = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
        const index: number = this.graphicalMusicSheet.GetInterpolatedIndexInVerticalContainers(timestamp);
        const leftIndex: number = Math.min(Math.floor(index), numEntries - 1);
        const rightIndex: number = Math.min(Math.ceil(index), numEntries - 1);
        if (leftIndex < 0 || verticalIndex < 0) {
            return relative;
        }
        leftStaffEntry = this.getFirstLeftNotNullStaffEntryFromContainer(leftIndex, verticalIndex, multiStaffInstrument);
        rightStaffEntry = this.getFirstRightNotNullStaffEntryFromContainer(rightIndex, verticalIndex, multiStaffInstrument);
        if (leftStaffEntry && rightStaffEntry) {
            let measureRelativeX: number = leftStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
            if (firstVisibleMeasureRelativeX > 0) {
                measureRelativeX = firstVisibleMeasureRelativeX;
            }
            let leftX: number = leftStaffEntry.PositionAndShape.RelativePosition.x + measureRelativeX;
            let rightX: number = rightStaffEntry.PositionAndShape.RelativePosition.x + rightStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
            const endAfterRightStaffEntry: boolean = timestamp.RealValue > rightStaffEntry.getAbsoluteTimestamp().RealValue;
            // endAfterRightStaffEntry is an unfortunate case where the timestamp isn't correct for the last note in the piece,
            //   see test_wedge_diminuendo_duplicated.musicxml
            if (firstVisibleMeasureRelativeX > 0) {
                rightX = rightStaffEntry.PositionAndShape.RelativePosition.x + measureRelativeX;
            } else if (useLeftStaffEntryBorder &&
                (leftStaffEntry.getAbsoluteTimestamp().RealValue === timestamp.RealValue || endAfterRightStaffEntry)
            ) {
                leftX = leftStaffEntry.PositionAndShape.RelativePosition.x + leftStaffEntry.PositionAndShape.BorderLeft + measureRelativeX;
                rightX = leftX;
            }
            let timestampQuotient: number = 0.0;
            if (leftStaffEntry !== rightStaffEntry) {
                const leftTimestamp: Fraction = leftStaffEntry.getAbsoluteTimestamp();
                const rightTimestamp: Fraction = rightStaffEntry.getAbsoluteTimestamp();
                const leftDifference: Fraction = Fraction.minus(timestamp, leftTimestamp);
                timestampQuotient = leftDifference.RealValue / Fraction.minus(rightTimestamp, leftTimestamp).RealValue;
            }
            if (leftStaffEntry.parentMeasure.ParentStaffLine !== rightStaffEntry.parentMeasure.ParentStaffLine) {
                if (leftStaffEntry.parentMeasure.ParentStaffLine === staffLine) {
                    rightX = staffLine.PositionAndShape.Size.width;
                } else {
                    leftX = staffLine.PositionAndShape.RelativePosition.x;
                }
            }
            relative = new PointF2D(leftX + (rightX - leftX) * timestampQuotient, 0.0);
        }
        return relative;
    }

    protected getRelativeXPositionFromTimestamp(timestamp: Fraction): number {
        const numEntries: number = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
        const index: number = this.graphicalMusicSheet.GetInterpolatedIndexInVerticalContainers(timestamp);
        const discreteIndex: number = Math.max(0, Math.min(Math.round(index), numEntries - 1));
        const gse: GraphicalStaffEntry = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[discreteIndex].getFirstNonNullStaffEntry();
        const posX: number = gse.PositionAndShape.RelativePosition.x + gse.parentMeasure.PositionAndShape.RelativePosition.x;
        return posX;
    }

    protected calculatePageLabels(page: GraphicalMusicPage): void {
        // The PositionAndShape child elements of page need to be manually connected to the lyricist, composer, subtitle, etc.
        // because the page is only available now
        if (this.rules.RenderSingleHorizontalStaffline && this.rules.RenderTitle) {
            //page.PositionAndShape.BorderRight = page.PositionAndShape.Size.width + this.rules.PageRightMargin;
            page.PositionAndShape.calculateBoundingBox([GraphicalMeasure.name]); // ignore measures
            // note: "GraphicalMeasure" instead of GraphicalMeasure.name doesn't work with minified builds (they change class names)
            // note: calculateBoundingBox by default changes measure.PositionAndShape.Size.width for some reason,
            //   inaccurate for RenderSingleHorizontalStaffline, e.g. the cursor type 3 that highlights the whole measure will get wrong width
            //   correct width was set previously via MusicSystemBuilder.setMeasureWidth().
            this.graphicalMusicSheet.ParentMusicSheet.pageWidth = page.PositionAndShape.Size.width; // doesn't seem to affect anything
            // page.PositionAndShape.BorderRight = page.PositionAndShape.Size.width; // doesn't seem to affect anything
        }

        let firstSystemAbsoluteTopMargin: number = 10;
        let lastSystemAbsoluteBottomMargin: number = -1;
        if (page.MusicSystems.length > 0) {
            const firstMusicSystem: MusicSystem = page.MusicSystems[0];
            firstSystemAbsoluteTopMargin = firstMusicSystem.PositionAndShape.RelativePosition.y + firstMusicSystem.PositionAndShape.BorderTop;
            const lastMusicSystem: MusicSystem = page.MusicSystems[page.MusicSystems.length - 1];
            lastSystemAbsoluteBottomMargin = lastMusicSystem.PositionAndShape.RelativePosition.y + lastMusicSystem.PositionAndShape.BorderBottom;
        }
        //const firstStaffLine: StaffLine = this.graphicalMusicSheet.MusicPages[0].MusicSystems[0].StaffLines[0];
        const title: GraphicalLabel = this.graphicalMusicSheet.Title;
        if (title && this.rules.RenderTitle) {
            title.PositionAndShape.Parent = page.PositionAndShape;
            //title.PositionAndShape.Parent = firstStaffLine.PositionAndShape;
            const relative: PointF2D = new PointF2D();
            relative.x = this.graphicalMusicSheet.ParentMusicSheet.pageWidth / 2;
            if (this.rules.RenderSingleHorizontalStaffline) {
                relative.x = Math.max(relative.x, title.PositionAndShape.Size.width);
            }
            //relative.x = firstStaffLine.PositionAndShape.RelativePosition.x + firstStaffLine.PositionAndShape.Size.width / 2; // half of first staffline width
            relative.y = this.rules.TitleTopDistance + this.rules.SheetTitleHeight;
            title.PositionAndShape.RelativePosition = relative;
            page.Labels.push(title);
        }
        if (this.graphicalMusicSheet.Subtitle && this.rules.RenderTitle && this.rules.RenderSubtitle) {
            const subtitle: GraphicalLabel = this.graphicalMusicSheet.Subtitle;
            // subtitle.PositionAndShape.Parent = firstStaffLine.PositionAndShape;
            subtitle.PositionAndShape.Parent = page.PositionAndShape;
            const relative: PointF2D = new PointF2D();
            relative.x = this.graphicalMusicSheet.ParentMusicSheet.pageWidth / 2;
            if (this.rules.RenderSingleHorizontalStaffline) {
                relative.x = title.PositionAndShape.RelativePosition.x; //Math.max(relative.x, title.PositionAndShape.Size.width);
            }
            //relative.x = firstStaffLine.PositionAndShape.RelativePosition.x + firstStaffLine.PositionAndShape.Size.width / 2; // half of first staffline width
            relative.y = this.rules.TitleTopDistance + this.rules.SheetTitleHeight + this.rules.SheetMinimumDistanceBetweenTitleAndSubtitle;
            const lines: number = subtitle.TextLines?.length;
            if (lines > 1) { // Don't want to affect existing behavior. but this doesn't check bboxes for clip
                relative.y += subtitle.PositionAndShape.BorderBottom * (lines - 1) / (lines);
            }
            subtitle.PositionAndShape.RelativePosition = relative;
            page.Labels.push(subtitle);
        }
        // Get the first system, first staffline skybottomcalculator
        // const topStaffline: StaffLine = page.MusicSystems[0].StaffLines[0];
        // const skyBottomLineCalculator: SkyBottomLineCalculator = topStaffline.SkyBottomLineCalculator;
        //   we don't need a skybottomcalculator currently, labels are put above system skyline anyways.
        const composer: GraphicalLabel = this.graphicalMusicSheet.Composer;
        let composerRelativeY: number;
        if (composer && this.rules.RenderComposer) {
            composer.PositionAndShape.Parent = page.PositionAndShape; // if using pageWidth. (which can currently be too wide) TODO fix pageWidth (#578)
            //composer.PositionAndShape.Parent = topStaffline.PositionAndShape; // if using firstStaffLine...width.
            //      y-collision problems, harder to y-align with lyrics
            composer.setLabelPositionAndShapeBorders();
            const relative: PointF2D = new PointF2D();
            //const firstStaffLineEndX: number = this.rules.PageLeftMargin + this.rules.SystemLeftMargin + this.rules.left
            //    firstStaffLine.PositionAndShape.RelativePosition.x + firstStaffLine.PositionAndShape.Size.width;
            //relative.x = Math.min(this.graphicalMusicSheet.ParentMusicSheet.pageWidth - this.rules.PageRightMargin,
            //  firstStaffLineEndX); // awkward with 2-bar score
            relative.x = this.graphicalMusicSheet.ParentMusicSheet.pageWidth - this.rules.PageRightMargin;
            // if (this.rules.RenderSingleHorizontalStaffline) {
            //     relative.x = page.PositionAndShape.BorderMarginLeft + title.PositionAndShape.Size.width * 2;
            // }
            //relative.x = firstStaffLine.PositionAndShape.Size.width;
            //when this is less, goes higher.
            //So 0 is top of the sheet, 22 or so is touching the music system margin

            relative.y = firstSystemAbsoluteTopMargin;
            //relative.y = - this.rules.SystemComposerDistance;
            //relative.y = -firstStaffLine.PositionAndShape.Size.height;
            // TODO only add measure label height if rendering labels and composer measure has label
            // TODO y-align with lyricist? which is harder if they have different bbox parents (page and firstStaffLine).
            // when the pageWidth gets fixed, we could use page as parent again.

            //Sufficient for now to just use the longest composer entry instead of bottom.
            //Otherwise we need to construct a 'bottom line' for the text block
            // const endX: number = topStaffline.PositionAndShape.BorderMarginRight;
            // const startX: number = endX - composer.PositionAndShape.Size.width;
            // const currentMin: number = skyBottomLineCalculator.getSkyLineMinInRange(startX, endX);

            relative.y -= this.rules.SystemComposerDistance;
            const lines: number = composer.TextLines?.length;
            if (lines > 1) { //Don't want to affect existing behavior. but this doesn't check bboxes for clip
                relative.y -= composer.PositionAndShape.BorderBottom * (lines - 1) / (lines);
            }
            //const newSkylineY: number = currentMin; // don't add composer label height to skyline
            //- firstSystemAbsoluteTopMargin - this.rules.SystemComposerDistance - composer.PositionAndShape.MarginSize.height;
            //skyBottomLineCalculator.updateSkyLineInRange(startX, endX, newSkylineY); // this can fix skyline for generateImages for some reason
            composerRelativeY = relative.y; // for lyricist label

            composer.PositionAndShape.RelativePosition = relative;
            page.Labels.push(composer);
        }
        const lyricist: GraphicalLabel = this.graphicalMusicSheet.Lyricist;
        if (lyricist && this.rules.RenderLyricist) {
            lyricist.PositionAndShape.Parent = page.PositionAndShape;
            lyricist.setLabelPositionAndShapeBorders();
            const relative: PointF2D = new PointF2D();
            relative.x = this.rules.PageLeftMargin;
            relative.y = firstSystemAbsoluteTopMargin;
            // const startX: number = topStaffline.PositionAndShape.BorderMarginLeft - relative.x;
            // const endX: number = startX + lyricist.PositionAndShape.Size.width;
            // const currentMin: number = skyBottomLineCalculator.getSkyLineMinInRange(startX, endX);

            relative.y -= this.rules.SystemLyricistDistance;
            relative.y += lyricist.PositionAndShape.BorderBottom;
            relative.y = Math.min(relative.y, composerRelativeY ?? Number.MAX_SAFE_INTEGER);
            // same height as composer label (at least not lower). ?? prevents undefined -> Math.min returns NaN

            //skyBottomLineCalculator.updateSkyLineInRange(startX, endX, currentMin - lyricist.PositionAndShape.MarginSize.height);
            //relative.y = Math.max(relative.y, composer.PositionAndShape.RelativePosition.y);
            lyricist.PositionAndShape.RelativePosition = relative;
            page.Labels.push(lyricist);
        }
        const copyright: GraphicalLabel = this.graphicalMusicSheet.Copyright;
        if (copyright && this.rules.RenderCopyright) {
            copyright.PositionAndShape.Parent = page.PositionAndShape;
            copyright.setLabelPositionAndShapeBorders();
            const relative: PointF2D = new PointF2D();
            relative.x = page.PositionAndShape.Size.width / 2;
            relative.y = lastSystemAbsoluteBottomMargin + this.rules.SheetCopyrightMargin;
            relative.y -= copyright.PositionAndShape.BorderTop;
            copyright.PositionAndShape.RelativePosition = relative;
            page.Labels.push(copyright);
        }
        // we need to do this again to not cut off the title for short scores:
        if (this.rules.RenderSingleHorizontalStaffline && this.rules.RenderTitle) {
            //page.PositionAndShape.BorderRight = page.PositionAndShape.Size.width + this.rules.PageRightMargin;
            page.PositionAndShape.calculateBoundingBox([GraphicalMeasure.name]); // ignore measures
            // note: calculateBoundingBox by default changes measure.PositionAndShape.Size.width for some reason,
            //   inaccurate for RenderSingleHorizontalStaffline, e.g. the cursor type 3 that highlights the whole measure will get wrong width
            //   correct width was set previously via MusicSystemBuilder.setMeasureWidth().
            this.graphicalMusicSheet.ParentMusicSheet.pageWidth = page.PositionAndShape.Size.width; // doesn't seem to affect anything
            // page.PositionAndShape.BorderRight = page.PositionAndShape.Size.width; // doesn't seem to affect anything
        }
    }

    protected createGraphicalTies(): void {
        for (let measureIndex: number = 0; measureIndex < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; measureIndex++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[measureIndex];
            for (let staffIndex: number = 0; staffIndex < sourceMeasure.CompleteNumberOfStaves; staffIndex++) {
                for (let j: number = 0; j < sourceMeasure.VerticalSourceStaffEntryContainers.length; j++) {
                    const sourceStaffEntry: SourceStaffEntry = sourceMeasure.VerticalSourceStaffEntryContainers[j].StaffEntries[staffIndex];
                    if (sourceStaffEntry) {
                        const startStaffEntry: GraphicalStaffEntry = this.graphicalMusicSheet.findGraphicalStaffEntryFromMeasureList(
                            staffIndex, measureIndex, sourceStaffEntry
                        );
                        if (startStaffEntry) {
                            startStaffEntry.GraphicalTies.clear(); // don't duplicate ties when calling render() again
                            startStaffEntry.ties.clear();
                        }

                        for (let idx: number = 0, len: number = sourceStaffEntry.VoiceEntries.length; idx < len; ++idx) {
                            const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
                            for (let idx2: number = 0, len2: number = voiceEntry.Notes.length; idx2 < len2; ++idx2) {
                                const note: Note = voiceEntry.Notes[idx2];
                                if (note.NoteTie) {
                                    const tie: Tie = note.NoteTie;
                                    if (note === note.NoteTie.Notes.last()) {
                                        continue; // nothing to do on last note. don't create last tie twice.
                                    }
                                    if (startStaffEntry) {
                                        for (const gTie of startStaffEntry.GraphicalTies) {
                                            if (gTie.Tie === tie) {
                                                continue; // don't handle the same tie on the same startStaffEntry twice
                                            }
                                        }
                                    }
                                    this.handleTie(tie, startStaffEntry, staffIndex, measureIndex);
                                }
                            }
                        }
                        this.setTieDirections(startStaffEntry);
                    }
                }
            }
        }
    }

    private handleTie(tie: Tie, startGraphicalStaffEntry: GraphicalStaffEntry, staffIndex: number, measureIndex: number): void {
        if (!startGraphicalStaffEntry) {
            // console.log('tie not found in measure number ' + measureIndex - 1);
            return;
        }
        startGraphicalStaffEntry.ties.push(tie);

        let startGse: GraphicalStaffEntry = startGraphicalStaffEntry;
        let startNote: GraphicalNote = undefined;
        let endGse: GraphicalStaffEntry = undefined;
        let endNote: GraphicalNote = undefined;
        for (let i: number = 1; i < tie.Notes.length; i++) {
            startNote = startGse.findTieGraphicalNoteFromNote(tie.Notes[i - 1]);
            endGse = this.graphicalMusicSheet.GetGraphicalFromSourceStaffEntry(tie.Notes[i].ParentStaffEntry);
            if (!endGse) {
                continue;
            }
            endNote = endGse.findTieGraphicalNoteFromNote(tie.Notes[i]);
            if (startNote !== undefined && endNote !== undefined && endGse) {
                if (!startNote.sourceNote.PrintObject || !endNote.sourceNote.PrintObject) {
                    continue;
                }
                const graphicalTie: GraphicalTie = this.createGraphicalTie(tie, startGse, endGse, startNote, endNote);
                startGse.GraphicalTies.push(graphicalTie);
                if (this.staffEntriesWithGraphicalTies.indexOf(startGse) >= 0) {
                    this.staffEntriesWithGraphicalTies.push(startGse);
                }
            }
            startGse = endGse;
        }
    }

    private setTieDirections(staffEntry: GraphicalStaffEntry): void {
        if (!staffEntry) {
            return;
        }
        const ties: Tie[] = staffEntry.ties;
        if (ties.length === 1) {
            const tie: Tie = ties[0];
            if (tie.TieDirection === PlacementEnum.NotYetDefined) {
                const voiceId: number = tie.Notes[0].ParentVoiceEntry.ParentVoice.VoiceId;
                // put ties of second voices (e.g. 2 for right hand, 6 left hand) below by default
                //   TODO could be more precise but also more complex by checking lower notes, other notes, etc.
                if (voiceId === 2 || voiceId === 6) {
                    tie.TieDirection = PlacementEnum.Below;
                }
            }
        }
        if (ties.length > 1) {
            let highestNote: Note = undefined;
            for (const gseTie of ties) {
                const tieNote: Note = gseTie.Notes[0];
                if (!highestNote || tieNote.Pitch.getHalfTone() > highestNote.Pitch.getHalfTone()) {
                    highestNote = tieNote;
                }
            }
            for (const gseTie of ties) {
                if (gseTie.TieDirection === PlacementEnum.NotYetDefined) { // only set/change if not already set by xml
                    if (gseTie.Notes[0] === highestNote) {
                        gseTie.TieDirection = PlacementEnum.Above;
                    } else {
                        gseTie.TieDirection = PlacementEnum.Below;
                    }
                }
            }
        }
    }

    private createAccidentalCalculators(): AccidentalCalculator[] {
        const accidentalCalculators: AccidentalCalculator[] = [];
        const firstSourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.getFirstSourceMeasure();
        if (firstSourceMeasure) {
            for (let i: number = 0; i < firstSourceMeasure.CompleteNumberOfStaves; i++) {
                const accidentalCalculator: AccidentalCalculator = new AccidentalCalculator();
                accidentalCalculators.push(accidentalCalculator);
                if (firstSourceMeasure.FirstInstructionsStaffEntries[i]) {
                    for (let idx: number = 0, len: number = firstSourceMeasure.FirstInstructionsStaffEntries[i].Instructions.length; idx < len; ++idx) {
                        const abstractNotationInstruction: AbstractNotationInstruction = firstSourceMeasure.FirstInstructionsStaffEntries[i].Instructions[idx];
                        if (abstractNotationInstruction instanceof KeyInstruction) {
                            const keyInstruction: KeyInstruction = <KeyInstruction>abstractNotationInstruction;
                            accidentalCalculator.ActiveKeyInstruction = keyInstruction;
                        }
                    }
                }
            }
        }
        return accidentalCalculators;
    }

    private calculateVerticalContainersList(): void {
        const numberOfEntries: number = this.graphicalMusicSheet.MeasureList[0].length;
        for (let i: number = 0; i < this.graphicalMusicSheet.MeasureList.length; i++) {
            for (let j: number = 0; j < numberOfEntries; j++) {
                const measure: GraphicalMeasure = this.graphicalMusicSheet.MeasureList[i][j];
                if (!measure) {
                    continue;
                }
                for (let idx: number = 0, len: number = measure.staffEntries.length; idx < len; ++idx) {
                    const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx];
                    const verticalContainer: VerticalGraphicalStaffEntryContainer =
                        this.graphicalMusicSheet.getOrCreateVerticalContainer(graphicalStaffEntry.getAbsoluteTimestamp());
                    if (verticalContainer) {
                        verticalContainer.StaffEntries[j] = graphicalStaffEntry;
                        graphicalStaffEntry.parentVerticalContainer = verticalContainer;
                    }
                }
            }
        }
    }

    private setIndicesToVerticalGraphicalContainers(): void {
        for (let i: number = 0; i < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length; i++) {
            this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[i].Index = i;
        }
    }

    private createGraphicalMeasuresForSourceMeasure(sourceMeasure: SourceMeasure, accidentalCalculators: AccidentalCalculator[],
                                                    openLyricWords: LyricWord[],
                                                    openOctaveShifts: OctaveShiftParams[], activeClefs: ClefInstruction[]): GraphicalMeasure[] {
        this.initGraphicalMeasuresCreation();
        const verticalMeasureList: GraphicalMeasure[] = []; // (VexFlowMeasure, extends GraphicalMeasure)
        const openBeams: Beam[] = [];
        const openTuplets: Tuplet[] = [];
        const staffEntryLinks: StaffEntryLink[] = [];
        let restInAllGraphicalMeasures: boolean = true;
        for (let staffIndex: number = 0; staffIndex < sourceMeasure.CompleteNumberOfStaves; staffIndex++) {
            const measure: GraphicalMeasure = this.createGraphicalMeasure( // (VexFlowMeasure)
                sourceMeasure, openTuplets, openBeams,
                accidentalCalculators[staffIndex], activeClefs, openOctaveShifts, openLyricWords, staffIndex, staffEntryLinks
            );
            restInAllGraphicalMeasures = restInAllGraphicalMeasures && measure.hasOnlyRests;
            verticalMeasureList.push(measure);
        }
        sourceMeasure.allRests = restInAllGraphicalMeasures;
        sourceMeasure.VerticalMeasureList = verticalMeasureList; // much easier way to link sourceMeasure to graphicalMeasures than Dictionary
        //this.graphicalMusicSheet.sourceToGraphicalMeasureLinks.setValue(sourceMeasure, verticalMeasureList); // overwrites entries because:
        //this.graphicalMusicSheet.sourceToGraphicalMeasureLinks[sourceMeasure] = verticalMeasureList; // can't use SourceMeasure as key.
        // to save the reference by dictionary we would need two Dictionaries, id -> sourceMeasure and id -> GraphicalMeasure.
        return verticalMeasureList;
    }

    private createGraphicalMeasure(sourceMeasure: SourceMeasure, openTuplets: Tuplet[], openBeams: Beam[],
                                   accidentalCalculator: AccidentalCalculator, activeClefs: ClefInstruction[],
                                   openOctaveShifts: OctaveShiftParams[], openLyricWords: LyricWord[], staffIndex: number,
                                   staffEntryLinks: StaffEntryLink[]): GraphicalMeasure {
        const staff: Staff = this.graphicalMusicSheet.ParentMusicSheet.getStaffFromIndex(staffIndex);
        let measure: GraphicalMeasure = undefined;
        if (activeClefs[staffIndex].ClefType === ClefEnum.TAB) {
            staff.isTab = true;
            measure = MusicSheetCalculator.symbolFactory.createTabStaffMeasure(sourceMeasure, staff);
        } else if (sourceMeasure.multipleRestMeasures && this.rules.RenderMultipleRestMeasures) {
            measure = MusicSheetCalculator.symbolFactory.createMultiRestMeasure(sourceMeasure, staff);
        } else if (sourceMeasure.multipleRestMeasureNumber > 1) {
            return undefined; // don't need to create a graphical measure that is within a multiple rest measure
        } else {
            measure = MusicSheetCalculator.symbolFactory.createGraphicalMeasure(sourceMeasure, staff);
        }
        measure.hasError = sourceMeasure.getErrorInMeasure(staffIndex);
        // check for key instruction changes
        if (sourceMeasure.FirstInstructionsStaffEntries[staffIndex]) {
            for (let idx: number = 0, len: number = sourceMeasure.FirstInstructionsStaffEntries[staffIndex].Instructions.length; idx < len; ++idx) {
                const instruction: AbstractNotationInstruction = sourceMeasure.FirstInstructionsStaffEntries[staffIndex].Instructions[idx];
                if (instruction instanceof KeyInstruction) {
                    const key: KeyInstruction = KeyInstruction.copy(instruction);
                    const transposeHalftones: number = measure.getTransposedHalftones();
                    if (transposeHalftones !== 0 &&
                        measure.ParentStaff.ParentInstrument.MidiInstrumentId !== MidiInstrument.Percussion &&
                        MusicSheetCalculator.transposeCalculator) {
                        MusicSheetCalculator.transposeCalculator.transposeKey(
                            key, transposeHalftones
                        );
                    }
                    accidentalCalculator.ActiveKeyInstruction = key;
                }
            }
        }
        // check for octave shifts
        const octaveShifts: MultiExpression[] = [];
        for (let idx: number = 0, len: number = sourceMeasure.StaffLinkedExpressions[staffIndex].length; idx < len; ++idx) {
            const multiExpression: MultiExpression = sourceMeasure.StaffLinkedExpressions[staffIndex][idx];
            let targetOctaveShift: OctaveShift;
            if (multiExpression.OctaveShiftStart) {
                targetOctaveShift = multiExpression.OctaveShiftStart;
            } else if (multiExpression.OctaveShiftEnd) {
                // also check for octave shift that is ending here but starting in earlier measure, see test_octaveshift_notes_shifted_octave_shift_end.musicxml
                targetOctaveShift = multiExpression.OctaveShiftEnd;
            }
            if (targetOctaveShift) {
                octaveShifts.push(multiExpression);
                const openOctaveShift: OctaveShift = targetOctaveShift;
                let absoluteEnd: Fraction = openOctaveShift?.ParentEndMultiExpression?.AbsoluteTimestamp;
                if (!openOctaveShift?.ParentEndMultiExpression) {
                    const measureEndTimestamp: Fraction = Fraction.plus(sourceMeasure.AbsoluteTimestamp, sourceMeasure.Duration);
                    absoluteEnd = measureEndTimestamp;
                    // TODO better handling if end expression missing
                    // old comment:
                    // TODO check if octaveshift end exists, otherwise set to last measure end. only necessary if xml was cut manually and is incomplete
                }
                openOctaveShifts[staffIndex] = new OctaveShiftParams(
                    openOctaveShift, openOctaveShift.ParentStartMultiExpression.AbsoluteTimestamp,
                    //openOctaveShift, multiExpression?.AbsoluteTimestamp,
                    absoluteEnd
                );
            }
        }
        // create GraphicalStaffEntries - always check for possible null Entry
        for (let entryIndex: number = 0; entryIndex < sourceMeasure.VerticalSourceStaffEntryContainers.length; entryIndex++) {
            const sourceStaffEntry: SourceStaffEntry = sourceMeasure.VerticalSourceStaffEntryContainers[entryIndex].StaffEntries[staffIndex];
            // is there a SourceStaffEntry at this Index
            if (sourceStaffEntry) {
                // a SourceStaffEntry exists
                // is there an inStaff ClefInstruction? -> update activeClef
                for (let idx: number = 0, len: number = sourceStaffEntry.Instructions.length; idx < len; ++idx) {
                    const abstractNotationInstruction: AbstractNotationInstruction = sourceStaffEntry.Instructions[idx];
                    if (abstractNotationInstruction instanceof ClefInstruction) {
                        activeClefs[staffIndex] = <ClefInstruction>abstractNotationInstruction;
                    }
                }
                // create new GraphicalStaffEntry
                const graphicalStaffEntry: GraphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(sourceStaffEntry, measure);
                if (entryIndex < measure.staffEntries.length) {
                    // a GraphicalStaffEntry has been inserted already at this Index (from Tie)
                    measure.addGraphicalStaffEntryAtTimestamp(graphicalStaffEntry);
                } else {
                    measure.addGraphicalStaffEntry(graphicalStaffEntry);
                }

                const linkedNotes: Note[] = [];
                if (sourceStaffEntry.Link) {
                    sourceStaffEntry.findLinkedNotes(linkedNotes);
                    this.handleStaffEntryLink(graphicalStaffEntry, staffEntryLinks);
                }
                // check for possible OctaveShift
                let octaveShiftValue: OctaveEnum = OctaveEnum.NONE;
                if (openOctaveShifts[staffIndex]) {
                    if (openOctaveShifts[staffIndex].getAbsoluteStartTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp) &&
                        sourceStaffEntry.AbsoluteTimestamp.lte(openOctaveShifts[staffIndex].getAbsoluteEndTimestamp)) {
                        octaveShiftValue = openOctaveShifts[staffIndex].getOpenOctaveShift.Type;
                    }
                }
                if (octaveShiftValue === OctaveEnum.NONE) {
                    // check for existing octave shifts outside openOctaveShifts
                    for (const octaveShift of octaveShifts) {
                        let targetOctaveShift: OctaveShift;
                        if (octaveShift.OctaveShiftStart) {
                            targetOctaveShift = octaveShift.OctaveShiftStart;
                        } else if (octaveShift.OctaveShiftEnd) {
                            targetOctaveShift = octaveShift.OctaveShiftEnd;
                        }
                        if (targetOctaveShift?.ParentStartMultiExpression?.AbsoluteTimestamp.lte(sourceStaffEntry.AbsoluteTimestamp) &&
                            !targetOctaveShift.ParentEndMultiExpression?.AbsoluteTimestamp.lt(sourceStaffEntry.AbsoluteTimestamp)) {
                                octaveShiftValue = targetOctaveShift.Type;
                                break;
                            }
                    }
                }
                // for each visible Voice create the corresponding GraphicalNotes
                for (let idx: number = 0, len: number = sourceStaffEntry.VoiceEntries.length; idx < len; ++idx) {
                    const voiceEntry: VoiceEntry = sourceStaffEntry.VoiceEntries[idx];
                    // Normal Notes...
                    octaveShiftValue = this.handleVoiceEntry(
                        voiceEntry, graphicalStaffEntry,
                        accidentalCalculator, openLyricWords,
                        activeClefs[staffIndex], openTuplets,
                        openBeams, octaveShiftValue, staffIndex,
                        linkedNotes, sourceStaffEntry
                    );
                }
                // SourceStaffEntry has inStaff ClefInstruction -> create graphical clef
                if (sourceStaffEntry.Instructions.length > 0) {
                    const clefInstruction: ClefInstruction = <ClefInstruction>sourceStaffEntry.Instructions[0];
                    MusicSheetCalculator.symbolFactory.createInStaffClef(graphicalStaffEntry, clefInstruction);
                }
                if (this.rules.RenderChordSymbols && sourceStaffEntry.ChordContainers?.length > 0) {
                    sourceStaffEntry.ParentStaff.ParentInstrument.HasChordSymbols = true;
                    MusicSheetCalculator.symbolFactory.createChordSymbols(
                        sourceStaffEntry,
                        graphicalStaffEntry,
                        accidentalCalculator.ActiveKeyInstruction,
                        this.graphicalMusicSheet.ParentMusicSheet.Transpose);
                }
            }
        }

        accidentalCalculator.doCalculationsAtEndOfMeasure();
        // update activeClef given at end of measure if needed
        if (sourceMeasure.LastInstructionsStaffEntries[staffIndex]) {
            const lastStaffEntry: SourceStaffEntry = sourceMeasure.LastInstructionsStaffEntries[staffIndex];
            for (let idx: number = 0, len: number = lastStaffEntry.Instructions.length; idx < len; ++idx) {
                const abstractNotationInstruction: AbstractNotationInstruction = lastStaffEntry.Instructions[idx];
                if (abstractNotationInstruction instanceof ClefInstruction) {
                    activeClefs[staffIndex] = <ClefInstruction>abstractNotationInstruction;
                }
            }
        }
        for (let idx: number = 0, len: number = sourceMeasure.StaffLinkedExpressions[staffIndex].length; idx < len; ++idx) {
            const multiExpression: MultiExpression = sourceMeasure.StaffLinkedExpressions[staffIndex][idx];
            if (multiExpression.OctaveShiftEnd !== undefined && openOctaveShifts[staffIndex] !== undefined &&
                multiExpression.OctaveShiftEnd === openOctaveShifts[staffIndex].getOpenOctaveShift) {
                    openOctaveShifts[staffIndex] = undefined;
            }
        }
        // check wantedStemDirections of beam notes at end of measure (e.g. for beam with grace notes)
        for (const staffEntry of measure.staffEntries) {
            for (const voiceEntry of staffEntry.graphicalVoiceEntries) {
                this.setBeamNotesWantedStemDirections(voiceEntry.parentVoiceEntry);
            }
        }
        // if there are no staffEntries in this measure, create a rest for the whole measure:
        // check OSMDOptions.fillEmptyMeasuresWithWholeRest
        if (this.rules.FillEmptyMeasuresWithWholeRest >= 1) { // fill measures with no notes given with whole rests, visible (1) or invisible (2)
            if (measure.staffEntries.length === 0) {
                const sourceStaffEntry: SourceStaffEntry = new SourceStaffEntry(
                    new VerticalSourceStaffEntryContainer(measure.parentSourceMeasure,
                                                          measure.parentSourceMeasure.AbsoluteTimestamp,
                                                          measure.parentSourceMeasure.CompleteNumberOfStaves),
                    staff);
                if (staff.Voices.length === 0) {
                    const newVoice: Voice = new Voice(measure.ParentStaff.ParentInstrument, -1);
                    // this is problematic because we don't know the MusicXML voice ids and how many voices with which ids will be created after this.
                    //   but it should only happen when the first measure of the piece is empty.
                    staff.Voices.push(newVoice);
                }
                const voiceEntry: VoiceEntry = new VoiceEntry(new Fraction(0, 1), staff.Voices[0], sourceStaffEntry);
                let duration: Fraction = sourceMeasure.Duration;
                if (duration.RealValue === 0) {
                    duration = sourceMeasure.ActiveTimeSignature.clone();
                }
                const note: Note = new Note(voiceEntry, sourceStaffEntry, duration, undefined, sourceMeasure, true);
                note.IsWholeMeasureRest = true; // there may be a more elegant solution
                note.PrintObject = this.rules.FillEmptyMeasuresWithWholeRest === FillEmptyMeasuresWithWholeRests.YesVisible;
                  // don't display whole rest that wasn't given in XML, only for layout/voice completion
                voiceEntry.Notes.push(note);
                const graphicalStaffEntry: GraphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(sourceStaffEntry, measure);
                measure.addGraphicalStaffEntry(graphicalStaffEntry);
                graphicalStaffEntry.relInMeasureTimestamp = voiceEntry.Timestamp;
                const gve: GraphicalVoiceEntry = MusicSheetCalculator.symbolFactory.createVoiceEntry(voiceEntry, graphicalStaffEntry);
                graphicalStaffEntry.graphicalVoiceEntries.push(gve);
                const graphicalNote: GraphicalNote = MusicSheetCalculator.symbolFactory.createNote(
                    note,
                    gve,
                    new ClefInstruction(),
                    OctaveEnum.NONE,
                    this.rules);
                MusicSheetCalculator.stafflineNoteCalculator.trackNote(graphicalNote);
                gve.notes.push(graphicalNote);
            }
        }

        measure.hasOnlyRests = true;
        //if staff entries empty, loop will not start. so true is valid
        for (const graphicalStaffEntry of measure.staffEntries) {
            //Loop until we get just one false
            measure.hasOnlyRests = graphicalStaffEntry.hasOnlyRests();
            if (!measure.hasOnlyRests) {
                break;
            }
        }

        return measure;
    }

    private checkNoteForAccidental(graphicalNote: GraphicalNote, accidentalCalculator: AccidentalCalculator, activeClef: ClefInstruction,
                                   octaveEnum: OctaveEnum): void {
        let pitch: Pitch = graphicalNote.sourceNote.Pitch;
        const transposeHalftones: number = graphicalNote.parentVoiceEntry.parentStaffEntry.parentMeasure.getTransposedHalftones();
        if (transposeHalftones !== 0 && graphicalNote.sourceNote.ParentStaffEntry.ParentStaff.ParentInstrument.MidiInstrumentId !== MidiInstrument.Percussion) {
            pitch = graphicalNote.Transpose(
                accidentalCalculator.ActiveKeyInstruction, activeClef, transposeHalftones, octaveEnum
            );
            graphicalNote.sourceNote.TransposedPitch = pitch;
        }
        graphicalNote.sourceNote.halfTone = pitch.getHalfTone();
        accidentalCalculator.checkAccidental(graphicalNote, pitch);
    }

    // private createStaffEntryForTieNote(measure: StaffMeasure, absoluteTimestamp: Fraction, openTie: Tie): GraphicalStaffEntry {
    //     let graphicalStaffEntry: GraphicalStaffEntry;
    //     graphicalStaffEntry = MusicSheetCalculator.symbolFactory.createStaffEntry(openTie.Start.ParentStaffEntry, measure);
    //     graphicalStaffEntry.relInMeasureTimestamp = Fraction.minus(absoluteTimestamp, measure.parentSourceMeasure.AbsoluteTimestamp);
    //     this.resetYPositionForLeadSheet(graphicalStaffEntry.PositionAndShape);
    //     measure.addGraphicalStaffEntryAtTimestamp(graphicalStaffEntry);
    //     return graphicalStaffEntry;
    // }

    private handleStaffEntries(staffIsPercussionArray: Array<boolean>): void {
        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MeasureList.length; idx < len; ++idx) {
            const measures: GraphicalMeasure[] = this.graphicalMusicSheet.MeasureList[idx];
            for (let idx2: number = 0, len2: number = measures.length; idx2 < len2; ++idx2) {
                const measure: GraphicalMeasure = measures[idx2];
                if (!measure) {
                    continue;
                }
                //This property is active...
                if (this.rules.PercussionOneLineCutoff > 0 && !this.rules.PercussionUseCajon2NoteSystem) {
                    //We have a percussion clef, check to see if this property applies...
                    if (staffIsPercussionArray[idx2]) {
                        //-1 means always trigger, or we are under the cutoff number specified
                        if (this.rules.PercussionOneLineCutoff === -1 ||
                            MusicSheetCalculator.stafflineNoteCalculator.getStafflineUniquePositionCount(idx2) < this.rules.PercussionOneLineCutoff) {
                            measure.ParentStaff.StafflineCount = 1;
                        }
                    }
                }
                for (const graphicalStaffEntry of measure.staffEntries) {
                    if (graphicalStaffEntry.parentMeasure !== undefined
                        && graphicalStaffEntry.graphicalVoiceEntries.length > 0
                        && graphicalStaffEntry.graphicalVoiceEntries[0].notes.length > 0) {
                        this.layoutVoiceEntries(graphicalStaffEntry, idx2);
                        this.layoutStaffEntry(graphicalStaffEntry);
                    }
                }
                this.graphicalMeasureCreatedCalculations(measure);
            }
        }
    }

    protected calculateSkyBottomLines(): void {
        // override
    }

    /**
     * Re-adjust the x positioning of expressions.
     */
    protected calculateExpressionAlignements(): void {
        // override
    }

    // does nothing for now, because layoutBeams() is an empty method
    // private calculateBeams(): void {
    //     for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
    //         const musicSystem: MusicSystem = this.musicSystems[idx2];
    //         for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
    //             const staffLine: StaffLine = musicSystem.StaffLines[idx3];
    //             for (let idx4: number = 0, len4: number = staffLine.Measures.length; idx4 < len4; ++idx4) {
    //                 const measure: GraphicalMeasure = staffLine.Measures[idx4];
    //                 for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
    //                     const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
    //                     this.layoutBeams(staffEntry);
    //                 }
    //             }
    //         }
    //     }
    // }

    private calculateStaffEntryArticulationMarks(): void {
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const system: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
                const line: StaffLine = system.StaffLines[idx3];
                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
                    const measure: GraphicalMeasure = line.Measures[idx4];
                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
                        for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
                            const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
                            if (voiceEntry.Articulations.length > 0) {
                                this.layoutArticulationMarks(voiceEntry.Articulations, voiceEntry, graphicalStaffEntry);
                            }
                        }
                    }
                }
            }
        }
    }

    private calculateOrnaments(): void {
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const system: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
                const line: StaffLine = system.StaffLines[idx3];
                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
                    const measure: GraphicalMeasure = line.Measures[idx4];
                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
                        for (let idx6: number = 0, len6: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx6 < len6; ++idx6) {
                            const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx6];
                            if (voiceEntry.OrnamentContainer) {
                                if (voiceEntry.hasTie() && !graphicalStaffEntry.relInMeasureTimestamp.Equals(voiceEntry.Timestamp)) {
                                    continue;
                                }
                                this.layoutOrnament(voiceEntry.OrnamentContainer, voiceEntry, graphicalStaffEntry);
                                if (!(this.staffEntriesWithOrnaments.indexOf(graphicalStaffEntry) !== -1)) {
                                    this.staffEntriesWithOrnaments.push(graphicalStaffEntry);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private getFingeringPlacement(measure: GraphicalMeasure): PlacementEnum {
        let placement: PlacementEnum = this.rules.FingeringPosition;
        if (placement === PlacementEnum.NotYetDefined || placement === PlacementEnum.AboveOrBelow) {
            placement = measure.isUpperStaffOfInstrument() ? PlacementEnum.Above : PlacementEnum.Below;
        }
        return placement;
    }

    public calculateFingerings(): void {
        if (this.rules.FingeringPosition === PlacementEnum.Left ||
            this.rules.FingeringPosition === PlacementEnum.Right) {
                return;
        }
        for (const system of this.musicSystems) {
            for (const line of system.StaffLines) {
                for (const measure of line.Measures) {
                    if (measure.isTabMeasure && !this.rules.TabFingeringsRendered) {
                        continue; // don't duplicate fingerings into tab measures. tab notes are already
                    }
                    const placement: PlacementEnum = this.getFingeringPlacement(measure);
                    for (const gse of measure.staffEntries) {
                        gse.FingeringEntries = [];
                        const skybottomcalculator: SkyBottomLineCalculator = line.SkyBottomLineCalculator;
                        const staffEntryPositionX: number = gse.PositionAndShape.RelativePosition.x +
                            measure.PositionAndShape.RelativePosition.x;
                        const fingerings: TechnicalInstruction[] = [];
                        for (const voiceEntry of gse.graphicalVoiceEntries) {
                            if (voiceEntry.parentVoiceEntry.IsGrace) {
                                continue;
                            }
                            // Sibelius: can have multiple fingerings per note, so we need to check voice entry instructions, not note.Fingering
                            for (const instruction of voiceEntry.parentVoiceEntry.TechnicalInstructions) {
                                if (instruction.type === TechnicalInstructionType.Fingering) {
                                    fingerings.push(instruction);
                                }
                            }
                            // for (const note of voiceEntry.notes) {
                            //     const sourceNote: Note = note.sourceNote;
                            //     if (sourceNote.Fingering && !sourceNote.IsGraceNote) {
                            //         fingerings.push(sourceNote.Fingering);
                            //     }
                            // }
                        }
                        if (placement === PlacementEnum.Below) {
                            fingerings.reverse();
                        }
                        for (let i: number = 0; i < fingerings.length; i++) {
                            const fingering: TechnicalInstruction = fingerings[i];
                            const alignment: TextAlignmentEnum =
                                placement === PlacementEnum.Above ? TextAlignmentEnum.CenterBottom : TextAlignmentEnum.CenterTop;
                            const label: Label = new Label(fingering.value, alignment);
                            const gLabel: GraphicalLabel = new GraphicalLabel(
                                label, this.rules.FingeringTextSize, label.textAlignment, this.rules, line.PositionAndShape);
                            const marginLeft: number = staffEntryPositionX + gLabel.PositionAndShape.BorderMarginLeft;
                            const marginRight: number = staffEntryPositionX + gLabel.PositionAndShape.BorderMarginRight;
                            let skybottomFurthest: number = undefined;
                            if (placement === PlacementEnum.Above) {
                                skybottomFurthest = skybottomcalculator.getSkyLineMinInRange(marginLeft, marginRight);
                            } else {
                                skybottomFurthest = skybottomcalculator.getBottomLineMaxInRange(marginLeft, marginRight);
                            }
                            let yShift: number = 0;
                            if (i === 0) {
                                yShift += this.rules.FingeringOffsetY;
                                if (placement === PlacementEnum.Above) {
                                    yShift += 0.1; // above fingerings are a bit closer to the notes than below ones for some reason
                                }
                            } else {
                                yShift += this.rules.FingeringPaddingY;
                            }
                            if (placement === PlacementEnum.Above) {
                                yShift *= -1;
                            }
                            gLabel.PositionAndShape.RelativePosition.y += skybottomFurthest + yShift;
                            gLabel.PositionAndShape.RelativePosition.x = staffEntryPositionX;
                            gLabel.setLabelPositionAndShapeBorders();
                            gLabel.PositionAndShape.calculateBoundingBox();
                            gse.FingeringEntries.push(gLabel);
                            const start: number = gLabel.PositionAndShape.RelativePosition.x + gLabel.PositionAndShape.BorderLeft;
                            //start -= line.PositionAndShape.RelativePosition.x;
                            const end: number = start - gLabel.PositionAndShape.BorderLeft + gLabel.PositionAndShape.BorderRight;
                            if (placement === PlacementEnum.Above) {
                                skybottomcalculator.updateSkyLineInRange(
                                    start, end, gLabel.PositionAndShape.RelativePosition.y + gLabel.PositionAndShape.BorderTop); // BorderMarginTop too much
                            } else if (placement === PlacementEnum.Below) {
                                skybottomcalculator.updateBottomLineInRange(
                                    start, end, gLabel.PositionAndShape.RelativePosition.y + gLabel.PositionAndShape.BorderBottom);
                            }
                        }
                    }
                }
            }
        }
    }

    private optimizeRestPlacement(): void {
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const system: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = system.StaffLines.length; idx3 < len3; ++idx3) {
                const line: StaffLine = system.StaffLines[idx3];
                for (let idx4: number = 0, len4: number = line.Measures.length; idx4 < len4; ++idx4) {
                    const measure: GraphicalMeasure = line.Measures[idx4];
                    for (let idx5: number = 0, len5: number = measure.staffEntries.length; idx5 < len5; ++idx5) {
                        const graphicalStaffEntry: GraphicalStaffEntry = measure.staffEntries[idx5];
                        this.optimizeRestNotePlacement(graphicalStaffEntry, measure);
                    }
                }
            }
        }
    }

    private calculateTwoRestNotesPlacementWithCollisionDetection(graphicalStaffEntry: GraphicalStaffEntry): void {
        const firstRestNote: GraphicalNote = graphicalStaffEntry.graphicalVoiceEntries[0].notes[0];
        const secondRestNote: GraphicalNote = graphicalStaffEntry.graphicalVoiceEntries[1].notes[0];
        secondRestNote.PositionAndShape.RelativePosition = new PointF2D(0.0, 2.5);
        graphicalStaffEntry.PositionAndShape.calculateAbsolutePositionsRecursiveWithoutTopelement();
        firstRestNote.PositionAndShape.computeNonOverlappingPositionWithMargin(
            graphicalStaffEntry.PositionAndShape, ColDirEnum.Up,
            new PointF2D(0.0, secondRestNote.PositionAndShape.RelativePosition.y)
        );
        const relative: PointF2D = firstRestNote.PositionAndShape.RelativePosition;
        relative.y -= 1.0;
        firstRestNote.PositionAndShape.RelativePosition = relative;
        graphicalStaffEntry.PositionAndShape.calculateBoundingBox();
    }

    private calculateRestNotePlacementWithCollisionDetectionFromGraphicalNote(graphicalStaffEntry: GraphicalStaffEntry): void {
        let restNote: GraphicalNote;
        let graphicalNotes: GraphicalNote[];
        if (graphicalStaffEntry.graphicalVoiceEntries[0].notes[0].sourceNote.isRest()) {
            restNote = graphicalStaffEntry.graphicalVoiceEntries[0].notes[0];
            graphicalNotes = graphicalStaffEntry.graphicalVoiceEntries[1].notes;
        } else {
            graphicalNotes = graphicalStaffEntry.graphicalVoiceEntries[0].notes;
            restNote = graphicalStaffEntry.graphicalVoiceEntries[1].notes[0];
        }
        //restNote.parallelVoiceEntryNotes = graphicalNotes; // TODO maybe save potentially colliding notes, check them in VexFlowConverter.StaveNote
        let collision: boolean = false;
        graphicalStaffEntry.PositionAndShape.calculateAbsolutePositionsRecursiveWithoutTopelement();
        for (let idx: number = 0, len: number = graphicalNotes.length; idx < len; ++idx) {
            const graphicalNote: GraphicalNote = graphicalNotes[idx];
            if (restNote.PositionAndShape.marginCollisionDetection(graphicalNote.PositionAndShape)) {
                // TODO bounding box of graphical note isn't set correctly yet.
                // we could do manual collision checking here
                collision = true;
                break;
            }
        }
        if (collision) {
            if (restNote.sourceNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice) {
                const bottomBorder: number = graphicalNotes[0].PositionAndShape.BorderMarginBottom + graphicalNotes[0].PositionAndShape.RelativePosition.y;
                restNote.PositionAndShape.RelativePosition = new PointF2D(0.0, bottomBorder - restNote.PositionAndShape.BorderMarginTop + 0.5);
            } else {
                const last: GraphicalNote = graphicalNotes[graphicalNotes.length - 1];
                const topBorder: number = last.PositionAndShape.BorderMarginTop + last.PositionAndShape.RelativePosition.y;
                if (graphicalNotes[0].sourceNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice) {
                    restNote.PositionAndShape.RelativePosition = new PointF2D(0.0, topBorder - restNote.PositionAndShape.BorderMarginBottom - 0.5);
                } else {
                    const bottomBorder: number = graphicalNotes[0].PositionAndShape.BorderMarginBottom + graphicalNotes[0].PositionAndShape.RelativePosition.y;
                    if (bottomBorder < 2.0) {
                        restNote.PositionAndShape.RelativePosition = new PointF2D(0.0, bottomBorder - restNote.PositionAndShape.BorderMarginTop + 0.5);
                    } else {
                        restNote.PositionAndShape.RelativePosition = new PointF2D(0.0, topBorder - restNote.PositionAndShape.BorderMarginBottom - 0.0);
                    }
                }
            }
        }
        graphicalStaffEntry.PositionAndShape.calculateBoundingBox();
    }

    private calculateTieCurves(): void {
        for (const musicSystem of this.musicSystems) {
            for (const staffLine of musicSystem.StaffLines) {
                for (const measure of staffLine.Measures) {
                    for (const staffEntry of measure.staffEntries) {
                        for (const graphicalTie of staffEntry.GraphicalTies) {
                            if (graphicalTie.StartNote !== undefined && graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry === staffEntry) {
                                const tieIsAtSystemBreak: boolean = (
                                    graphicalTie.StartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine !==
                                    graphicalTie.EndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.ParentStaffLine
                                );
                                this.layoutGraphicalTie(graphicalTie, tieIsAtSystemBreak, measure.ParentStaff.isTab);
                            }
                        }
                    }
                }
            }
        }
    }

    private calculateLyricsPosition(): void {
        const lyricStaffEntriesDict: Dictionary<StaffLine, GraphicalStaffEntry[]> = new Dictionary<StaffLine, GraphicalStaffEntry[]>();
        // sort the lyriceVerseNumbers for every Instrument that has Lyrics
        for (let idx: number = 0, len: number = this.graphicalMusicSheet.ParentMusicSheet.Instruments.length; idx < len; ++idx) {
            const instrument: Instrument = this.graphicalMusicSheet.ParentMusicSheet.Instruments[idx];
            if (instrument.HasLyrics && instrument.LyricVersesNumbers.length > 0) {
                instrument.LyricVersesNumbers.sort();
            }
        }
        // first calc lyrics text positions
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const musicSystem: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
                const lyricsStaffEntries: GraphicalStaffEntry[] =
                    this.calculateSingleStaffLineLyricsPosition(staffLine, staffLine.ParentStaff.ParentInstrument.LyricVersesNumbers);
                lyricStaffEntriesDict.setValue(staffLine, lyricsStaffEntries);
                this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
            }
        }
        // then fill in the lyric word dashes and lyrics extends/underscores
        for (let idx2: number = 0, len2: number = this.musicSystems.length; idx2 < len2; ++idx2) {
            const musicSystem: MusicSystem = this.musicSystems[idx2];
            for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                const staffLine: StaffLine = musicSystem.StaffLines[idx3];
                this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
            }
        }
    }

    /**
     * This method calculates the dashes within the syllables of a LyricWord
     * @param lyricEntry
     */
    private calculateSingleLyricWord(lyricEntry: GraphicalLyricEntry): void {
        // const skyBottomLineCalculator: SkyBottomLineCalculator = new SkyBottomLineCalculator (this.rules);
        const graphicalLyricWord: GraphicalLyricWord = lyricEntry.ParentLyricWord;
        const index: number = graphicalLyricWord.GraphicalLyricsEntries.indexOf(lyricEntry);
        let nextLyricEntry: GraphicalLyricEntry = undefined;
        if (index >= 0) {
            nextLyricEntry = graphicalLyricWord.GraphicalLyricsEntries[index + 1];
        }
        if (!nextLyricEntry) {
            return;
        }
        const startStaffLine: StaffLine = <StaffLine>lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine;
        const nextStaffLine: StaffLine = <StaffLine>nextLyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine;
        const startStaffEntry: GraphicalStaffEntry = lyricEntry.StaffEntryParent;
        const endStaffentry: GraphicalStaffEntry = nextLyricEntry.StaffEntryParent;

        // if on the same StaffLine
        if (lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine === nextLyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine) {
            // start- and End margins from the text Labels
            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                startStaffEntry.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight -
                lyricEntry.GraphicalLabel.CenteringXShift; // TODO not sure why this is necessary, see Christbaum measure 9+11, Land der Berge 11-12

            const endX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
                endStaffentry.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.x +
                nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
            const y: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
            let numberOfDashes: number = 1;
            if ((endX - startX) > this.rules.MinimumDistanceBetweenDashes * 3) {
                // *3: need distance between word to first dash, dash to dash, dash to next word
                numberOfDashes = Math.floor((endX - startX) / this.rules.MinimumDistanceBetweenDashes) - 1;
            }
            // check distance and create the adequate number of Dashes
            if (numberOfDashes === 1) {
                // distance between the two GraphicalLyricEntries is big for only one Dash, position in the middle
                this.calculateSingleDashForLyricWord(startStaffLine, startX, endX, y);
            } else {
                // distance is big enough for more Dashes
                // calculate the adequate number of Dashes from the distance between the two LyricEntries
                // distance between the Dashes should be equal
                this.calculateDashes(startStaffLine, startX, endX, y);
            }
        } else {
            // start and end on different StaffLines
            // start margin from the text Label until the End of StaffLine
            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                startStaffEntry.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
            const lastGraphicalMeasure: GraphicalMeasure = startStaffLine.Measures[startStaffLine.Measures.length - 1];
            const endX: number = lastGraphicalMeasure.PositionAndShape.RelativePosition.x + lastGraphicalMeasure.PositionAndShape.Size.width;
            let y: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;

            // calculate Dashes for the first StaffLine
            this.calculateDashes(startStaffLine, startX, endX, y);

            // calculate Dashes for the second StaffLine (only if endStaffEntry isn't the first StaffEntry of the StaffLine)
            if (nextStaffLine && // check for undefined objects e.g. when drawingRange given
                nextStaffLine.Measures[0] &&
                endStaffentry.parentMeasure.ParentStaffLine &&
                !(endStaffentry === endStaffentry.parentMeasure.staffEntries[0] &&
                endStaffentry.parentMeasure === endStaffentry.parentMeasure.ParentStaffLine.Measures[0])) {
                const secondStartX: number = nextStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
                const secondEndX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
                    endStaffentry.PositionAndShape.RelativePosition.x +
                    nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
                y = nextLyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
                this.calculateDashes(nextStaffLine, secondStartX, secondEndX, y);
            }
        }
    }

    /**
     * This method calculates Dashes for a LyricWord.
     * @param staffLine
     * @param startX
     * @param endX
     * @param y
     */
    private calculateDashes(staffLine: StaffLine, startX: number, endX: number, y: number): void {
        let distance: number = endX - startX;
        if (distance < this.rules.MinimumDistanceBetweenDashes * 3) {
            this.calculateSingleDashForLyricWord(staffLine, startX, endX, y);
        } else {
            // enough distance for more Dashes
            const numberOfDashes: number = Math.floor(distance / this.rules.MinimumDistanceBetweenDashes) - 1;
            const distanceBetweenDashes: number = distance / (numberOfDashes + 1);
            let counter: number = 0;

            startX += distanceBetweenDashes;
            endX -= distanceBetweenDashes;
            while (counter <= Math.floor(numberOfDashes / 2.0) && endX > startX) {
                distance = this.calculateRightAndLeftDashesForLyricWord(staffLine, startX, endX, y);
                startX += distanceBetweenDashes;
                endX -= distanceBetweenDashes;
                counter++;
            }

            // if the remaining distance isn't big enough for two Dashes,
            // but long enough for a middle dash inbetween,
            // then put the last Dash in the middle of the remaining distance
            if (distance > distanceBetweenDashes * 2) {
                this.calculateSingleDashForLyricWord(staffLine, startX, endX, y);
            }
        }
    }

    /**
     * This method calculates a single Dash for a LyricWord, positioned in the middle of the given distance.
     * @param {StaffLine} staffLine
     * @param {number} startX
     * @param {number} endX
     * @param {number} y
     */
    private calculateSingleDashForLyricWord(staffLine: StaffLine, startX: number, endX: number, y: number): void {
        const label: Label = new Label("-");
        label.colorDefault = this.rules.DefaultColorLyrics; // if undefined, no change. saves an if check
        let textHeight: number = this.rules.LyricsHeight;
        if (endX - startX < 0.8) {
            textHeight *= 0.8;
            y -= 0.1 * textHeight; // dash moves downwards when textHeight is reduced. counteract that.
            //xShift = -0.1;
            // x-position is situational, sometimes it's slightly right-leaning and tends to overlap with the right LyricsEntry
            //   (see Cornelius - Christbaum, measure 9 and 11 ("li-che", "li-ger"), due to centering x-shift = GraphicalLabel.CenteringXShift)
            // sometimes the x-position is perfect and the interval is extremely narrow
            //   (see Mozart/Holzer Land der Berge measure 11-12)
            // or even slightly too far left (Beethoven Geliebte measure 4, due to centering x-shift = GraphicalLabel.CenteringXShift)
        }
        const dash: GraphicalLabel = new GraphicalLabel(
            label, textHeight, TextAlignmentEnum.CenterBottom, this.rules);
        dash.setLabelPositionAndShapeBorders();
        staffLine.LyricsDashes.push(dash);
        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
            this.staffLinesWithLyricWords.push(staffLine);
        }
        dash.PositionAndShape.Parent = staffLine.PositionAndShape;
        const relative: PointF2D = new PointF2D(startX + (endX - startX) / 2, y);
        dash.PositionAndShape.RelativePosition = relative;
    }

    /**
     * Layouts the underscore line when a lyric entry is marked as extend
     * @param {GraphicalLyricEntry} lyricEntry
     */
    private calculateLyricExtend(lyricEntry: GraphicalLyricEntry): void {
        let startY: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
        const startStaffEntry: GraphicalStaffEntry = lyricEntry.StaffEntryParent;
        const startStaffLine: StaffLine = startStaffEntry.parentMeasure.ParentStaffLine;

        // find endstaffEntry and staffLine
        let endStaffEntry: GraphicalStaffEntry = undefined;
        let endStaffLine: StaffLine = undefined;
        const staffIndex: number = startStaffEntry.parentMeasure.ParentStaff.idInMusicSheet;
        for (let index: number = startStaffEntry.parentVerticalContainer.Index + 1;
            index < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
            ++index) {
            const gse: GraphicalStaffEntry = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[index].StaffEntries[staffIndex];
            if (!gse) {
                continue;
            }
            if (gse.hasOnlyRests()) {
                break;
            }
            if (gse.LyricsEntries.length > 0) {
                break;
            }
            endStaffEntry = gse;
            endStaffLine = endStaffEntry.parentMeasure.ParentStaffLine;
            if (!endStaffLine) {
                endStaffLine = startStaffEntry.parentMeasure.ParentStaffLine;
            }
        }
        if (!endStaffEntry || !endStaffLine) {
            return;
        }
        // if on the same StaffLine
        if (startStaffLine === endStaffLine && endStaffEntry.parentMeasure.ParentStaffLine) {
            // start- and End margins from the text Labels
            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                startStaffEntry.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
            // + startStaffLine.PositionAndShape.AbsolutePosition.x; // doesn't work, done in drawer
            const endX: number = endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                endStaffEntry.PositionAndShape.RelativePosition.x +
                endStaffEntry.PositionAndShape.BorderMarginRight;
            // + endStaffLine.PositionAndShape.AbsolutePosition.x; // doesn't work, done in drawer
            // TODO maybe add half-width of following note.
            // though we don't have the vexflow note's bbox yet and extend layouting is unconstrained,
            // we have more room for spacing without it.
            // needed in order to line up with the Label's text bottom line (is the y position of the underscore)
            startY -= lyricEntry.GraphicalLabel.PositionAndShape.Size.height / 4;
            // create a Line (as underscore after the LyricLabel's End)
            this.calculateSingleLyricWordWithUnderscore(startStaffLine, startX, endX, startY);
        } else { // start and end on different StaffLines
            // start margin from the text Label until the End of StaffLine
            const lastMeasureBb: BoundingBox = startStaffLine.Measures[startStaffLine.Measures.length - 1].PositionAndShape;
            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                startStaffEntry.PositionAndShape.RelativePosition.x +
                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
            const endX: number = lastMeasureBb.RelativePosition.x +
                lastMeasureBb.Size.width;
            // needed in order to line up with the Label's text bottom line
            startY -= lyricEntry.GraphicalLabel.PositionAndShape.Size.height / 4;
            // first Underscore until the StaffLine's End
            this.calculateSingleLyricWordWithUnderscore(startStaffLine, startX, endX, startY);
            if (!endStaffEntry) {
                return;
            }
            // second Underscore in the endStaffLine until endStaffEntry (if endStaffEntry isn't the first StaffEntry of the StaffLine))
            if (endStaffEntry.parentMeasure.ParentStaffLine && endStaffEntry.parentMeasure.staffEntries &&
                !(endStaffEntry === endStaffEntry.parentMeasure.staffEntries[0] &&
                endStaffEntry.parentMeasure === endStaffEntry.parentMeasure.ParentStaffLine.Measures[0])) {
                const secondStartX: number = endStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
                const secondEndX: number = endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
                    endStaffEntry.PositionAndShape.RelativePosition.x +
                    endStaffEntry.PositionAndShape.BorderMarginRight;
                this.calculateSingleLyricWordWithUnderscore(endStaffLine, secondStartX, secondEndX, startY);
            }
        }
    }

    /**
     * This method calculates a single underscoreLine.
     * @param staffLine
     * @param startX
     * @param end
     * @param y
     */
    private calculateSingleLyricWordWithUnderscore(staffLine: StaffLine, startX: number, endX: number, y: number): void {
        const lineStart: PointF2D = new PointF2D(startX, y);
        const lineEnd: PointF2D = new PointF2D(endX, y);
        const graphicalLine: GraphicalLine = new GraphicalLine(lineStart, lineEnd, this.rules.LyricUnderscoreLineWidth);
        graphicalLine.colorHex = this.rules.DefaultColorLyrics; // if undefined, no change. saves an if check
        staffLine.LyricLines.push(graphicalLine);
        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
            this.staffLinesWithLyricWords.push(staffLine);
        }
    }

    /**
     * This method calculates two Dashes for a LyricWord, positioned at the the two ends of the given distance.
     * @param {StaffLine} staffLine
     * @param {number} startX
     * @param {number} endX
     * @param {number} y
     * @returns {number}
     */
    private calculateRightAndLeftDashesForLyricWord(staffLine: StaffLine, startX: number, endX: number, y: number): number {
        const leftLabel: Label = new Label("-");
        leftLabel.colorDefault = this.rules.DefaultColorLyrics; // if undefined, no change. saves an if check
        const leftDash: GraphicalLabel = new GraphicalLabel(
            leftLabel, this.rules.LyricsHeight, TextAlignmentEnum.CenterBottom, this.rules);
        leftDash.setLabelPositionAndShapeBorders();
        staffLine.LyricsDashes.push(leftDash);
        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
            this.staffLinesWithLyricWords.push(staffLine);
        }
        leftDash.PositionAndShape.Parent = staffLine.PositionAndShape;
        const leftDashRelative: PointF2D = new PointF2D(startX, y);
        leftDash.PositionAndShape.RelativePosition = leftDashRelative;

        const rightLabel: Label = new Label("-");
        const rightDash: GraphicalLabel = new GraphicalLabel(
            rightLabel, this.rules.LyricsHeight, TextAlignmentEnum.CenterBottom, this.rules);
        rightDash.setLabelPositionAndShapeBorders();
        staffLine.LyricsDashes.push(rightDash);
        rightDash.PositionAndShape.Parent = staffLine.PositionAndShape;
        const rightDashRelative: PointF2D = new PointF2D(endX, y);
        rightDash.PositionAndShape.RelativePosition = rightDashRelative;
        return (rightDash.PositionAndShape.RelativePosition.x - leftDash.PositionAndShape.RelativePosition.x);
    }

    //So we can track shared notes bounding boxes to avoid collision + skyline issues
    protected dynamicExpressionMap: Map<number, BoundingBox> = new Map<number, BoundingBox>();

    private calculateDynamicExpressions(): void {
        const maxIndex: number = Math.min(this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length - 1, this.rules.MaxMeasureToDrawIndex);
        const minIndex: number = Math.min(this.rules.MinMeasureToDrawIndex, this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length);
        for (let i: number = minIndex; i <= maxIndex; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            //Reset, beginning of new measure
            this.dynamicExpressionMap.clear();
            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
                    continue;
                }

                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                        if (sourceMeasure.StaffLinkedExpressions[j][k].InstantaneousDynamic !== undefined ||
                            (sourceMeasure.StaffLinkedExpressions[j][k].StartingContinuousDynamic !== undefined &&
                                sourceMeasure.StaffLinkedExpressions[j][k].StartingContinuousDynamic.StartMultiExpression ===
                                sourceMeasure.StaffLinkedExpressions[j][k] && sourceMeasure.StaffLinkedExpressions[j][k].UnknownList.length === 0)
                        ) {
                            this.calculateDynamicExpressionsForMultiExpression(sourceMeasure.StaffLinkedExpressions[j][k], i, j);
                        }
                    }
                }
            }
        }
        this.dynamicExpressionMap.clear();
    }

    private calculateOctaveShifts(): void {
        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
                    continue;
                }
                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                        if ((sourceMeasure.StaffLinkedExpressions[j][k].OctaveShiftStart)) {
                            this.calculateSingleOctaveShift(sourceMeasure, sourceMeasure.StaffLinkedExpressions[j][k], i, j);
                        }
                    }
                }
            }
        }
    }

    private calculatePedals(): void {
        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
                    continue;
                }
                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                        if ((sourceMeasure.StaffLinkedExpressions[j][k].PedalStart)) {
                            this.calculateSinglePedal(sourceMeasure, sourceMeasure.StaffLinkedExpressions[j][k], i, j);
                        }
                    }
                }
            }
        }
    }

    private getFirstLeftNotNullStaffEntryFromContainer(horizontalIndex: number, verticalIndex: number, multiStaffInstrument: boolean): GraphicalStaffEntry {
        if (this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex]) {
            return this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex];
        }
        for (let i: number = horizontalIndex - 1; i >= 0; i--) {
            if (this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[i].StaffEntries[verticalIndex]) {
                return this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[i].StaffEntries[verticalIndex];
            }
        }
        return undefined;
    }

    private getFirstRightNotNullStaffEntryFromContainer(horizontalIndex: number, verticalIndex: number, multiStaffInstrument: boolean): GraphicalStaffEntry {
        if (this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex]) {
            return this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[horizontalIndex].StaffEntries[verticalIndex];
        }
        for (let i: number = horizontalIndex + 1; i < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length; i++) {
            if (this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[i].StaffEntries[verticalIndex]) {
                return this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[i].StaffEntries[verticalIndex];
            }
        }
        return undefined;
    }

    private calculateWordRepetitionInstructions(): void {
        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            for (let idx: number = 0, len: number = sourceMeasure.FirstRepetitionInstructions.length; idx < len; ++idx) {
                const instruction: RepetitionInstruction = sourceMeasure.FirstRepetitionInstructions[idx];
                this.calculateWordRepetitionInstruction(instruction, i);
            }
            for (let idx: number = 0, len: number = sourceMeasure.LastRepetitionInstructions.length; idx < len; ++idx) {
                const instruction: RepetitionInstruction = sourceMeasure.LastRepetitionInstructions[idx];
                this.calculateWordRepetitionInstruction(instruction, i);
            }
        }
    }

    private calculateRepetitionEndings(): void {
        const musicsheet: MusicSheet = this.graphicalMusicSheet.ParentMusicSheet;
        for (let idx: number = 0, len: number = musicsheet.Repetitions.length; idx < len; ++idx) {
            const repetition: Repetition = musicsheet.Repetitions[idx];
            this.calcGraphicalRepetitionEndingsRecursively(repetition);
        }
    }

    private calculateTempoExpressions(): void {
        const maxIndex: number = Math.min(this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length - 1, this.rules.MaxMeasureToDrawIndex);
        const minIndex: number = this.rules.MinMeasureToDrawIndex;
        for (let i: number = minIndex; i <= maxIndex; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            for (let j: number = 0; j < sourceMeasure.TempoExpressions.length; j++) {
                this.calculateTempoExpressionsForMultiTempoExpression(sourceMeasure, sourceMeasure.TempoExpressions[j], i);
            }
        }
    }

    private calculateRehearsalMarks(): void {
        if (!this.rules.RenderRehearsalMarks) {
            return;
        }
        for (const measure of this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures) {
            this.calculateRehearsalMark(measure);
        }
    }

    protected calculateRehearsalMark(measure: SourceMeasure): void {
        throw new Error(this.abstractNotImplementedErrorMessage);
    }

    private calculateMoodAndUnknownExpressions(): void {
        for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
            const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];
            for (let j: number = 0; j < sourceMeasure.StaffLinkedExpressions.length; j++) {
                if (!this.graphicalMusicSheet.MeasureList[i] || !this.graphicalMusicSheet.MeasureList[i][j]) {
                    continue;
                }
                if (this.graphicalMusicSheet.MeasureList[i][j].ParentStaff.ParentInstrument.Visible) {
                    for (let k: number = 0; k < sourceMeasure.StaffLinkedExpressions[j].length; k++) {
                        if ((sourceMeasure.StaffLinkedExpressions[j][k].MoodList.length > 0) ||
                            (sourceMeasure.StaffLinkedExpressions[j][k].UnknownList.length > 0)) {
                            this.calculateMoodAndUnknownExpression(sourceMeasure.StaffLinkedExpressions[j][k], i, j);
                        }
                    }
                }
            }
        }
    }

    /**
     * Calculates the desired stem direction depending on the number (or type) of voices.
     * If more than one voice is there, the main voice (typically the first or upper voice) will get stem up direction.
     * The others get stem down direction.
     * @param voiceEntry the voiceEntry for which the stem direction has to be calculated
     */
    private calculateStemDirectionFromVoices(voiceEntry: VoiceEntry): void {
        // Stem direction calculation:
        const hasLink: boolean = voiceEntry.ParentSourceStaffEntry.Link !== undefined;
        if (hasLink) {
            // in case of StaffEntryLink don't check mainVoice / linkedVoice
            if (voiceEntry === voiceEntry.ParentSourceStaffEntry.VoiceEntries[0]) {
                // set stem up:
                voiceEntry.WantedStemDirection = StemDirectionType.Up;
                return;
            } else {
                // set stem down:
                voiceEntry.WantedStemDirection = StemDirectionType.Down;
                return;
            }
        } else {
            if (voiceEntry.ParentVoice instanceof LinkedVoice) {
                // Linked voice: set stem down:
                voiceEntry.WantedStemDirection = StemDirectionType.Down;
            } else {
                // if this voiceEntry belongs to the mainVoice:
                // check first that there are also more voices present:
                if (voiceEntry.ParentSourceStaffEntry.VoiceEntries.length > 1) {
                    // as this voiceEntry belongs to the mainVoice: stem Up
                    voiceEntry.WantedStemDirection = StemDirectionType.Up;
                }
            }
        }
        // setBeamNotesWantedStemDirections() will be called at end of measure (createGraphicalMeasure)
    }

    /** Sets a voiceEntry's stem direction to one already set in other notes in its beam, if it has one. */
    private setBeamNotesWantedStemDirections(voiceEntry: VoiceEntry): void {
        if (!(voiceEntry.Notes.length > 0)) {
            return;
        }
        // don't just set direction if undefined. if there's a note in the beam with a different stem direction, Vexflow draws it with an unending stem.
        // if (voiceEntry.WantedStemDirection === StemDirectionType.Undefined) {
        const beam: Beam = voiceEntry.Notes[0].NoteBeam;
        if (beam) {
            // if there is a beam, find any already set stemDirection in the beam:
            for (const note of beam.Notes) {
                // if (note.ParentVoiceEntry === voiceEntry) {
                //     continue; // this could cause a misreading, also potentially in cross-staf beams, in any case it's unnecessary.
                //} else if
                if (note.ParentVoiceEntry.WantedStemDirection !== StemDirectionType.Undefined) {
                    if (note.ParentVoiceEntry.ParentSourceStaffEntry.ParentStaff.Id === voiceEntry.ParentSourceStaffEntry.ParentStaff.Id) {
                        // set the stem direction
                        voiceEntry.WantedStemDirection = note.ParentVoiceEntry.WantedStemDirection;
                        break;
                    }
                }
            }
        }
    }
}