src/MusicalScore/Graphical/GraphicalStaffEntry.ts
import {SourceStaffEntry} from "../VoiceData/SourceStaffEntry";
import {BoundingBox} from "./BoundingBox";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {VerticalGraphicalStaffEntryContainer} from "./VerticalGraphicalStaffEntryContainer";
import {Note} from "../VoiceData/Note";
import {Slur} from "../VoiceData/Expressions/ContinuousExpressions/Slur";
import {Voice} from "../VoiceData/Voice";
import {VoiceEntry} from "../VoiceData/VoiceEntry";
import {GraphicalTie} from "./GraphicalTie";
import {GraphicalObject} from "./GraphicalObject";
import {GraphicalMeasure} from "./GraphicalMeasure";
import {GraphicalNote} from "./GraphicalNote";
import {GraphicalChordSymbolContainer} from "./GraphicalChordSymbolContainer";
import {GraphicalLyricEntry} from "./GraphicalLyricEntry";
import {AbstractGraphicalInstruction} from "./AbstractGraphicalInstruction";
import {GraphicalStaffEntryLink} from "./GraphicalStaffEntryLink";
import {CollectionUtil} from "../../Util/CollectionUtil";
import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
import { MusicSheetCalculator } from "./MusicSheetCalculator";
import { Tie } from "../VoiceData/Tie";
import { GraphicalLabel } from "./GraphicalLabel";
import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
/**
* The graphical counterpart of a [[SourceStaffEntry]].
*/
export abstract class GraphicalStaffEntry extends GraphicalObject {
constructor(parentMeasure: GraphicalMeasure, sourceStaffEntry: SourceStaffEntry = undefined, staffEntryParent: GraphicalStaffEntry = undefined) {
super();
this.parentMeasure = parentMeasure;
this.graphicalVoiceEntries = [];
this.sourceStaffEntry = sourceStaffEntry;
if (staffEntryParent) {
this.staffEntryParent = staffEntryParent;
this.parentVerticalContainer = staffEntryParent.parentVerticalContainer;
this.PositionAndShape = new BoundingBox(this, staffEntryParent.PositionAndShape);
} else {
this.PositionAndShape = new BoundingBox(this, parentMeasure.PositionAndShape);
}
if (sourceStaffEntry) {
this.relInMeasureTimestamp = sourceStaffEntry.Timestamp;
}
this.FingeringEntries = [];
}
public graphicalChordContainers: GraphicalChordSymbolContainer[] = [];
public graphicalLink: GraphicalStaffEntryLink;
// Extra member needed, as tie notes have no direct source entry with the right time stamp.
public relInMeasureTimestamp: Fraction;
public sourceStaffEntry: SourceStaffEntry;
public parentMeasure: GraphicalMeasure;
public graphicalVoiceEntries: GraphicalVoiceEntry[];
public staffEntryParent: GraphicalStaffEntry;
public parentVerticalContainer: VerticalGraphicalStaffEntryContainer;
public tabStaffEntry: GraphicalStaffEntry = undefined;
public MaxAccidentals: number = 0;
private graphicalInstructions: AbstractGraphicalInstruction[] = [];
public ties: Tie[] = [];
private graphicalTies: GraphicalTie[] = [];
private lyricsEntries: GraphicalLyricEntry[] = [];
public FingeringEntries: GraphicalLabel[];
public get GraphicalInstructions(): AbstractGraphicalInstruction[] {
return this.graphicalInstructions;
}
public get GraphicalTies(): GraphicalTie[] {
return this.graphicalTies;
}
public get LyricsEntries(): GraphicalLyricEntry[] {
return this.lyricsEntries;
}
public set LyricsEntries(value: GraphicalLyricEntry[]) {
this.lyricsEntries = value;
}
/**
* Calculate the absolute Timestamp.
* @returns {Fraction}
*/
public getAbsoluteTimestamp(): Fraction {
const result: Fraction = this.parentMeasure.parentSourceMeasure.AbsoluteTimestamp.clone();
if (this.relInMeasureTimestamp) {
result.Add(this.relInMeasureTimestamp);
}
return result;
}
/**
* Search through all the GraphicalNotes to find the suitable one for a TieEndNote.
* @param tieNote
* @returns {any}
*/
public findTieGraphicalNoteFromNote(tieNote: Note): GraphicalNote {
for (const gve of this.graphicalVoiceEntries) {
for (const graphicalNote of gve.notes) {
const note: Note = graphicalNote.sourceNote;
if (!note.isRest()
&& note.Pitch.FundamentalNote === tieNote.Pitch.FundamentalNote
&& note.Pitch.Octave === tieNote.Pitch.Octave
&& note.getAbsoluteTimestamp().Equals(tieNote.getAbsoluteTimestamp())) {
return graphicalNote;
}
}
}
return undefined;
}
/**
* Search through all [[GraphicalNote]]s to find the suitable one for an StartSlurNote (that 's also an EndTieNote).
* @param tieNote
* @param slur
* @returns {any}
*/
public findEndTieGraphicalNoteFromNoteWithStartingSlur(tieNote: Note, slur: Slur): GraphicalNote {
if (!tieNote) {
return undefined;
}
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry !== tieNote.ParentVoiceEntry) {
continue;
}
for (const graphicalNote of gve.notes) {
const note: Note = graphicalNote.sourceNote;
if (note.NoteTie && note.NoteSlurs.indexOf(slur) !== -1) {
return graphicalNote;
}
}
}
return undefined;
}
public findGraphicalNoteFromGraceNote(graceNote: Note): GraphicalNote {
if (!graceNote) {
return undefined;
}
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry !== graceNote.ParentVoiceEntry) {
continue;
}
for (const graphicalNote of gve.notes) {
if (graphicalNote.sourceNote === graceNote) {
return graphicalNote;
}
}
}
return undefined;
}
public findGraphicalNoteFromNote(note: Note): GraphicalNote {
if (!note) {
return undefined;
}
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry !== note.ParentVoiceEntry) {
continue;
}
for (const graphicalNote of gve.notes) {
if (graphicalNote.sourceNote === note && this.getAbsoluteTimestamp().Equals(note.getAbsoluteTimestamp())) {
return graphicalNote;
}
}
}
return undefined;
}
public getGraphicalNoteDurationFromVoice(voice: Voice): Fraction {
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry.ParentVoice !== voice) {
continue;
}
return gve.notes[0].graphicalNoteLength;
}
return new Fraction(0, 1);
}
/**
* Find the [[StaffEntry]]'s [[GraphicalNote]]s that correspond to the given [[VoiceEntry]]'s [[Note]]s.
* @param voiceEntry
* @returns {any}
*/
public findVoiceEntryGraphicalNotes(voiceEntry: VoiceEntry): GraphicalNote[] {
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry === voiceEntry) {
return gve.notes;
}
}
return undefined;
}
/**
* Check if the given [[VoiceEntry]] is part of the [[StaffEntry]]'s Linked [[VoiceEntry]].
* @param voiceEntry
* @returns {boolean}
*/
public isVoiceEntryPartOfLinkedVoiceEntry(voiceEntry: VoiceEntry): boolean {
if (this.sourceStaffEntry.Link) {
for (let idx: number = 0, len: number = this.sourceStaffEntry.Link.LinkStaffEntries.length; idx < len; ++idx) {
const sEntry: SourceStaffEntry = this.sourceStaffEntry.Link.LinkStaffEntries[idx];
if (sEntry.VoiceEntries.indexOf(voiceEntry) !== -1 && sEntry !== this.sourceStaffEntry) {
return true;
}
}
}
return false;
}
/**
* Return the [[StaffEntry]]'s Minimum NoteLength.
* @returns {Fraction}
*/
public findStaffEntryMinNoteLength(): Fraction {
let minLength: Fraction = new Fraction(Number.MAX_VALUE, 1);
for (const gve of this.graphicalVoiceEntries) {
for (const graphicalNote of gve.notes) {
const calNoteLen: Fraction = graphicalNote.graphicalNoteLength;
if (calNoteLen.lt(minLength) && calNoteLen.GetExpandedNumerator() > 0) {
minLength = calNoteLen;
}
}
}
return minLength;
}
public findStaffEntryMaxNoteLength(): Fraction {
let maxLength: Fraction = new Fraction(0, 1);
for (const gve of this.graphicalVoiceEntries) {
for (const graphicalNote of gve.notes) {
const calNoteLen: Fraction = graphicalNote.graphicalNoteLength;
if (maxLength.lt(calNoteLen) && calNoteLen.GetExpandedNumerator() > 0) {
maxLength = calNoteLen;
}
}
}
return maxLength;
}
/**
* Find or creates the list of [[GraphicalNote]]s in case of a [[VoiceEntry]] (not from TiedNote).
* @param voiceEntry
* @returns {GraphicalNote[]}
*/
public findOrCreateGraphicalVoiceEntry(voiceEntry: VoiceEntry): GraphicalVoiceEntry {
for (const gve of this.graphicalVoiceEntries) {
if (gve.parentVoiceEntry === voiceEntry) {
return gve;
}
}
// if not found in list, create new one and add to list:
const graphicalVoiceEntry: GraphicalVoiceEntry = MusicSheetCalculator.symbolFactory.createVoiceEntry(voiceEntry, this);
this.graphicalVoiceEntries.push(graphicalVoiceEntry);
return graphicalVoiceEntry;
}
/**
* Find or creates the list of [[GraphicalNote]]s in case of a TiedNote.
* @param graphicalNote
* @returns {GraphicalNote[]}
*/
public findOrCreateGraphicalVoiceEntryFromGraphicalNote(graphicalNote: GraphicalNote): GraphicalVoiceEntry {
for (const gve of this.graphicalVoiceEntries) {
if (gve === graphicalNote.parentVoiceEntry) {
return gve;
}
}
// if not found in list, create new one and add to list:
const graphicalVoiceEntry: GraphicalVoiceEntry = MusicSheetCalculator.symbolFactory.createVoiceEntry(graphicalNote.sourceNote.ParentVoiceEntry, this);
this.graphicalVoiceEntries.push(graphicalVoiceEntry);
return graphicalVoiceEntry;
}
/**
* Insert the [[GraphicalNote]] to the correct index of the [[GraphicalNote]]s list,
* so that the order of the [[GraphicalNote]]'s in the list corresponds to the [[VoiceEntry]]'s [[Note]]s order.
* (needed when adding Tie-EndNotes).
* @param graphicalNotes
* @param graphicalNote
*/
public addGraphicalNoteToListAtCorrectYPosition(gve: GraphicalVoiceEntry, graphicalNote: GraphicalNote): void {
const graphicalNotes: GraphicalNote[] = gve.notes;
if (graphicalNotes.length === 0 ||
graphicalNote.PositionAndShape.RelativePosition.y < CollectionUtil.last(graphicalNotes).PositionAndShape.RelativePosition.y) {
graphicalNotes.push(graphicalNote);
} else {
for (let i: number = graphicalNotes.length - 1; i >= 0; i--) {
if (graphicalNotes[i].PositionAndShape.RelativePosition.y > graphicalNote.PositionAndShape.RelativePosition.y) {
graphicalNotes.splice(i + 1, 0, graphicalNote);
break;
}
if (i === 0) {
graphicalNotes.splice(0, 0, graphicalNote);
break;
}
}
}
}
/**
* Returns true if this staff entry has only rests
*/
public hasOnlyRests(): boolean {
return this.sourceStaffEntry.hasOnlyRests;
}
public getSkylineMin(): number {
const skybottomcalculator: SkyBottomLineCalculator = this.parentMeasure?.ParentStaffLine.SkyBottomLineCalculator;
if (!skybottomcalculator) {
return undefined;
}
const [start, end] = this.getAbsoluteStartAndEnd();
return skybottomcalculator.getSkyLineMinInRange(start, end);
}
/** Highest Y around the staff entry and notes in OSMD units (pixels / 10). Note that negative y is up. */
public getHighestYAtEntry(): number {
const baseY: number = this.parentMeasure.ParentStaffLine.PositionAndShape.AbsolutePosition.y;
return baseY + this.getSkylineMin();
}
/** Lowest Y around the staff entry and notes in OSMD units (pixels / 10). Note that positive y is down. */
public getLowestYAtEntry(): number {
const baseY: number = this.parentMeasure.ParentStaffLine.PositionAndShape.AbsolutePosition.y;
return baseY + this.getBottomlineMax();
}
public getBottomlineMax(): number {
const skybottomcalculator: SkyBottomLineCalculator = this.parentMeasure?.ParentStaffLine.SkyBottomLineCalculator;
if (!skybottomcalculator) {
return undefined;
}
const [start, end] = this.getAbsoluteStartAndEnd();
return skybottomcalculator.getBottomLineMaxInRange(start, end);
}
public getAbsoluteStartAndEnd(): [number, number] {
let start: number = this.PositionAndShape.AbsolutePosition.x;
start -= this.parentMeasure.ParentStaffLine.PositionAndShape.AbsolutePosition.x;
const end: number = start + this.PositionAndShape.Size.width;
return [start, end];
}
}