opensheetmusicdisplay/opensheetmusicdisplay

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

Summary

Maintainability
F
5 days
Test Coverage
import {StaffLine} from "./StaffLine";
import {Instrument} from "../Instrument";
import {BoundingBox} from "./BoundingBox";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {SourceMeasure} from "../VoiceData/SourceMeasure";
import {InstrumentalGroup} from "../InstrumentalGroup";
import {TextAlignmentEnum} from "../../Common/Enums/TextAlignment";
import {GraphicalMusicPage} from "./GraphicalMusicPage";
import {GraphicalLabel} from "./GraphicalLabel";
import {GraphicalMeasure} from "./GraphicalMeasure";
import {GraphicalObject} from "./GraphicalObject";
import {EngravingRules} from "./EngravingRules";
import {PointF2D} from "../../Common/DataObjects/PointF2D";
import {GraphicalStaffEntry} from "./GraphicalStaffEntry";
import {SystemLinesEnum} from "./SystemLinesEnum";
import { Dictionary } from "typescript-collections";
import {GraphicalComment} from "./GraphicalComment";
import {GraphicalMarkedArea} from "./GraphicalMarkedArea";
import {SystemLine} from "./SystemLine";
import {SystemLinePosition} from "./SystemLinePosition";
import {Staff} from "../VoiceData/Staff";
import { Label } from "../Label";

/**
 * A MusicSystem contains the [[StaffLine]]s for all instruments, until a line break
 */
export abstract class MusicSystem extends GraphicalObject {
    public needsToBeRedrawn: boolean = true;
    public rules: EngravingRules;
    protected parent: GraphicalMusicPage;
    protected id: number;
    protected staffLines: StaffLine[] = [];
    protected graphicalMeasures: GraphicalMeasure[][] = [];
    /** Dictionary of (Instruments and) labels.
     * note that the key needs to be unique, GraphicalLabel is not unique yet.
     * That is why the labels are labels.values() and not labels.keys().
     */
    protected labels: Dictionary<Instrument, GraphicalLabel> = new Dictionary<Instrument, GraphicalLabel>();
    protected measureNumberLabels: GraphicalLabel[] = [];
    protected maxLabelLength: number;
    protected objectsToRedraw: [Object[], Object][] = [];
    protected instrumentBrackets: GraphicalObject[] = [];
    protected groupBrackets: GraphicalObject[] = [];
    protected graphicalMarkedAreas: GraphicalMarkedArea[] = [];
    protected graphicalComments: GraphicalComment[] = [];
    protected systemLines: SystemLine[] = [];
    public breaksPage: boolean = false;

    constructor(id: number) {
        super();
        this.id = id;
        this.boundingBox = new BoundingBox(this);
        this.maxLabelLength = 0.0;
    }

    public get Parent(): GraphicalMusicPage {
        return this.parent;
    }

    public set Parent(value: GraphicalMusicPage) {
        // remove from old page
        if (this.parent) {
            const index: number = this.parent.MusicSystems.indexOf(this, 0);
            if (index > -1) {
                this.parent.MusicSystems.splice(index, 1);
            }
        }

        this.parent = value;
        this.boundingBox.Parent = value.PositionAndShape;
    }

    public get NextSystem(): MusicSystem {
        const idxInParent: number = this.Parent.MusicSystems.indexOf(this);
        return idxInParent !== this.Parent.MusicSystems.length ? this.Parent.MusicSystems[idxInParent + 1] : undefined;
    }

    public get StaffLines(): StaffLine[] {
        return this.staffLines;
    }

    public get GraphicalMeasures(): GraphicalMeasure[][] {
        return this.graphicalMeasures;
    }

    public get MeasureNumberLabels(): GraphicalLabel[] {
        return this.measureNumberLabels;
    }

    public get Labels(): GraphicalLabel[] {
        return this.labels.values();
    }

    public get ObjectsToRedraw(): [Object[], Object][] {
        return this.objectsToRedraw;
    }

    public get InstrumentBrackets(): GraphicalObject[] {
        return this.instrumentBrackets;
    }

    public get GroupBrackets(): GraphicalObject[] {
        return this.groupBrackets;
    }

    public get GraphicalMarkedAreas(): GraphicalMarkedArea[] {
        return this.graphicalMarkedAreas;
    }

    public get GraphicalComments(): GraphicalComment[] {
        return this.graphicalComments;
    }

    public get SystemLines(): SystemLine[] {
        return this.systemLines;
    }

    public get Id(): number {
        return this.id;
    }

