text: ﻿// Vex Flow Notation
  // Mohit Muthanna <mohit@muthanna.com>
  //
  // Copyright Mohit Muthanna 2010
  //
  // Requires: vex.js, vexmusic.js, note.js
  
  /**
   * Create a new beam from the specified notes. The notes must
   * be part of the same line, and have the same duration (in ticks).
   *
   * @constructor
   * @param {Array.<Vex.Flow.StaveNote>} A set of notes.
   */
  Vex.Flow.Beam = function (notes, auto_stem) {
      if (arguments.length > 0) this.init(notes, auto_stem);
  }
  
  /**
   * Set the notes to attach this beam to.
   *
   * @param {Array.<Vex.Flow.StaveNote>} The notes.
   * @param auto_stem If true, will automatically set stem direction.
   */
  Vex.Flow.Beam.prototype.init = function (notes, auto_stem) {
      if (!notes || notes == []) {
          throw new Vex.RuntimeError("BadArguments", "No notes provided for beam.");
      }
  
      if (notes.length == 1) {
          throw new Vex.RuntimeError("BadArguments", "Too few notes for beam.");
      }
  
      this.unbeamable = false;
      // Quit if first or last note is a rest.
      if (!notes[0].hasStem() || !notes[notes.length - 1].hasStem()) {
          this.unbeamable = true;
          return;
      }
  
      // Validate beam line, direction and ticks.
      this.stem_direction = notes[0].getStemDirection();
      this.ticks = notes[0].getIntrinsicTicks();
  
      if (this.ticks >= Vex.Flow.durationToTicks("4")) {
          throw new Vex.RuntimeError("BadArguments",
              "Beams can only be applied to notes shorter than a quarter note.");
      }
  
      if (!auto_stem) {
          for (var i = 1; i < notes.length; ++i) {
              var note = notes[i];
              if (note.getStemDirection() != this.stem_direction) {
                  throw new Vex.RuntimeError("BadArguments",
                      "Notes in a beam all have the same stem direction");
              }
          }
      }
  
      var stem_direction = -1;
  
      if (auto_stem) {
          // Figure out optimal stem direction based on given notes
          this.min_line = 1000;
  
          for (var i = 0; i < notes.length; ++i) {
              var note = notes[i];
              this.min_line = Vex.Min(note.getKeyProps()[0].line, this.min_line);
          }
  
          if (this.min_line < 3) stem_direction = 1;
      }
  
      for (var i = 0; i < notes.length; ++i) {
          var note = notes[i];
          if (auto_stem) {
              note.setStemDirection(stem_direction);
              this.stem_direction = stem_direction;
          }
          note.setBeam(this);
      }
  
      this.notes = notes;
      this.beam_count = this.notes[0].getGlyph().beam_count;
      this.render_options = {
          beam_width: 5,
          max_slope: 0.25,
          min_slope: -0.25,
          slope_iterations: 20,
          slope_cost: 25
      };
  }
  
  Vex.Flow.Beam.prototype.setContext = function (context) {
      this.context = context; return this;
  }
  
  /**
   * @return {Array.<Vex.Flow.Note>} Returns notes in this beam.
   */
  Vex.Flow.Beam.prototype.getNotes = function () {
      return this.notes;
  }
  
  Vex.Flow.Beam.prototype.draw = function (notes) {
      if (!this.context) throw new Vex.RERR("NoCanvasContext",
          "Can't draw without a canvas context.");
  
      if (this.unbeamable) return;
  
      var first_note = this.notes[0];
      var last_note = this.notes[this.notes.length - 1];
  
      var first_y_px = first_note.getStemExtents().topY;
      var last_y_px = last_note.getStemExtents().topY;
  
      var first_x_px = first_note.getStemX();
      var last_x_px = last_note.getStemX();
  
      var beam_width = this.render_options.beam_width * this.stem_direction;
  
      // Returns the Y coordinate for the slope at position X.
      function getSlopeY(x) {
          return first_y_px + ((x - first_x_px) * slope);
      }
  
      var inc = (this.render_options.max_slope - this.render_options.min_slope) /
          this.render_options.slope_iterations;
      var min_cost = Number.MAX_VALUE;
      var best_slope = 0;
      var y_shift = 0;
  
      // iterate through slope values to find best weighted fit
      for (var slope = this.render_options.min_slope;
           slope <= this.render_options.max_slope;
           slope += inc) {
          var total_stem_extension = 0;
          var y_shift_tmp = 0;
  
          // iterate through notes, calculating y shift and stem extension
          for (var i = 1; i < this.notes.length; ++i) {
              var note = this.notes[i];
  
              var x_px = note.getStemX();
              var y_px = note.getStemExtents().topY;
              var slope_y_px = getSlopeY(x_px) + y_shift_tmp;
  
              // beam needs to be shifted up to accommodate note
              if (y_px * this.stem_direction < slope_y_px * this.stem_direction) {
                  var diff = Math.abs(y_px - slope_y_px);
                  y_shift_tmp += diff * -this.stem_direction;
                  total_stem_extension += (diff * i);
              } else { // beam overshoots note, account for the difference
                  total_stem_extension += (y_px - slope_y_px) * this.stem_direction;
              }
  
          }
          var cost = this.render_options.slope_cost * Math.abs(slope) +
            Math.abs(total_stem_extension);
  
          // update state when a more ideal slope is found
          if (cost < min_cost) {
              min_cost = cost;
              best_slope = slope;
              y_shift = y_shift_tmp;
          }
      }
  
      slope = best_slope;
  
      // Draw the stems
      for (var i = 0; i < this.notes.length; ++i) {
          var note = this.notes[i];
  
          // Do not draw stem for rests
          if (!note.hasStem()) {
              continue;
          }
  
          var x_px = note.getStemX();
          var y_extents = note.getStemExtents();
          var base_y_px = y_extents.baseY;
  
          // For harmonic note heads, shorten stem length by 3 pixels
          base_y_px += this.stem_direction * note.glyph.stem_offset;
  
          // Draw the stem
          this.context.fillRect(x_px, base_y_px, 1,
              ((Math.abs(base_y_px - (getSlopeY(x_px) + y_shift)))) *
              -this.stem_direction);
      }
  
      var that = this;
      function getBeamLines(duration) {
          var beam_lines = [];
          var beam_started = false;
  
          for (var i = 0; i < that.notes.length; ++i) {
              var note = that.notes[i];
              var ticks = note.getIntrinsicTicks();
  
              // Check whether to apply beam(s)
              if (ticks < Vex.Flow.durationToTicks(duration)) {
                  if (!beam_started) {
                      beam_lines.push({ start: note.getStemX(), end: null });
                      beam_started = true;
                  } else {
                      var current_beam = beam_lines[beam_lines.length - 1];
                      current_beam.end = note.getStemX();
                  }
              } else {
                  if (!beam_started) {
                      // we don't care
                  } else {
                      var current_beam = beam_lines[beam_lines.length - 1];
                      if (current_beam.end == null) {
                          // single note
                          current_beam.end = current_beam.start + 10; // TODO
                      } else {
                          // we don't care
                      }
                  }
  
                  beam_started = false;
              }
          }
  
          if (beam_started == true) {
              var current_beam = beam_lines[beam_lines.length - 1];
              if (current_beam.end == null) {
                  // single note
                  current_beam.end = current_beam.start - 10; // TODO
              }
          }
  
          return beam_lines;
      }
  
      var valid_beam_durations = ["4", "8", "16", "32"];
  
      // Draw the beams.
      for (var i = 0; i < valid_beam_durations.length; ++i) {
          var duration = valid_beam_durations[i];
          var beam_lines = getBeamLines(duration);
  
          for (var j = 0; j < beam_lines.length; ++j) {
              var beam_line = beam_lines[j];
              var first_x = beam_line.start;
              var first_y = getSlopeY(first_x);
  
              var last_x = beam_line.end;
              var last_y = getSlopeY(last_x);
  
              this.context.beginPath();
              this.context.moveTo(first_x, first_y + y_shift);
              this.context.lineTo(first_x, first_y + beam_width + y_shift);
              this.context.lineTo(last_x + 1, last_y + beam_width + y_shift);
              this.context.lineTo(last_x + 1, last_y + y_shift);
              this.context.closePath();
              this.context.fill();
          }
  
          first_y_px += beam_width * 1.5;
          last_y_px += beam_width * 1.5;
      }
  
      return true;
  }
  
  
  Vex.Flow.Beam.applyAndGetBeams = function (voice) {
      var unprocessedNotes = voice.tickables;
      var ticksPerGroup = 4096;
      var noteGroups = [];
      var currentGroup = [];
  
      function getTotalTicks(vf_notes) {
          return vf_notes.reduce(function (memo, note) {
              return note.getTicks().value() + memo;
          }, 0);
      }
  
      function createGroups() {
          var nextGroup = [];
  
          unprocessedNotes.forEach(function (unprocessedNote) {
              nextGroup = [];
              if (unprocessedNote.shouldIgnoreTicks()) return; // Ignore untickables (like bar notes)
  
              currentGroup.push(unprocessedNote);
  
              // If the note that was just added overflows the group tick total
              if (getTotalTicks(currentGroup) > ticksPerGroup) {
                  nextGroup.push(currentGroup.pop());
                  noteGroups.push(currentGroup);
                  currentGroup = nextGroup;
              } else if (getTotalTicks(currentGroup) == ticksPerGroup) {
                  noteGroups.push(currentGroup);
                  currentGroup = nextGroup;
              }
          });
  
          // Adds any remainder notes
          if (currentGroup.length > 0)
              noteGroups.push(currentGroup);
      }
  
      function getBeamGroups() {
          return noteGroups.filter(function (group) {
              return group.length > 1;
          });
      }
  
      function formatStems() {
          noteGroups.forEach(function (group) {
              var stemDirection = determineStemDirection(group);
  
              applyStemDirection(group, stemDirection);
          });
      }
  
      function determineStemDirection(group) {
          var lineSum = 0;
  
          group.forEach(function (note) {
              note.keyProps.forEach(function (keyProp) {
                  lineSum += (keyProp.line - 3);
              });
          });
  
          if (lineSum > 0)
              return -1;
          return 1;
      }
  
      function applyStemDirection(group, direction) {
          group.forEach(function (note) {
              note.setStemDirection(direction);
          });
      }
  
      function getTupletGroups() {
          return noteGroups.filter(function (group) {
              if (group[0]) return group[0].tuplet;
          });
      }
  
      // Using closures to store the variables throughout the various functions
      // IMO Keeps it this process lot cleaner - but not super consistent with
      // the rest of the API's style - Silverwolf90 (Cyril)
      createGroups();
      formatStems();
  
      // Get the notes to be beamed
      var beamedNoteGroups = getBeamGroups();
  
      // Get the tuplets in order to format them accurately
      var tupletGroups = getTupletGroups();
  
      // Create a Vex.Flow.Beam from each group of notes to be beamed
      var beams = [];
      beamedNoteGroups.forEach(function (group) {
          beams.push(new Vex.Flow.Beam(group));
      });
  
      // Reformat tuplets
      tupletGroups.forEach(function (group) {
          var firstNote = group[0];
          var tuplet = firstNote.tuplet;
  
          if (firstNote.beam) tuplet.setBracketed(false);
          if (firstNote.stem_direction == -1) {
              tuplet.setTupletLocation(Vex.Flow.Tuplet.LOCATION_BOTTOM);
          }
      });
  
      return beams;
  };