123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035 |
- 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 {CollectionUtil} from "../../Util/CollectionUtil";
- import {SelectionStartSymbol} from "./SelectionStartSymbol";
- import {SelectionEndSymbol} from "./SelectionEndSymbol";
- import {OutlineAndFillStyleEnum} from "./DrawingEnums";
- import { Clickable } from "./Clickable";
- import { StaffLine } from "./StaffLine";
- 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
- }
- /**
- * 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 {
- // return this.GetNearestGraphicalObject<GraphicalNote>
- // (clickPosition, GraphicalNote.name, 10, 10, 1,
- // function(note: GraphicalNote): boolean {
- // if (Math.abs(note.PositionAndShape.AbsolutePosition.x - clickPosition.x) < maxClickDist.x
- // && Math.abs(note.PositionAndShape.AbsolutePosition.y - clickPosition.y) < maxClickDist.y) {
- // return true;
- // } else {
- // return false;
- // }
- // });
- //TODO: This functions a little bit different than our GetGraphicalObject method... Perhaps close enough to consolidate
- //Investigate
- 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 GetPossibleCommentAnchor(clickPosition: PointF2D): SourceStaffEntry {
- const entry: GraphicalVoiceEntry = this.GetNearestVoiceEntry(clickPosition);
- if (!entry) {
- return undefined;
- }
- return entry.parentStaffEntry.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;
- }
- //Comment copied from Bounding box method:
- //Generics don't work like this in TS. Casting doesn't filter out objects.
- //instanceof doesn't work either with generic types. Hopefully instanceof becomes available at some point, for now we have to do annoyingly
- //specific implementations after calling this to filter the objects.
- public GetClickedClickable(positionOnMusicSheet: PointF2D): Clickable {
- for (let idx: number = 0, len: number = this.musicPages.length; idx < len; ++idx) {
- const page: GraphicalMusicPage = this.musicPages[idx];
- const o: Object = page.PositionAndShape.getClickedClickable(positionOnMusicSheet);
- if (o && o instanceof Clickable) {
- return o;
- }
- }
- 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: GraphicalVoiceEntry = this.GetNearestVoiceEntry(positionOnMusicSheet);
- if (!entry) {
- return undefined;
- }
- return entry.parentStaffEntry.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, any] = 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, GraphicalStaffEntry] {
- 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, 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;
- }
- let prevMeasure: SourceMeasure;
- 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();
- prevMeasure = previousStaffEntry.parentMeasure.parentSourceMeasure;
- fraction = (currentTimeStamp - previousStaffEntry.getAbsoluteTimestamp().RealValue) / (
- Fraction.plus(prevMeasure.AbsoluteTimestamp, prevMeasure.Duration).RealValue - previousStaffEntry.getAbsoluteTimestamp().RealValue);
- } else {
- previousStaffEntryPositionX = previousStaffEntry.PositionAndShape.AbsolutePosition.x;
- nextStaffEntryPositionX = nextStaffEntry.PositionAndShape.AbsolutePosition.x;
- prevMeasure = previousStaffEntry.parentMeasure.parentSourceMeasure;
- 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));
- let limitX: number = nextStaffEntryPositionX;
- if (prevMeasure?.Rules.LimitCursorPositionToCurrentMeasure) {
- limitX = Math.min(previousStaffEntry.parentMeasure.PositionAndShape.AbsolutePosition.x +
- previousStaffEntry.parentMeasure.PositionAndShape.BorderRight, limitX);
- }
- const interpolatedXPosition: number = previousStaffEntryPositionX + fraction * (limitX - previousStaffEntryPositionX);
- return [interpolatedXPosition, currentMusicSystem, previousStaffEntry];
- } else {
- const nextSystemLeftBorderTimeStamp: number = nextStaffEntry.parentMeasure.parentSourceMeasure.AbsoluteTimestamp.RealValue;
- let fraction: number;
- let interpolatedXPosition: number;
- if (currentTimeStamp < nextSystemLeftBorderTimeStamp) {
- 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, previousStaffEntry];
- }
- }
- public calculateCursorPoints(xPos: number, correspondingSystem: MusicSystem): [PointF2D, PointF2D] {
- if (correspondingSystem === undefined || correspondingSystem.StaffLines.length === 0) {
- return [new PointF2D(), new PointF2D()];
- }
- const yCoordinate: number = correspondingSystem.PositionAndShape.AbsolutePosition.y;
- const lastStaffLine: StaffLine = correspondingSystem.StaffLines.last();
- const height: number = lastStaffLine.PositionAndShape.RelativePosition.y + lastStaffLine.StaffHeight;
- return [new PointF2D(xPos, yCoordinate), new PointF2D(xPos, yCoordinate + height)];
- }
- 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;
- }
|