    /**
     * Create the left vertical Line connecting all staves of the [[MusicSystem]].
     * @param lineWidth
     * @param systemLabelsRightMargin
     */
    public createSystemLeftLine(lineWidth: number, systemLabelsRightMargin: number, isFirstSystem: boolean): void {
        let xPosition: number = -lineWidth / 2;
        if (isFirstSystem) {
            xPosition = this.maxLabelLength + systemLabelsRightMargin - lineWidth / 2;
        }
        const top: GraphicalMeasure = this.staffLines[0].Measures[0];
        let bottom: GraphicalMeasure = undefined;
        if (this.staffLines.length > 1) {
            bottom = this.staffLines[this.staffLines.length - 1].Measures[0];
        }
        const leftSystemLine: SystemLine = this.createSystemLine(xPosition, lineWidth, SystemLinesEnum.SingleThin,
                                                                 SystemLinePosition.MeasureBegin, this, top, bottom);
        this.SystemLines.push(leftSystemLine);
        leftSystemLine.PositionAndShape.RelativePosition = new PointF2D(xPosition, 0);
        leftSystemLine.PositionAndShape.BorderLeft = 0;
        leftSystemLine.PositionAndShape.BorderRight = lineWidth;
        leftSystemLine.PositionAndShape.BorderTop = leftSystemLine.PositionAndShape.Parent.BorderTop;
        leftSystemLine.PositionAndShape.BorderBottom = leftSystemLine.PositionAndShape.Parent.BorderBottom;
        // TODO this is arguably still too large for the systemline bbox, but at least not larger than the MusicSystem anymore.
        //   see https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1245
        this.createLinesForSystemLine(leftSystemLine);
    }

    /**
     * Create the vertical Lines after the End of all [[StaffLine]]'s Measures
     * @param xPosition
     * @param lineWidth
     * @param lineType
     * @param linePosition indicates if the line belongs to start or end of measure
     * @param measureIndex the measure index within the staffline
     * @param measure
     */
    public createVerticalLineForMeasure(xPosition: number, lineWidth: number, lineType: SystemLinesEnum, linePosition: SystemLinePosition,
                                        measureIndex: number, measure: GraphicalMeasure): void {
        //return; // TODO check why there's a bold line here for the double barline sample
        const staffLine: StaffLine = measure.ParentStaffLine;
        const staffLineRelative: PointF2D = new PointF2D(staffLine.PositionAndShape.RelativePosition.x,
                                                         staffLine.PositionAndShape.RelativePosition.y);
        const staves: Staff[] = staffLine.ParentStaff.ParentInstrument.Staves;
        if (staffLine.ParentStaff === staves[0]) {
            let bottomMeasure: GraphicalMeasure = undefined;
            if (staves.length > 1) {
                bottomMeasure = this.getBottomStaffLine(staffLine).Measures[measureIndex];
            }
            const singleVerticalLineAfterMeasure: SystemLine = this.createSystemLine(xPosition, lineWidth, lineType,
                                                                                     linePosition, this, measure, bottomMeasure);
            const systemXPosition: number = staffLineRelative.x + xPosition;
            singleVerticalLineAfterMeasure.PositionAndShape.RelativePosition = new PointF2D(systemXPosition, 0);
            singleVerticalLineAfterMeasure.PositionAndShape.BorderLeft = 0;
            singleVerticalLineAfterMeasure.PositionAndShape.BorderRight = lineWidth;
            this.SystemLines.push(singleVerticalLineAfterMeasure);
        }
    }

    /**
     * Set the y-Positions of all the system lines in the system and creates the graphical Lines and dots within.
     * @param rules
     */
    public setYPositionsToVerticalLineObjectsAndCreateLines(rules: EngravingRules): void {
        // empty
    }

    public calculateBorders(rules: EngravingRules): void {
        // empty
    }

    public alignBeginInstructions(): void {
        // empty
    }

    public GetLeftBorderAbsoluteXPosition(): number {
        return this.StaffLines[0].PositionAndShape.AbsolutePosition.x + this.StaffLines[0].Measures[0].beginInstructionsWidth;
    }

    public GetRightBorderAbsoluteXPosition(): number {
        return this.StaffLines[0].PositionAndShape.AbsolutePosition.x + this.StaffLines[0].StaffLines[0].End.x;
    }

    public AddGraphicalMeasures(graphicalMeasures: GraphicalMeasure[]): void {
        for (let idx: number = 0, len: number = graphicalMeasures.length; idx < len; ++idx) {
            const graphicalMeasure: GraphicalMeasure = graphicalMeasures[idx];
            graphicalMeasure.ParentMusicSystem = this;
        }
        this.graphicalMeasures.push(graphicalMeasures);
    }

