OSMD.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import {IXmlElement} from "./../Common/FileIO/Xml";
  2. import {VexFlowMusicSheetCalculator} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetCalculator";
  3. import {MusicSheetReader} from "./../MusicalScore/ScoreIO/MusicSheetReader";
  4. import {GraphicalMusicSheet} from "./../MusicalScore/Graphical/GraphicalMusicSheet";
  5. import {MusicSheetCalculator} from "./../MusicalScore/Graphical/MusicSheetCalculator";
  6. import {VexFlowMusicSheetDrawer} from "./../MusicalScore/Graphical/VexFlow/VexFlowMusicSheetDrawer";
  7. import {MusicSheet} from "./../MusicalScore/MusicSheet";
  8. import {Cursor} from "./Cursor";
  9. import {MXLHelper} from "../Common/FileIO/Mxl";
  10. import {Promise} from "es6-promise";
  11. import {AJAX} from "./AJAX";
  12. import {Logging} from "../Common/Logging";
  13. import * as log from "loglevel";
  14. export class OSMD {
  15. /**
  16. * The easy way of displaying a MusicXML sheet music file
  17. * @param container is either the ID, or the actual "div" element which will host the music sheet
  18. * @autoResize automatically resize the sheet to full page width on window resize
  19. */
  20. constructor(container: string|HTMLElement, autoResize: boolean = false) {
  21. // Store container element
  22. if (typeof container === "string") {
  23. // ID passed
  24. this.container = document.getElementById(<string>container);
  25. } else if (container && "appendChild" in <any>container) {
  26. // Element passed
  27. this.container = <HTMLElement>container;
  28. }
  29. if (!this.container) {
  30. throw new Error("Please pass a valid div container to OSMD");
  31. }
  32. // Create the elements inside the container
  33. this.canvas = document.createElement("canvas");
  34. this.canvas.style.zIndex = "0";
  35. let inner: HTMLElement = document.createElement("div");
  36. inner.style.position = "relative";
  37. inner.appendChild(this.canvas);
  38. this.container.appendChild(inner);
  39. // Create the drawer
  40. this.drawer = new VexFlowMusicSheetDrawer(this.canvas);
  41. // Create the cursor
  42. this.cursor = new Cursor(inner, this);
  43. if (autoResize) {
  44. this.autoResize();
  45. }
  46. }
  47. public cursor: Cursor;
  48. public zoom: number = 1.0;
  49. private container: HTMLElement;
  50. private canvas: HTMLCanvasElement;
  51. private sheet: MusicSheet;
  52. private drawer: VexFlowMusicSheetDrawer;
  53. private graphic: GraphicalMusicSheet;
  54. /**
  55. * Load a MusicXML file
  56. * @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
  57. */
  58. public load(content: string|Document): Promise<{}> {
  59. // Warning! This function is asynchronous! No error handling is done here.
  60. this.reset();
  61. if (typeof content === "string") {
  62. let str: string = <string>content;
  63. let self: OSMD = this;
  64. if (str.substr(0, 4) === "\x50\x4b\x03\x04") {
  65. // This is a zip file, unpack it first
  66. return MXLHelper.MXLtoXMLstring(str).then(
  67. (x: string) => {
  68. return self.load(x);
  69. },
  70. (err: any) => {
  71. Logging.debug(err);
  72. throw new Error("OSMD: Invalid MXL file");
  73. }
  74. );
  75. }
  76. if (str.substr(0, 5) === "<?xml") {
  77. // Parse the string representing an xml file
  78. let parser: DOMParser = new DOMParser();
  79. content = parser.parseFromString(str, "text/xml");
  80. } else if (str.length < 2083) {
  81. // Assume now "str" is a URL
  82. // Retrieve the file at the given URL
  83. return AJAX.ajax(str).then(
  84. (s: string) => { return self.load(s); },
  85. (exc: Error) => { throw exc; }
  86. );
  87. }
  88. }
  89. if (!content || !(<any>content).nodeName) {
  90. return Promise.reject(new Error("OSMD: The document which was provided is invalid"));
  91. }
  92. let children: NodeList = (<Document>content).childNodes;
  93. let elem: Element;
  94. for (let i: number = 0, length: number = children.length; i < length; i += 1) {
  95. let node: Node = children[i];
  96. if (node.nodeType === Node.ELEMENT_NODE && node.nodeName.toLowerCase() === "score-partwise") {
  97. elem = <Element>node;
  98. break;
  99. }
  100. }
  101. if (!elem) {
  102. return Promise.reject(new Error("OSMD: Document is not a valid 'partwise' MusicXML"));
  103. }
  104. let score: IXmlElement = new IXmlElement(elem);
  105. let calc: MusicSheetCalculator = new VexFlowMusicSheetCalculator();
  106. let reader: MusicSheetReader = new MusicSheetReader();
  107. this.sheet = reader.createMusicSheet(score, "Unknown path");
  108. this.graphic = new GraphicalMusicSheet(this.sheet, calc);
  109. this.cursor.init(this.sheet.MusicPartManager, this.graphic);
  110. log.info(`Loaded sheet ${this.sheet.TitleString} successfully.`);
  111. return Promise.resolve({});
  112. }
  113. /**
  114. * Render the music sheet in the container
  115. */
  116. public render(): void {
  117. if (!this.graphic) {
  118. throw new Error("OSMD: Before rendering a music sheet, please load a MusicXML file");
  119. }
  120. let width: number = this.container.offsetWidth;
  121. // Before introducing the following optimization (maybe irrelevant), tests
  122. // have to be modified to ensure that width is > 0 when executed
  123. //if (isNaN(width) || width === 0) {
  124. // return;
  125. //}
  126. // Set page width
  127. this.sheet.pageWidth = width / this.zoom / 10.0;
  128. // Calculate again
  129. this.graphic.reCalculate();
  130. this.graphic.Cursors.length = 0;
  131. /*this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(0, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  132. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(1, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  133. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(2, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  134. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(3, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  135. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(4, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  136. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(5, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  137. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(6, 4), OutlineAndFillStyleEnum.PlaybackCursor));
  138. this.graphic.Cursors.push(this.graphic.calculateCursorLineAtTimestamp(new Fraction(7, 4), OutlineAndFillStyleEnum.PlaybackCursor));*/
  139. // Update Sheet Page
  140. let height: number = this.graphic.MusicPages[0].PositionAndShape.BorderBottom * 10.0 * this.zoom;
  141. this.drawer.resize(width, height);
  142. this.drawer.scale(this.zoom);
  143. // Finally, draw
  144. this.drawer.drawSheet(this.graphic);
  145. // Update the cursor position
  146. this.cursor.update();
  147. }
  148. /**
  149. * Sets the logging level for this OSMD instance. By default, this is set to `warn`.
  150. *
  151. * @param: content can be `trace`, `debug`, `info`, `warn` or `error`.
  152. */
  153. public setLogLevel(level: string): void {
  154. switch (level) {
  155. case "trace":
  156. log.setDefaultLevel(LogLevel.TRACE);
  157. break;
  158. case "debug":
  159. log.setDefaultLevel(LogLevel.DEBUG);
  160. break;
  161. case "info":
  162. log.setDefaultLevel(LogLevel.INFO);
  163. break;
  164. case "warn":
  165. log.setDefaultLevel(LogLevel.WARN);
  166. break;
  167. case "error":
  168. log.setDefaultLevel(LogLevel.ERROR);
  169. break;
  170. default:
  171. log.warn(`Could not set log level to ${level}. Using warn instead.`);
  172. log.setDefaultLevel(LogLevel.WARN);
  173. break;
  174. }
  175. }
  176. /**
  177. * Initialize this object to default values
  178. * FIXME: Probably unnecessary
  179. */
  180. private reset(): void {
  181. this.cursor.hide();
  182. this.sheet = undefined;
  183. this.graphic = undefined;
  184. this.zoom = 1.0;
  185. this.canvas.width = 0;
  186. this.canvas.height = 0;
  187. }
  188. /**
  189. * Attach the appropriate handler to the window.onResize event
  190. */
  191. private autoResize(): void {
  192. let self: OSMD = this;
  193. this.handleResize(
  194. () => {
  195. // empty
  196. },
  197. () => {
  198. // The following code is probably not needed
  199. // (the width should adapt itself to the max allowed)
  200. //let width: number = Math.max(
  201. // document.documentElement.clientWidth,
  202. // document.body.scrollWidth,
  203. // document.documentElement.scrollWidth,
  204. // document.body.offsetWidth,
  205. // document.documentElement.offsetWidth
  206. //);
  207. //self.container.style.width = width + "px";
  208. self.render();
  209. }
  210. );
  211. }
  212. /**
  213. * Helper function for managing window's onResize events
  214. * @param startCallback is the function called when resizing starts
  215. * @param endCallback is the function called when resizing (kind-of) ends
  216. */
  217. private handleResize(startCallback: () => void, endCallback: () => void): void {
  218. let rtime: number;
  219. let timeout: number = undefined;
  220. let delta: number = 200;
  221. function resizeEnd(): void {
  222. timeout = undefined;
  223. window.clearTimeout(timeout);
  224. if ((new Date()).getTime() - rtime < delta) {
  225. timeout = window.setTimeout(resizeEnd, delta);
  226. } else {
  227. endCallback();
  228. }
  229. }
  230. function resizeStart(): void {
  231. rtime = (new Date()).getTime();
  232. if (!timeout) {
  233. startCallback();
  234. rtime = (new Date()).getTime();
  235. timeout = window.setTimeout(resizeEnd, delta);
  236. }
  237. }
  238. if ((<any>window).attachEvent) {
  239. // Support IE<9
  240. (<any>window).attachEvent("onresize", resizeStart);
  241. } else {
  242. window.addEventListener("resize", resizeStart);
  243. }
  244. window.setTimeout(startCallback, 0);
  245. window.setTimeout(endCallback, 1);
  246. }
  247. }