opensheetmusicdisplay/opensheetmusicdisplay

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

Summary

Maintainability
F
1 wk
Test Coverage
import {MusicSheet} from "../MusicSheet";
import {SourceMeasure} from "../VoiceData/SourceMeasure";
import {GraphicalMeasure} from "./GraphicalMeasure";
import {GraphicalMusicPage} from "./GraphicalMusicPage";
import {VerticalGraphicalStaffEntryContainer} from "./VerticalGraphicalStaffEntryContainer";
import {GraphicalLabel} from "./GraphicalLabel";
import {GraphicalLine} from "./GraphicalLine";
import {MusicSystem} from "./MusicSystem";
import {GraphicalStaffEntry} from "./GraphicalStaffEntry";
import {SourceStaffEntry} from "../VoiceData/SourceStaffEntry";
import {PointF2D} from "../../Common/DataObjects/PointF2D";
import {ClefInstruction} from "../VoiceData/Instructions/ClefInstruction";
import {AbstractNotationInstruction} from "../VoiceData/Instructions/AbstractNotationInstruction";
import {KeyInstruction} from "../VoiceData/Instructions/KeyInstruction";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {GraphicalNote} from "./GraphicalNote";
import {Instrument} from "../Instrument";
import {BoundingBox} from "./BoundingBox";
import {MusicSheetCalculator} from "./MusicSheetCalculator";
import log from "loglevel";
//import { Dictionary } from "typescript-collections"; // unused for now
import {CollectionUtil} from "../../Util/CollectionUtil";
import {SelectionStartSymbol} from "./SelectionStartSymbol";
import {SelectionEndSymbol} from "./SelectionEndSymbol";
import {OutlineAndFillStyleEnum} from "./DrawingEnums";
import { MusicSheetDrawer } from "./MusicSheetDrawer";
import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
import { GraphicalObject } from "./GraphicalObject";
// import { VexFlowMusicSheetDrawer } from "./VexFlow/VexFlowMusicSheetDrawer";
// import { SvgVexFlowBackend } from "./VexFlow/SvgVexFlowBackend"; // causes build problem with npm start

/**
 * The graphical counterpart of a [[MusicSheet]]
 */
export class GraphicalMusicSheet {
    constructor(musicSheet: MusicSheet, calculator: MusicSheetCalculator) {
        this.musicSheet = musicSheet;
        this.numberOfStaves = this.musicSheet.Staves.length;
        this.calculator = calculator;
        this.calculator.initialize(this);
    }

    private musicSheet: MusicSheet;
    //private fontInfo: FontInfo = FontInfo.Info;
    private calculator: MusicSheetCalculator;
    public drawer: MusicSheetDrawer;
    private musicPages: GraphicalMusicPage[] = [];
    /** measures (i,j) where i is the measure number and j the staff index (e.g. staff indices 0, 1 for two piano parts) */
    private measureList: GraphicalMeasure[][] = [];
    private verticalGraphicalStaffEntryContainers: VerticalGraphicalStaffEntryContainer[] = [];
    private title: GraphicalLabel;
    private subtitle: GraphicalLabel;
    private composer: GraphicalLabel;
    private lyricist: GraphicalLabel;
    private copyright: GraphicalLabel;
    private cursors: GraphicalLine[] = [];
    private selectionStartSymbol: SelectionStartSymbol;
    private selectionEndSymbol: SelectionEndSymbol;
    private minAllowedSystemWidth: number;
    //private systemImages: Dictionary<MusicSystem, SystemImageProperties> = new Dictionary<MusicSystem, SystemImageProperties>();
    private numberOfStaves: number;
    private leadSheet: boolean = false;

    public get ParentMusicSheet(): MusicSheet {
        return this.musicSheet;
    }

    public get GetCalculator(): MusicSheetCalculator {
        return this.calculator;
    }

    public get MusicPages(): GraphicalMusicPage[] {
        return this.musicPages;
    }

    public set MusicPages(value: GraphicalMusicPage[]) {
        this.musicPages = value;
    }

    //public get FontInfo(): FontInfo {
    //    return this.fontInfo;
    //}

    public get MeasureList(): GraphicalMeasure[][] {
        return this.measureList;
    }

    public set MeasureList(value: GraphicalMeasure[][]) {
        this.measureList = value;
    }

    public get VerticalGraphicalStaffEntryContainers(): VerticalGraphicalStaffEntryContainer[] {
        return this.verticalGraphicalStaffEntryContainers;
    }

    public set VerticalGraphicalStaffEntryContainers(value: VerticalGraphicalStaffEntryContainer[]) {
        this.verticalGraphicalStaffEntryContainers = value;
    }

    public get Title(): GraphicalLabel {
        return this.title;
    }

    public set Title(value: GraphicalLabel) {
        this.title = value;
    }

    public get Subtitle(): GraphicalLabel {
        return this.subtitle;
    }

    public set Subtitle(value: GraphicalLabel) {
        this.subtitle = value;
    }

    public get Composer(): GraphicalLabel {
        return this.composer;
    }

    public set Composer(value: GraphicalLabel) {
        this.composer = value;
    }

    public get Lyricist(): GraphicalLabel {
        return this.lyricist;
    }

    public set Lyricist(value: GraphicalLabel) {
        this.lyricist = value;
    }

    public get Copyright(): GraphicalLabel {
        return this.copyright;
    }

    public set Copyright(value: GraphicalLabel) {
        this.copyright = value;
    }

    public get Cursors(): GraphicalLine[] {
        return this.cursors;
    }

    public get SelectionStartSymbol(): SelectionStartSymbol {
        return this.selectionStartSymbol;
    }

    public get SelectionEndSymbol(): SelectionEndSymbol {
        return this.selectionEndSymbol;
    }

    public get MinAllowedSystemWidth(): number {
        return this.minAllowedSystemWidth;
    }

    public set MinAllowedSystemWidth(value: number) {
        this.minAllowedSystemWidth = value;
    }

    // public get SystemImages(): Dictionary<MusicSystem, SystemImageProperties> {
    //     return this.systemImages;
    // }