    public GetSystemsFirstTimeStamp(): Fraction {
        return this.graphicalMeasures[0][0].parentSourceMeasure.AbsoluteTimestamp;
    }

    public GetSystemsLastTimeStamp(): Fraction {
        const m: SourceMeasure = this.graphicalMeasures[this.graphicalMeasures.length - 1][0].parentSourceMeasure;
        return Fraction.plus(m.AbsoluteTimestamp, m.Duration);
    }

    /**
     * Create an InstrumentBracket for each multiStave Instrument.
     * @param instruments
     * @param staffHeight
     */
    public createInstrumentBrackets(instruments: Instrument[], staffHeight: number): void {
        for (let idx: number = 0, len: number = instruments.length; idx < len; ++idx) {
            const instrument: Instrument = instruments[idx];
            if (instrument.Staves.length > 1) {
                let firstStaffLine: StaffLine = undefined, lastStaffLine: StaffLine = undefined;
                for (let idx2: number = 0, len2: number = this.staffLines.length; idx2 < len2; ++idx2) {
                    const staffLine: StaffLine = this.staffLines[idx2];
                    if (staffLine.ParentStaff === instrument.Staves[0]) {
                        firstStaffLine = staffLine;
                    }
                    if (staffLine.ParentStaff === instrument.Staves[instrument.Staves.length - 1]) {
                        lastStaffLine = staffLine;
                    }
                }
                if (firstStaffLine && lastStaffLine) {
                    this.createInstrumentBracket(firstStaffLine, lastStaffLine);
                }
            }
        }
    }

    /**
     * Create a GroupBracket for an [[InstrumentalGroup]].
     * @param instrumentGroups
     * @param staffHeight
     * @param recursionDepth
     */
    public createGroupBrackets(instrumentGroups: InstrumentalGroup[], staffHeight: number, recursionDepth: number): void {
        for (let idx: number = 0, len: number = instrumentGroups.length; idx < len; ++idx) {
            const instrumentGroup: InstrumentalGroup = instrumentGroups[idx];
            if (instrumentGroup.InstrumentalGroups.length < 1) {
                continue;
            }
            const instrument1: Instrument = this.findFirstVisibleInstrumentInInstrumentalGroup(instrumentGroup);
            const instrument2: Instrument = this.findLastVisibleInstrumentInInstrumentalGroup(instrumentGroup);
            if (!instrument1 || !instrument2) {
                continue;
            }
            let firstStaffLine: StaffLine = undefined;
            let lastStaffLine: StaffLine = undefined;
            for (let idx2: number = 0, len2: number = this.staffLines.length; idx2 < len2; ++idx2) {
                const staffLine: StaffLine = this.staffLines[idx2];
                if (staffLine.ParentStaff === instrument1.Staves[0]) {
                    firstStaffLine = staffLine;
                }
                if (staffLine.ParentStaff === instrument2.Staves[0]) {
                    lastStaffLine = staffLine;
                }
            }
            if (firstStaffLine && lastStaffLine) {
                this.createGroupBracket(firstStaffLine, lastStaffLine, recursionDepth);
            }
            if (instrumentGroup.InstrumentalGroups.length < 1) {
                continue;
            }
            this.createGroupBrackets(instrumentGroup.InstrumentalGroups, staffHeight, recursionDepth + 1);
        }
    }

