Explorar o código

Merge branch 'develop' of https://github.com/opensheetmusicdisplay/opensheetmusicdisplay into develop

# Conflicts:
#	src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts
Matthias Uiberacker %!s(int64=7) %!d(string=hai) anos
pai
achega
f117e8a46b

+ 1 - 1
demo/index.js

@@ -193,7 +193,7 @@ import { OSMD } from '../src/OSMD/OSMD';
             function() {
                 return onLoadingEnd(isCustom);
             }, function(e) {
-                error("Error rendering sheet: " + e);
+                error("Error rendering sheet: " + process.env.DEBUG ? e.stack : e);
                 onLoadingEnd(isCustom);
             }
         );

+ 20 - 1
external/vexflow/vexflow.d.ts

@@ -27,6 +27,16 @@ declare namespace Vex {
             public getW(): number;
 
             public getH(): number;
+
+            public draw(ctx: Vex.Flow.RenderContext) : void;            
+        }
+
+        export class Tickable {
+            public reset(): void;
+
+            public setStave(stave: Stave);
+
+            public getBoundingBox(): BoundingBox;
         }
 
         export class Voice {
@@ -34,6 +44,10 @@ declare namespace Vex {
 
             public static Mode: any;
 
+            public context: RenderContext;
+
+            public tickables: Tickable[];
+
             public getBoundingBox(): BoundingBox;
 
             public setStave(stave: Stave): Voice;
@@ -47,7 +61,7 @@ declare namespace Vex {
             public draw(ctx: any, stave: Stave): void;
         }
 
-        export class StaveNote {
+        export class StaveNote extends Tickable{
             constructor(note_struct: any);
 
             public getNoteHeadBounds(): any;
@@ -56,6 +70,8 @@ declare namespace Vex {
 
             public getNoteHeadEndX(): number;
 
+            public getGlyphWidth(): number;
+
             public addAccidental(index: number, accidental: Accidental): StaveNote;
 
             public addAnnotation(index: number, annotation: Annotation): StaveNote;
@@ -99,6 +115,8 @@ declare namespace Vex {
             public setWidth(width: number): Stave;
 
             public getNoteStartX(): number;
+            
+            public getModifierXShift(): number;
 
             public getNoteEndX(): number;
 
@@ -115,6 +133,7 @@ declare namespace Vex {
             public getLineForY(y: number): number;
 
             public getModifiers(pos: any, cat: any): Clef[]; // FIXME
+            
             public setContext(ctx: RenderContext): Stave;
 
             public addModifier(mod: any, pos: any): void;

+ 1 - 0
src/MusicalScore/Graphical/GraphicalLyricEntry.ts

@@ -27,6 +27,7 @@ export class GraphicalLyricEntry {
         this.graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(0.0, staffHeight);
     }
 
+    // FIXME: This should actually be called LyricsEntry or be a function
     public get GetLyricsEntry(): LyricsEntry {
         return this.lyricsEntry;
     }

+ 1 - 0
src/MusicalScore/Graphical/GraphicalLyricWord.ts

@@ -35,6 +35,7 @@ export class GraphicalLyricWord {
     }
 
     private initialize(): void {
+        // FIXME: This is actually not needed in Javascript as we have dynamic memory allication?
         for (let i: number = 0; i < this.lyricWord.Syllables.length; i++) {
             this.graphicalLyricsEntries.push(undefined);
         }

+ 18 - 0
src/MusicalScore/Graphical/GraphicalStaffEntry.ts

@@ -69,6 +69,10 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
         return this.lyricsEntries;
     }
 
+    public set LyricsEntries(value: GraphicalLyricEntry[]) {
+        this.lyricsEntries = value;
+    }
+
     /**
      * Calculate the absolute Timestamp.
      * @returns {Fraction}
@@ -349,4 +353,18 @@ export abstract class GraphicalStaffEntry extends GraphicalObject {
             }
         }
     }
+
+    // FIXME: implement
+    public hasOnlyRests(): boolean {
+        const hasOnlyRests: boolean = true;
+        for (const graphicalNotes of this.notes) {
+            for (const graphicalNote of graphicalNotes) {
+                const note: Note = graphicalNote.sourceNote;
+                if (!note.isRest()) {
+                    return false;
+                }
+            }
+        }
+        return hasOnlyRests;
+    }
 }

+ 408 - 13
src/MusicalScore/Graphical/MusicSheetCalculator.ts

@@ -14,7 +14,6 @@ import {GraphicalMusicPage} from "./GraphicalMusicPage";
 import {GraphicalNote} from "./GraphicalNote";
 import {Beam} from "../VoiceData/Beam";
 import {OctaveEnum} from "../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
-import {LyricsEntry} from "../VoiceData/Lyrics/LyricsEntry";
 import {VoiceEntry} from "../VoiceData/VoiceEntry";
 import {OrnamentContainer} from "../VoiceData/OrnamentContainer";
 import {ArticulationEnum} from "../VoiceData/VoiceEntry";
@@ -51,6 +50,10 @@ import {OctaveShift} from "../VoiceData/Expressions/ContinuousExpressions/Octave
 import {Logging} from "../../Common/Logging";
 import Dictionary from "typescript-collections/dist/lib/Dictionary";
 import {CollectionUtil} from "../../Util/CollectionUtil";
+import {GraphicalLyricEntry} from "./GraphicalLyricEntry";
+import {GraphicalLyricWord} from "./GraphicalLyricWord";
+import {GraphicalLine} from "./GraphicalLine";
+import {Label} from "../Label";
 
 /**
  * Class used to do all the calculations in a MusicSheet, which in the end populates a GraphicalMusicSheet.
@@ -65,6 +68,8 @@ export abstract class MusicSheetCalculator {
     protected staffLinesWithLyricWords: StaffLine[] = [];
     protected staffLinesWithGraphicalExpressions: StaffLine[] = [];
 
+    protected graphicalLyricWords: GraphicalLyricWord[] = [];
+
     protected graphicalMusicSheet: GraphicalMusicSheet;
     protected rules: EngravingRules;
     protected symbolFactory: IGraphicalSymbolFactory;
@@ -194,6 +199,11 @@ export abstract class MusicSheetCalculator {
         // create new MusicSystems and StaffLines (as many as necessary) and populate them with Measures from measureList
         this.calculateMusicSystems();
 
+        this.formatMeasures();
+
+        // calculate all LyricWords Positions
+        this.calculateLyricsPosition();
+
         // Add some white space at the end of the piece:
         this.graphicalMusicSheet.MusicPages[0].PositionAndShape.BorderMarginBottom += 9;
 
@@ -222,6 +232,9 @@ export abstract class MusicSheetCalculator {
         this.graphicalMusicSheet.MinAllowedSystemWidth = minLength;
     }
 
+    protected formatMeasures(): void {
+        throw new Error("abstract, not implemented");
+    }
     /**
      * Calculates the x layout of the staff entries within the staff measures belonging to one source measure.
      * All staff entries are x-aligned throughout all the measures.
@@ -263,7 +276,7 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
-    protected handleVoiceEntryLyrics(lyricsEntries: Dictionary<number, LyricsEntry>, voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry,
+    protected handleVoiceEntryLyrics(voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry,
                                      openLyricWords: LyricWord[]): void {
         throw new Error("abstract, not implemented");
     }
@@ -318,13 +331,142 @@ export abstract class MusicSheetCalculator {
         throw new Error("abstract, not implemented");
     }
 
+    // FIXME: There are several HACKS in this function to make multiline lyrics work without the skyline.
+    // These need to be reverted once the skyline is available
     /**
      * Calculate the Lyrics YPositions for a single [[StaffLine]].
      * @param staffLine
      * @param lyricVersesNumber
      */
-    protected calculateSingleStaffLineLyricsPosition(staffLine: StaffLine, lyricVersesNumber: number[]): void {
-        throw new Error("abstract, not implemented");
+    protected calculateSingleStaffLineLyricsPosition(staffLine: StaffLine, lyricVersesNumber: number[]): GraphicalStaffEntry[] {
+        let numberOfVerses: number = 0;
+        // FIXME: There is no class SkyBottomLineCalculator -> Fix value
+        let lyricsStartYPosition: number = this.rules.StaffHeight + 6.0; // Add offset to prevent collision
+        const lyricsStaffEntriesList: GraphicalStaffEntry[] = [];
+        // const skyBottomLineCalculator: SkyBottomLineCalculator = new SkyBottomLineCalculator(this.rules);
+
+        // first find maximum Ycoordinate for the whole StaffLine
+        let len: number = staffLine.Measures.length;
+        for (let idx: number = 0; idx < len; ++idx) {
+            const measure: StaffMeasure = staffLine.Measures[idx];
+            const measureRelativePosition: PointF2D = measure.PositionAndShape.RelativePosition;
+            const len2: number = measure.staffEntries.length;
+            for (let idx2: number = 0; idx2 < len2; ++idx2) {
+                const staffEntry: GraphicalStaffEntry = measure.staffEntries[idx2];
+                if (staffEntry.LyricsEntries.length > 0) {
+                    lyricsStaffEntriesList.push(staffEntry);
+                    numberOfVerses = Math.max(numberOfVerses, staffEntry.LyricsEntries.length);
+
+                    // Position of Staffentry relative to StaffLine
+                    const staffEntryPositionX: number = staffEntry.PositionAndShape.RelativePosition.x +
+                                                        measureRelativePosition.x;
+
+                    let minMarginLeft: number = Number.MAX_VALUE;
+                    let maxMarginRight: number = Number.MAX_VALUE;
+
+                    // if more than one LyricEntry in StaffEntry, find minMarginLeft, maxMarginRight of all corresponding Labels
+                    for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
+                        const lyricsEntryLabel: GraphicalLabel = staffEntry.LyricsEntries[i].GraphicalLabel;
+                        minMarginLeft = Math.min(minMarginLeft, staffEntryPositionX + lyricsEntryLabel.PositionAndShape.BorderMarginLeft);
+                        maxMarginRight = Math.max(maxMarginRight, staffEntryPositionX + lyricsEntryLabel.PositionAndShape.BorderMarginRight);
+                    }
+
+                    // check BottomLine in this range and take the maximum between the two values
+                    // FIXME: There is no class SkyBottomLineCalculator -> Fix value
+                    // float bottomLineMax = skyBottomLineCalculator.getBottomLineMaxInRange(staffLine, minMarginLeft, maxMarginRight);
+                    const bottomLineMax: number = 0.0;
+                    lyricsStartYPosition = Math.max(lyricsStartYPosition, bottomLineMax);
+                }
+            }
+        }
+
+        let maxPosition: number = 4.0;
+        // iterate again through the Staffentries with LyricEntries
+        len = lyricsStaffEntriesList.length;
+        for (let idx: number = 0; idx < len; ++idx) {
+            const staffEntry: GraphicalStaffEntry = lyricsStaffEntriesList[idx];
+            // set LyricEntryLabel RelativePosition
+            for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
+                const lyricEntry: GraphicalLyricEntry = staffEntry.LyricsEntries[i];
+                const lyricsEntryLabel: GraphicalLabel = lyricEntry.GraphicalLabel;
+
+                // read the verseNumber and get index of this number in the sorted LyricVerseNumbersList of Instrument
+                // eg verseNumbers: 2,3,4,6 => 1,2,3,4
+                const verseNumber: number = lyricEntry.GetLyricsEntry.VerseNumber;
+                const sortedLyricVerseNumberIndex: number = lyricVersesNumber.indexOf(verseNumber);
+                const firstPosition: number = lyricsStartYPosition + this.rules.LyricsHeight;
+
+                // Y-position calculated according to aforementioned mapping
+                let position: number = firstPosition + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
+                if (this.leadSheet) {
+                    position = 3.4 + (this.rules.VerticalBetweenLyricsDistance + this.rules.LyricsHeight) * (sortedLyricVerseNumberIndex);
+                }
+                lyricsEntryLabel.PositionAndShape.RelativePosition = new PointF2D(0, position);
+                maxPosition = Math.max(maxPosition, position);
+            }
+        }
+
+        // update BottomLine (on the whole StaffLine's length)
+        if (lyricsStaffEntriesList.length > 0) {
+            /**
+             * HACK START
+             */
+            let additionalPageLength: number = 0;
+            maxPosition -= this.rules.StaffHeight;
+            let iterator: StaffLine = staffLine.NextStaffLine;
+            let systemMaxCount: number = 0;
+            while (iterator !== undefined) {
+                iterator.PositionAndShape.RelativePosition.y += maxPosition;
+                iterator = iterator.NextStaffLine;
+                systemMaxCount += maxPosition;
+                additionalPageLength += maxPosition;
+            }
+
+            systemMaxCount -= this.rules.BetweenStaffDistance;
+            let systemIterator: MusicSystem = staffLine.ParentMusicSystem.NextSystem;
+            while (systemIterator !== undefined) {
+                systemIterator.PositionAndShape.RelativePosition.y += systemMaxCount;
+                systemIterator = systemIterator.NextSystem;
+                additionalPageLength += systemMaxCount;
+            }
+            staffLine.ParentMusicSystem.Parent.PositionAndShape.BorderBottom += additionalPageLength;
+            // Update the instrument labels
+            staffLine.ParentMusicSystem.setMusicSystemLabelsYPosition();
+            /**
+             * HACK END
+             */
+            // const endX: number = staffLine.PositionAndShape.Size.width;
+            // const startX: number = lyricsStaffEntriesList[0].PositionAndShape.RelativePosition.x +
+            // lyricsStaffEntriesList[0].PositionAndShape.BorderMarginLeft +
+            // lyricsStaffEntriesList[0].parentMeasure.PositionAndShape.RelativePosition.x;
+            // FIXME: There is no class SkyBottomLineCalculator. This call should update the positions according to the last run
+            // skyBottomLineCalculator.updateBottomLineInRange(staffLine, startX, endX, maxPosition);
+        }
+        return lyricsStaffEntriesList;
+    }
+
+    /**
+     * calculates the dashes of lyric words and the extending underscore lines of syllables sung on more than one note.
+     * @param lyricsStaffEntries
+     */
+    protected calculateLyricsExtendsAndDashes(lyricsStaffEntries: GraphicalStaffEntry[]): void {
+        // iterate again to create now the extend lines and dashes for words
+        for (let idx: number = 0, len: number = lyricsStaffEntries.length; idx < len; ++idx) {
+            const staffEntry: GraphicalStaffEntry = lyricsStaffEntries[idx];
+            // set LyricEntryLabel RelativePosition
+            for (let i: number = 0; i < staffEntry.LyricsEntries.length; i++) {
+                const lyricEntry: GraphicalLyricEntry = staffEntry.LyricsEntries[i];
+                // calculate LyricWord's Dashes and underscoreLine
+                if (lyricEntry.ParentLyricWord !== undefined &&
+                    lyricEntry.ParentLyricWord.GraphicalLyricsEntries[lyricEntry.ParentLyricWord.GraphicalLyricsEntries.length - 1] !== lyricEntry) {
+                    this.calculateSingleLyricWord(lyricEntry);
+                }
+                // calculate the underscore line extend if needed
+                if (lyricEntry.GetLyricsEntry.extend) {
+                    this.calculateLyricExtend(lyricEntry);
+                }
+            }
+        }
     }
 
     /**
@@ -485,8 +627,7 @@ export abstract class MusicSheetCalculator {
         if (!this.leadSheet) {
             this.calculateTempoExpressions();
         }
-        // calculate all LyricWords Positions
-        this.calculateLyricsPosition();
+
         // update all StaffLine's Borders
         // create temporary Object, just to call the methods (in order to avoid declaring them static)
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
@@ -736,7 +877,7 @@ export abstract class MusicSheetCalculator {
             this.checkVoiceEntriesForTechnicalInstructions(voiceEntry, graphicalStaffEntry);
         }
         if (voiceEntry.LyricsEntries.size() > 0) {
-            this.handleVoiceEntryLyrics(voiceEntry.LyricsEntries, voiceEntry, graphicalStaffEntry, openLyricWords);
+            this.handleVoiceEntryLyrics(voiceEntry, graphicalStaffEntry, openLyricWords);
         }
         if (voiceEntry.OrnamentContainer !== undefined) {
             this.handleVoiceEntryOrnaments(voiceEntry.OrnamentContainer, voiceEntry, graphicalStaffEntry);
@@ -967,8 +1108,8 @@ export abstract class MusicSheetCalculator {
         let rightStaffEntry: GraphicalStaffEntry = undefined;
         const numEntries: number = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
         const index: number = this.graphicalMusicSheet.GetInterpolatedIndexInVerticalContainers(timestamp);
-        const leftIndex: number = <number>Math.min(Math.floor(index), numEntries - 1);
-        const rightIndex: number = <number>Math.min(Math.ceil(index), numEntries - 1);
+        const leftIndex: number = Math.min(Math.floor(index), numEntries - 1);
+        const rightIndex: number = Math.min(Math.ceil(index), numEntries - 1);
         if (leftIndex < 0 || verticalIndex < 0) {
             return relative;
         }
@@ -1006,7 +1147,7 @@ export abstract class MusicSheetCalculator {
     protected getRelativeXPositionFromTimestamp(timestamp: Fraction): number {
         const numEntries: number = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
         const index: number = this.graphicalMusicSheet.GetInterpolatedIndexInVerticalContainers(timestamp);
-        const discreteIndex: number = <number>Math.max(0, Math.min(Math.round(index), numEntries - 1));
+        const discreteIndex: number = Math.max(0, Math.min(Math.round(index), numEntries - 1));
         const gse: GraphicalStaffEntry = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[discreteIndex].getFirstNonNullStaffEntry();
         const posX: number = gse.PositionAndShape.RelativePosition.x + gse.parentMeasure.PositionAndShape.RelativePosition.x;
         return posX;
@@ -1159,8 +1300,6 @@ export abstract class MusicSheetCalculator {
                     if (verticalContainer !== undefined) {
                         verticalContainer.StaffEntries[j] = graphicalStaffEntry;
                         graphicalStaffEntry.parentVerticalContainer = verticalContainer;
-                    } else {
-                        // TODO ?
                     }
                 }
             }
@@ -1618,24 +1757,280 @@ export abstract class MusicSheetCalculator {
     //}
 
     private calculateLyricsPosition(): void {
+        const lyricStaffEntriesDict: Dictionary<StaffLine, GraphicalStaffEntry[]> = new Dictionary<StaffLine, GraphicalStaffEntry[]>();
+        // sort the lyriceVerseNumbers for every Instrument that has Lyrics
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.ParentMusicSheet.Instruments.length; idx < len; ++idx) {
             const instrument: Instrument = this.graphicalMusicSheet.ParentMusicSheet.Instruments[idx];
             if (instrument.HasLyrics && instrument.LyricVersesNumbers.length > 0) {
                 instrument.LyricVersesNumbers.sort();
             }
         }
+        // first calc lyrics text positions
+        for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
+            const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
+            for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
+                const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
+                for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
+                    const staffLine: StaffLine = musicSystem.StaffLines[idx3];
+                    const lyricsStaffEntries: GraphicalStaffEntry[] =
+                        this.calculateSingleStaffLineLyricsPosition(staffLine, staffLine.ParentStaff.ParentInstrument.LyricVersesNumbers);
+                    lyricStaffEntriesDict.setValue(staffLine, lyricsStaffEntries);
+                    this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
+                }
+            }
+        }
+        // the fill in the lyric word dashes and lyrics extends/underscores
         for (let idx: number = 0, len: number = this.graphicalMusicSheet.MusicPages.length; idx < len; ++idx) {
             const graphicalMusicPage: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[idx];
             for (let idx2: number = 0, len2: number = graphicalMusicPage.MusicSystems.length; idx2 < len2; ++idx2) {
                 const musicSystem: MusicSystem = graphicalMusicPage.MusicSystems[idx2];
                 for (let idx3: number = 0, len3: number = musicSystem.StaffLines.length; idx3 < len3; ++idx3) {
                     const staffLine: StaffLine = musicSystem.StaffLines[idx3];
-                    this.calculateSingleStaffLineLyricsPosition(staffLine, staffLine.ParentStaff.ParentInstrument.LyricVersesNumbers);
+                    this.calculateLyricsExtendsAndDashes(lyricStaffEntriesDict.getValue(staffLine));
                 }
             }
         }
     }
 
+    /**
+     * This method calculates the dashes within the syllables of a LyricWord
+     * @param lyricEntry
+     */
+    private calculateSingleLyricWord(lyricEntry: GraphicalLyricEntry): void {
+        // const skyBottomLineCalculator: SkyBottomLineCalculator = new SkyBottomLineCalculator (this.rules);
+        const graphicalLyricWord: GraphicalLyricWord = lyricEntry.ParentLyricWord;
+        const index: number = graphicalLyricWord.GraphicalLyricsEntries.indexOf(lyricEntry);
+        let nextLyricEntry: GraphicalLyricEntry = undefined;
+        if (index >= 0) {
+            nextLyricEntry = graphicalLyricWord.GraphicalLyricsEntries[index + 1];
+        }
+        if (nextLyricEntry === undefined) {
+            return;
+        }
+        const startStaffLine: StaffLine = <StaffLine>lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine;
+        const nextStaffLine: StaffLine = <StaffLine>nextLyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine;
+        const startStaffEntry: GraphicalStaffEntry = lyricEntry.StaffEntryParent;
+        const endStaffentry: GraphicalStaffEntry = nextLyricEntry.StaffEntryParent;
+
+        // if on the same StaffLine
+        if (lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine === nextLyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine) {
+            // start- and End margins from the text Labels
+            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+            startStaffEntry.PositionAndShape.RelativePosition.x +
+            lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
+            const endX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
+            endStaffentry.PositionAndShape.RelativePosition.x +
+            nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
+            const y: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
+            let numberOfDashes: number = 1;
+            if ((endX - startX) > this.rules.BetweenSyllabelMaximumDistance) {
+                numberOfDashes = Math.ceil((endX - startX) / this.rules.BetweenSyllabelMaximumDistance);
+            }
+            // check distance and create the adequate number of Dashes
+            if (numberOfDashes === 1) {
+                // distance between the two GraphicalLyricEntries is big for only one Dash, position in the middle
+                this.calculateSingleDashForLyricWord(startStaffLine, startX, endX, y);
+            } else {
+                // distance is big enough for more Dashes
+                // calculate the adequate number of Dashes from the distance between the two LyricEntries
+                // distance between the Dashes should be equal
+                this.calculateDashes(startStaffLine, startX, endX, y);
+            }
+        } else {
+            // start and end on different StaffLines
+            // start margin from the text Label until the End of StaffLine
+            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+                startStaffEntry.PositionAndShape.RelativePosition.x +
+                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
+            const lastStaffMeasure: StaffMeasure = startStaffLine.Measures[startStaffLine.Measures.length - 1];
+            const endX: number = lastStaffMeasure.PositionAndShape.RelativePosition.x + lastStaffMeasure.PositionAndShape.Size.width;
+            let y: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
+
+            // calculate Dashes for the first StaffLine
+            this.calculateDashes(startStaffLine, startX, endX, y);
+
+            // calculate Dashes for the second StaffLine (only if endStaffEntry isn't the first StaffEntry of the StaffLine)
+            if (!(endStaffentry === endStaffentry.parentMeasure.staffEntries[0] &&
+                    endStaffentry.parentMeasure === endStaffentry.parentMeasure.ParentStaffLine.Measures[0])) {
+                const secondStartX: number = nextStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
+                const secondEndX: number = endStaffentry.parentMeasure.PositionAndShape.RelativePosition.x +
+                    endStaffentry.PositionAndShape.RelativePosition.x +
+                    nextLyricEntry.GraphicalLabel.PositionAndShape.BorderMarginLeft;
+                y = nextLyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
+                this.calculateDashes(nextStaffLine, secondStartX, secondEndX, y);
+            }
+        }
+    }
+
+    /**
+     * This method calculates Dashes for a LyricWord.
+     * @param staffLine
+     * @param startX
+     * @param endX
+     * @param y
+     */
+    private calculateDashes(staffLine: StaffLine, startX: number, endX: number, y: number): void {
+        let distance: number = endX - startX;
+        if (distance < this.rules.MinimumDistanceBetweenDashes) {
+            this.calculateSingleDashForLyricWord(staffLine, startX, endX, y);
+        } else {
+            // enough distance for more Dashes
+            const numberOfDashes: number = Math.floor(distance / this.rules.MinimumDistanceBetweenDashes);
+            const distanceBetweenDashes: number = distance / this.rules.MinimumDistanceBetweenDashes;
+            let counter: number = 0;
+
+            startX += distanceBetweenDashes / 2;
+            endX -= distanceBetweenDashes / 2;
+            while (counter <= Math.floor(numberOfDashes / 2.0) && endX > startX) {
+                distance = this.calculateRightAndLeftDashesForLyricWord(staffLine, startX, endX, y);
+                startX += distanceBetweenDashes;
+                endX -= distanceBetweenDashes;
+                counter++;
+            }
+
+            // if the remaining distance isn't big enough for two Dashes (another check would be if numberOfDashes is uneven),
+            // then put the last Dash in the middle of the remaining distance
+            if (distance > distanceBetweenDashes) {
+                this.calculateSingleDashForLyricWord(staffLine, startX, endX, y);
+            }
+        }
+    }
+
+    /**
+     * This method calculates a single Dash for a LyricWord, positioned in the middle of the given distance.
+     * @param {StaffLine} staffLine
+     * @param {number} startX
+     * @param {number} endX
+     * @param {number} y
+     */
+    private calculateSingleDashForLyricWord(staffLine: StaffLine, startX: number, endX: number, y: number): void {
+        const dash: GraphicalLabel = new GraphicalLabel(new Label("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
+        dash.setLabelPositionAndShapeBorders();
+        staffLine.LyricsDashes.push(dash);
+        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
+            this.staffLinesWithLyricWords.push(staffLine);
+        }
+        dash.PositionAndShape.Parent = staffLine.PositionAndShape;
+        const relative: PointF2D = new PointF2D(startX + (endX - startX) / 2, y);
+        dash.PositionAndShape.RelativePosition = relative;
+    }
+
+    /**
+     * Layouts the underscore line when a lyric entry is marked as extend
+     * @param {GraphicalLyricEntry} lyricEntry
+     */
+    private calculateLyricExtend(lyricEntry: GraphicalLyricEntry): void {
+        let startY: number = lyricEntry.GraphicalLabel.PositionAndShape.RelativePosition.y;
+        const startStaffEntry: GraphicalStaffEntry = lyricEntry.StaffEntryParent;
+        const startStaffLine: StaffLine = <StaffLine>lyricEntry.StaffEntryParent.parentMeasure.ParentStaffLine;
+
+        // find endstaffEntry and staffLine
+        let endStaffEntry: GraphicalStaffEntry = undefined;
+        let endStaffLine: StaffLine = undefined;
+        const staffIndex: number = startStaffEntry.parentMeasure.ParentStaff.idInMusicSheet;
+        for (let index: number = startStaffEntry.parentVerticalContainer.Index + 1;
+             index < this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers.length;
+             ++index) {
+            const gse: GraphicalStaffEntry = this.graphicalMusicSheet.VerticalGraphicalStaffEntryContainers[index].StaffEntries[staffIndex];
+            if (gse === undefined) {
+                continue;
+            }
+            if (gse.hasOnlyRests()) {
+                break;
+            }
+            if (gse.LyricsEntries.length > 0) {
+                break;
+            }
+            endStaffEntry = gse;
+            endStaffLine = <StaffLine>endStaffEntry.parentMeasure.ParentStaffLine;
+        }
+        if (endStaffEntry === undefined) {
+            return;
+        }
+        // if on the same StaffLine
+        if (startStaffLine === endStaffLine) {
+            // start- and End margins from the text Labels
+            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+                startStaffEntry.PositionAndShape.RelativePosition.x +
+                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
+            const endX: number = endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+                endStaffEntry.PositionAndShape.RelativePosition.x +
+                endStaffEntry.PositionAndShape.BorderMarginRight;
+            // needed in order to line up with the Label's text bottom line (is the y psoition of the underscore)
+            startY -= lyricEntry.GraphicalLabel.PositionAndShape.Size.height / 4;
+            // create a Line (as underscope after the LyricLabel's End)
+            this.calculateSingleLyricWordWithUnderscore(startStaffLine, startX, endX, startY);
+        } else { // start and end on different StaffLines
+            // start margin from the text Label until the End of StaffLine
+            const lastMeasureBb: BoundingBox = startStaffLine.Measures[startStaffLine.Measures.length - 1].PositionAndShape;
+            const startX: number = startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+                startStaffEntry.PositionAndShape.RelativePosition.x +
+                lyricEntry.GraphicalLabel.PositionAndShape.BorderMarginRight;
+            const endX: number = lastMeasureBb.RelativePosition.x +
+                lastMeasureBb.Size.width;
+            // needed in order to line up with the Label's text bottom line
+            startY -= lyricEntry.GraphicalLabel.PositionAndShape.Size.height / 4;
+            // first Underscore until the StaffLine's End
+            this.calculateSingleLyricWordWithUnderscore(startStaffLine, startX, endX, startY);
+            if (endStaffEntry === undefined) {
+                return;
+            }
+            // second Underscore in the endStaffLine until endStaffEntry (if endStaffEntry isn't the first StaffEntry of the StaffLine))
+            if (!(endStaffEntry === endStaffEntry.parentMeasure.staffEntries[0] &&
+                    endStaffEntry.parentMeasure === endStaffEntry.parentMeasure.ParentStaffLine.Measures[0])) {
+                const secondStartX: number = endStaffLine.Measures[0].staffEntries[0].PositionAndShape.RelativePosition.x;
+                const secondEndX: number = endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x +
+                    endStaffEntry.PositionAndShape.RelativePosition.x +
+                    endStaffEntry.PositionAndShape.BorderMarginRight;
+                this.calculateSingleLyricWordWithUnderscore(endStaffLine, secondStartX, secondEndX, startY);
+            }
+        }
+    }
+
+    /**
+     * This method calculates a single underscoreLine.
+     * @param staffLine
+     * @param startX
+     * @param end
+     * @param y
+     */
+    private calculateSingleLyricWordWithUnderscore(staffLine: StaffLine, startX: number, endX: number, y: number): void {
+        const lineStart: PointF2D = new PointF2D(startX, y);
+        const lineEnd: PointF2D = new PointF2D(endX, y);
+        const graphicalLine: GraphicalLine = new GraphicalLine(lineStart, lineEnd, this.rules.LyricUnderscoreLineWidth);
+        staffLine.LyricLines.push(graphicalLine);
+        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
+            this.staffLinesWithLyricWords.push(staffLine);
+        }
+    }
+
+    /**
+     * This method calculates two Dashes for a LyricWord, positioned at the the two ends of the given distance.
+     * @param {StaffLine} staffLine
+     * @param {number} startX
+     * @param {number} endX
+     * @param {number} y
+     * @returns {number}
+     */
+    private calculateRightAndLeftDashesForLyricWord (staffLine: StaffLine, startX: number, endX: number, y: number): number {
+        const leftDash: GraphicalLabel = new GraphicalLabel (new Label ("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
+        leftDash.setLabelPositionAndShapeBorders();
+        staffLine.LyricsDashes.push(leftDash);
+        if (this.staffLinesWithLyricWords.indexOf(staffLine) === -1) {
+            this.staffLinesWithLyricWords.push(staffLine);
+        }
+        leftDash.PositionAndShape.Parent = staffLine.PositionAndShape;
+        const leftDashRelative: PointF2D = new PointF2D (startX, y);
+        leftDash.PositionAndShape.RelativePosition = leftDashRelative;
+        const rightDash: GraphicalLabel = new GraphicalLabel (new Label ("-"), this.rules.LyricsHeight, TextAlignment.CenterBottom);
+        rightDash.setLabelPositionAndShapeBorders();
+        staffLine.LyricsDashes.push(rightDash);
+        rightDash.PositionAndShape.Parent = staffLine.PositionAndShape;
+        const rightDashRelative: PointF2D = new PointF2D (endX, y);
+        rightDash.PositionAndShape.RelativePosition = rightDashRelative;
+        return (rightDash.PositionAndShape.RelativePosition.x - leftDash.PositionAndShape.RelativePosition.x);
+    }
+
     private calculateDynamicExpressions(): void {
         for (let i: number = 0; i < this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures.length; i++) {
             const sourceMeasure: SourceMeasure = this.graphicalMusicSheet.ParentMusicSheet.SourceMeasures[i];

+ 13 - 0
src/MusicalScore/Graphical/MusicSheetDrawer.ts

@@ -334,6 +334,19 @@ export abstract class MusicSheetDrawer {
         for (const measure of staffLine.Measures) {
             this.drawMeasure(measure);
         }
+
+        if (staffLine.LyricsDashes.length > 0) {
+            this.drawDashes(staffLine.LyricsDashes);
+        }
+    }
+
+    /**
+     * Draw all dashes to the canvas
+     * @param lyricsDashes Array of lyric dashes to be drawn
+     * @param layer Number of the layer that the lyrics should be drawn in
+     */
+    protected drawDashes(lyricsDashes: GraphicalLabel[]): void {
+        lyricsDashes.forEach(dash => this.drawLabel(dash, <number>GraphicalLayers.Notes));
     }
 
     // protected drawSlur(slur: GraphicalSlur, abs: PointF2D): void {

+ 6 - 3
src/MusicalScore/Graphical/MusicSystem.ts

@@ -57,6 +57,11 @@ export abstract class MusicSystem extends GraphicalObject {
         this.parent = value;
     }
 
+    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;
     }
@@ -274,11 +279,9 @@ export abstract class MusicSystem extends GraphicalObject {
                 );
                 graphicalLabel.setLabelPositionAndShapeBorders();
                 this.labels.setValue(graphicalLabel, instrument);
-                //graphicalLabel.PositionAndShape.Parent = this.PositionAndShape;
-
                 // 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);
+                // graphicalLabel.PositionAndShape.RelativePosition = new PointF2D(0.0, 0.0);
             }
 
             // calculate maxLabelLength (needed for X-Spacing)

+ 24 - 0
src/MusicalScore/Graphical/StaffLine.ts

@@ -8,6 +8,7 @@ import {StaffMeasure} from "./StaffMeasure";
 import {MusicSystem} from "./MusicSystem";
 import {StaffLineActivitySymbol} from "./StaffLineActivitySymbol";
 import {PointF2D} from "../../Common/DataObjects/PointF2D";
+import {GraphicalLabel} from "./GraphicalLabel";
 
 /**
  * A StaffLine contains the [[Measure]]s in one line of the music sheet
@@ -20,6 +21,8 @@ export abstract class StaffLine extends GraphicalObject {
     protected parentStaff: Staff;
     protected skyLine: number[];
     protected bottomLine: number[];
+    protected lyricLines: GraphicalLine[] = [];
+    protected lyricsDashes: GraphicalLabel[] = [];
 
     constructor(parentSystem: MusicSystem, parentStaff: Staff) {
         super();
@@ -44,6 +47,27 @@ export abstract class StaffLine extends GraphicalObject {
         this.staffLines = value;
     }
 
+    public get NextStaffLine(): StaffLine {
+        const idxInParent: number = this.parentMusicSystem.StaffLines.indexOf(this);
+        return idxInParent !== this.parentMusicSystem.StaffLines.length ? this.parentMusicSystem.StaffLines[idxInParent + 1] : undefined;
+    }
+
+    public get LyricLines(): GraphicalLine[] {
+        return this.lyricLines;
+    }
+
+    public set LyricLines(value: GraphicalLine[]) {
+        this.lyricLines = value;
+    }
+
+    public get LyricsDashes(): GraphicalLabel[] {
+        return this.lyricsDashes;
+    }
+
+    public set LyricsDashes(value: GraphicalLabel[]) {
+        this.lyricsDashes = value;
+    }
+
     public get ParentMusicSystem(): MusicSystem {
         return this.parentMusicSystem;
     }

+ 11 - 29
src/MusicalScore/Graphical/VexFlow/VexFlowMeasure.ts

@@ -273,15 +273,7 @@ export class VexFlowMeasure extends StaffMeasure {
      * @param ctx
      */
     public draw(ctx: Vex.Flow.RenderContext): void {
-        // If this is the first stave in the vertical measure, call the format
-        // method to set the width of all the voices
-        if (this.formatVoices) {
-            // The width of the voices does not include the instructions (StaveModifiers)
-            this.formatVoices((this.PositionAndShape.BorderRight - this.beginInstructionsWidth - this.endInstructionsWidth) * unitInPixels);
-        }
 
-        // Force the width of the Begin Instructions
-        this.stave.setNoteStartX(this.stave.getX() + unitInPixels * this.beginInstructionsWidth);
         // Draw stave lines
         this.stave.setContext(ctx).draw();
         // Draw all voices
@@ -317,9 +309,18 @@ export class VexFlowMeasure extends StaffMeasure {
         for (const connector of this.connectors) {
             connector.setContext(ctx).draw();
         }
+    }
 
-        // now we can finally set the vexflow x positions back into the osmd object model:
-        this.setStaffEntriesXPositions();
+    public format(): void {
+        // If this is the first stave in the vertical measure, call the format
+        // method to set the width of all the voices
+        if (this.formatVoices) {
+            // The width of the voices does not include the instructions (StaveModifiers)
+            this.formatVoices((this.PositionAndShape.BorderRight - this.beginInstructionsWidth - this.endInstructionsWidth) * unitInPixels);
+        }
+
+        // Force the width of the Begin Instructions
+        this.stave.setNoteStartX(this.stave.getX() + unitInPixels * this.beginInstructionsWidth);
     }
 
     /**
@@ -525,23 +526,4 @@ export class VexFlowMeasure extends StaffMeasure {
         //this.beginInstructionsWidth =  (this.stave.getNoteStartX() - this.stave.getX()) / unitInPixels;
         //this.endInstructionsWidth = (this.stave.getX() + this.stave.getWidth() - this.stave.getNoteEndX()) / unitInPixels;
     }
-
-    /**
-     * sets the vexflow x positions back into the bounding boxes of the staff entries in the osmd object model.
-     * The positions are needed for cursor placement and mouse/tap interactions
-     */
-    private setStaffEntriesXPositions(): void {
-        for (let idx3: number = 0, len3: number = this.staffEntries.length; idx3 < len3; ++idx3) {
-            const gse: VexFlowStaffEntry = (<VexFlowStaffEntry> this.staffEntries[idx3]);
-            const measure: StaffMeasure = gse.parentMeasure;
-            const x: number =
-                gse.getX() -
-                measure.PositionAndShape.RelativePosition.x -
-                measure.ParentStaffLine.PositionAndShape.RelativePosition.x -
-                measure.parentMusicSystem.PositionAndShape.RelativePosition.x;
-            gse.PositionAndShape.RelativePosition.x = x;
-            gse.PositionAndShape.calculateAbsolutePosition();
-            gse.PositionAndShape.calculateAbsolutePositionsOfChildren();
-        }
-    }
 }

+ 65 - 18
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator.ts

@@ -16,12 +16,10 @@ import {Beam} from "../../VoiceData/Beam";
 import {ClefInstruction} from "../../VoiceData/Instructions/ClefInstruction";
 import {OctaveEnum} from "../../VoiceData/Expressions/ContinuousExpressions/OctaveShift";
 import {Fraction} from "../../../Common/DataObjects/Fraction";
-import {LyricsEntry} from "../../VoiceData/Lyrics/LyricsEntry";
 import {LyricWord} from "../../VoiceData/Lyrics/LyricsWord";
 import {OrnamentContainer} from "../../VoiceData/OrnamentContainer";
 import {ArticulationEnum} from "../../VoiceData/VoiceEntry";
 import {Tuplet} from "../../VoiceData/Tuplet";
-import Dictionary from "typescript-collections/dist/lib/Dictionary";
 import {VexFlowMeasure} from "./VexFlowMeasure";
 import {VexFlowTextMeasurer} from "./VexFlowTextMeasurer";
 
@@ -29,6 +27,11 @@ import Vex = require("vexflow");
 import {Logging} from "../../../Common/Logging";
 import {unitInPixels} from "./VexFlowMusicSheetDrawer";
 import {VexFlowGraphicalNote} from "./VexFlowGraphicalNote";
+import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
+import { GraphicalLabel } from "../GraphicalLabel";
+import { LyricsEntry } from "../../VoiceData/Lyrics/LyricsEntry";
+import { GraphicalLyricWord } from "../GraphicalLyricWord";
+import { VexFlowStaffEntry } from "./VexFlowStaffEntry";
 
 export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     constructor() {
@@ -45,6 +48,17 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         }
     }
 
+    protected formatMeasures(): void {
+        for (const staffMeasures of this.graphicalMusicSheet.MeasureList) {
+            for (const staffMeasure of staffMeasures) {
+                (<VexFlowMeasure>staffMeasure).format();
+                for (const staffEntry of staffMeasure.staffEntries) {
+                    (<VexFlowStaffEntry>staffEntry).calculateXPosition();
+                }
+            }
+        }
+    }
+
     //protected clearSystemsAndMeasures(): void {
     //    for (let measure of measures) {
     //
@@ -91,18 +105,23 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
 
         let width: number = 200;
         if (allVoices.length > 0) {
-            const firstMeasure: VexFlowMeasure = measures[0] as VexFlowMeasure;
             // FIXME: The following ``+ 5.0'' is temporary: it was added as a workaround for
             // FIXME: a more relaxed formatting of voices
             width = formatter.preCalculateMinTotalWidth(allVoices) / unitInPixels + 5.0;
+            // firstMeasure.formatVoices = (w: number) => {
+            //     formatter.format(allVoices, w);
+            // };
             for (const measure of measures) {
                 measure.minimumStaffEntriesWidth = width;
+                if (measure !== measures[0]) {
                 (measure as VexFlowMeasure).formatVoices = undefined;
-            }
-            firstMeasure.formatVoices = (w: number) => {
+                } else {
+                    (measure as VexFlowMeasure).formatVoices = (w: number) => {
                 formatter.format(allVoices, w);
             };
         }
+            }
+        }
 
         return width;
     }
@@ -185,7 +204,7 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
 
     /**
-     * Calculate the shape (Bézier curve) for this tie.
+     * Calculate the shape (Bézier curve) for this tie.
      * @param tie
      * @param tieIsAtSystemBreak
      */
@@ -227,15 +246,6 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
     }
 
     /**
-     * Calculate the Lyrics YPositions for a single [[StaffLine]].
-     * @param staffLine
-     * @param lyricVersesNumber
-     */
-    protected calculateSingleStaffLineLyricsPosition(staffLine: StaffLine, lyricVersesNumber: number[]): void {
-        return;
-    }
-
-    /**
      * Calculate a single OctaveShift for a [[MultiExpression]].
      * @param sourceMeasure
      * @param multiExpression
@@ -301,9 +311,46 @@ export class VexFlowMusicSheetCalculator extends MusicSheetCalculator {
         (graphicalNote.parentStaffEntry.parentMeasure as VexFlowMeasure).handleBeam(graphicalNote, beam);
     }
 
-    protected handleVoiceEntryLyrics(lyricsEntries: Dictionary<number, LyricsEntry>, voiceEntry: VoiceEntry,
-                                     graphicalStaffEntry: GraphicalStaffEntry, openLyricWords: LyricWord[]): void {
-        return;
+    protected handleVoiceEntryLyrics(voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry, lyricWords: LyricWord[]): void {
+        voiceEntry.LyricsEntries.forEach((key: number, lyricsEntry: LyricsEntry) => {
+            const graphicalLyricEntry: GraphicalLyricEntry = new GraphicalLyricEntry(lyricsEntry,
+                                                                                     graphicalStaffEntry,
+                                                                                     this.rules.LyricsHeight,
+                                                                                     this.rules.StaffHeight);
+
+            graphicalStaffEntry.LyricsEntries.push(graphicalLyricEntry);
+
+            // create corresponding GraphicalLabel
+            const graphicalLabel: GraphicalLabel = graphicalLyricEntry.GraphicalLabel;
+            graphicalLabel.setLabelPositionAndShapeBorders();
+
+            if (lyricsEntry.Word !== undefined) {
+                const lyricsEntryIndex: number = lyricsEntry.Word.Syllables.indexOf(lyricsEntry);
+                let index: number = lyricWords.indexOf(lyricsEntry.Word);
+                if (index === -1) {
+                    lyricWords.push(lyricsEntry.Word);
+                    index = lyricWords.indexOf(lyricsEntry.Word);
+    }
+
+                if (this.graphicalLyricWords.length === 0 || index > this.graphicalLyricWords.length - 1) {
+                    const graphicalLyricWord: GraphicalLyricWord = new GraphicalLyricWord(lyricsEntry.Word);
+
+                    graphicalLyricEntry.ParentLyricWord = graphicalLyricWord;
+                    graphicalLyricWord.GraphicalLyricsEntries[lyricsEntryIndex] = graphicalLyricEntry;
+                    this.graphicalLyricWords.push(graphicalLyricWord);
+                } else {
+                    const graphicalLyricWord: GraphicalLyricWord = this.graphicalLyricWords[index];
+
+                    graphicalLyricEntry.ParentLyricWord = graphicalLyricWord;
+                    graphicalLyricWord.GraphicalLyricsEntries[lyricsEntryIndex] = graphicalLyricEntry;
+
+                    if (graphicalLyricWord.isFilled()) {
+                        lyricWords.splice(index, 1);
+                        this.graphicalLyricWords.splice(this.graphicalLyricWords.indexOf(graphicalLyricWord), 1);
+                    }
+                }
+            }
+        });
     }
 
     protected handleVoiceEntryOrnaments(ornamentContainer: OrnamentContainer, voiceEntry: VoiceEntry, graphicalStaffEntry: GraphicalStaffEntry): void {

+ 23 - 0
src/MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer.ts

@@ -10,6 +10,7 @@ import {GraphicalLayers} from "../DrawingEnums";
 import {GraphicalStaffEntry} from "../GraphicalStaffEntry";
 import {VexFlowBackend} from "./VexFlowBackend";
 import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
+import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
 
 /**
  * This is a global constant which denotes the height in pixels of the space between two lines of the stave
@@ -70,6 +71,16 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
             measure.PositionAndShape.AbsolutePosition.y * unitInPixels
         );
         measure.draw(this.backend.getContext());
+        for (const voiceID in measure.vfVoices) {
+            if (measure.vfVoices.hasOwnProperty(voiceID)) {
+                const tickables: Vex.Flow.Tickable[] = measure.vfVoices[voiceID].tickables;
+                for (const tick of tickables) {
+                    if ((<any>tick).getAttribute("type") === "StaveNote" && process.env.DEBUG) {
+                        tick.getBoundingBox().draw(this.backend.getContext());
+                    }
+                }
+            }
+        }
 
         // Draw the StaffEntries
         for (const staffEntry of measure.staffEntries) {
@@ -82,6 +93,18 @@ export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
         if (staffEntry.graphicalChordContainer !== undefined) {
             this.drawLabel(staffEntry.graphicalChordContainer.GetGraphicalLabel, <number>GraphicalLayers.Notes);
         }
+        if (staffEntry.LyricsEntries.length > 0) {
+            this.drawLyrics(staffEntry.LyricsEntries, <number>GraphicalLayers.Notes);
+        }
+    }
+
+    /**
+     * Draw all lyrics to the canvas
+     * @param lyricEntries Array of lyric entries to be drawn
+     * @param layer Number of the layer that the lyrics should be drawn in
+     */
+    private drawLyrics(lyricEntries: GraphicalLyricEntry[], layer: number): void {
+        lyricEntries.forEach(lyricsEntry => this.drawLabel(lyricsEntry.GraphicalLabel, layer));
     }
 
     protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void {

+ 23 - 11
src/MusicalScore/Graphical/VexFlow/VexFlowStaffEntry.ts

@@ -15,22 +15,34 @@ export class VexFlowStaffEntry extends GraphicalStaffEntry {
     public vfNotes: { [voiceID: number]: Vex.Flow.StaveNote; } = {};
 
     /**
-     *
-     * @returns {number} the x-position (in units) of this StaffEntry
+     * Calculates the staff entry positions from the VexFlow stave information and the tickabels inside the staff.
+     * This is needed in order to set the OSMD staff entries (which are almost the same as tickables) to the correct positionts.
+     * It is also needed to be done after formatting!
      */
-    public getX(): number {
-        let x: number = 0;
-        let n: number = 0;
+    public calculateXPosition(): void {
         const vfNotes: { [voiceID: number]: Vex.Flow.StaveNote; } = this.vfNotes;
+        const stave: Vex.Flow.Stave = (this.parentMeasure as VexFlowMeasure).getVFStave();
+        let tickablePosition: number = 0;
+        let numberOfValidTickables: number = 0;
         for (const voiceId in vfNotes) {
             if (vfNotes.hasOwnProperty(voiceId)) {
-                x += (vfNotes[voiceId].getNoteHeadBeginX() + vfNotes[voiceId].getNoteHeadEndX()) / 2;
-                n += 1;
+                const tickable: Vex.Flow.StaveNote = vfNotes[voiceId];
+                // This will let the tickable know how to calculate it's bounding box
+                tickable.setStave(stave);
+                // The middle of the tickable is also the OSMD BoundingBox center
+                const staveNote: Vex.Flow.StaveNote = (<Vex.Flow.StaveNote>tickable);
+                tickablePosition += staveNote.getNoteHeadEndX() - staveNote.getGlyphWidth() / 2;
+                numberOfValidTickables++;
             }
         }
-        if (n === 0) {
-            return 0;
-        }
-        return x / n / unitInPixels;
+        tickablePosition = tickablePosition / numberOfValidTickables;
+        // Calculate parent absolute position and reverse calculate the relative position
+        // All the modifiers signs, clefs, you name it have an offset in the measure. Therefore remove it.
+        // NOTE: Somehow vexflows shift is off by 25px.
+        const modifierOffset: number = stave.getModifierXShift() - (this.parentMeasure.MeasureNumber === 1 ? 25 : 0);
+        // const modifierOffset: number = 0;
+        // sets the vexflow x positions back into the bounding boxes of the staff entries in the osmd object model.
+        // The positions are needed for cursor placement and mouse/tap interactions
+        this.PositionAndShape.RelativePosition.x = (tickablePosition - stave.getNoteStartX() + modifierOffset) / unitInPixels;
     }
 }

+ 140 - 0
src/MusicalScore/ScoreIO/MusicSymbolModules/LyricsReader.ts

@@ -0,0 +1,140 @@
+import {LyricWord} from "../../VoiceData/Lyrics/LyricsWord";
+import {VoiceEntry} from "../../VoiceData/VoiceEntry";
+import {IXmlElement} from "../../../Common/FileIO/Xml";
+import {LyricsEntry} from "../../VoiceData/Lyrics/LyricsEntry";
+import {ITextTranslation} from "../../Interfaces/ITextTranslation";
+import {MusicSheet} from "../../MusicSheet";
+
+export class LyricsReader {
+    private openLyricWords: { [_: number]: LyricWord; } = {};
+    private currentLyricWord: LyricWord;
+    private musicSheet: MusicSheet;
+
+    constructor(musicSheet: MusicSheet) {
+        this.musicSheet = musicSheet;
+    }
+    /**
+     * This method adds a single LyricEntry to a VoiceEntry
+     * @param {IXmlElement[]} lyricNodeList
+     * @param {VoiceEntry} currentVoiceEntry
+     */
+    public addLyricEntry(lyricNodeList: IXmlElement[], currentVoiceEntry: VoiceEntry): void {
+        if (lyricNodeList !== undefined) {
+            const lyricNodeListArr: IXmlElement[] = lyricNodeList;
+            for (let idx: number = 0, len: number = lyricNodeListArr.length; idx < len; ++idx) {
+                const lyricNode: IXmlElement = lyricNodeListArr[idx];
+                try {
+                    let syllabic: string = "single"; // Single as default
+                    if (lyricNode.element("text") !== undefined) {
+                        let textNode: IXmlElement = lyricNode.element("text");
+                        if (lyricNode.element("syllabic") !== undefined) {
+                            syllabic = lyricNode.element("syllabic").value;
+                        }
+                        if (textNode !== undefined) {
+                            const text: string = textNode.value;
+                            // <elision> separates Multiple syllabels on a single LyricNote
+                            // "-" text indicating separated syllabel should be ignored
+                            // we calculate the Dash element much later
+                            if (lyricNode.element("elision") !== undefined && text === "-") {
+                                const lyricNodeChildren: IXmlElement[] = lyricNode.elements();
+                                let elisionIndex: number = 0;
+                                for (let i: number = 0; i < lyricNodeChildren.length; i++) {
+                                    const child: IXmlElement = lyricNodeChildren[i];
+                                    if (child.name === "elision") {
+                                        elisionIndex = i;
+                                        break;
+                                    }
+                                }
+                                let nextText: IXmlElement = undefined;
+                                let nextSyllabic: IXmlElement = undefined;
+                                // read the next nodes
+                                if (elisionIndex > 0) {
+                                    for (let i: number = elisionIndex; i < lyricNodeChildren.length; i++) {
+                                        const child: IXmlElement = lyricNodeChildren[i];
+                                        if (child.name === "text") {
+                                            nextText = child;
+                                        }
+                                        if (child.name === "syllabic") {
+                                            nextSyllabic = child;
+                                        }
+                                    }
+                                }
+                                if (nextText !== undefined && nextSyllabic !== undefined) {
+                                    textNode = nextText;
+                                    syllabic = "middle";
+                                }
+                            }
+                            let currentLyricVerseNumber: number = 1;
+                            if (lyricNode.attributes() !== undefined && lyricNode.attribute("number") !== undefined) {
+                                try {
+                                    currentLyricVerseNumber = parseInt(lyricNode.attribute("number").value, 10);
+                                } catch (err) {
+                                    try {
+                                        const result: string[] = lyricNode.attribute("number").value.toLowerCase().split("verse");
+                                        if (result.length > 1) {
+                                            currentLyricVerseNumber = parseInt(result[1], 10);
+                                        }
+                                    } catch (err) {
+                                        const errorMsg: string =
+                                        ITextTranslation.translateText("ReaderErrorMessages/LyricVerseNumberError", "Invalid lyric verse number");
+                                        this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                                        continue;
+                                    }
+                                }
+                            }
+                            let lyricsEntry: LyricsEntry = undefined;
+                            if (syllabic === "single" || syllabic === "end") {
+                                if (this.openLyricWords[currentLyricVerseNumber] !== undefined) { // word end given or some word still open
+                                    this.currentLyricWord = this.openLyricWords[currentLyricVerseNumber];
+                                    lyricsEntry = new LyricsEntry(text, currentLyricVerseNumber, this.currentLyricWord, currentVoiceEntry);
+                                    this.currentLyricWord.Syllables.push(lyricsEntry);
+                                    delete this.openLyricWords[currentLyricVerseNumber];
+                                    this.currentLyricWord = undefined;
+                                } else { // single syllable given or end given while no word has been started
+                                    lyricsEntry = new LyricsEntry(text, currentLyricVerseNumber, undefined, currentVoiceEntry);
+                                }
+                                lyricsEntry.extend = lyricNode.element("extend") !== undefined;
+                            } else if (syllabic === "begin") { // first finishing, if a word already is open (can only happen, when wrongly given)
+                                if (this.openLyricWords[currentLyricVerseNumber] !== undefined) {
+                                    delete this.openLyricWords[currentLyricVerseNumber];
+                                    this.currentLyricWord = undefined;
+                                }
+                                this.currentLyricWord = new LyricWord();
+                                this.openLyricWords[currentLyricVerseNumber] = this.currentLyricWord;
+                                lyricsEntry = new LyricsEntry(text, currentLyricVerseNumber, this.currentLyricWord, currentVoiceEntry);
+                                this.currentLyricWord.Syllables.push(lyricsEntry);
+                            } else if (syllabic === "middle") {
+                                if (this.openLyricWords[currentLyricVerseNumber] !== undefined) {
+                                    this.currentLyricWord = this.openLyricWords[currentLyricVerseNumber];
+                                    lyricsEntry = new LyricsEntry(text, currentLyricVerseNumber, this.currentLyricWord, currentVoiceEntry);
+                                    this.currentLyricWord.Syllables.push(lyricsEntry);
+                                } else {
+                                    // in case the wrong syllabel information is given, create a single Entry and add it to currentVoiceEntry
+                                    lyricsEntry = new LyricsEntry(text, currentLyricVerseNumber, undefined, currentVoiceEntry);
+                                }
+                            }
+                            // add each LyricEntry to currentVoiceEntry
+                            if (lyricsEntry !== undefined) {
+                                // only add the lyric entry if not another entry has already been given:
+                                if (!currentVoiceEntry.LyricsEntries[currentLyricVerseNumber] !== undefined) {
+                                    currentVoiceEntry.LyricsEntries.setValue(currentLyricVerseNumber, lyricsEntry);
+                                }
+                                // save in currentInstrument the verseNumber (only once)
+                                if (!currentVoiceEntry.ParentVoice.Parent.LyricVersesNumbers[currentLyricVerseNumber] !== undefined) {
+                                    currentVoiceEntry.ParentVoice.Parent.LyricVersesNumbers.push(currentLyricVerseNumber);
+                                }
+                            }
+                        }
+                    }
+                } catch (err) {
+                    const errorMsg: string = ITextTranslation.translateText("ReaderErrorMessages/LyricError", "Error while reading lyric entry.");
+                    this.musicSheet.SheetErrors.pushMeasureError(errorMsg);
+                    continue;
+                }
+            }
+            // Squash to unique numbers
+            currentVoiceEntry.ParentVoice.Parent.LyricVersesNumbers =
+            currentVoiceEntry.ParentVoice.Parent.LyricVersesNumbers.filter((lvn, index, self) => self.indexOf(lvn) === index);
+        }
+    }
+}

+ 8 - 7
src/MusicalScore/ScoreIO/VoiceGenerator.ts

@@ -16,6 +16,7 @@ import {ITextTranslation} from "../Interfaces/ITextTranslation";
 import {ArticulationEnum} from "../VoiceData/VoiceEntry";
 import {Slur} from "../VoiceData/Expressions/ContinuousExpressions/Slur";
 import {LyricsEntry} from "../VoiceData/Lyrics/LyricsEntry";
+import {LyricsReader} from "../ScoreIO/MusicSymbolModules/LyricsReader";
 import {MusicSheetReadingException} from "../Exceptions";
 import {AccidentalEnum} from "../../Common/DataObjects/Pitch";
 import {NoteEnum} from "../../Common/DataObjects/Pitch";
@@ -43,12 +44,12 @@ export class VoiceGenerator {
             this.voice = new Voice(instrument, voiceId);
         }
         instrument.Voices.push(this.voice);
-        //this.lyricsReader = MusicSymbolModuleFactory.createLyricsReader(this.musicSheet);
+        this.lyricsReader = new LyricsReader(this.musicSheet);
         //this.articulationReader = MusicSymbolModuleFactory.createArticulationReader();
     }
 
     // private slurReader: SlurReader;
-    //private lyricsReader: LyricsReader;
+    private lyricsReader: LyricsReader;
     //private articulationReader: ArticulationReader;
     private musicSheet: MusicSheet;
     private voice: Voice;
@@ -117,11 +118,11 @@ export class VoiceGenerator {
             this.currentNote = restNote
                 ? this.addRestNote(noteDuration)
                 : this.addSingleNote(noteNode, noteDuration, graceNote, chord, guitarPro);
-            // (*)
-            //if (this.lyricsReader !== undefined && noteNode.element("lyric") !== undefined) {
-            //    this.lyricsReader.addLyricEntry(noteNode, this.currentVoiceEntry);
-            //    this.voice.Parent.HasLyrics = true;
-            //}
+
+            if (this.lyricsReader !== undefined && noteNode.elements("lyric") !== undefined) {
+               this.lyricsReader.addLyricEntry(noteNode.elements("lyric"), this.currentVoiceEntry);
+               this.voice.Parent.HasLyrics = true;
+            }
             let hasTupletCommand: boolean = false;
             const notationNode: IXmlElement = noteNode.element("notations");
             if (notationNode !== undefined) {

+ 8 - 1
src/MusicalScore/VoiceData/Lyrics/LyricsEntry.ts

@@ -2,14 +2,17 @@ import {LyricWord} from "./LyricsWord";
 import {VoiceEntry} from "../VoiceEntry";
 
 export class LyricsEntry {
-    constructor(text: string, word: LyricWord, parent: VoiceEntry) {
+    constructor(text: string, verseNumber: number, word: LyricWord, parent: VoiceEntry) {
         this.text = text;
         this.word = word;
         this.parent = parent;
+        this.verseNumber = verseNumber;
     }
     private text: string;
     private word: LyricWord;
     private parent: VoiceEntry;
+    private verseNumber: number;
+    public extend: boolean;
 
     public get Text(): string {
         return this.text;
@@ -26,4 +29,8 @@ export class LyricsEntry {
     public set Parent(value: VoiceEntry) {
         this.parent = value;
     }
+
+    public get VerseNumber(): number {
+        return this.verseNumber;
+    }
 }

+ 4 - 0
src/MusicalScore/VoiceData/Note.ts

@@ -103,6 +103,10 @@ export class Note {
         this.playbackInstrumentId = value;
     }
 
+    public isRest(): boolean {
+        return this.Pitch === undefined;
+    }
+
     public calculateNoteLengthWithoutTie(): Fraction {
         const withoutTieLength: Fraction = this.length.clone();
         if (this.tie !== undefined) {

+ 1 - 1
test/data/Beethoven_AnDieFerneGeliebte.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <score-partwise version="2.0">
   <work>
     <work-number>Op. 98</work-number>

BIN=BIN
test/data/Cornelius_P_Christbaum_Opus_8_1_1865.mxl


+ 1 - 1
test/data/Mozart_DasVeilchen.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <score-partwise version="2.0">
   <work>
     <work-number>K. 476</work-number>