import {IXmlElement} from "./../Common/FileIO/Xml"; import {VexFlowMusicSheetCalculator} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator"; import {VexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/VexFlowBackend"; import {MusicSheetReader} from "./../MusicalScore/ScoreIO/MusicSheetReader"; import {GraphicalMusicSheet} from "./../MusicalScore/Graphical/GraphicalMusicSheet"; import {MusicSheetCalculator} from "./../MusicalScore/Graphical/MusicSheetCalculator"; import {VexFlowMusicSheetDrawer} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer"; import {SvgVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/SvgVexFlowBackend"; import {CanvasVexFlowBackend} from "./../MusicalScore/Graphical/VexFlow/CanvasVexFlowBackend"; import {MusicSheet} from "./../MusicalScore/MusicSheet"; import {Cursor} from "./Cursor"; import {MXLHelper} from "../Common/FileIO/Mxl"; import {Promise} from "es6-promise"; import {AJAX} from "./AJAX"; import * as log from "loglevel"; import {DrawingParametersEnum, DrawingParameters} from "../MusicalScore/Graphical/DrawingParameters"; import {IOSMDOptions, OSMDOptions, AutoBeamOptions} from "./OSMDOptions"; import {EngravingRules} from "../MusicalScore/Graphical/EngravingRules"; import {AbstractExpression} from "../MusicalScore/VoiceData/Expressions/AbstractExpression"; /** * The main class and control point of OpenSheetMusicDisplay.
* It can display MusicXML sheet music files in an HTML element container.
* After the constructor, use load() and render() to load and render a MusicXML file. */ export class OpenSheetMusicDisplay { private version: string = "0.6.7-release"; // getter: this.Version /** * Creates and attaches an OpenSheetMusicDisplay object to an HTML element container.
* After the constructor, use load() and render() to load and render a MusicXML file. * @param container The container element OSMD will be rendered into.
* Either a string specifying the ID of an HTML container element,
* or a reference to the HTML element itself (e.g. div) * @param options An object for rendering options like the backend (svg/canvas) or autoResize.
* For defaults see the OSMDOptionsStandard method in the [[OSMDOptions]] class. */ constructor(container: string|HTMLElement, options: IOSMDOptions = OSMDOptions.OSMDOptionsStandard()) { // Store container element if (typeof container === "string") { // ID passed this.container = document.getElementById(container); } else if (container && "appendChild" in container) { // Element passed this.container = container; } if (!this.container) { throw new Error("Please pass a valid div container to OpenSheetMusicDisplay"); } if (options.autoResize === undefined) { options.autoResize = true; } this.setOptions(options); } public cursor: Cursor; public zoom: number = 1.0; private container: HTMLElement; private canvas: HTMLElement; private backend: VexFlowBackend; private innerElement: HTMLElement; private sheet: MusicSheet; private drawer: VexFlowMusicSheetDrawer; private graphic: GraphicalMusicSheet; private drawingParameters: DrawingParameters; private autoResizeEnabled: boolean; private resizeHandlerAttached: boolean; /** * Load a MusicXML file * @param content is either the url of a file, or the root node of a MusicXML document, or the string content of a .xml/.mxl file */ public load(content: string|Document): Promise<{}> { // Warning! This function is asynchronous! No error handling is done here. this.reset(); if (typeof content === "string") { const str: string = content; const self: OpenSheetMusicDisplay = this; if (str.substr(0, 4) === "\x50\x4b\x03\x04") { // This is a zip file, unpack it first return MXLHelper.MXLtoXMLstring(str).then( (x: string) => { return self.load(x); }, (err: any) => { log.debug(err); throw new Error("OpenSheetMusicDisplay: Invalid MXL file"); } ); } // Javascript loads strings as utf-16, which is wonderful BS if you want to parse UTF-8 :S if (str.substr(0, 3) === "\uf7ef\uf7bb\uf7bf") { // UTF with BOM detected, truncate first three bytes and pass along return self.load(str.substr(3)); } if (str.substr(0, 5) === " { return self.load(s); }, (exc: Error) => { throw exc; } ); } } if (!content || !(content).nodeName) { return Promise.reject(new Error("OpenSheetMusicDisplay: The document which was provided is invalid")); } const children: NodeList = (content).childNodes; let elem: Element; for (let i: number = 0, length: number = children.length; i < length; i += 1) { const node: Node = children[i]; if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === "score-partwise") { elem = node; break; } } if (!elem) { return Promise.reject(new Error("OpenSheetMusicDisplay: Document is not a valid 'partwise' MusicXML")); } const score: IXmlElement = new IXmlElement(elem); const calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator(); const reader: MusicSheetReader = new MusicSheetReader(); this.sheet = reader.createMusicSheet(score, "Untitled Score"); if (this.sheet === undefined) { // error loading sheet, probably already logged, do nothing return Promise.reject(new Error("given music sheet was incomplete or could not be loaded.")); } this.graphic = new GraphicalMusicSheet(this.sheet, calc); if (this.drawingParameters.drawCursors && this.cursor) { this.cursor.init(this.sheet.MusicPartManager, this.graphic); } log.info(`Loaded sheet ${this.sheet.TitleString} successfully.`); return Promise.resolve({}); } /** * Render the music sheet in the container */ public render(): void { if (!this.graphic) { throw new Error("OpenSheetMusicDisplay: Before rendering a music sheet, please load a MusicXML file"); } this.drawer.clear(); // clear canvas before setting width // Set page width const width: number = this.container.offsetWidth; this.sheet.pageWidth = width / this.zoom / 10.0; // Before introducing the following optimization (maybe irrelevant), tests // have to be modified to ensure that width is > 0 when executed //if (isNaN(width) || width === 0) { // return; //} // Calculate again this.graphic.reCalculate(); const height: number = this.graphic.MusicPages[0].PositionAndShape.BorderBottom * 10.0 * this.zoom; if (this.drawingParameters.drawCursors) { this.graphic.Cursors.length = 0; } // Update Sheet Page this.drawer.resize(width, height); this.drawer.scale(this.zoom); // Finally, draw this.drawer.drawSheet(this.graphic); if (this.drawingParameters.drawCursors && this.cursor) { // Update the cursor position this.cursor.update(); } } /** States whether the render() function can be safely called. */ public IsReadyToRender(): boolean { return this.graphic !== undefined; } /** Clears what OSMD has drawn on its canvas. */ public clear(): void { this.drawer.clear(); this.reset(); // without this, resize will draw loaded sheet again } /** Set OSMD rendering options using an IOSMDOptions object. * Can be called during runtime. Also called by constructor. * For example, setOptions({autoResize: false}) will disable autoResize even during runtime. */ public setOptions(options: IOSMDOptions): void { this.drawingParameters = new DrawingParameters(); if (options.drawingParameters) { this.drawingParameters.DrawingParametersEnum = (DrawingParametersEnum)[options.drawingParameters.toLowerCase()]; } const updateExistingBackend: boolean = this.backend !== undefined; if (options.backend !== undefined || this.backend === undefined) { if (updateExistingBackend) { // TODO doesn't work yet, still need to create a new OSMD object this.drawer.clear(); // musicSheetCalculator.clearSystemsAndMeasures() // maybe? don't have reference though // musicSheetCalculator.clearRecreatedObjects(); } if (options.backend === undefined || options.backend.toLowerCase() === "svg") { this.backend = new SvgVexFlowBackend(); } else { this.backend = new CanvasVexFlowBackend(); } this.backend.initialize(this.container); this.canvas = this.backend.getCanvas(); this.innerElement = this.backend.getInnerElement(); this.enableOrDisableCursor(this.drawingParameters.drawCursors); // Create the drawer this.drawer = new VexFlowMusicSheetDrawer(this.canvas, this.backend, this.drawingParameters); } // individual drawing parameters options if (options.autoBeam !== undefined) { EngravingRules.Rules.AutoBeamNotes = options.autoBeam; } const autoBeamOptions: AutoBeamOptions = options.autoBeamOptions; if (autoBeamOptions) { if (autoBeamOptions.maintain_stem_directions === undefined) { autoBeamOptions.maintain_stem_directions = false; } EngravingRules.Rules.AutoBeamOptions = autoBeamOptions; if (autoBeamOptions.groups && autoBeamOptions.groups.length) { for (const fraction of autoBeamOptions.groups) { if (fraction.length !== 2) { throw new Error("Each fraction in autoBeamOptions.groups must be of length 2, e.g. [3,4] for beaming three fourths"); } } } } if (options.coloringEnabled !== undefined) { EngravingRules.Rules.ColoringEnabled = options.coloringEnabled; } if (options.disableCursor) { this.drawingParameters.drawCursors = false; this.enableOrDisableCursor(this.drawingParameters.drawCursors); } // alternative to if block: this.drawingsParameters.drawCursors = options.drawCursors !== false. No if, but always sets drawingParameters. // note that every option can be undefined, which doesn't mean the option should be set to false. if (options.drawHiddenNotes) { this.drawingParameters.drawHiddenNotes = true; } if (options.drawTitle !== undefined) { this.drawingParameters.DrawTitle = options.drawTitle; // TODO these settings are duplicate in drawingParameters and EngravingRules. Maybe we only need them in EngravingRules. // this sets the parameter in DrawingParameters, which in turn sets the parameter in EngravingRules. // see settings below that don't call drawingParameters for the immediate approach } if (options.drawSubtitle !== undefined) { this.drawingParameters.DrawSubtitle = options.drawSubtitle; } if (options.drawLyricist !== undefined) { this.drawingParameters.DrawLyricist = options.drawLyricist; } if (options.drawCredits !== undefined) { this.drawingParameters.drawCredits = options.drawCredits; } if (options.drawPartNames !== undefined) { this.drawingParameters.DrawPartNames = options.drawPartNames; } if (options.drawFingerings === false) { EngravingRules.Rules.RenderFingerings = false; } if (options.fingeringPosition !== undefined) { EngravingRules.Rules.FingeringPosition = AbstractExpression.PlacementEnumFromString(options.fingeringPosition); } if (options.fingeringInsideStafflines !== undefined) { EngravingRules.Rules.FingeringInsideStafflines = options.fingeringInsideStafflines; } if (options.setWantedStemDirectionByXml !== undefined) { EngravingRules.Rules.SetWantedStemDirectionByXml = options.setWantedStemDirectionByXml; } if (options.defaultColorNotehead) { EngravingRules.Rules.DefaultColorNotehead = options.defaultColorNotehead; } if (options.defaultColorRest) { EngravingRules.Rules.DefaultColorRest = options.defaultColorRest; } if (options.defaultColorStem) { EngravingRules.Rules.DefaultColorStem = options.defaultColorStem; } if (options.defaultColorLabel) { EngravingRules.Rules.DefaultColorLabel = options.defaultColorLabel; } if (options.defaultColorTitle) { EngravingRules.Rules.DefaultColorTitle = options.defaultColorTitle; } if (options.drawUpToMeasureNumber) { EngravingRules.Rules.MaxMeasureToDrawIndex = options.drawUpToMeasureNumber; } if (options.tupletsRatioed) { EngravingRules.Rules.TupletsRatioed = true; } if (options.tupletsBracketed) { EngravingRules.Rules.TupletsBracketed = true; } if (options.tripletsBracketed) { EngravingRules.Rules.TripletsBracketed = true; } if (options.autoResize) { if (!this.resizeHandlerAttached) { this.autoResize(); } this.autoResizeEnabled = true; } else if (options.autoResize === false) { // not undefined this.autoResizeEnabled = false; // we could remove the window EventListener here, but not necessary. } } /** * Sets the logging level for this OSMD instance. By default, this is set to `warn`. * * @param: content can be `trace`, `debug`, `info`, `warn` or `error`. */ public setLogLevel(level: string): void { switch (level) { case "trace": log.setLevel(log.levels.TRACE); break; case "debug": log.setLevel(log.levels.DEBUG); break; case "info": log.setLevel(log.levels.INFO); break; case "warn": log.setLevel(log.levels.WARN); break; case "error": log.setLevel(log.levels.ERROR); break; default: log.warn(`Could not set log level to ${level}. Using warn instead.`); log.setLevel(log.levels.WARN); break; } } /** * Initialize this object to default values * FIXME: Probably unnecessary */ private reset(): void { if (this.drawingParameters.drawCursors && this.cursor) { this.cursor.hide(); } this.sheet = undefined; this.graphic = undefined; this.zoom = 1.0; } /** * Attach the appropriate handler to the window.onResize event */ private autoResize(): void { const self: OpenSheetMusicDisplay = this; this.handleResize( () => { // empty }, () => { // The following code is probably not needed // (the width should adapt itself to the max allowed) //let width: number = Math.max( // document.documentElement.clientWidth, // document.body.scrollWidth, // document.documentElement.scrollWidth, // document.body.offsetWidth, // document.documentElement.offsetWidth //); //self.container.style.width = width + "px"; if (self.IsReadyToRender()) { self.render(); } } ); } /** * Helper function for managing window's onResize events * @param startCallback is the function called when resizing starts * @param endCallback is the function called when resizing (kind-of) ends */ private handleResize(startCallback: () => void, endCallback: () => void): void { let rtime: number; let timeout: number = undefined; const delta: number = 200; const self: OpenSheetMusicDisplay = this; function resizeStart(): void { if (!self.AutoResizeEnabled) { return; } rtime = (new Date()).getTime(); if (!timeout) { startCallback(); rtime = (new Date()).getTime(); timeout = window.setTimeout(resizeEnd, delta); } } function resizeEnd(): void { timeout = undefined; window.clearTimeout(timeout); if ((new Date()).getTime() - rtime < delta) { timeout = window.setTimeout(resizeEnd, delta); } else { endCallback(); } } if ((window).attachEvent) { // Support IE<9 (window).attachEvent("onresize", resizeStart); } else { window.addEventListener("resize", resizeStart); } this.resizeHandlerAttached = true; window.setTimeout(startCallback, 0); window.setTimeout(endCallback, 1); } /** Enable or disable (hide) the cursor. * @param enable whether to enable (true) or disable (false) the cursor */ public enableOrDisableCursor(enable: boolean): void { this.drawingParameters.drawCursors = enable; if (enable) { if (!this.cursor) { this.cursor = new Cursor(this.innerElement, this); if (this.sheet && this.graphic) { // else init is called in load() this.cursor.init(this.sheet.MusicPartManager, this.graphic); } } } else { // disable cursor if (!this.cursor) { return; } this.cursor.hide(); // this.cursor = undefined; // TODO cursor should be disabled, not just hidden. otherwise user can just call osmd.cursor.hide(). // however, this could cause null calls (cursor.next() etc), maybe that needs some solution. } } //#region GETTER / SETTER public set DrawSkyLine(value: boolean) { if (this.drawer) { this.drawer.skyLineVisible = value; this.render(); } } public get DrawSkyLine(): boolean { return this.drawer.skyLineVisible; } public set DrawBottomLine(value: boolean) { if (this.drawer) { this.drawer.bottomLineVisible = value; this.render(); } } public get DrawBottomLine(): boolean { return this.drawer.bottomLineVisible; } public set DrawBoundingBox(value: string) { this.drawer.drawableBoundingBoxElement = value; this.render(); } public get DrawBoundingBox(): string { return this.drawer.drawableBoundingBoxElement; } public get AutoResizeEnabled(): boolean { return this.autoResizeEnabled; } public set AutoResizeEnabled(value: boolean) { this.autoResizeEnabled = value; } public get Sheet(): MusicSheet { return this.sheet; } public get Drawer(): VexFlowMusicSheetDrawer { return this.drawer; } public get GraphicSheet(): GraphicalMusicSheet { return this.graphic; } public get DrawingParameters(): DrawingParameters { return this.drawingParameters; } public get Version(): string { return this.version; } //#endregion }