    /**
     * Create the Instrument's Labels (only for the first [[MusicSystem]] of the first MusicPage).
     * @param instrumentLabelTextHeight
     * @param systemLabelsRightMargin
     * @param labelMarginBorderFactor
     */
    public createMusicSystemLabel(  instrumentLabelTextHeight: number, systemLabelsRightMargin: number,
                                    labelMarginBorderFactor: number, isFirstSystem: boolean = false): void {

        const originalSystemLabelsRightMargin: number = systemLabelsRightMargin;
        for (let idx: number = 0, len: number = this.staffLines.length; idx < len; ++idx) {
            const instrument: Instrument = this.staffLines[idx].ParentStaff.ParentInstrument;
            let instrNameLabel: Label;
            if (isFirstSystem) {
                instrNameLabel = instrument.NameLabel;
                if (!this.rules.RenderPartNames || !instrNameLabel?.print) {
                    instrNameLabel = new Label("", instrument.NameLabel.textAlignment, instrument.NameLabel.font);
                    systemLabelsRightMargin = 0; // might affect lyricist/tempo placement. but without this there's still some extra x-spacing.
                }
            } else {
                if (!this.rules.RenderPartAbbreviations || !this.rules.RenderPartNames // don't render abbreviations if we don't render part names
                    || this.staffLines.length === 1 // don't render part abbreviations if there's only one instrument/part (could be an option in the future)
                    || !instrument.PartAbbreviation
                    || instrument.PartAbbreviation === "") {
                    return;
                }
                const labelText: string = instrument.PartAbbreviation;
                // const labelText: string = instrument.NameLabel.text[0] + ".";
                instrNameLabel = new Label(labelText, instrument.NameLabel.textAlignment, instrument.NameLabel.font);
            }
            if (instrument?.NameLabel?.print) {
                const graphicalLabel: GraphicalLabel = new GraphicalLabel(
                    instrNameLabel, instrumentLabelTextHeight, TextAlignmentEnum.LeftCenter, this.rules, this.boundingBox
                );
                graphicalLabel.setLabelPositionAndShapeBorders();
                this.labels.setValue(instrument, graphicalLabel);
                // X-Position will be 0 (Label starts at the same PointF_2D with MusicSystem)
                // Y-Position will be calculated after the y-Spacing
                // graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
            } else {
                systemLabelsRightMargin = 0;
            }
        }

        // calculate maxLabelLength (needed for X-Spacing)
        this.maxLabelLength = 0.0;
        const labels: GraphicalLabel[] = this.labels.values();
        for (let idx: number = 0, len: number = labels.length; idx < len; ++idx) {
            const label: GraphicalLabel = labels[idx];
            if (!label.Label.print) {
                continue;
            }
            if (label.PositionAndShape.Size.width > this.maxLabelLength) {
                this.maxLabelLength = label.PositionAndShape.Size.width;
                systemLabelsRightMargin = originalSystemLabelsRightMargin;
            }
        }
        this.updateMusicSystemStaffLineXPosition(systemLabelsRightMargin);
    }

    /**
     * Set the Y-Positions for the MusicSystem's Labels.
     */
    public setMusicSystemLabelsYPosition(): void {
        this.labels.forEach((key: Instrument, value: GraphicalLabel): void => {
            let ypositionSum: number = 0;
            let staffCounter: number = 0;
            for (let i: number = 0; i < this.staffLines.length; i++) {
                if (this.staffLines[i].ParentStaff.ParentInstrument === key) {
                    for (let j: number = i; j < this.staffLines.length; j++) {
                        const staffLine: StaffLine = this.staffLines[j];
                        if (staffLine.ParentStaff.ParentInstrument !== key) {
                            break;
                        }
                        ypositionSum += staffLine.PositionAndShape.RelativePosition.y;
                        staffCounter++;
                    }
                    break;
                }
            }
            if (staffCounter > 0) {
                value.PositionAndShape.RelativePosition = new PointF2D(0.0, ypositionSum / staffCounter + 2.0);
            }
        });
    }