    public get NumberOfStaves(): number {
        return this.numberOfStaves;
    }

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

    public set LeadSheet(value: boolean) {
        this.leadSheet = value;
    }

    /**
     * Calculate the Absolute Positions from the Relative Positions.
     * @param graphicalMusicSheet
     */
    public static transformRelativeToAbsolutePosition(graphicalMusicSheet: GraphicalMusicSheet): void {
        for (let i: number = 0; i < graphicalMusicSheet.MusicPages.length; i++) {
            const pageAbsolute: PointF2D = graphicalMusicSheet.MusicPages[i].setMusicPageAbsolutePosition(i, graphicalMusicSheet.ParentMusicSheet.Rules);
            const page: GraphicalMusicPage = graphicalMusicSheet.MusicPages[i];
            page.PositionAndShape.calculateAbsolutePositionsRecursive(pageAbsolute.x, pageAbsolute.y);
        }
    }

    public Initialize(): void {
        this.verticalGraphicalStaffEntryContainers = [];
        this.musicPages = [];
        this.measureList = [];
    }

    public reCalculate(): void {
        this.calculator.calculate();
    }

    // unused method
    // public prepare(): void {
    //     this.calculator.prepareGraphicalMusicSheet();
    // }

    public EnforceRedrawOfMusicSystems(): void {
        for (let idx: number = 0, len: number = this.musicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.musicPages[idx];
            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                musicSystem.needsToBeRedrawn = true;
            }
        }
    }

    public getClickedObject<T>(positionOnMusicSheet: PointF2D): T {
        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
            return graphicalMusicPage.PositionAndShape.getClickedObjectOfType<T>(positionOnMusicSheet);
        }
        return undefined;
    }

    public findGraphicalMeasure(measureIndex: number, staffIndex: number): GraphicalMeasure {
        // note the cursor calls this with measureIndex 1 (measure 2) when advancing beyond the end of a 1-measure piece
        for (let i: number = measureIndex; i >= 0; i--) {
            const gMeasure: GraphicalMeasure = this.measureList[i]?.[staffIndex];
            if (gMeasure) {
                return gMeasure;
            }
            // else look backwards (previous measures). this is only really valid for MultipleRestMeasures of course.
        }
        return undefined; // shouldn't happen
    }

    public findGraphicalMeasureByMeasureNumber(measureNumber: number, staffIndex: number): GraphicalMeasure {
        // start with index = measureNumber, as a piece with a pickup measure starts with measure number 0
        for (let i: number = measureNumber; i >= 0; i--) {
            if (this.MeasureList[i]) {
                const measure: GraphicalMeasure = this.MeasureList[i][staffIndex];
                if (measure?.MeasureNumber === measureNumber) {
                    return measure;
                }
            }
        }
        return undefined;
    }

    /**
     * Search the MeasureList for a certain GraphicalStaffEntry with the given SourceStaffEntry,
     * at a certain verticalIndex (eg a corresponding Staff), starting at a specific horizontalIndex (eg specific GraphicalMeasure).
     * @param staffIndex
     * @param measureIndex
     * @param sourceStaffEntry
     * @returns {any}
     */
    public findGraphicalStaffEntryFromMeasureList(staffIndex: number, measureIndex: number, sourceStaffEntry: SourceStaffEntry): GraphicalStaffEntry {
        for (let i: number = measureIndex; i < this.measureList.length; i++) {
            const graphicalMeasure: GraphicalMeasure = this.measureList[i][staffIndex];
            if (!graphicalMeasure) {
                continue;
            }
            for (let idx: number = 0, len: number = graphicalMeasure.staffEntries.length; idx < len; ++idx) {
                const graphicalStaffEntry: GraphicalStaffEntry = graphicalMeasure.staffEntries[idx];
                if (graphicalStaffEntry.sourceStaffEntry === sourceStaffEntry) {
                    return graphicalStaffEntry;
                }
            }
        }
        return undefined;
    }

    /**
     * Return the next (to the right) not null GraphicalStaffEntry from a given Index.
     * @param staffIndex
     * @param measureIndex
     * @param graphicalStaffEntry
     * @returns {any}
     */
    public findNextGraphicalStaffEntry(staffIndex: number, measureIndex: number, graphicalStaffEntry: GraphicalStaffEntry): GraphicalStaffEntry {
        const graphicalMeasure: GraphicalMeasure = graphicalStaffEntry.parentMeasure;
        const graphicalStaffEntryIndex: number = graphicalMeasure.staffEntries.indexOf(graphicalStaffEntry);
        if (graphicalStaffEntryIndex < graphicalMeasure.staffEntries.length - 1) {
            return graphicalMeasure.staffEntries[graphicalStaffEntryIndex + 1];
        } else if (measureIndex < this.measureList.length - 1) {
            const nextMeasure: GraphicalMeasure = this.measureList[measureIndex + 1][staffIndex];
            if (nextMeasure.staffEntries.length > 0) {
                return nextMeasure.staffEntries[0];
            }
        }
        return undefined;
    }

    public getFirstVisibleMeasuresListFromIndices(start: number, end: number): GraphicalMeasure[] {
        const graphicalMeasures: GraphicalMeasure[] = [];
        const numberOfStaves: number = this.measureList[0].length;
        for (let i: number = start; i <= end; i++) {
            for (let j: number = 0; j < numberOfStaves; j++) {
                if (this.measureList[i][j].isVisible()) {
                    graphicalMeasures.push(this.measureList[i][j]);
                    break;
                }
            }
        }
        return graphicalMeasures;
    }

    public orderMeasuresByStaffLine(measures: GraphicalMeasure[]): GraphicalMeasure[][] {
        const orderedMeasures: GraphicalMeasure[][] = [];
        let mList: GraphicalMeasure[] = [];
        orderedMeasures.push(mList);
        for (let i: number = 0; i < measures.length; i++) {
            if (i === 0) {
                mList.push(measures[0]);
            } else {
                if (measures[i].ParentStaffLine === measures[i - 1].ParentStaffLine) {
                    mList.push(measures[i]);
                } else {
                    if (orderedMeasures.indexOf(mList) === -1) {
                        orderedMeasures.push(mList);
                    }
                    mList = [];
                    orderedMeasures.push(mList);
                    mList.push(measures[i]);
                }
            }
        }
        return orderedMeasures;
    }

    /**
     * Return the active Clefs at the start of the first SourceMeasure.
     * @returns {ClefInstruction[]}
     */
    public initializeActiveClefs(): ClefInstruction[] {
        const activeClefs: ClefInstruction[] = [];
        const firstSourceMeasure: SourceMeasure = this.musicSheet.getFirstSourceMeasure();
        if (firstSourceMeasure) {
            for (let i: number = 0; i < firstSourceMeasure.CompleteNumberOfStaves; i++) {
                let clef: ClefInstruction = new ClefInstruction();
                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 ClefInstruction) {
                            clef = <ClefInstruction>abstractNotationInstruction;

                        }
                    }
                }
                activeClefs.push(clef);
            }
        }
        return activeClefs;
    }

    public GetMainKey(): KeyInstruction {
        const firstSourceMeasure: SourceMeasure = this.musicSheet.getFirstSourceMeasure();
        if (firstSourceMeasure) {
            for (let i: number = 0; i < firstSourceMeasure.CompleteNumberOfStaves; 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) {
                        return <KeyInstruction>abstractNotationInstruction;
                    }
                }
            }
        }
        return undefined;
    }

    /**
     * Create the VerticalContainer and adds it to the List at the correct Timestamp position.
     * @param timestamp
     * @returns {any}
     */
    public getOrCreateVerticalContainer(timestamp: Fraction): VerticalGraphicalStaffEntryContainer {
        if (this.verticalGraphicalStaffEntryContainers.length === 0 ||
            (CollectionUtil.getLastElement(this.verticalGraphicalStaffEntryContainers).AbsoluteTimestamp).lt(timestamp)) {
            const verticalGraphicalStaffEntryContainer: VerticalGraphicalStaffEntryContainer =
                new VerticalGraphicalStaffEntryContainer(this.numberOfStaves, timestamp);
            this.verticalGraphicalStaffEntryContainers.push(verticalGraphicalStaffEntryContainer);
            return verticalGraphicalStaffEntryContainer;
        }
        for (let i: number = this.verticalGraphicalStaffEntryContainers.length - 1; i >= 0; i--) {
            if (this.verticalGraphicalStaffEntryContainers[i].AbsoluteTimestamp.lt(timestamp)) {
                const verticalGraphicalStaffEntryContainer: VerticalGraphicalStaffEntryContainer =
                    new VerticalGraphicalStaffEntryContainer(this.numberOfStaves, timestamp);
                this.verticalGraphicalStaffEntryContainers.splice(i + 1, 0, verticalGraphicalStaffEntryContainer);
                return verticalGraphicalStaffEntryContainer;
            }
            if (this.verticalGraphicalStaffEntryContainers[i].AbsoluteTimestamp.Equals(timestamp)) {
                return this.verticalGraphicalStaffEntryContainers[i];
            }
        }
        return undefined;
    }

    /**
     * Does a binary search on the container list and returns the VerticalContainer with the given Timestamp.
     * The search begins at startIndex, if given.
     * If the timestamp cannot be found, null is returned.
     * @param timestamp - The timestamp for which the container shall be found.
     * @param startIndex - The index from which the search starts in the container list.
     * @returns {any}
     * @constructor
     */
    public GetVerticalContainerFromTimestamp(timestamp: Fraction, startIndex: number = 0): VerticalGraphicalStaffEntryContainer {
        const index: number = CollectionUtil.binarySearch(this.verticalGraphicalStaffEntryContainers,
                                                          new VerticalGraphicalStaffEntryContainer(0, timestamp),
                                                          VerticalGraphicalStaffEntryContainer.compareByTimestamp,
                                                          startIndex);
        if (index >= 0) {
            return this.verticalGraphicalStaffEntryContainers[index];
        }
        return undefined;
    }

    /**
     * Perform a binary search for the absolute given Timestamp in all the GraphicalVerticalContainers.
     * @param musicTimestamp
     * @returns {number}
     * @constructor
     */
    public GetInterpolatedIndexInVerticalContainers(musicTimestamp: Fraction): number {
        const containers: VerticalGraphicalStaffEntryContainer[] = this.verticalGraphicalStaffEntryContainers;
        if (containers.length === 1) {
            return 0; // this fixes an error with Noteflight samples, see below (#1473). It may also be faster.
        }
        let leftIndex: number = 0;
        let rightIndex: number = containers.length - 1;
        let leftTS: Fraction = undefined;
        let rightTS: Fraction = undefined;
        // TODO AbsoluteTimestamp can be NaN in some erroneous MusicXML files like from Noteflight, see omd issue #1473
        //   (though in the sample tested, there is only one container, so above containers.length === 1 prevents the error)
        if (musicTimestamp.lte(containers[containers.length - 1].AbsoluteTimestamp)) {
            while (rightIndex - leftIndex > 1) {
                const middleIndex: number = Math.floor((rightIndex + leftIndex) / 2);
                if (containers[leftIndex].AbsoluteTimestamp.Equals(musicTimestamp)) {
                    rightIndex = leftIndex;
                    break;
                } else if (containers[rightIndex].AbsoluteTimestamp.Equals(musicTimestamp)) {
                    leftIndex = rightIndex;
                    break;
                } else if (containers[middleIndex].AbsoluteTimestamp.Equals(musicTimestamp)) {
                    return this.verticalGraphicalStaffEntryContainers.indexOf(containers[middleIndex]);
                } else if (musicTimestamp.lt(containers[middleIndex].AbsoluteTimestamp)) {
                    rightIndex = middleIndex;
                } else {
                    leftIndex = middleIndex;
                }
            }

            // no interpolation needed
            if (leftIndex === rightIndex) {
                return this.verticalGraphicalStaffEntryContainers.indexOf(containers[leftIndex]);
            }
            leftTS = containers[leftIndex].AbsoluteTimestamp;
            rightTS = containers[rightIndex].AbsoluteTimestamp;
        } else {
            leftTS = containers[containers.length - 1].AbsoluteTimestamp;
            rightTS = Fraction.plus(this.getLongestStaffEntryDuration(containers.length - 1), leftTS);
            rightIndex = containers.length;
        }
        const diff: number = rightTS.RealValue - leftTS.RealValue;
        const diffTS: number = rightTS.RealValue - musicTimestamp.RealValue;

        // estimate the interpolated index
        const foundIndex: number = rightIndex - (diffTS / diff);
        return Math.min(foundIndex, this.verticalGraphicalStaffEntryContainers.length - 1);
    }

    /**
     * Get a List with the indices of all the visible GraphicalMeasures and calculates their
     * corresponding indices in the first SourceMeasure, taking into account Instruments with multiple Staves.
     * @param visibleMeasures
     * @returns {number[]}
     */
    public getVisibleStavesIndicesFromSourceMeasure(visibleMeasures: GraphicalMeasure[]): number[] {
        const visibleInstruments: Instrument[] = [];
        const visibleStavesIndices: number[] = [];
        for (let idx: number = 0, len: number = visibleMeasures.length; idx < len; ++idx) {
            const graphicalMeasure: GraphicalMeasure = visibleMeasures[idx];
            const instrument: Instrument = graphicalMeasure.ParentStaff.ParentInstrument;
            if (visibleInstruments.indexOf(instrument) === -1) {
                visibleInstruments.push(instrument);
            }
        }
        for (let idx: number = 0, len: number = visibleInstruments.length; idx < len; ++idx) {
            const instrument: Instrument = visibleInstruments[idx];
            const index: number = this.musicSheet.getGlobalStaffIndexOfFirstStaff(instrument);
            for (let j: number = 0; j < instrument.Staves.length; j++) {
                visibleStavesIndices.push(index + j);
            }
        }
        return visibleStavesIndices;
    }

    /**
     * Returns the GraphicalMeasure with the given SourceMeasure as Parent at the given staff index.
     * @param sourceMeasure
     * @param staffIndex
     * @returns {any}
     */
    public getGraphicalMeasureFromSourceMeasureAndIndex(sourceMeasure: SourceMeasure, staffIndex: number): GraphicalMeasure {
        for (let i: number = 0; i < this.measureList.length; i++) {
            if (this.measureList[i][0]?.parentSourceMeasure === sourceMeasure) {
                return this.measureList[i][staffIndex];
            }
        }
        return undefined;
    }

    public getLastGraphicalMeasureFromIndex(staffIndex: number, lastRendered: boolean = true): GraphicalMeasure {
        let measureIndex: number = this.measureList.length - 1;
        if (lastRendered) {
            measureIndex = Math.min(measureIndex, this.musicSheet.Rules.MaxMeasureToDrawIndex);
        }
        let measure: GraphicalMeasure = this.measureList[measureIndex][staffIndex];
        while (!measure && measureIndex >= 0) { // check for undefined measures, e.g. multi-measure-rest
            measure = this.measureList[--measureIndex][staffIndex];
        }
        return this.measureList[measureIndex][staffIndex];
    }

    public getMeasureIndex(graphicalMeasure: GraphicalMeasure, measureIndex: number, inListIndex: number): boolean {
        measureIndex = 0;
        inListIndex = 0;
        for (; measureIndex < this.measureList.length; measureIndex++) {
            for (let idx: number = 0, len: number = this.measureList[measureIndex].length; idx < len; ++idx) {
                const measure: GraphicalMeasure = this.measureList[measureIndex][idx];
                if (measure === graphicalMeasure) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Generic method to find graphical objects on the sheet at a given location.
     * @param clickPosition Position in units where we are searching on the sheet
     * @param className String representation of the class we want to find. Must extend GraphicalObject
     * @param startSearchArea The area in units around our point to look for our graphical object, default 5
     * @param maxSearchArea The max area we want to search around our point
     * @param searchAreaIncrement The amount we expand our search area for each iteration that we don't find an object of the given type
     * @param shouldBeIncludedTest A callback that determines if the object should be included in our results- return false for no, true for yes
     */
    private GetNearestGraphicalObject<T extends GraphicalObject>(
        clickPosition: PointF2D, className: string = GraphicalObject.name,
        startSearchArea: number = 5, maxSearchArea: number = 20, searchAreaIncrement: number = 5,
        shouldBeIncludedTest: (objectToTest: T) => boolean = undefined): T {
        const foundEntries: T[] = [];
        //Loop until we find some, or our search area is out of bounds
        while (foundEntries.length === 0 && startSearchArea <= maxSearchArea) {
            //Prepare search area
            const region: BoundingBox = new BoundingBox(undefined);
            region.BorderLeft = clickPosition.x - startSearchArea;
            region.BorderTop = clickPosition.y - startSearchArea;
            region.BorderRight = clickPosition.x + startSearchArea;
            region.BorderBottom = clickPosition.y + startSearchArea;
            region.AbsolutePosition = new PointF2D(clickPosition.x, clickPosition.y);
            region.calculateAbsolutePosition();
            //Loop through music pages
            for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
                const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
                const entries: T[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<T>(region, false, className);
                //If we have no entries on this page, skip to next (if exists)
                if (!entries || entries.length === 0) {
                    continue;
                } else {
                    //Otherwise test all our entries if applicable, store on our found list
                    for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
                        if (!shouldBeIncludedTest) {
                            foundEntries.push(entries[idx2]);
                        } else if (shouldBeIncludedTest(entries[idx2])) {
                            foundEntries.push(entries[idx2]);
                        }
                    }
                }
            }
            //Expand search area, we haven't found anything yet
            startSearchArea += searchAreaIncrement;
        }
        // Get closest entry
        let closest: T = undefined;
        for (let idx: number = 0, len: number = foundEntries.length; idx < len; ++idx) {
            const object: T = foundEntries[idx];
            if (closest === undefined) {
                closest = object;
            } else {
                const deltaNew: number = this.CalculateDistance(object.PositionAndShape.AbsolutePosition, clickPosition);
                const deltaOld: number = this.CalculateDistance(closest.PositionAndShape.AbsolutePosition, clickPosition);
                if (deltaNew < deltaOld) {
                    closest = object;
                }
            }
        }
        if (closest) {
            return closest;
        }
        return undefined;
    }

    public GetNearestVoiceEntry(clickPosition: PointF2D): GraphicalVoiceEntry {
        return this.GetNearestGraphicalObject<GraphicalVoiceEntry>(clickPosition, GraphicalVoiceEntry.name, 5, 20, 5,
                                                                   (object: GraphicalVoiceEntry) =>
                                                                        object.parentStaffEntry?.relInMeasureTimestamp !== undefined);
    }

    public GetNearestNote(clickPosition: PointF2D, maxClickDist: PointF2D): GraphicalNote {
        const nearestVoiceEntry: GraphicalVoiceEntry = this.GetNearestVoiceEntry(clickPosition);
        if (!nearestVoiceEntry) {
            return undefined;
        }
        let closestNote: GraphicalNote;
        let closestDist: number = Number.MAX_SAFE_INTEGER;
        // debug: show position in sheet. line starts from the click position, until clickposition.x + 2
        // (this.drawer as any).DrawOverlayLine( // as VexFlowMusicSheetDrawer
        //     clickPosition,
        //     new PointF2D(clickPosition.x + 2, clickPosition.y),
        //     this.MusicPages[0]);
        for (const note of nearestVoiceEntry.notes) {
            const posY: number = note.PositionAndShape.AbsolutePosition.y;
            const distX: number = Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x);
            const distY: number = Math.abs(posY - clickPosition.y);
            // console.log("note: " + note.sourceNote.Pitch.ToString());
            if (distX + distY < closestDist) {
                closestNote = note;
                closestDist = distX + distY;
            }
        }
        return closestNote;
    }

    public domToSvg(point: PointF2D): PointF2D {
        return this.domToSvgTransform(point, true);
    }

    public svgToDom(point: PointF2D): PointF2D {
        return this.domToSvgTransform(point, false);
    }

    public svgToOsmd(point: PointF2D): PointF2D {
        const pt: PointF2D = new PointF2D(point.x, point.y);
        pt.x /= 10; // unitInPixels would need to be imported from VexFlowMusicSheetDrawer
        pt.y /= 10;
        return pt;
    }

    // TODO move to VexFlowMusicSheetDrawer? better fit for imports
    private domToSvgTransform(point: PointF2D, inverse: boolean): PointF2D {
        const svgBackend: any = (this.drawer as any).Backends[0]; // as SvgVexFlowBackend;
        // TODO importing SvgVexFlowBackend here causes build problems. Importing VexFlowMusicSheetDrawer seems to be fine, but unnecessary.
        // if (!(svgBackend instanceof SvgVexFlowBackend)) {
        //     return undefined;
        // }
        const svg: SVGSVGElement = svgBackend.getSvgElement() as SVGSVGElement;
        const pt: SVGPoint = svg.createSVGPoint();
        pt.x = point.x;
        pt.y = point.y;
        let transformMatrix: DOMMatrix = svg.getScreenCTM();
        if (inverse) {
            transformMatrix = transformMatrix.inverse();
        }
        const sp: SVGPoint = pt.matrixTransform(transformMatrix);
        return new PointF2D(sp.x, sp.y);
    }

    public GetClickableLabel(clickPosition: PointF2D): GraphicalLabel {
        const initialSearchAreaX: number = 4;
        const initialSearchAreaY: number = 4;
        // Prepare search area
        const region: BoundingBox = new BoundingBox();
        region.BorderLeft = clickPosition.x - initialSearchAreaX;
        region.BorderTop = clickPosition.y - initialSearchAreaY;
        region.BorderRight = clickPosition.x + initialSearchAreaX;
        region.BorderBottom = clickPosition.y + initialSearchAreaY;
        region.AbsolutePosition = new PointF2D(0, 0);
        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
            const entries: GraphicalLabel[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<GraphicalLabel>(region);
            if (entries.length !== 1) {
                continue;
            } else {
                for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
                    const clickedLabel: GraphicalLabel = entries[idx2];
                    return clickedLabel;
                }
            }
        }
        return undefined;
    }

    public GetNearestStaffEntry(clickPosition: PointF2D): GraphicalStaffEntry {
        const initialSearchArea: number = 10;
        const foundEntries: GraphicalStaffEntry[] = [];
        // Prepare search area
        const region: BoundingBox = new BoundingBox(undefined);
        region.BorderLeft = clickPosition.x - initialSearchArea;
        region.BorderTop = clickPosition.y - initialSearchArea;
        region.BorderRight = clickPosition.x + initialSearchArea;
        region.BorderBottom = clickPosition.y + initialSearchArea;
        region.AbsolutePosition = new PointF2D(0, 0);
        // Search for StaffEntries in region
        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
            const entries: GraphicalStaffEntry[] = graphicalMusicPage.PositionAndShape.
                getObjectsInRegion<GraphicalStaffEntry>(region, false, GraphicalStaffEntry.name);
                // note that "GraphicalStaffEntry" instead of GraphicalStaffEntry.name doesn't work with minified builds
            if (!entries || entries.length === 0) {
                continue;
            } else {
                for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
                    const gse: GraphicalStaffEntry = entries[idx2];
                    foundEntries.push(gse);
                }
            }
        }
        // Get closest entry
        let closest: GraphicalStaffEntry = undefined;
        for (let idx: number = 0, len: number = foundEntries.length; idx < len; ++idx) {
            const gse: GraphicalStaffEntry = foundEntries[idx];
            if (closest === undefined) {
                closest = gse;
            } else {
                if (!gse.relInMeasureTimestamp) {
                    continue;
                }
                const deltaNew: number = this.CalculateDistance(gse.PositionAndShape.AbsolutePosition, clickPosition);
                const deltaOld: number = this.CalculateDistance(closest.PositionAndShape.AbsolutePosition, clickPosition);
                if (deltaNew < deltaOld) {
                    closest = gse;
                }
            }
        }
        if (closest) {
            return closest;
        }
        // TODO No staff entry was found. Feedback?
        // throw new ArgumentException("No staff entry found");
        return undefined;
    }

    /** Returns nearest object of type T near clickPosition.
     * E.g. GetNearestObject<GraphicalMeasure>(pos, GraphicalMeasure.name) returns the nearest measure.
     * Note that there is also GetNearestStaffEntry(), which has a bit more specific code for staff entries.
     * */
    public GetNearestObject<T extends GraphicalObject>(clickPosition: PointF2D, className: string): T {
        const initialSearchArea: number = 10;
        const foundEntries: T[] = [];
        // Prepare search area
        const region: BoundingBox = new BoundingBox(undefined);
        region.BorderLeft = clickPosition.x - initialSearchArea;
        region.BorderTop = clickPosition.y - initialSearchArea;
        region.BorderRight = clickPosition.x + initialSearchArea;
        region.BorderBottom = clickPosition.y + initialSearchArea;
        region.AbsolutePosition = new PointF2D(0, 0);
        // Search for StaffEntries in region
        for (let idx: number = 0, len: number = this.MusicPages.length; idx < len; ++idx) {
            const graphicalMusicPage: GraphicalMusicPage = this.MusicPages[idx];
            const entries: T[] = graphicalMusicPage.PositionAndShape.getObjectsInRegion<T>(region, false, className);
            if (!entries || entries.length === 0) {
                continue;
            } else {
                for (let idx2: number = 0, len2: number = entries.length; idx2 < len2; ++idx2) {
                    const entry: T = entries[idx2];
                    foundEntries.push(entry);
                }
            }
        }
        // Get closest entry
        let closest: T = undefined;
        for (let idx: number = 0, len: number = foundEntries.length; idx < len; ++idx) {
            const foundEntry: T = foundEntries[idx];
            if (closest === undefined) {
                closest = foundEntry;
            } else {
                // if (!foundEntry.relInMeasureTimestamp) {
                // relInMeasureTimestamp doesn't necessarily exist on generic type T, as it does on GraphicalStaffEntry
                //     continue;
                // }
                const deltaNew: number = this.CalculateDistance(foundEntry.PositionAndShape.AbsolutePosition, clickPosition);
                const deltaOld: number = this.CalculateDistance(closest.PositionAndShape.AbsolutePosition, clickPosition);
                if (deltaNew < deltaOld) {
                    closest = foundEntry;
                }
            }
        }
        if (closest) {
            return closest;
        }
        // TODO No object of type T was found. Feedback?
        // throw new ArgumentException(`No object of type ${className} found`);
        return undefined;
    }


    public GetPossibleCommentAnchor(clickPosition: PointF2D): SourceStaffEntry {
        const entry: GraphicalStaffEntry = this.GetNearestStaffEntry(clickPosition);
        if (!entry) {
            return undefined;
        }
        return entry.sourceStaffEntry;
    }

    public getClickedObjectOfType<T>(positionOnMusicSheet: PointF2D): T {
        for (let idx: number = 0, len: number = this.musicPages.length; idx < len; ++idx) {
            const page: GraphicalMusicPage = this.musicPages[idx];
            const o: Object = page.PositionAndShape.getClickedObjectOfType<T>(positionOnMusicSheet);
            if (o) {
                return (o as T);
            }
        }
        return undefined;
    }

    public tryGetTimestampFromPosition(positionOnMusicSheet: PointF2D): Fraction {
        const entry: GraphicalStaffEntry = this.getClickedObjectOfType<GraphicalStaffEntry>(positionOnMusicSheet);
        if (!entry) {
            return undefined;
        }
        return entry.getAbsoluteTimestamp();
    }

    public tryGetClickableLabel(positionOnMusicSheet: PointF2D): GraphicalLabel {
        try {
            return this.GetClickableLabel(positionOnMusicSheet);
        } catch (ex) {
            log.info("GraphicalMusicSheet.tryGetClickableObject", "positionOnMusicSheet: " + positionOnMusicSheet, ex);
        }

        return undefined;
    }

    public tryGetTimeStampFromPosition(positionOnMusicSheet: PointF2D): Fraction {
        try {
            const entry: GraphicalStaffEntry = this.GetNearestStaffEntry(positionOnMusicSheet);
            if (!entry) {
                return undefined;
            }
            return entry.getAbsoluteTimestamp();
        } catch (ex) {
            log.info(
                "GraphicalMusicSheet.tryGetTimeStampFromPosition",
                "positionOnMusicSheet: " + positionOnMusicSheet, ex
            );
        }

        return undefined;
    }

    /**
     * Get visible staffentry for the container given by the index.
     * @param index
     * @returns {GraphicalStaffEntry}
     */
    public getStaffEntry(index: number): GraphicalStaffEntry {
        const container: VerticalGraphicalStaffEntryContainer = this.VerticalGraphicalStaffEntryContainers[index];
        let staffEntry: GraphicalStaffEntry = undefined;
        try {
            for (let idx: number = 0, len: number = container.StaffEntries.length; idx < len; ++idx) {
                const entry: GraphicalStaffEntry = container.StaffEntries[idx];
                if (!entry || !entry.sourceStaffEntry.ParentStaff.ParentInstrument.Visible) {
                    continue;
                }
                if (!staffEntry) {
                    staffEntry = entry;
                } else if (entry.PositionAndShape && staffEntry.PositionAndShape) {
                    if (staffEntry.PositionAndShape.RelativePosition.x > entry.PositionAndShape.RelativePosition.x) {
                        staffEntry = entry;
                    }
                }
            }
        } catch (ex) {
            log.info("GraphicalMusicSheet.getStaffEntry", ex);
        }

        return staffEntry;
    }

    /**
     * Returns the index of the closest previous (earlier) vertical container which has at least some visible staff entry, with respect to the given index.
     * @param index
     * @returns {number}
     * @constructor
     */
    public GetPreviousVisibleContainerIndex(index: number): number {
        for (let i: number = index - 1; i >= 0; i--) {
            const entries: GraphicalStaffEntry[] = this.verticalGraphicalStaffEntryContainers[i].StaffEntries;
            for (let idx: number = 0, len: number = entries.length; idx < len; ++idx) {
                const entry: GraphicalStaffEntry = entries[idx];
                if (entry && entry.sourceStaffEntry.ParentStaff.ParentInstrument.Visible) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * Returns the index of the closest next (later) vertical container which has at least some visible staff entry, with respect to the given index.
     * @param index
     * @returns {number}
     * @constructor
     */
    public GetNextVisibleContainerIndex(index: number): number {
        for (let i: number = index + 1; i < this.verticalGraphicalStaffEntryContainers.length; ++i) {
            const entries: GraphicalStaffEntry[] = this.verticalGraphicalStaffEntryContainers[i].StaffEntries;
            for (let idx: number = 0, len: number = entries.length; idx < len; ++idx) {
                const entry: GraphicalStaffEntry = entries[idx];
                if (entry && entry.sourceStaffEntry.ParentStaff.ParentInstrument.Visible) {
                    return i;
                }
            }
        }
        return -1;
    }

    public findClosestLeftStaffEntry(fractionalIndex: number, searchOnlyVisibleEntries: boolean): GraphicalStaffEntry {
        let foundEntry: GraphicalStaffEntry = undefined;
        let leftIndex: number = Math.floor(fractionalIndex);
        leftIndex = Math.min(this.VerticalGraphicalStaffEntryContainers.length - 1, leftIndex);
        for (let i: number = leftIndex; i >= 0; i--) {
            foundEntry = this.getStaffEntry(i);
            if (foundEntry) {
                if (searchOnlyVisibleEntries) {
                    if (foundEntry.sourceStaffEntry.ParentStaff.ParentInstrument.Visible) {
                        return foundEntry;
                    }
                } else {
                    return foundEntry;
                }
            }
        }
        return undefined;
    }

    public findClosestRightStaffEntry(fractionalIndex: number, returnOnlyVisibleEntries: boolean): GraphicalStaffEntry {
        let foundEntry: GraphicalStaffEntry = undefined;
        const rightIndex: number = Math.max(0, Math.ceil(fractionalIndex));
        for (let i: number = rightIndex; i < this.VerticalGraphicalStaffEntryContainers.length; i++) {
            foundEntry = this.getStaffEntry(i);
            if (foundEntry) {
                if (returnOnlyVisibleEntries) {
                    if (foundEntry.sourceStaffEntry.ParentStaff.ParentInstrument.Visible) {
                        return foundEntry;
                    }
                } else {
                    return foundEntry;
                }
            }
        }
        return undefined;
    }

    public calculateCursorLineAtTimestamp(musicTimestamp: Fraction, styleEnum: OutlineAndFillStyleEnum): GraphicalLine {
        const result: [number, MusicSystem] = this.calculateXPositionFromTimestamp(musicTimestamp);
        const xPos: number = result[0];
        const correspondingMusicSystem: MusicSystem = result[1];
        if (!correspondingMusicSystem || correspondingMusicSystem.StaffLines.length === 0) {
            return undefined;
        }
        const yCoordinate: number = correspondingMusicSystem.PositionAndShape.AbsolutePosition.y;
        const height: number = CollectionUtil.last(correspondingMusicSystem.StaffLines).PositionAndShape.RelativePosition.y + 4;
        return new GraphicalLine(new PointF2D(xPos, yCoordinate), new PointF2D(xPos, yCoordinate + height), 3, styleEnum);
    }

    public calculateXPositionFromTimestamp(timeStamp: Fraction): [number, MusicSystem] {
        let currentMusicSystem: MusicSystem = undefined;
        const fractionalIndex: number = this.GetInterpolatedIndexInVerticalContainers(timeStamp);
        const previousStaffEntry: GraphicalStaffEntry = this.findClosestLeftStaffEntry(fractionalIndex, true);
        const nextStaffEntry: GraphicalStaffEntry = this.findClosestRightStaffEntry(fractionalIndex, true);
        const currentTimeStamp: number = timeStamp.RealValue;
        if (!previousStaffEntry && !nextStaffEntry) {
            return [0, undefined];
        }
        let previousStaffEntryMusicSystem: MusicSystem = undefined;
        if (previousStaffEntry) {
            // TODO sometimes one of these ParentStaffLine is undefined, either fix this or handle it here
            previousStaffEntryMusicSystem = previousStaffEntry.parentMeasure.ParentStaffLine?.ParentMusicSystem;
        } else {
            previousStaffEntryMusicSystem = nextStaffEntry.parentMeasure.ParentStaffLine?.ParentMusicSystem;
        }
        let nextStaffEntryMusicSystem: MusicSystem = undefined;
        if (nextStaffEntry) {
            nextStaffEntryMusicSystem = nextStaffEntry.parentMeasure.ParentStaffLine?.ParentMusicSystem;
        } else {
            nextStaffEntryMusicSystem = previousStaffEntry.parentMeasure.ParentStaffLine?.ParentMusicSystem;
        }
        if (previousStaffEntryMusicSystem === nextStaffEntryMusicSystem) {
            currentMusicSystem = previousStaffEntryMusicSystem;
            let fraction: number;
            let previousStaffEntryPositionX: number;
            let nextStaffEntryPositionX: number;
            if (!previousStaffEntry) {
                previousStaffEntryPositionX = nextStaffEntryPositionX = nextStaffEntry.PositionAndShape.AbsolutePosition.x;
                fraction = 0;
            } else if (!nextStaffEntry) {
                previousStaffEntryPositionX = previousStaffEntry.PositionAndShape.AbsolutePosition.x;
                nextStaffEntryPositionX = currentMusicSystem.GetRightBorderAbsoluteXPosition();
                const sm: SourceMeasure = previousStaffEntry.parentMeasure.parentSourceMeasure;
                fraction = (currentTimeStamp - previousStaffEntry.getAbsoluteTimestamp().RealValue) / (
                    Fraction.plus(sm.AbsoluteTimestamp, sm.Duration).RealValue - previousStaffEntry.getAbsoluteTimestamp().RealValue);
            } else {
                previousStaffEntryPositionX = previousStaffEntry.PositionAndShape.AbsolutePosition.x;
                nextStaffEntryPositionX = nextStaffEntry.PositionAndShape.AbsolutePosition.x;
                if (previousStaffEntry === nextStaffEntry) {
                    fraction = 0;
                } else {
                    fraction = (currentTimeStamp - previousStaffEntry.getAbsoluteTimestamp().RealValue) /
                        (nextStaffEntry.getAbsoluteTimestamp().RealValue - previousStaffEntry.getAbsoluteTimestamp().RealValue);
                }
            }
            fraction = Math.min(1, Math.max(0, fraction));
            const interpolatedXPosition: number = previousStaffEntryPositionX + fraction * (nextStaffEntryPositionX - previousStaffEntryPositionX);
            return [interpolatedXPosition, currentMusicSystem];
        } else {
            const nextSystemLeftBorderTimeStamp: number = nextStaffEntry.parentMeasure.parentSourceMeasure.AbsoluteTimestamp.RealValue;
            let fraction: number;
            let interpolatedXPosition: number;
            if (currentTimeStamp < nextSystemLeftBorderTimeStamp && previousStaffEntryMusicSystem.StaffLines[0]) {
                // previousStaffEntryMusicSystem.StaffLines[0]: fix for drawing range set (previous system not rendered)
                currentMusicSystem = previousStaffEntryMusicSystem;
                const previousStaffEntryPositionX: number = previousStaffEntry.PositionAndShape.AbsolutePosition.x;
                const previousSystemRightBorderX: number = currentMusicSystem.GetRightBorderAbsoluteXPosition();
                fraction = (currentTimeStamp - previousStaffEntry.getAbsoluteTimestamp().RealValue) /
                    (nextSystemLeftBorderTimeStamp - previousStaffEntry.getAbsoluteTimestamp().RealValue);
                fraction = Math.min(1, Math.max(0, fraction));
                interpolatedXPosition = previousStaffEntryPositionX + fraction * (previousSystemRightBorderX - previousStaffEntryPositionX);
            } else {
                currentMusicSystem = nextStaffEntryMusicSystem;
                const nextStaffEntryPositionX: number = nextStaffEntry.PositionAndShape.AbsolutePosition.x;
                const nextSystemLeftBorderX: number = currentMusicSystem.GetLeftBorderAbsoluteXPosition();
                fraction = (currentTimeStamp - nextSystemLeftBorderTimeStamp) /
                    (nextStaffEntry.getAbsoluteTimestamp().RealValue - nextSystemLeftBorderTimeStamp);
                fraction = Math.min(1, Math.max(0, fraction));
                interpolatedXPosition = nextSystemLeftBorderX + fraction * (nextStaffEntryPositionX - nextSystemLeftBorderX);
            }
            return [interpolatedXPosition, currentMusicSystem];
        }
    }

    public GetNumberOfVisibleInstruments(): number {
        let visibleInstrumentCount: number = 0;
        for (let idx: number = 0, len: number = this.musicSheet.Instruments.length; idx < len; ++idx) {
            const instrument: Instrument = this.musicSheet.Instruments[idx];
            if (instrument.Visible === true) {
                visibleInstrumentCount++;
            }
        }
        return visibleInstrumentCount;
    }

    public GetNumberOfFollowedInstruments(): number {
        let followedInstrumentCount: number = 0;
        for (let idx: number = 0, len: number = this.musicSheet.Instruments.length; idx < len; ++idx) {
            const instrument: Instrument = this.musicSheet.Instruments[idx];
            if (instrument.Following === true) {
                followedInstrumentCount++;
            }
        }
        return followedInstrumentCount;
    }

    /*public GetGraphicalFromSourceMeasure(sourceMeasure: SourceMeasure): GraphicalMeasure[] {
        return this.sourceToGraphicalMeasureLinks.getValue(sourceMeasure); // TODO gets wrong measure because sourceMeasure is not a valid key
    }*/

    public GetGraphicalFromSourceStaffEntry(sourceStaffEntry: SourceStaffEntry): GraphicalStaffEntry {
        if (!sourceStaffEntry.VerticalContainerParent.ParentMeasure?.VerticalMeasureList) {
            return undefined;
        }
        const graphicalMeasure: GraphicalMeasure = sourceStaffEntry.VerticalContainerParent.ParentMeasure.VerticalMeasureList
            [sourceStaffEntry.ParentStaff.idInMusicSheet];
        return graphicalMeasure?.findGraphicalStaffEntryFromTimestamp(sourceStaffEntry.Timestamp);
    }

    private CalculateDistance(pt1: PointF2D, pt2: PointF2D): number {
        const deltaX: number = pt1.x - pt2.x;
        const deltaY: number = pt1.y - pt2.y;
        return (deltaX * deltaX) + (deltaY * deltaY);
    }

    /**
     * Return the longest StaffEntry duration from a GraphicalVerticalContainer.
     * @param index the index of the vertical container
     * @returns {Fraction}
     */
    private getLongestStaffEntryDuration(index: number): Fraction {
        let maxLength: Fraction = new Fraction(0, 1);
        for (const graphicalStaffEntry of this.verticalGraphicalStaffEntryContainers[index].StaffEntries) {
            if (!graphicalStaffEntry) {
                continue;
            }
            const maxLengthInStaffEntry: Fraction = graphicalStaffEntry.findStaffEntryMaxNoteLength();
            if (maxLength.lt(maxLengthInStaffEntry)) {
                maxLength = maxLengthInStaffEntry;
            }
        }
        return maxLength;
    }
}

export class SystemImageProperties {
    public positionInPixels: PointF2D;
    public systemImageId: number;
    public system: MusicSystem;
}