/** @constructor */
MusicXMLAnalyzer.ResultView = function(){
var that = {},
$patternValue = null,
patternCanvas = null,
$carousel = null,
$artist = null,
$title = null,
$exportButton = null,
finishedLoading = false,
$logMessages = null,
resultMessageCounter = null,
/**
* Init function
* @function
* @public
*/
init = function(){
if ($('#patternCanvas').length) {
$patternValue = $('#patternValue');
initPatternCanvas(JSON.parse($patternValue.val()));
}
if ($('#extract-carousel').length) {
$carousel = $('#extract-carousel');
initResultItems();
}
$artist = $('#artist');
$title = $('#title');
$exportButton = $('.exportButton');
$exportButton.on('click', generateExportPdf);
$exportButton.addClass('disabled');
$('.list-item').on('click', onListItemClick);
$logMessages = $('#resultMessages');
resultMessageCounter = 0;
},
/**
* Method preapares model export
* @function
* @public
*/
setModelReady = function() {
console.info("MusicXMLAnalyzer.ResultView.setModelReady");
finishedLoading = true;
prepareExport();
},
/**
* Method inits result items
* @function
* @public
*/
initResultItems = function() {
var numItems = $carousel.find('.item').length;
$carousel.find('.item').each(function(index) {
var result = JSON.parse($(this).find('.resultItem').val());
$(that).trigger('addResultItem', [numItems, result]);
});
},
/**
* Method renders result extract
* @function
* @public
*
* @param {int} index counter
* @param {object} data contains information about extract position
*/
renderResultExtract = function(index, data) {
var measuresText = '<strong>Measures: </strong>' + data.start_extract + ' - ' + data.end_extract;
$carousel.find('#item' + index).find('.facts-list').find('.measures').html(measuresText);
$carousel.find('#resultExtract' + index).val(JSON.stringify(data));
// initialize canvas
var canvas = document.createElement('canvas');
canvas.id = "canvas" + index;
canvas.className = "canvas";
canvas.width = 970;
canvas.height = Math.ceil(data.measures.length / 2) * 180;
var canvasContainer = document.getElementById('canvasContainer' + index);
canvasContainer.innerHTML = "";
canvasContainer.appendChild(canvas);
var measures = generateVexflowNotes(data, true);
var renderer = new Vex.Flow.Renderer(canvas, Vex.Flow.Renderer.Backends.CANVAS);
var context = renderer.getContext();
var stave = new Vex.Flow.Stave(10, 0, 700);
stave.addClef("treble").setContext(context).draw();
renderNotes(measures, canvas, renderer, context, stave, false);
},
/**
* Method prepares pdf export
* @function
* @public
*/
prepareExport = function() {
$('.item').each(function(index) {
var dimensions = calculateDimensions(this);
resultImage = resizedataURL(dimensions.canvasImg, dimensions.width, dimensions.height, index);
});
$exportButton.removeClass('disabled');
},
/**
* Method to calculate width & height for resizing images (pdf donwload)
* @function
* @public
*/
calculateDimensions = function(ele) {
var itemCanvas = $(ele).find('.canvas')[0];
var canvasImg = itemCanvas.toDataURL("image/jpeg", 1.0);
width = 700;
if (itemCanvas.width > 0 || itemCanvas.height > 0) {
height = (parseFloat(width) * parseFloat(itemCanvas.height)) / parseFloat(itemCanvas.width);
return { canvasImg: canvasImg, width: width, height: height };
} else {
alert("Something went wrong. Try reloading the page.");
}
},
/**
* Method creates images from carousel data
* @function
* @public
*
* @param {URI} datas carousel image uri
* @param {float} wantedWidth width of the image
* @param {float} wantedHeight height of the image
* @param {int} index counter
*/
resizedataURL = function(datas, wantedWidth, wantedHeight, index) {
// We create an image to receive the Data URI
var img = document.createElement('img');
// When the event "onload" is triggered we can resize the image.
img.onload = function() {
// We create a canvas and get its context.
var can = document.createElement('canvas');
// We set the dimensions at the wanted size.
can.width = wantedWidth;
can.height = wantedHeight;
// We resize the image with the canvas method drawImage();
can.getContext('2d').drawImage(img, 0, 0, wantedWidth, wantedHeight);
var dataURI = can.toDataURL("image/jpeg", 1.0);
// return dataURI;
addImageToDOM(index, dataURI);
};
// We put the Data URI in the image's src attribute
img.src = datas;
},
/**
* Method adds image to dom
* @function
* @public
*
* @param {int} index counter
* @param {string} dataURI uri to image data
*/
addImageToDOM = function(index, dataURI) {
$('#image' + index).val(dataURI);
},
/**
* Method generates pdf export
* @function
* @public
*/
generateExportPdf = function() {
var doc = new jsPDF();
// add title page
doc.setFontSize(36);
doc.text(15, 30, "SEARCH RESULTS");
// insert pattern
doc.setFontSize(14);
doc.text(15, 70, "for your pattern:");
var patternImg = patternCanvas.toDataURL("image/jpeg", 1.0);
doc.addImage(patternImg, "JPEG", 15, 80);
// insert artist and title
doc.setFontSize(24);
doc.text(15, 180, "Artist: " + $artist.text());
doc.text(15, 200, "Title: " + $title.text());
doc.setFontSize(14);
doc.text(15, 240, "created with MusicXMLAnalyzer");
doc.text(15, 250, "http://musicxmlanalyzer.de");
var pageHeader = $artist.text() + " - " + $title.text();
// add page for each result
$('.item').each(function(index) {
doc.addPage();
var pageNumber = "" + (index + 2);
// insert page number
doc.setFontSize(10);
doc.text(15, 20, pageHeader);
doc.text(190, 20, pageNumber);
// insert facts
doc.setFontSize(14);
doc.text(15, 40, $(this).find('.partName').text());
doc.text(15, 50, $(this).find('.partId').text());
doc.text(15, 60, $(this).find('.voice').text());
doc.text(15, 70, $(this).find('.key').text());
doc.text(15, 80, $(this).find('.measures').text());
// insert result extract
var resultimg = $(this).find('.image').val();
try {
doc.addImage(resultimg, "JPEG", 15, 100);
} catch (e) {
alert("An error occured generating the pdf");
console.error(e);
return false;
}
});
// save doc
doc.save("search_results.pdf");
},
/**
* Method renders pattern canvas above result carousel
* @function
* @public
*
* @param {object} pattern user created pattern
*/
initPatternCanvas = function(pattern) {
patternCanvas = document.getElementById('patternCanvas');
var vexflowNotes = generateVexflowNotes({ measures: [{ notes: pattern.notes }], type: pattern.type }, false);
var renderer = new Vex.Flow.Renderer(patternCanvas, Vex.Flow.Renderer.Backends.CANVAS);
var context = renderer.getContext();
var stave = new Vex.Flow.Stave(10, 0, 700);
renderNotes(vexflowNotes, patternCanvas, renderer, context, stave, true);
},
/**
* Method renders result extract
* @function
* @public
*
* @param {array} measures array containing the measures of result extract
* @param {object} canvas the canvas proportions
* @param {canvas} context the canvas context
* @param {object} pattern the user pattern
*/
renderNotes = function(measures, canvas, renderer, context, stave, pattern) {
// clear canvas
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#EEEEEE';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#000000';
var voice = new Vex.Flow.Voice({
num_beats: 4,
beat_value: 4,
resolution: Vex.Flow.RESOLUTION
});
// disable strict timing
voice.setStrict(false);
var tieStart = null;
var tieStop = null;
var ties = [];
for (var i = 0; i < measures.length; i++) {
// calculate x & y coordinates and width of the current measure
var x, y, width;
width = 480;
height = 180;
padding = 5;
if (i%2 === 0) {
x = padding;
y = i * (height / 2);
} else {
x = padding + width;
y = (i - 1) * (height / 2);
}
if (pattern) {
width = 690;
height = 120;
}
// Add offset from top to center vertical
y += 25;
staveBar = new Vex.Flow.Stave(x, y, width); // generate new stave for measure
if (i%2 === 0) {
if (pattern && measures[i].pattern.type === 1) {
staveBar.addClef("percussion");
} else {
staveBar.addClef("treble"); // add clef to every measure starting in a new line
}
}
if (measures[i].time_signature) {
staveBar.addTimeSignature(measures[i].time_signature); // add time signature if changed
}
if (i > 0 && i < measures.length-1) {
staveBar.setBegBarType(Vex.Flow.Barline.type.SINGLE); // set measure bar line
}
if (i === measures.length-1) {
staveBar.setEndBarType(Vex.Flow.Barline.type.END); // set double measure bar line at last measure
}
// creating ties between notes
for (var j = 0; j < measures[i].notes.length; j++) {
// ties
if (measures[i].ties) {
if (measures[i].ties[j] !== false && measures[i].ties[j] !== undefined) {
if (measures[i].ties[j].indexOf("stop") > -1) {
tieStop = measures[i].notes[j];
if (tieStart !== null) {
var tie = new Vex.Flow.StaveTie({ first_note: tieStart, last_note: tieStop, first_indices: [0], last_indices: [0] });
ties.push(tie);
tieStart = null;
tieStop = null;
}
}
if (measures[i].ties[j].indexOf("start") > -1) {
tieStart = measures[i].notes[j];
}
}
}
}
// tuplets
var tuplets = [];
for (var j = 0; j < measures[i].notes.length; j++) {
if (measures[i].tuplets && measures[i].tuplets[j]) {
if (measures[i].tuplets[j].toString() !== 'false' && measures[i].tuplets[j].toString() !== 'undefined') {
var tupletNotes = measures[i].notes.slice(j, (j + parseInt(measures[i].tuplets[j])));
var tupletLocation = tupletNotes[0].stem.stem_direction;
var tuplet = new Vex.Flow.Tuplet(tupletNotes);
tuplet.setTupletLocation(tupletLocation);
tuplets.push(tuplet);
j = (j + parseInt(measures[i].tuplets[j])-1);
}
}
}
// draw measure bar line
staveBar.setContext(context).draw();
// draw measure with notes
var beams = Vex.Flow.Beam.generateBeams(measures[i].notes);
Vex.Flow.Formatter.FormatAndDraw(context, staveBar, measures[i].notes);
beams.forEach(function(beam) {
beam.setContext(context).draw();
});
// draw tuplets
for (var t = 0; t < tuplets.length; t++) {
tuplets[t].setContext(context).draw();
}
}
for (var t2 = 0; t2 < ties.length; t2++) {
ties[t2].setContext(context).draw();
}
},
/**
* Method generates vexflow notes
* @function
* @public
*
* @param {object} pattern the user pattern
* @param {object} result search result
*/
generateVexflowNotes = function(pattern, result) {
// prepare pattern if no result from ResultController.php
if (!result) {
for (var i = 0; i < pattern.measures.length; i++) {
for (var j = 0; j < pattern.measures[i].notes.length; j++) {
if (pattern.measures[i].notes[j].pitch && pattern.measures[i].notes[j].pitch.beam) {
pattern.measures[i].notes[j].pitch.tuplet = "3";
} else if(pattern.measures[i].notes[j].pitch) {
pattern.measures[i].notes[j].pitch.tuplet = false;
}
}
}
}
var measures = [];
switch (pattern.type) {
case 0:
// sound sequence
for (var i = 0; i < pattern.measures.length; i++) { // iterate over measures in result
var notes = []; //creating notes array for notes in current measure
var time_signature = pattern.measures[i].time_signature;
for (var j = 0; j < pattern.measures[i].notes.length; j++) { // iterate over all notes in current measure
var step = pattern.measures[i].notes[j].pitch.step; // determine the step
var octave = pattern.measures[i].notes[j].pitch.octave; // determine the octave
var alter = pattern.measures[i].notes[j].pitch.alter; // determine the alter
var keys = [getVexflowKey(step, octave, alter )]; // generating key in vexflow format
var note = new Vex.Flow.StaveNote({ keys: keys, duration: "q", auto_stem: true });
if (alter == -1) { // if accidental should be "b"
note.addAccidental(0, new Vex.Flow.Accidental("b")); // add "b"
} else if (alter == 1) { // if accidental should be "#"
note.addAccidental(0, new Vex.Flow.Accidental("#")); // add "#"
}
notes.push(note);
}
measures.push({ notes: notes, time_signature: time_signature, pattern: pattern }); // push note to array
}
break;
case 1:
// rhythm
for (var i = 0; i < pattern.measures.length; i++) {
var notes = [];
var tuplets = [];
var time_signature = pattern.measures[i].time_signature;
for (var j = 0; j < pattern.measures[i].notes.length; j++) {
// set color of current note
var note;
if (pattern.measures[i].notes[j].type === "note") {
// determine note variables
var type = pattern.measures[i].notes[j].pitch.type;
if (type === "whole" || type === "half") {
var keys = ["b/4/d0"];
} else {
var keys = ["b/4/d2"];
}
var tuplet = false;
if (pattern.measures[i].notes[j].pitch.beam) {
var beam = pattern.measures[i].notes[j].pitch.beam;
if (beam === "begin" || beam === "continue" || beam === "end") {
tuplet = "3";
}
}
var durationType = 0;
if (pattern.measures[i].notes[j].pitch.dot) {
durationType = 2;
}
var noteDuration = getVexflowDuration(type, durationType);
note = new Vex.Flow.StaveNote({ keys: keys, duration: noteDuration, auto_stem: true });
if (pattern.measures[i].notes[j].pitch.dot) {
note.addDotToAll();
}
tuplets[j] = tuplet;
notes.push(note);
} else if (pattern.measures[i].notes[j].type === "rest") {
var durationType = 1; // rests type is 1
var noteDuration = getVexflowDuration(pattern.measures[i].notes[j].duration, durationType);
note = new Vex.Flow.StaveNote({ keys: ["b/4"], duration: noteDuration });
if (pattern.measures[i].notes[j].dot) {
note.addDotToAll();
}
tuplets[j] = [false];
notes.push(note);
}
}
measures.push({ notes: notes, tuplets: tuplets, time_signature: time_signature, pattern: pattern });
}
break;
case 2:
// melody
for (var i = 0; i < pattern.measures.length; i++) {
var notes = [];
noteCounter = 0;
// var beams = [];
var ties = [];
var tuplets = [];
var time_signature = pattern.measures[i].time_signature;
if (pattern.measures[i].notes) {
for (var j = 0; j < pattern.measures[i].notes.length; j++) {
// set color of current note
var color = pattern.measures[i].notes[j].color;
var note;
if (pattern.measures[i].notes[j].type === "note") {
if (!pattern.measures[i].notes[j].pitch.chord) {
// determine note variables
var step = pattern.measures[i].notes[j].pitch.step;
var octave = pattern.measures[i].notes[j].pitch.octave;
var alter = pattern.measures[i].notes[j].pitch.alter;
var keys = [getVexflowKey(step, octave, alter )];
var noteTies = [false];
if (pattern.measures[i].notes[j].pitch.ties) {
noteTies = pattern.measures[i].notes[j].pitch.ties;
}
var tuplet = false;
if (pattern.measures[i].notes[j].pitch.tuplet) {
tuplet = pattern.measures[i].notes[j].pitch.tuplet;
}
var type = pattern.measures[i].notes[j].pitch.type;
var durationType = 0;
if (pattern.measures[i].notes[j].pitch.dot) {
durationType = 2;
}
var noteDuration = getVexflowDuration(type, durationType);
note = new Vex.Flow.StaveNote({ keys: keys, duration: noteDuration, auto_stem: true });
note.color = color;
note = checkNextNotes(pattern, note, i, j);
switch (alter) {
case -2: note.addAccidental(0, new Vex.Flow.Accidental("bb")); break;
case -1: note.addAccidental(0, new Vex.Flow.Accidental("b")); break;
case 1: note.addAccidental(0, new Vex.Flow.Accidental("#")); break;
case 2: note.addAccidental(0, new Vex.Flow.Accidental("#")); break;
}
if (pattern.measures[i].notes[j].pitch.dot) {
note.addDotToAll();
}
ties[noteCounter] = noteTies;
tuplets[noteCounter] = tuplet;
notes.push(note);
noteCounter++;
}
} else if (pattern.measures[i].notes[j].type === "rest") {
var durationType = 1; // rests type is 1
var noteDuration = getVexflowDuration(pattern.measures[i].notes[j].duration, durationType);
note = new Vex.Flow.StaveNote({ keys: ["b/4"], duration: noteDuration });
note.color = color;
if (pattern.measures[i].notes[j].dot) {
note.addDotToAll();
}
ties[noteCounter] = [false];
notes.push(note);
noteCounter++;
} else if (pattern.measures[i].notes[j].type === "unpitched") {
var step = pattern.measures[i].notes[j].pitch.step;
var octave = pattern.measures[i].notes[j].pitch.octave;
var alter = pattern.measures[i].notes[j].pitch.alter;
var keys = [getVexflowKey(step, octave, alter ) + "/d2"];
var noteTies = [false];
if (pattern.measures[i].notes[j].pitch.ties) {
noteTies = pattern.measures[i].notes[j].pitch.ties;
}
var type = pattern.measures[i].notes[j].pitch.type;
var durationType = 0;
if (pattern.measures[i].notes[j].pitch.dot) {
durationType = 2;
}
var noteDuration = getVexflowDuration(type, durationType);
note = new Vex.Flow.StaveNote({ keys: keys, duration: noteDuration, auto_stem: true});
note.color = color;
note = checkNextNotes(pattern, note, i, j);
if (pattern.measures[i].notes[j].pitch.dot) {
note.addDotToAll();
}
ties[noteCounter] = noteTies;
notes.push(note);
noteCounter++;
}
}
}
measures.push({ notes: notes, ties: ties, tuplets: tuplets, time_signature: time_signature, pattern: pattern });
}
break;
}
return measures;
},
/**
* Method checks if the following note belongs to current chord
* Also handles highlighting of chords in results
* @function
* @public
*
* @param {object} pattern array containing notes
* @param {Vex.Flow.Note} note the current vexflow note
* @param {int} i counter of the current measure
* @param {int} j counter of the current note
*
* @return {Vex.Flow.Note} Returns a vexflow compatible note object
*/
checkNextNotes = function(pattern, note, i, j) {
j++;
var newNote = note;
var newKeys = note.keys;
if (pattern.measures[i].notes[j]) {
if (pattern.measures[i].notes[j].pitch) {
if (pattern.measures[i].notes[j].pitch.chord) {
var step = pattern.measures[i].notes[j].pitch.step;
var octave = pattern.measures[i].notes[j].pitch.octave;
var alter = pattern.measures[i].notes[j].pitch.alter;
newKeys.push(getVexflowKey(step, octave, alter));
newNote = null;
newNote = new Vex.Flow.StaveNote({ keys: newKeys, duration: note.duration, auto_stem: true });
newNote = checkNextNotes(pattern, newNote, i, j);
if (pattern.measures[i].notes[j].color == "#b71c1c" || note.color == "#b71c1c") {
newNote.color = "#b71c1c";
} else {
newNote.color = note.color;
}
}
}
}
return newNote;
},
/**
* Method converts duration from string to number values as 1/64
* @function
* @public
*
* @param {string} duration string of note duration
*
* @return {number} duration value as number
*
*/
getDurationIn64th = function(duration) {
switch (duration) {
case "whole":
return 64; break;
case "half":
return 32; break;
case "quarter":
return 16; break;
case "eighth":
return 8; break;
case "16th":
return 4; break;
case "32nd":
return 2; break;
case "64th":
return 1; break;
default:
return 0; break;
}
},
/**
* Method returns the note duration in vexflow style
* @function
* @public
*
* @param {string} duration duration of note
* @param {number} type type of note (0 = note, 1 = rest, 2 = dotted note)
*
* @return {string} duration for vexflow
*/
getVexflowDuration = function(duration, type) {
switch (duration) {
case "whole":
switch (type) {
case 0: return "w"; break; // 0 is normal note
case 1: return "wr"; break; // 1 is a rest
case 2: return "wd"; break; // 2 is a dotted note
}
break;
case "half":
switch (type) {
case 0: return "h"; break;
case 1: return "hr"; break;
case 2: return "hd"; break;
}
break;
case "quarter":
switch (type) {
case 0: return "q"; break;
case 1: return "qr"; break;
case 2: return "qd"; break;
}
break;
case "eighth":
switch (type) {
case 0: return "8"; break;
case 1: return "8r"; break;
case 2: return "8d"; break;
}
break;
case "16th":
switch (type) {
case 0: return "16"; break;
case 1: return "16r"; break;
case 2: return "16d"; break;
}
break;
case "32nd":
switch (type) {
case 0: return "32"; break;
case 1: return "32r"; break;
case 2: return "32d"; break;
}
break;
case "64th":
switch (type) {
case 0: return "64"; break;
case 1: return "64r"; break;
case 2: return "64d"; break;
}
break;
default:
switch (type) {
case 0: return "128"; break;
case 1: return "128r"; break;
case 2: return "128d"; break;
}
break;
}
},
/**
* Method returns key description for vexflow
* @function
* @public
*
* @param {string} step note name
* @param {string} octave octave number
* @param {string} alter accidential of the note
*
* @return {string} key description for vexflow
*/
getVexflowKey = function(step, octave, alter) {
key = step.toLowerCase();
switch (alter) {
case -2:
key += "bb"; break;
case -1:
key += "b"; break;
case 1:
key += "#"; break;
case 2:
key += "##"; break;
default:
break;
}
key += "/";
key += octave;
return key;
},
/**
* Gets called when a list item has been clicked
* @function
* @public
*
* @param {Event} event the triggered click event
*
*/
onListItemClick = function(event) {
initLogMessages();
addLogMessage("We're preparing your results.");
window.setTimeout(function() {
addLogMessage("We're working.");
window.setTimeout(function() {
addLogMessage("Please be patient.");
window.setTimeout(function() {
addLogMessage("Don't worry we didn't forget you.");
window.setTimeout(function() {
addLogMessage("Okay. We're ready soon. We promise.");
window.setTimeout(function() {
addLogMessage("Maybe a little coffee?");
}, 3000);
}, 3000);
}, 3000);
}, 3000);
}, 3000);
},
/**
* Inits the log messages
* @function
* @public
*
*/
initLogMessages = function() {
resultMessageCounter = 0;
$logMessages.show();
$logMessages.animate({
height: 100
}, 500);
},
/**
* Disposes log messages
* @function
* @public
*
*/
disposeLogMessages = function() {
window.setTimeout(function() {
$logMessages.animate({
height: 0
},
700,
function() {
$logMessages.hide();
$logMessages.empty();
});
}, 100);
},
/**
* Adds a log message
* @function
* @public
*
* @param {string} msg log message
*
*/
addLogMessage = function(msg) {
$('#log' + (resultMessageCounter - 3)).animate({
"marginTop": "-30px"
}, 200);
$logMessages.append('<div id="log' + resultMessageCounter + '"></div>');
$('#log' + resultMessageCounter).typed({
strings: ['<p>' + msg + '</p>'],
backDelay: 100000000000000,
typeSpeed: 0,
backSpeed: 0,
loop: true,
});
resultMessageCounter++;
};
that.init = init;
that.renderResultExtract = renderResultExtract;
that.setModelReady = setModelReady;
return that;
}