    /**
     * Check if two "adjacent" StaffLines have BOTH a StaffEntry with a StaffEntryLink.
     * This is needed for the y-spacing algorithm.
     * @returns {boolean}
     */
    public checkStaffEntriesForStaffEntryLink(): boolean {
        let first: boolean = false;
        let second: boolean = false;
        for (let i: number = 0; i < this.staffLines.length - 1; i++) {
            for (let idx: number = 0, len: number = this.staffLines[i].Measures.length; idx < len; ++idx) {
                const measure: GraphicalMeasure = this.staffLines[i].Measures[idx];
                for (let idx2: number = 0, len2: number = measure.staffEntries.length; idx2 < len2; ++idx2) {
                    const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx2];
                    if (staffEntry.sourceStaffEntry.Link) {
                        first = true;
                    }
                }
            }
            for (let idx: number = 0, len: number = this.staffLines[i + 1].Measures.length; idx < len; ++idx) {
                const measure: GraphicalMeasure = this.staffLines[i + 1].Measures[idx];
                for (let idx2: number = 0, len2: number = measure.staffEntries.length; idx2 < len2; ++idx2) {
                    const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx2];
                    if (staffEntry.sourceStaffEntry.Link) {
                        second = true;
                    }
                }
            }
        }
        if (first && second) {
            return true;
        }
        return false;
    }

    public getBottomStaffLine(topStaffLine: StaffLine): StaffLine {
        const staves: Staff[] = topStaffLine.ParentStaff.ParentInstrument.Staves;
        const last: Staff = staves[staves.length - 1];
        for (const line of topStaffLine.ParentMusicSystem.staffLines) {
            if (line.ParentStaff === last) {
                return line;
            }
        }
        return undefined;
    }

    /**
     * Here the system line is generated, which acts as the container of graphical lines and dots that will be finally rendered.
     * It holds al the logical parameters of the system line.
     * @param xPosition The x position within the system
     * @param lineWidth The total x width
     * @param lineType The line type enum
     * @param linePosition indicates if the line belongs to start or end of measure
     * @param musicSystem
     * @param topMeasure
     * @param bottomMeasure
     */
    protected createSystemLine(xPosition: number, lineWidth: number, lineType: SystemLinesEnum, linePosition: SystemLinePosition,
                               musicSystem: MusicSystem, topMeasure: GraphicalMeasure, bottomMeasure: GraphicalMeasure = undefined): SystemLine {
        throw new Error("not implemented");
    }

    /**
     * Create all the graphical lines and dots needed to render a system line (e.g. bold-thin-dots..).
     * @param systemLine
     */
    protected createLinesForSystemLine(systemLine: SystemLine): void {
        //Empty
    }

    /**
     * Calculates the summed x-width of a possibly given Instrument Brace and/or Group Bracket(s).
     * @returns {number} the x-width
     */
    protected calcBracketsWidth(): number {
        let width: number = 0;
        for (let idx: number = 0, len: number = this.GroupBrackets.length; idx < len; ++idx) {
            const groupBracket: GraphicalObject = this.GroupBrackets[idx];
            width = Math.max(width, groupBracket.PositionAndShape.Size.width);
        }
        for (let idx2: number = 0, len2: number = this.InstrumentBrackets.length; idx2 < len2; ++idx2) {
            const instrumentBracket: GraphicalObject = this.InstrumentBrackets[idx2];
            width = Math.max(width, instrumentBracket.PositionAndShape.Size.width);
        }
        return width;
    }

    protected createInstrumentBracket(firstStaffLine: StaffLine, lastStaffLine: StaffLine): void {
        // no impl here
    }

    protected createGroupBracket(firstStaffLine: StaffLine, lastStaffLine: StaffLine, recursionDepth: number): void {
        // no impl here
    }

    private findFirstVisibleInstrumentInInstrumentalGroup(instrumentalGroup: InstrumentalGroup): Instrument {
        for (let idx: number = 0, len: number = instrumentalGroup.InstrumentalGroups.length; idx < len; ++idx) {
            const groupOrInstrument: InstrumentalGroup = instrumentalGroup.InstrumentalGroups[idx];
            if (groupOrInstrument instanceof Instrument) {
                if ((<Instrument>groupOrInstrument).Visible === true) {
                    return <Instrument>groupOrInstrument;
                }
                continue;
            }
            return this.findFirstVisibleInstrumentInInstrumentalGroup(groupOrInstrument);
        }
        return undefined;
    }

    private findLastVisibleInstrumentInInstrumentalGroup(instrumentalGroup: InstrumentalGroup): Instrument {
        let groupOrInstrument: InstrumentalGroup;
        for (let i: number = instrumentalGroup.InstrumentalGroups.length - 1; i >= 0; i--) {
            groupOrInstrument = instrumentalGroup.InstrumentalGroups[i];
            if (groupOrInstrument instanceof Instrument) {
                if ((<Instrument>groupOrInstrument).Visible === true) {
                    return <Instrument>groupOrInstrument;
                }
                continue;
            }
            return this.findLastVisibleInstrumentInInstrumentalGroup(groupOrInstrument);
        }
        return undefined;
    }

    /**
     * Update the xPosition of the [[MusicSystem]]'s [[StaffLine]]'s due to [[Label]] positioning.
     * @param systemLabelsRightMargin
     */
    private updateMusicSystemStaffLineXPosition(systemLabelsRightMargin: number): void {
        for (let idx: number = 0, len: number = this.StaffLines.length; idx < len; ++idx) {
            const staffLine: StaffLine = this.StaffLines[idx];
            const relative: PointF2D = staffLine.PositionAndShape.RelativePosition;
            relative.x = this.maxLabelLength + systemLabelsRightMargin;
            staffLine.PositionAndShape.RelativePosition = relative;
            staffLine.PositionAndShape.BorderRight = this.boundingBox.Size.width - this.maxLabelLength - systemLabelsRightMargin;
            for (let i: number = 0; i < staffLine.StaffLines.length; i++) {
                const lineEnd: PointF2D = new PointF2D(staffLine.PositionAndShape.Size.width, staffLine.StaffLines[i].End.y);
                staffLine.StaffLines[i].End = lineEnd;
            }
        }
    }
}