<commit_before>function day(){
    // if day is not over, continue. else end day
    if(daylight_passed < 5){
        // if day just started, clear previous days events and change start-day link to text
        if(daylight_passed === 0){
            interval_value = document.getElementById('day-duration').value;
            if(isNaN(interval_value)
              || interval_value <= 0){
                interval_value = 600;
            }

            document.getElementById('day-events').innerHTML = '';
            document.getElementById('start-day').innerHTML = 'Day Progressing...';
        }

        var output = '';

        // generate random event value
        var event = Math.random();
        var event_result = 0;

        // no event
        if(event < .72){
            output = '-----';

        // food event
        }else if(event < .78){
            // generate how much food
            event_result = random_number(2);

            if(Math.random() < .5){
                // food lost
                food -= event_result;

                if(food < 0){
                    // lose 2 gold per food under 0
                    gold += food * 2;
                    food = 0;
                }

                output = 'Bugs! -';

            }else{
                // food gained
                food += event_result;
                output = 'Rain! +';
            }
            output += event_result + ' Food';

        // gold event
        }else if(event < .84){
            // generate how much gold
            event_result = random_number(2);

            if(Math.random() < .5){
                // lose gold
                gold -= event_result;
                output = 'Theives! -';

            }else{
                // gain gold
                gold += event_result;
                output = 'Mining! +';
            }

            output += event_result + ' Gold';

        // stone event
        }else if(event < .9){
            // generate how much stone
            event_result = random_number(2);

            if(Math.random() < .5){
                // lose stone
                stone -= event_result;

                if(stone < 0){
                    // lose 2 gold per stone under 0
                    gold += stone * 2;
                    stone = 0;
                }

                output = 'Repair! -';

            }else{
                // gain stone
                stone += event_result;
                output = 'Mining! +';
            }
            output += event_result + ' Stone';

        // population event
        }else if(event < .96){
            if(Math.random() < .5){
                // lose people
                if(people > 0){
                    people -= 1;
                    delete_people(0);
                }
                output = 'Sickness! -';

            }else{
                // gain people
                people += 1;
                unemployed_workers += 1;
                output = 'Recruitment! +';
            }

            // update food bonus based on current population
            food_bonus = food_workers * 2 - people;

            output += '1 Population';

        // other events, not yet implemented
        }else if(event < .99){
            event_result = random_number(2);
            if(event_result === 0){
                output = 'Battle Event (NYI)';

            }else{
                output = 'Other Event (NYI)';
            }

        // resource bonus event
        }else{
            // generate which resource
            event_result = random_number(4);

            if(event_result === 0){
                output = 'Seeds! +1 Food/day';
                food_bonus += 1;
                document.getElementById('food-bonus').innerHTML = food_bonus;

            }else if(event_result === 1){
                output = 'Veins! +1 Gold/day';
                gold_bonus += 1;
                document.getElementById('gold-bonus').innerHTML = gold_bonus;

            }else if(event_result === 2){
                output = 'Popularity! +1 Population/day';
                people_bonus += 1;
                document.getElementById('people-bonus').innerHTML = people_bonus;

            }else{
                output = 'Rocks! +1 Stone/day';
                stone_bonus += 1;
                document.getElementById('stone-bonus').innerHTML = stone_bonus;

            }
        }

        // add event to list of day-events
        document.getElementById('day-events').innerHTML +=
          output + '
';

        // more daylight has passed
        daylight_passed += 1;

        // if day is not over, wait interval_value ms for next event
        // interval_value is set by the day-duration input field
        if(daylight_passed < 5){
            setTimeout(
              'day()',
              interval_value
            );

        // otherwise move on
        }else{
            day();
        }

    }else{
        // end day
        block_unload = 1;
        daylight_passed = 0;

        // update resources with daily bonuses
        food += food_bonus;
        food_bonus -= people_bonus;
        gold += gold_bonus;
        people += people_bonus;
        stone += stone_bonus;
        unemployed_workers += people_bonus;

        // calculate if there is not enough food to feed population
        if(food + food_bonus < 0){
            // if not enough food, decrease population and workers
            delete_people(people - 1);
            people -= people - (food + food_bonus);

            if(people < 0){
                people = 0;
            }

            // update new food_bonus
            food = 0;
            food_bonus = 0;
        }

        // update start-day text with start new day or game over message
        document.getElementById('start-day').innerHTML = people > 0
          ? 'Start New Day'
          : 'Your Castle Has Fallen. :(
Start Over';
    }

    // update text displays
    document.getElementById('food').innerHTML = food;
    document.getElementById('food-bonus').innerHTML = (food_bonus >= 0 ? '+' : '') + food_bonus;
    document.getElementById('gold').innerHTML = gold;
    document.getElementById('people').innerHTML = people;
    document.getElementById('stone').innerHTML = stone;
    document.getElementById('unemployed-workers').innerHTML = unemployed_workers;
}

function delete_people(count){
    do{
        // if no workers are unemployed
        if(unemployed_workers < 1){
            // decrease people workers first
            if(people_workers > 0){
                people_bonus -= 1;
                people_workers -= 1;

                document.getElementById('people-bonus').innerHTML = people_workers;
                document.getElementById('people-workers').innerHTML = people_workers;

            // if no people workers, decrease stone workers
            }else if(stone_workers > 0){
                stone_bonus -= 1;
                stone_workers -= 1;

                document.getElementById('stone-bonus').innerHTML = stone_workers;
                document.getElementById('stone-workers').innerHTML = stone_workers;

            // if no stone workers, decrease gold workers
            }else if(gold_workers > 0){
                gold_bonus -= 1;
                gold_workers -= 1;

                document.getElementById('gold-bonus').innerHTML = gold_workers;
                document.getElementById('gold-workers').innerHTML = gold_workers;

            // if no gold workers, decrease food workers
            }else{
                food_workers -= 1;
                document.getElementById('food-workers').innerHTML = food_workers;
            }

        // else decrease unemployed workers
        }else{
            unemployed_workers -= 1;
        }
    }while(count--);
}

function distribute_workers(resource,amount){
    // positive amount = decrease workers, negative amount = increase workers

    // if a day is not in progress and there are either unemployed workers or workers being decreased
    if(daylight_passed === 0
      && (unemployed_workers > 0 || amount < 0)){
        // alter food workers
        if(resource === 0){
            if(food_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                food_bonus += amount * 2;
                food_workers += amount;

            }else{
                food_workers = 0;

            }
            document.getElementById('food-bonus').innerHTML = (food_bonus >= 0 ? '+' : '') + food_bonus;
            document.getElementById('food-workers').innerHTML = food_workers;

        // alter gold workers
        }else if(resource === 1){
            if(gold_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                gold_bonus += amount;
                gold_workers += amount;

            }else{
                gold_workers = 0;

            }
            document.getElementById('gold-bonus').innerHTML = gold_bonus;
            document.getElementById('gold-workers').innerHTML = gold_workers;

        // alter people workers
        }else if(resource === 2){
            if(people_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                people_bonus += amount;
                people_workers += amount;

            }else{
                people_workers = 0;
            }

            document.getElementById('people-bonus').innerHTML = people_bonus;
            document.getElementById('people-workers').innerHTML = people_workers;

        // alter stone workers
        }else{
            if(stone_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                stone_bonus += amount;
                stone_workers += amount;

            }else{
                stone_workers = 0;
            }

            document.getElementById('stone-bonus').innerHTML = stone_bonus;
            document.getElementById('stone-workers').innerHTML = stone_workers;
        }
        document.getElementById('unemployed-workers').innerHTML = unemployed_workers;
    }
}

function new_game(){
    block_unload = 0;
    daylight_passed = 0;
    food = 10;
    food_bonus = -1;
    food_workers = 0;
    gold = 0;
    gold_bonus = 0;
    gold_workers = 0;
    people = 1;
    people_bonus = 0;
    people_workers = 0;
    stone = 0;
    stone_bonus = 0;
    stone_workers = 0;
    unemployed_workers = 1;

    // reset all text displays
    document.getElementById('food').innerHTML = food;
    document.getElementById('food-bonus').innerHTML = food_bonus;
    document.getElementById('food-workers').innerHTML = food_workers;
    document.getElementById('gold').innerHTML = gold;
    document.getElementById('gold-bonus').innerHTML = gold_bonus;
    document.getElementById('gold-workers').innerHTML = gold_workers;
    document.getElementById('people').innerHTML = people;
    document.getElementById('people-bonus').innerHTML = people_bonus;
    document.getElementById('people-workers').innerHTML = people_workers;
    document.getElementById('stone').innerHTML = stone;
    document.getElementById('stone-bonus').innerHTML = stone_bonus;
    document.getElementById('stone-workers').innerHTML = stone_workers;
    document.getElementById('unemployed-workers').innerHTML = unemployed_workers;

    document.getElementById('day-events').innerHTML = '';
    document.getElementById('start-day').innerHTML = 'Start New Day';
}

function random_number(i){
    return Math.floor(Math.random() * i) + 1;
}

var block_unload = 0;
var daylight_passed = 0;
var food = 10;
var food_bonus = -1;
var food_workers = 0;
var gold = 0;
var gold_bonus = 0;
var gold_workers = 0;
var interval_value = 0;
var people = 1;
var people_bonus = 0;
var people_workers = 0;
var stone = 0;
var stone_bonus = 0;
var stone_workers = 0;
var unemployed_workers = 1;

window.onbeforeunload = function(){
    // warn players if they have already made progress
    if(block_unload
      && people > 0){
        return 'Save feature will be implemented in the future.';
    }
};

window.onkeydown = function(e){
    var key = window.event ? event : e;
    key = key.charCode ? key.charCode : key.keyCode;

    // if new day can be started, any key except for integer keys will start it
    if(daylight_passed === 0
      && people > 0
      && (key < 48 || key > 57)){
        day();
    }
}
<commit_msg>removed custom random_number function<commit_after>function day(){
    // if day is not over, continue. else end day
    if(daylight_passed < 5){
        // if day just started, clear previous days events and change start-day link to text
        if(daylight_passed === 0){
            interval_value = document.getElementById('day-duration').value;
            if(isNaN(interval_value)
              || interval_value <= 0){
                interval_value = 600;
            }

            document.getElementById('day-events').innerHTML = '';
            document.getElementById('start-day').innerHTML = 'Day Progressing...';
        }

        var output = '';

        // generate random event value
        var event = Math.random();
        var event_result = 0;

        // no event
        if(event < .72){
            output = '-----';

        // food event
        }else if(event < .78){
            // generate how much food
            event_result = Math.floor(Math.random() * 2) + 1;

            if(Math.random() < .5){
                // food lost
                food -= event_result;

                if(food < 0){
                    // lose 2 gold per food under 0
                    gold += food * 2;
                    food = 0;
                }

                output = 'Bugs! -';

            }else{
                // food gained
                food += event_result;
                output = 'Rain! +';
            }
            output += event_result + ' Food';

        // gold event
        }else if(event < .84){
            // generate how much gold
            event_result = Math.floor(Math.random() * 2) + 1;

            if(Math.random() < .5){
                // lose gold
                gold -= event_result;
                output = 'Theives! -';

            }else{
                // gain gold
                gold += event_result;
                output = 'Mining! +';
            }

            output += event_result + ' Gold';

        // stone event
        }else if(event < .9){
            // generate how much stone
            event_result = Math.floor(Math.random() * 2) + 1;

            if(Math.random() < .5){
                // lose stone
                stone -= event_result;

                if(stone < 0){
                    // lose 2 gold per stone under 0
                    gold += stone * 2;
                    stone = 0;
                }

                output = 'Repair! -';

            }else{
                // gain stone
                stone += event_result;
                output = 'Mining! +';
            }
            output += event_result + ' Stone';

        // population event
        }else if(event < .96){
            if(Math.random() < .5){
                // lose people
                if(people > 0){
                    people -= 1;
                    delete_people(0);
                }
                output = 'Sickness! -';

            }else{
                // gain people
                people += 1;
                unemployed_workers += 1;
                output = 'Recruitment! +';
            }

            // update food bonus based on current population
            food_bonus = food_workers * 2 - people;

            output += '1 Population';

        // other events, not yet implemented
        }else if(event < .99){
            event_result = Math.floor(Math.random() * 2);
            if(event_result === 0){
                output = 'Battle Event (NYI)';

            }else{
                output = 'Other Event (NYI)';
            }

        // resource bonus event
        }else{
            // generate which resource
            event_result = Math.floor(Math.random() * 4);

            if(event_result === 0){
                output = 'Seeds! +1 Food/day';
                food_bonus += 1;
                document.getElementById('food-bonus').innerHTML = food_bonus;

            }else if(event_result === 1){
                output = 'Veins! +1 Gold/day';
                gold_bonus += 1;
                document.getElementById('gold-bonus').innerHTML = gold_bonus;

            }else if(event_result === 2){
                output = 'Popularity! +1 Population/day';
                people_bonus += 1;
                document.getElementById('people-bonus').innerHTML = people_bonus;

            }else{
                output = 'Rocks! +1 Stone/day';
                stone_bonus += 1;
                document.getElementById('stone-bonus').innerHTML = stone_bonus;

            }
        }

        // add event to list of day-events
        document.getElementById('day-events').innerHTML +=
          output + '
';

        // more daylight has passed
        daylight_passed += 1;

        // if day is not over, wait interval_value ms for next event
        // interval_value is set by the day-duration input field
        if(daylight_passed < 5){
            setTimeout(
              'day()',
              interval_value
            );

        // otherwise move on
        }else{
            day();
        }

    }else{
        // end day
        block_unload = 1;
        daylight_passed = 0;

        // update resources with daily bonuses
        food += food_bonus;
        food_bonus -= people_bonus;
        gold += gold_bonus;
        people += people_bonus;
        stone += stone_bonus;
        unemployed_workers += people_bonus;

        // calculate if there is not enough food to feed population
        if(food + food_bonus < 0){
            // if not enough food, decrease population and workers
            delete_people(people - 1);
            people -= people - (food + food_bonus);

            if(people < 0){
                people = 0;
            }

            // update new food_bonus
            food = 0;
            food_bonus = 0;
        }

        // update start-day text with start new day or game over message
        document.getElementById('start-day').innerHTML = people > 0
          ? 'Start New Day'
          : 'Your Castle Has Fallen. :(
Start Over';
    }

    // update text displays
    document.getElementById('food').innerHTML = food;
    document.getElementById('food-bonus').innerHTML = (food_bonus >= 0 ? '+' : '') + food_bonus;
    document.getElementById('gold').innerHTML = gold;
    document.getElementById('people').innerHTML = people;
    document.getElementById('stone').innerHTML = stone;
    document.getElementById('unemployed-workers').innerHTML = unemployed_workers;
}

function delete_people(count){
    do{
        // if no workers are unemployed
        if(unemployed_workers < 1){
            // decrease people workers first
            if(people_workers > 0){
                people_bonus -= 1;
                people_workers -= 1;

                document.getElementById('people-bonus').innerHTML = people_workers;
                document.getElementById('people-workers').innerHTML = people_workers;

            // if no people workers, decrease stone workers
            }else if(stone_workers > 0){
                stone_bonus -= 1;
                stone_workers -= 1;

                document.getElementById('stone-bonus').innerHTML = stone_workers;
                document.getElementById('stone-workers').innerHTML = stone_workers;

            // if no stone workers, decrease gold workers
            }else if(gold_workers > 0){
                gold_bonus -= 1;
                gold_workers -= 1;

                document.getElementById('gold-bonus').innerHTML = gold_workers;
                document.getElementById('gold-workers').innerHTML = gold_workers;

            // if no gold workers, decrease food workers
            }else{
                food_workers -= 1;
                document.getElementById('food-workers').innerHTML = food_workers;
            }

        // else decrease unemployed workers
        }else{
            unemployed_workers -= 1;
        }
    }while(count--);
}

function distribute_workers(resource,amount){
    // positive amount = decrease workers, negative amount = increase workers

    // if a day is not in progress and there are either unemployed workers or workers being decreased
    if(daylight_passed === 0
      && (unemployed_workers > 0 || amount < 0)){
        // alter food workers
        if(resource === 0){
            if(food_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                food_bonus += amount * 2;
                food_workers += amount;

            }else{
                food_workers = 0;

            }
            document.getElementById('food-bonus').innerHTML = (food_bonus >= 0 ? '+' : '') + food_bonus;
            document.getElementById('food-workers').innerHTML = food_workers;

        // alter gold workers
        }else if(resource === 1){
            if(gold_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                gold_bonus += amount;
                gold_workers += amount;

            }else{
                gold_workers = 0;

            }
            document.getElementById('gold-bonus').innerHTML = gold_bonus;
            document.getElementById('gold-workers').innerHTML = gold_workers;

        // alter people workers
        }else if(resource === 2){
            if(people_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                people_bonus += amount;
                people_workers += amount;

            }else{
                people_workers = 0;
            }

            document.getElementById('people-bonus').innerHTML = people_bonus;
            document.getElementById('people-workers').innerHTML = people_workers;

        // alter stone workers
        }else{
            if(stone_workers > 0
              || amount > 0){
                unemployed_workers -= amount;
                stone_bonus += amount;
                stone_workers += amount;

            }else{
                stone_workers = 0;
            }

            document.getElementById('stone-bonus').innerHTML = stone_bonus;
            document.getElementById('stone-workers').innerHTML = stone_workers;
        }
        document.getElementById('unemployed-workers').innerHTML = unemployed_workers;
    }
}

function new_game(){
    block_unload = 0;
    daylight_passed = 0;
    food = 10;
    food_bonus = -1;
    food_workers = 0;
    gold = 0;
    gold_bonus = 0;
    gold_workers = 0;
    people = 1;
    people_bonus = 0;
    people_workers = 0;
    stone = 0;
    stone_bonus = 0;
    stone_workers = 0;
    unemployed_workers = 1;

    // reset all text displays
    document.getElementById('food').innerHTML = food;
    document.getElementById('food-bonus').innerHTML = food_bonus;
    document.getElementById('food-workers').innerHTML = food_workers;
    document.getElementById('gold').innerHTML = gold;
    document.getElementById('gold-bonus').innerHTML = gold_bonus;
    document.getElementById('gold-workers').innerHTML = gold_workers;
    document.getElementById('people').innerHTML = people;
    document.getElementById('people-bonus').innerHTML = people_bonus;
    document.getElementById('people-workers').innerHTML = people_workers;
    document.getElementById('stone').innerHTML = stone;
    document.getElementById('stone-bonus').innerHTML = stone_bonus;
    document.getElementById('stone-workers').innerHTML = stone_workers;
    document.getElementById('unemployed-workers').innerHTML = unemployed_workers;

    document.getElementById('day-events').innerHTML = '';
    document.getElementById('start-day').innerHTML = 'Start New Day';
}

var block_unload = 0;
var daylight_passed = 0;
var food = 10;
var food_bonus = -1;
var food_workers = 0;
var gold = 0;
var gold_bonus = 0;
var gold_workers = 0;
var interval_value = 0;
var people = 1;
var people_bonus = 0;
var people_workers = 0;
var stone = 0;
var stone_bonus = 0;
var stone_workers = 0;
var unemployed_workers = 1;

window.onbeforeunload = function(){
    // warn players if they have already made progress
    if(block_unload
      && people > 0){
        return 'Save feature will be implemented in the future.';
    }
};

window.onkeydown = function(e){
    var key = window.event ? event : e;
    key = key.charCode ? key.charCode : key.keyCode;

    // if new day can be started, any key except for integer keys will start it
    if(daylight_passed === 0
      && people > 0
      && (key < 48 || key > 57)){
        day();
    }
}
<|endoftext|>


<commit_before>var _ = require('lodash');

var DEFAULT_BOX_PADDING = 5;

var DEFAULT_LABEL_SIZE = {
  width: 150,
  height: 50
};

function linePadding(height) {
  return Math.floor(height * 0.1);
}

function parseAlign(align) {

  var parts = align.split('-');

  return {
    horizontal: parts[0] || 'center',
    vertical: parts[1] || 'top'
  };
}

/**
 * Creates a new label utility
 *
 * @param {Object} config
 * @param {Dimensions} config.size
 * @param {Number} config.padding
 * @param {Object} config.style
 * @param {String} config.align
 */
function LabelUtil(config) {

  config = _.extend({}, {
    size: DEFAULT_LABEL_SIZE,
    padding: DEFAULT_BOX_PADDING,
    style: {},
    align: 'center-top'
  }, config || {});

  /**
   * Create a label in the parent node.
   *
   * @method LabelUtil#createLabel
   *
   * @param {SVGElement} parent the parent to draw the label on
   * @param {String} text the text to render on the label
   * @param {Object} options
   * @param {String} options.align how to align in the bounding box.
   *                             Any of { 'center-middle', 'center-top' }, defaults to 'center-top'.
   * @param {String} options.style style to be applied to the text
   *
   * @return {SVGText} the text element created
   */
  function createLabel(parent, text, options) {

    var box = _.merge({}, config.size, options.box || {}),
        style = _.merge({}, config.style, options.style || {}),
        align = parseAlign(options.align || config.align),
        padding = options.padding !== undefined ? options.padding : config.padding;

    var lines = text.split(/\r?\n/g),
        layouted = [];

    var maxWidth = box.width - padding * 2;

    /**
     * Layout the next line and return the layouted element.
     *
     * Alters the lines passed.
     *
     * @param  {Array<String>} lines
     * @return {Object} the line descriptor, an object { width, height, text }
     */
    function layoutNext(lines) {

      var originalLine = lines.shift(),
          fitLine = originalLine;

      var textBBox;

      function fit() {
        if (fitLine.length < originalLine.length) {
          var nextLine = lines[0] || '',
              remainder = originalLine.slice(fitLine.length);

          if (/-\s*$/.test(remainder)) {
            nextLine = remainder.replace(/-\s*$/, '') + nextLine.replace(/^\s+/, '');
          } else {
            nextLine = remainder + ' ' + nextLine;
          }

          lines[0] = nextLine;
        }
        return { width: textBBox.width, height: textBBox.height, text: fitLine };
      }

      function getTextBBox(text) {
        var textElement = parent.text(0, 0, fitLine).attr(style);

        var bbox = textElement.getBBox();

        textElement.remove();
        return bbox;
      }

      /**
       * Shortens a line based on spacing and hyphens.
       * Returns the shortened result on success.
       *
       * @param  {String} line
       * @param  {Number} maxLength the maximum characters of the string
       * @return {String} the shortened string
       */
      function semanticShorten(line, maxLength) {
        var parts = line.split(/(\s|-)/g),
            part,
            shortenedParts = [],
            length = 0;

        // try to shorten via spaces + hyphens
        if (parts.length > 1) {
          while ((part = parts.shift())) {

            if (part.length + length < maxLength) {
              shortenedParts.push(part);
              length += part.length;
            } else {
              // remove previous part, too if hyphen does not fit anymore
              if (part === '-') {
                shortenedParts.pop();
              }

              break;
            }
          }
        }

        return shortenedParts.join('');
      }

      function shortenLine(line, width, maxWidth) {
        var shortenedLine = '';

        var approximateLength = line.length * (maxWidth / width);

        // try to shorten semantically (i.e. based on spaces and hyphens)
        shortenedLine = semanticShorten(line, approximateLength);

        if (!shortenedLine) {

          // force shorten by cutting the long word
          shortenedLine = line.slice(0, Math.floor(approximateLength - 1));
        }

        return shortenedLine;
      }

      while (true) {

        textBBox = getTextBBox(fitLine);

        // try to fit
        if (textBBox.width < maxWidth) {
          return fit();
        }

        fitLine = shortenLine(fitLine, textBBox.width, maxWidth);
      }
    }

    while (lines.length) {
      layouted.push(layoutNext(lines));
    }

    var totalHeight = _.reduce(layouted, function(sum, line, idx) {
      return sum + (idx ? linePadding(line.height) : 0) + line.height;
    }, 0);

    // the center x position to align against
    var cx = box.width / 2;

    // the y position of the next line
    var y, x;

    switch (align.vertical) {
      case 'middle':
        y = (box.height - totalHeight) / 2 - layouted[0].height / 4;
        break;

      default:
        y = -1;
    }

    var textElement = parent.group().attr(style);

    _.forEach(layouted, function(line) {
      y += line.height;

      switch (align.horizontal) {
        case 'left':
          x = padding;
          break;

        case 'right':
          x = (maxWidth - padding - line.width);
          break;

        default:
          // aka center
          x = (maxWidth - line.width) / 2 + padding;
      }

      parent.text(x, y, line.text).appendTo(textElement);

      y += linePadding(line.height);
    });

    return textElement;
  }

  // API
  this.createLabel = createLabel;
}

module.exports = LabelUtil;<commit_msg>feat(util/LabelUtil): allow non-uniform padding<commit_after>var _ = require('lodash');

var DEFAULT_BOX_PADDING = 5;

var DEFAULT_LABEL_SIZE = {
  width: 150,
  height: 50
};

function linePadding(height) {
  return Math.floor(height * 0.1);
}

function parseAlign(align) {

  var parts = align.split('-');

  return {
    horizontal: parts[0] || 'center',
    vertical: parts[1] || 'top'
  };
}

function parsePadding(padding) {

  if (_.isObject(padding)) {
    return _.extend({ top: 0, left: 0, right: 0, bottom: 0 }, padding);
  } else {
    return {
      top: padding,
      left: padding,
      right: padding,
      bottom: padding
    };
  }
}
/**
 * Creates a new label utility
 *
 * @param {Object} config
 * @param {Dimensions} config.size
 * @param {Number} config.padding
 * @param {Object} config.style
 * @param {String} config.align
 */
function LabelUtil(config) {

  config = _.extend({}, {
    size: DEFAULT_LABEL_SIZE,
    padding: DEFAULT_BOX_PADDING,
    style: {},
    align: 'center-top'
  }, config || {});

  /**
   * Create a label in the parent node.
   *
   * @method LabelUtil#createLabel
   *
   * @param {SVGElement} parent the parent to draw the label on
   * @param {String} text the text to render on the label
   * @param {Object} options
   * @param {String} options.align how to align in the bounding box.
   *                             Any of { 'center-middle', 'center-top' }, defaults to 'center-top'.
   * @param {String} options.style style to be applied to the text
   *
   * @return {SVGText} the text element created
   */
  function createLabel(parent, text, options) {

    var box = _.merge({}, config.size, options.box || {}),
        style = _.merge({}, config.style, options.style || {}),
        align = parseAlign(options.align || config.align),
        padding = parsePadding(options.padding !== undefined ? options.padding : config.padding);

    var lines = text.split(/\r?\n/g),
        layouted = [];

    var maxWidth = box.width - padding.left - padding.right;

    /**
     * Layout the next line and return the layouted element.
     *
     * Alters the lines passed.
     *
     * @param  {Array<String>} lines
     * @return {Object} the line descriptor, an object { width, height, text }
     */
    function layoutNext(lines) {

      var originalLine = lines.shift(),
          fitLine = originalLine;

      var textBBox;

      function fit() {
        if (fitLine.length < originalLine.length) {
          var nextLine = lines[0] || '',
              remainder = originalLine.slice(fitLine.length);

          if (/-\s*$/.test(remainder)) {
            nextLine = remainder.replace(/-\s*$/, '') + nextLine.replace(/^\s+/, '');
          } else {
            nextLine = remainder + ' ' + nextLine;
          }

          lines[0] = nextLine;
        }
        return { width: textBBox.width, height: textBBox.height, text: fitLine };
      }

      function getTextBBox(text) {
        var textElement = parent.text(0, 0, fitLine).attr(style);

        var bbox = textElement.getBBox();

        textElement.remove();
        return bbox;
      }

      /**
       * Shortens a line based on spacing and hyphens.
       * Returns the shortened result on success.
       *
       * @param  {String} line
       * @param  {Number} maxLength the maximum characters of the string
       * @return {String} the shortened string
       */
      function semanticShorten(line, maxLength) {
        var parts = line.split(/(\s|-)/g),
            part,
            shortenedParts = [],
            length = 0;

        // try to shorten via spaces + hyphens
        if (parts.length > 1) {
          while ((part = parts.shift())) {

            if (part.length + length < maxLength) {
              shortenedParts.push(part);
              length += part.length;
            } else {
              // remove previous part, too if hyphen does not fit anymore
              if (part === '-') {
                shortenedParts.pop();
              }

              break;
            }
          }
        }

        return shortenedParts.join('');
      }

      function shortenLine(line, width, maxWidth) {
        var shortenedLine = '';

        var approximateLength = line.length * (maxWidth / width);

        // try to shorten semantically (i.e. based on spaces and hyphens)
        shortenedLine = semanticShorten(line, approximateLength);

        if (!shortenedLine) {

          // force shorten by cutting the long word
          shortenedLine = line.slice(0, Math.floor(approximateLength - 1));
        }

        return shortenedLine;
      }

      while (true) {

        textBBox = getTextBBox(fitLine);

        // try to fit
        if (textBBox.width < maxWidth) {
          return fit();
        }

        fitLine = shortenLine(fitLine, textBBox.width, maxWidth);
      }
    }

    while (lines.length) {
      layouted.push(layoutNext(lines));
    }

    var totalHeight = _.reduce(layouted, function(sum, line, idx) {
      return sum + (idx ? linePadding(line.height) : 0) + line.height;
    }, 0);

    // the center x position to align against
    var cx = box.width / 2;

    // the y position of the next line
    var y, x;

    switch (align.vertical) {
      case 'middle':
        y = (box.height - totalHeight) / 2 - layouted[0].height / 4;
        break;

      default:
        y = padding.top - 1;
    }

    var textElement = parent.group().attr(style);

    _.forEach(layouted, function(line) {
      y += line.height;

      switch (align.horizontal) {
        case 'left':
          x = padding.left;
          break;

        case 'right':
          x = (maxWidth - padding.right - line.width);
          break;

        default:
          // aka center
          x = (maxWidth - line.width) / 2 + padding.left;
      }

      parent.text(x, y, line.text).appendTo(textElement);

      y += linePadding(line.height);
    });

    return textElement;
  }

  // API
  this.createLabel = createLabel;
}

module.exports = LabelUtil;<|endoftext|>


<commit_before>angular.module('app.utils.directives').directive('historyList', HistoryListDirective);

HistoryListDirective.$inject = ['$filter', '$http', '$uibModal', '$q', '$state', 'EmailAccount',
'Note', 'NoteDetail', 'Case', 'DealDetail', 'EmailDetail', 'User', 'HLGravatar'];
function HistoryListDirective($filter, $http, $uibModal, $q, $state, EmailAccount,
Note, NoteDetail, Case, DealDetail, EmailDetail, User, HLGravatar) {
    return {
        restrict: 'E',
        replace: true,
        scope: {
            target: '=',
            object: '=',
        },
        templateUrl: 'utils/directives/historylist.html',
        link: function(scope, element, attrs) {
            var noteTargets = ['account', 'contact', 'deal', 'case'];
            var caseTargets = ['account', 'contact'];
            var dealTargets = ['account'];
            var emailTargets = ['account', 'contact'];
            var page = 0;
            var pageSize = 50;

            scope.history = {};
            scope.history.list = [];
            scope.history.types = {
                '': {name: 'All', visible: true},
                'note': {name: 'Notes', visible: false},
                'case': {name: 'Cases', visible: false},
                'deal': {name: 'Deals', visible: false},
                'email': {name: 'Emails', visible: false},
            };
            scope.history.activeFilter = '';
            scope.history.showMoreText = 'Show more';
            scope.history.loadMore = loadMore;
            scope.history.reloadHistory = reloadHistory;
            scope.history.addNote = addNote;
            scope.history.editNote = editNote;
            scope.history.pinNote = pinNote;
            scope.history.deleteNote = deleteNote;
            scope.history.filterType = filterType;

            scope.note = {};
            scope.note.type = 0;

            activate();

            ////////

            function activate() {
                // Somehow calling autosize on page content load does not work
                // in the historylist.
                autosize($('textarea'));
                loadMore();
            }

            function filterType(value) {
                var key;
                var selectedCount;

                scope.history.activeFilter = value;
                // Loop through the months to hide the monthname when there
                // aren't any items in that month that are shown due to
                // the filter that is being selected.
                for (key in scope.history.list.nonPinned) {
                    selectedCount = $filter('filter')(scope.history.list.nonPinned[key].items, {historyType: value}).length;
                    scope.history.list.nonPinned[key].isVisible = !!selectedCount;
                }
            }

            function loadMore() {
                if (!scope.object.$resolved) {
                    scope.object.$promise.then(function(obj) {
                        _fetchHistory(obj);
                    });
                } else {
                    _fetchHistory(scope.object);
                }
            }

            function reloadHistory() {
                page -= 1;
                loadMore();
            }

            function _fetchHistory(obj) {
                var history = [];
                var promises = [];
                page += 1;
                var neededLength = page * pageSize;
                var requestLength = neededLength + 1;

                // Check if we need to fetch notes
                if (noteTargets.indexOf(scope.target) !== -1) {
                    var notePromise;

                    if (scope.target === 'account' && obj.contact && obj.contact.length) {
                        var filterquery = '(content_type:' + scope.target + ' AND object_id:' + obj.id + ')';

                        // Show all notes of contacts linked to the account
                        for (var i = 0; i < obj.contact.length; i++) {
                            filterquery += ' OR (content_type:contact AND object_id:' + obj.contact[i] + ')';
                        }

                        notePromise = NoteDetail.query({filterquery: filterquery, size: requestLength}).$promise;
                    } else {
                        notePromise = NoteDetail.query({filterquery: 'content_type:' + scope.target + ' AND object_id:' + obj.id, size: requestLength}).$promise;
                    }

                    promises.push(notePromise);  // Add promise to list of all promises for later handling

                    notePromise.then(function(results) {
                        results.forEach(function(note) {
                            // Get user for notes to show profile picture correctly.
                            User.get({id: note.author.id}, function(userObject) {
                                note.author = userObject;
                            });

                            // Set notes shown property to true to have toggled open
                            // as default.
                            if (note.is_pinned) {
                                note.shown = true;
                            }

                            // If it's a contact's note, add extra attribute to the note
                            // so we can identify it in the template
                            if (scope.target === 'account' && note.content_type === 'contact') {
                                note.showContact = true;
                            }

                            history.push(note);
                        });
                    });
                }

                // Check if we need to fetch cases
                if (caseTargets.indexOf(scope.target) !== -1) {
                    var casePromise = Case.query({filterquery: scope.target + ':' + obj.id, size: 100}).$promise;
                    promises.push(casePromise);  // Add promise to list of all promises for later handling

                    casePromise.then(function(response) {
                        response.objects.forEach(function(caseItem) {
                            // Get user object for the assigned to user.
                            User.get({id: caseItem.assigned_to_id}, function(userObject) {
                                caseItem.assigned_to = userObject;
                            });

                            // Get user object for the created by user.
                            User.get({id: caseItem.created_by.id}, function(userObject) {
                                caseItem.created_by = userObject;
                            });

                            caseItem.historyType = 'case';

                            history.push(caseItem);
                            NoteDetail.query({filterquery: 'content_type:case AND object_id:' + caseItem.id, size: 15})
                                .$promise.then(function(notes) {
                                    angular.forEach(notes, function(note) {
                                        // Get user for notes to show profile picture correctly.
                                        User.get({id: note.author.id}, function(author) {
                                            note.author = author;
                                        });
                                    });

                                    caseItem.notes = notes;
                                });
                        });
                    });
                }

                // Check if we need to fetch deals
                if (dealTargets.indexOf(scope.target) !== -1) {
                    var dealPromise = DealDetail.query({filterquery: scope.target + ':' + obj.id, size: requestLength}).$promise;
                    promises.push(dealPromise);  // Add promise to list of all promises for later handling

                    dealPromise.then(function(results) {
                        results.forEach(function(deal) {
                            // Get user object for the assigned to user.
                            User.get({id: deal.assigned_to_id}, function(userObject) {
                                deal.assigned_to = userObject;
                            });

                            // Get user object to show profile picture correctly.
                            User.get({id: deal.created_by.id}, function(userObject) {
                                deal.created_by = userObject;
                            });

                            NoteDetail.query({
                                filterquery: 'content_type:deal AND object_id:' + deal.id,
                                size: 5,
                            }).$promise.then(function(notes) {
                                angular.forEach(notes, function(note) {
                                    // Get user for notes to show profile picture correctly.
                                    User.get({id: note.author.id}, function(author) {
                                        note.author = author;
                                    });
                                });
                                deal.notes = notes;
                            });
                            history.push(deal);
                        });
                    });
                }

                // Check if we need to fetch emails
                if (emailTargets.indexOf(scope.target) !== -1) {
                    var tenantEmailAccountPromise = EmailAccount.query().$promise;
                    promises.push(tenantEmailAccountPromise); // Add tenant email query to promises list

                    var emailPromise;
                    if (scope.target === 'account') {
                        emailPromise = EmailDetail.query({account_related: obj.id, size: requestLength}).$promise;
                    } else {
                        emailPromise = EmailDetail.query({contact_related: obj.id, size: requestLength}).$promise;
                    }
                    promises.push(emailPromise);  // Add promise to list of all promises for later handling

                    $q.all([tenantEmailAccountPromise, emailPromise]).then(function(results) {
                        var tenantEmailAccountList = results[0].results;
                        var emailMessageList = results[1];

                        emailMessageList.forEach(function(email) {
                            email.gravatar = HLGravatar.getGravatar(email.sender_email);

                            tenantEmailAccountList.forEach(function(emailAddress) {
                                if (emailAddress.email_address === email.sender_email) {
                                    email.right = true;
                                }
                            });
                            history.push(email);
                        });
                    });
                }

                // Get all history types and add them to a common history
                $q.all(promises).then(function() {
                    var orderedHistoryList = {pinned: [], nonPinned: {}, totalItems: history.length};

                    // To properly sort the history list we need to compare dates
                    // because email doesn't have the modified key we decided
                    // to add an extra key called historySortDate which the list
                    // uses to sort properly. We add the sent_date of email to the
                    // object, and the modified date for the other types.
                    for (var i = 0; i < history.length; i++) {
                        if (history[i].historyType === 'email') {
                            history[i].historySortDate = history[i].sent_date;
                        } else {
                            history[i].historySortDate = history[i].modified;
                        }
                    }

                    $filter('orderBy')(history, 'historySortDate', true).forEach(function(item) {
                        scope.history.types[item.historyType].visible = true;

                        var date = '';
                        var key = '';

                        if (item.is_pinned) {
                            orderedHistoryList.pinned.push(item);
                        } else {
                            if (item.hasOwnProperty('modified')) {
                                date = item.modified;
                            } else {
                                date = item.sent_date;
                            }

                            key = moment(date).year() + '-' + (moment(date).month() + 1);

                            if (!orderedHistoryList.nonPinned.hasOwnProperty(key)) {
                                orderedHistoryList.nonPinned[key] = {isVisible: true, items: []};
                            }

                            orderedHistoryList.nonPinned[key].items.push(item);
                        }
                    });

                    scope.history.list = orderedHistoryList;
                });
            }

            function addNote(note) {
                $http({
                    method: 'POST',
                    url: '/notes/create/',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    data: $.param({
                        content: note.content,
                        type: note.type,
                        content_type: scope.target,
                        object_id: scope.object.id,
                    }),
                }).success(function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function editNote(note) {
                var modalInstance = $uibModal.open({
                    templateUrl: 'utils/controllers/note_edit.html',
                    controller: 'EditNoteModalController',
                    size: 'lg',
                    resolve: {
                        note: function() {
                            return note;
                        },
                    },
                });

                modalInstance.result.then(function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function pinNote(note, isPinned) {
                Note.update({id: note.id}, {is_pinned: isPinned}, function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function deleteNote(note) {
                var month = moment(note.modified).format('M');
                var year = moment(note.modified).format('YYYY');
                var index;

                if (confirm('Are you sure?')) {
                    Note.delete({
                        id: note.id,
                    }, function() {  // On success
                        if (note.is_pinned) {
                            index = scope.history.list.pinned.indexOf(note);
                            scope.history.list.pinned.splice(index, 1);
                        } else {
                            index = scope.history.list.nonPinned[year + '-' + month].indexOf(note);
                            scope.history.list.nonPinned[year + '-' + month].splice(index, 1);
                        }
                    }, function(error) {  // On error
                        alert('something went wrong.');
                    });
                }
            }
        },
    };
}
<commit_msg>LILY-1541: Foldout email items as default in the historylist.<commit_after>angular.module('app.utils.directives').directive('historyList', HistoryListDirective);

HistoryListDirective.$inject = ['$filter', '$http', '$uibModal', '$q', '$state', 'EmailAccount',
'Note', 'NoteDetail', 'Case', 'DealDetail', 'EmailDetail', 'User', 'HLGravatar'];
function HistoryListDirective($filter, $http, $uibModal, $q, $state, EmailAccount,
Note, NoteDetail, Case, DealDetail, EmailDetail, User, HLGravatar) {
    return {
        restrict: 'E',
        replace: true,
        scope: {
            target: '=',
            object: '=',
        },
        templateUrl: 'utils/directives/historylist.html',
        link: function(scope, element, attrs) {
            var noteTargets = ['account', 'contact', 'deal', 'case'];
            var caseTargets = ['account', 'contact'];
            var dealTargets = ['account'];
            var emailTargets = ['account', 'contact'];
            var page = 0;
            var pageSize = 50;

            scope.history = {};
            scope.history.list = [];
            scope.history.types = {
                '': {name: 'All', visible: true},
                'note': {name: 'Notes', visible: false},
                'case': {name: 'Cases', visible: false},
                'deal': {name: 'Deals', visible: false},
                'email': {name: 'Emails', visible: false},
            };
            scope.history.activeFilter = '';
            scope.history.showMoreText = 'Show more';
            scope.history.loadMore = loadMore;
            scope.history.reloadHistory = reloadHistory;
            scope.history.addNote = addNote;
            scope.history.editNote = editNote;
            scope.history.pinNote = pinNote;
            scope.history.deleteNote = deleteNote;
            scope.history.filterType = filterType;

            scope.note = {};
            scope.note.type = 0;

            activate();

            ////////

            function activate() {
                // Somehow calling autosize on page content load does not work
                // in the historylist.
                autosize($('textarea'));
                loadMore();
            }

            function filterType(value) {
                var key;
                var selectedCount;

                scope.history.activeFilter = value;
                // Loop through the months to hide the monthname when there
                // aren't any items in that month that are shown due to
                // the filter that is being selected.
                for (key in scope.history.list.nonPinned) {
                    selectedCount = $filter('filter')(scope.history.list.nonPinned[key].items, {historyType: value}).length;
                    scope.history.list.nonPinned[key].isVisible = !!selectedCount;
                }
            }

            function loadMore() {
                if (!scope.object.$resolved) {
                    scope.object.$promise.then(function(obj) {
                        _fetchHistory(obj);
                    });
                } else {
                    _fetchHistory(scope.object);
                }
            }

            function reloadHistory() {
                page -= 1;
                loadMore();
            }

            function _fetchHistory(obj) {
                var history = [];
                var promises = [];
                page += 1;
                var neededLength = page * pageSize;
                var requestLength = neededLength + 1;

                // Check if we need to fetch notes
                if (noteTargets.indexOf(scope.target) !== -1) {
                    var notePromise;

                    if (scope.target === 'account' && obj.contact && obj.contact.length) {
                        var filterquery = '(content_type:' + scope.target + ' AND object_id:' + obj.id + ')';

                        // Show all notes of contacts linked to the account
                        for (var i = 0; i < obj.contact.length; i++) {
                            filterquery += ' OR (content_type:contact AND object_id:' + obj.contact[i] + ')';
                        }

                        notePromise = NoteDetail.query({filterquery: filterquery, size: requestLength}).$promise;
                    } else {
                        notePromise = NoteDetail.query({filterquery: 'content_type:' + scope.target + ' AND object_id:' + obj.id, size: requestLength}).$promise;
                    }

                    promises.push(notePromise);  // Add promise to list of all promises for later handling

                    notePromise.then(function(results) {
                        results.forEach(function(note) {
                            // Get user for notes to show profile picture correctly.
                            User.get({id: note.author.id}, function(userObject) {
                                note.author = userObject;
                            });

                            // Set notes shown property to true to have toggled open
                            // as default.
                            if (note.is_pinned) {
                                note.shown = true;
                            }

                            // If it's a contact's note, add extra attribute to the note
                            // so we can identify it in the template
                            if (scope.target === 'account' && note.content_type === 'contact') {
                                note.showContact = true;
                            }

                            history.push(note);
                        });
                    });
                }

                // Check if we need to fetch cases
                if (caseTargets.indexOf(scope.target) !== -1) {
                    var casePromise = Case.query({filterquery: scope.target + ':' + obj.id, size: 100}).$promise;
                    promises.push(casePromise);  // Add promise to list of all promises for later handling

                    casePromise.then(function(response) {
                        response.objects.forEach(function(caseItem) {
                            // Get user object for the assigned to user.
                            User.get({id: caseItem.assigned_to_id}, function(userObject) {
                                caseItem.assigned_to = userObject;
                            });

                            // Get user object for the created by user.
                            User.get({id: caseItem.created_by.id}, function(userObject) {
                                caseItem.created_by = userObject;
                            });

                            caseItem.historyType = 'case';

                            history.push(caseItem);
                            NoteDetail.query({filterquery: 'content_type:case AND object_id:' + caseItem.id, size: 15})
                                .$promise.then(function(notes) {
                                    angular.forEach(notes, function(note) {
                                        // Get user for notes to show profile picture correctly.
                                        User.get({id: note.author.id}, function(author) {
                                            note.author = author;
                                        });
                                    });

                                    caseItem.notes = notes;
                                });
                        });
                    });
                }

                // Check if we need to fetch deals
                if (dealTargets.indexOf(scope.target) !== -1) {
                    var dealPromise = DealDetail.query({filterquery: scope.target + ':' + obj.id, size: requestLength}).$promise;
                    promises.push(dealPromise);  // Add promise to list of all promises for later handling

                    dealPromise.then(function(results) {
                        results.forEach(function(deal) {
                            // Get user object for the assigned to user.
                            User.get({id: deal.assigned_to_id}, function(userObject) {
                                deal.assigned_to = userObject;
                            });

                            // Get user object to show profile picture correctly.
                            User.get({id: deal.created_by.id}, function(userObject) {
                                deal.created_by = userObject;
                            });

                            NoteDetail.query({
                                filterquery: 'content_type:deal AND object_id:' + deal.id,
                                size: 5,
                            }).$promise.then(function(notes) {
                                angular.forEach(notes, function(note) {
                                    // Get user for notes to show profile picture correctly.
                                    User.get({id: note.author.id}, function(author) {
                                        note.author = author;
                                    });
                                });
                                deal.notes = notes;
                            });
                            history.push(deal);
                        });
                    });
                }

                // Check if we need to fetch emails
                if (emailTargets.indexOf(scope.target) !== -1) {
                    var tenantEmailAccountPromise = EmailAccount.query().$promise;
                    promises.push(tenantEmailAccountPromise); // Add tenant email query to promises list

                    var emailPromise;
                    if (scope.target === 'account') {
                        emailPromise = EmailDetail.query({account_related: obj.id, size: requestLength}).$promise;
                    } else {
                        emailPromise = EmailDetail.query({contact_related: obj.id, size: requestLength}).$promise;
                    }
                    promises.push(emailPromise);  // Add promise to list of all promises for later handling

                    $q.all([tenantEmailAccountPromise, emailPromise]).then(function(results) {
                        var tenantEmailAccountList = results[0].results;
                        var emailMessageList = results[1];

                        emailMessageList.forEach(function(email) {
                            email.gravatar = HLGravatar.getGravatar(email.sender_email);

                            // Foldout email on default in the historylist.
                            email.shown = true;

                            tenantEmailAccountList.forEach(function(emailAddress) {
                                if (emailAddress.email_address === email.sender_email) {
                                    email.right = true;
                                }
                            });
                            history.push(email);
                        });
                    });
                }

                // Get all history types and add them to a common history
                $q.all(promises).then(function() {
                    var orderedHistoryList = {pinned: [], nonPinned: {}, totalItems: history.length};

                    // To properly sort the history list we need to compare dates
                    // because email doesn't have the modified key we decided
                    // to add an extra key called historySortDate which the list
                    // uses to sort properly. We add the sent_date of email to the
                    // object, and the modified date for the other types.
                    for (var i = 0; i < history.length; i++) {
                        if (history[i].historyType === 'email') {
                            history[i].historySortDate = history[i].sent_date;
                        } else {
                            history[i].historySortDate = history[i].modified;
                        }
                    }

                    $filter('orderBy')(history, 'historySortDate', true).forEach(function(item) {
                        scope.history.types[item.historyType].visible = true;

                        var date = '';
                        var key = '';

                        if (item.is_pinned) {
                            orderedHistoryList.pinned.push(item);
                        } else {
                            if (item.hasOwnProperty('modified')) {
                                date = item.modified;
                            } else {
                                date = item.sent_date;
                            }

                            key = moment(date).year() + '-' + (moment(date).month() + 1);

                            if (!orderedHistoryList.nonPinned.hasOwnProperty(key)) {
                                orderedHistoryList.nonPinned[key] = {isVisible: true, items: []};
                            }

                            orderedHistoryList.nonPinned[key].items.push(item);
                        }
                    });

                    scope.history.list = orderedHistoryList;
                });
            }

            function addNote(note) {
                $http({
                    method: 'POST',
                    url: '/notes/create/',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    data: $.param({
                        content: note.content,
                        type: note.type,
                        content_type: scope.target,
                        object_id: scope.object.id,
                    }),
                }).success(function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function editNote(note) {
                var modalInstance = $uibModal.open({
                    templateUrl: 'utils/controllers/note_edit.html',
                    controller: 'EditNoteModalController',
                    size: 'lg',
                    resolve: {
                        note: function() {
                            return note;
                        },
                    },
                });

                modalInstance.result.then(function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function pinNote(note, isPinned) {
                Note.update({id: note.id}, {is_pinned: isPinned}, function() {
                    $state.go($state.current, {}, {reload: true});
                });
            }

            function deleteNote(note) {
                var month = moment(note.modified).format('M');
                var year = moment(note.modified).format('YYYY');
                var index;

                if (confirm('Are you sure?')) {
                    Note.delete({
                        id: note.id,
                    }, function() {  // On success
                        if (note.is_pinned) {
                            index = scope.history.list.pinned.indexOf(note);
                            scope.history.list.pinned.splice(index, 1);
                        } else {
                            index = scope.history.list.nonPinned[year + '-' + month].indexOf(note);
                            scope.history.list.nonPinned[year + '-' + month].splice(index, 1);
                        }
                    }, function(error) {  // On error
                        alert('something went wrong.');
                    });
                }
            }
        },
    };
}
<|endoftext|>


<commit_before>/*
 * LazyK-JS /lib/lazy.js
 * copyright (c) 2015 Susisu
 */

"use strict";

module.exports = Object.freeze({
    "lambda": require("./lambda.js")
});
<commit_msg>Export all<commit_after>/*
 * LazyK-JS /lib/lazy.js
 * copyright (c) 2015 Susisu
 */

"use strict";

module.exports = Object.freeze({
    "lambda"     : require("./lambda.js"),
    "tokens"     : require("./tokens.js"),
    "parser"     : require("./parser.js"),
    "interpreter": require("./interpreter.js"),
    "stream"     : require("./stream.js")
});
<|endoftext|>


<commit_before>(function() {

  function getCoords(oCoords) {
    return [
      new fabric.Point(oCoords.tl.x, oCoords.tl.y),
      new fabric.Point(oCoords.tr.x, oCoords.tr.y),
      new fabric.Point(oCoords.br.x, oCoords.br.y),
      new fabric.Point(oCoords.bl.x, oCoords.bl.y)
    ];
  }

  var degreesToRadians = fabric.util.degreesToRadians,
      multiplyMatrices = fabric.util.multiplyTransformMatrices;

  fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {

    /**
     * Object containing coordinates of object's controls
     * @type Object
     * @default
     */
    oCoords: null,

    /**
     * Checks if object intersects with an area formed by 2 points
     * @param {Object} pointTL top-left point of area
     * @param {Object} pointBR bottom-right point of area
     * @return {Boolean} true if object intersects with an area formed by 2 points
     */
    intersectsWithRect: function(pointTL, pointBR) {
      var oCoords = getCoords(this.oCoords),
          intersection = fabric.Intersection.intersectPolygonRectangle(
            oCoords,
            pointTL,
            pointBR
          );
      return intersection.status === 'Intersection';
    },

    /**
     * Checks if object intersects with another object
     * @param {Object} other Object to test
     * @return {Boolean} true if object intersects with another object
     */
    intersectsWithObject: function(other) {
      var intersection = fabric.Intersection.intersectPolygonPolygon(
            getCoords(this.oCoords),
            getCoords(other.oCoords)
          );

      return intersection.status === 'Intersection'
        || other.isContainedWithinObject(this)
        || this.isContainedWithinObject(other);
    },

    /**
     * Checks if object is fully contained within area of another object
     * @param {Object} other Object to test
     * @return {Boolean} true if object is fully contained within area of another object
     */
    isContainedWithinObject: function(other) {
      var points = getCoords(this.oCoords),
          i = 0;
      for (; i < 4; i++) {
        if (!other.containsPoint(points[i])) {
          return false;
        }
      }
      return true;
    },

    /**
     * Checks if object is fully contained within area formed by 2 points
     * @param {Object} pointTL top-left point of area
     * @param {Object} pointBR bottom-right point of area
     * @return {Boolean} true if object is fully contained within area formed by 2 points
     */
    isContainedWithinRect: function(pointTL, pointBR) {
      var boundingRect = this.getBoundingRect();

      return (
        boundingRect.left >= pointTL.x &&
        boundingRect.left + boundingRect.width <= pointBR.x &&
        boundingRect.top >= pointTL.y &&
        boundingRect.top + boundingRect.height <= pointBR.y
      );
    },

    /**
     * Checks if point is inside the object
     * @param {fabric.Point} point Point to check against
     * @return {Boolean} true if point is inside the object
     */
    containsPoint: function(point) {
      if (!this.oCoords) {
        this.setCoords();
      }
      var lines = this._getImageLines(this.oCoords),
          xPoints = this._findCrossPoints(point, lines);

      // if xPoints is odd then point is inside the object
      return (xPoints !== 0 && xPoints % 2 === 1);
    },

    /**
     * Method that returns an object with the object edges in it, given the coordinates of the corners
     * @private
     * @param {Object} oCoords Coordinates of the object corners
     */
    _getImageLines: function(oCoords) {
      return {
        topline: {
          o: oCoords.tl,
          d: oCoords.tr
        },
        rightline: {
          o: oCoords.tr,
          d: oCoords.br
        },
        bottomline: {
          o: oCoords.br,
          d: oCoords.bl
        },
        leftline: {
          o: oCoords.bl,
          d: oCoords.tl
        }
      };
    },

    /**
     * Helper method to determine how many cross points are between the 4 object edges
     * and the horizontal line determined by a point on canvas
     * @private
     * @param {fabric.Point} point Point to check
     * @param {Object} oCoords Coordinates of the object being evaluated
     */
     // remove yi, not used but left code here just in case.
    _findCrossPoints: function(point, oCoords) {
      var b1, b2, a1, a2, xi, // yi,
          xcount = 0,
          iLine;

      for (var lineKey in oCoords) {
        iLine = oCoords[lineKey];
        // optimisation 1: line below point. no cross
        if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) {
          continue;
        }
        // optimisation 2: line above point. no cross
        if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) {
          continue;
        }
        // optimisation 3: vertical line case
        if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) {
          xi = iLine.o.x;
          // yi = point.y;
        }
        // calculate the intersection point
        else {
          b1 = 0;
          b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x);
          a1 = point.y - b1 * point.x;
          a2 = iLine.o.y - b2 * iLine.o.x;

          xi = -(a1 - a2) / (b1 - b2);
          // yi = a1 + b1 * xi;
        }
        // dont count xi < point.x cases
        if (xi >= point.x) {
          xcount += 1;
        }
        // optimisation 4: specific for square images
        if (xcount === 2) {
          break;
        }
      }
      return xcount;
    },

    /**
     * Returns width of an object's bounding rectangle
     * @deprecated since 1.0.4
     * @return {Number} width value
     */
    getBoundingRectWidth: function() {
      return this.getBoundingRect().width;
    },

    /**
     * Returns height of an object's bounding rectangle
     * @deprecated since 1.0.4
     * @return {Number} height value
     */
    getBoundingRectHeight: function() {
      return this.getBoundingRect().height;
    },

    /**
     * Returns coordinates of object's bounding rectangle (left, top, width, height)
     * @return {Object} Object with left, top, width, height properties
     */
    getBoundingRect: function() {
      this.oCoords || this.setCoords();
      return fabric.util.makeBoundingBoxFromPoints([
        this.oCoords.tl,
        this.oCoords.tr,
        this.oCoords.br,
        this.oCoords.bl
      ]);
    },

    /**
     * Returns width of an object bounding box counting transformations
     * @return {Number} width value
     */
    getWidth: function() {
      return this._getTransformedDimensions().x;
    },

    /**
     * Returns height of an object bounding box counting transformations
     * to be renamed in 2.0
     * @return {Number} height value
     */
    getHeight: function() {
      return this._getTransformedDimensions().y;
    },

    /**
     * Makes sure the scale is valid and modifies it if necessary
     * @private
     * @param {Number} value
     * @return {Number}
     */
    _constrainScale: function(value) {
      if (Math.abs(value) < this.minScaleLimit) {
        if (value < 0) {
          return -this.minScaleLimit;
        }
        else {
          return this.minScaleLimit;
        }
      }
      return value;
    },

    /**
     * Scales an object (equally by x and y)
     * @param {Number} value Scale factor
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scale: function(value) {
      value = this._constrainScale(value);

      if (value < 0) {
        this.flipX = !this.flipX;
        this.flipY = !this.flipY;
        value *= -1;
      }

      this.scaleX = value;
      this.scaleY = value;
      this.setCoords();
      return this;
    },

    /**
     * Scales an object to a given width, with respect to bounding box (scaling by x/y equally)
     * @param {Number} value New width value
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scaleToWidth: function(value) {
      // adjust to bounding rect factor so that rotated shapes would fit as well
      var boundingRectFactor = this.getBoundingRect().width / this.getWidth();
      return this.scale(value / this.width / boundingRectFactor);
    },

    /**
     * Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
     * @param {Number} value New height value
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scaleToHeight: function(value) {
      // adjust to bounding rect factor so that rotated shapes would fit as well
      var boundingRectFactor = this.getBoundingRect().height / this.getHeight();
      return this.scale(value / this.height / boundingRectFactor);
    },

    /**
     * Calculate and returns the .coords of an object.
     * @return {Object} Object with tl, tr, br, bl ....
     * @chainable
     */
    calcCoords: function(ignoreZoom) {
      var theta = degreesToRadians(this.angle),
          vpt = this.getViewportTransform(),
          dim = ignoreZoom ? this._getTransformedDimensions() : this._calculateCurrentDimensions(),
          currentWidth = dim.x, currentHeight = dim.y,
          sinTh = Math.sin(theta),
          cosTh = Math.cos(theta),
          _angle = currentWidth > 0 ? Math.atan(currentHeight / currentWidth) : 0,
          _hypotenuse = (currentWidth / Math.cos(_angle)) / 2,
          offsetX = Math.cos(_angle + theta) * _hypotenuse,
          offsetY = Math.sin(_angle + theta) * _hypotenuse,
          center = this.getCenterPoint(),
          // offset added for rotate and scale actions
          coords = ignoreZoom ? center : fabric.util.transformPoint(center, vpt),
          tl  = new fabric.Point(coords.x - offsetX, coords.y - offsetY),
          tr  = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)),
          bl  = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)),
          br  = new fabric.Point(coords.x + offsetX, coords.y + offsetY),
          ml  = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
          mt  = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
          mr  = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
          mb  = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),
          mtr = new fabric.Point(mt.x + sinTh * this.rotatingPointOffset, mt.y - cosTh * this.rotatingPointOffset);
      // debugging

      /* setTimeout(function() {
         canvas.contextTop.fillStyle = 'green';
         canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
         canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
         canvas.contextTop.fillRect(br.x, br.y, 3, 3);
         canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
         canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
         canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
         canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
         canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
         canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3);
       }, 50); */

      return {
        // corners
        tl: tl, tr: tr, br: br, bl: bl,
        // middle
        ml: ml, mt: mt, mr: mr, mb: mb,
        // rotating point
        mtr: mtr
      };
    },

    /**
     * Sets corner position coordinates based on current angle, width and height
     * See https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords
     * @return {fabric.Object} thisArg
     * @chainable
     */
    setCoords: function(ignoreZoom) {
      this.oCoords = this.calcCoords(ignoreZoom);

      // set coordinates of the draggable boxes in the corners used to scale/rotate the image
      ignoreZoom || this._setCornerCoords && this._setCornerCoords();

      return this;
    },

    /*
     * calculate rotation matrix of an object
     * @return {Array} rotation matrix for the object
     */
    _calcRotateMatrix: function() {
      if (this.angle) {
        var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta);
        return [cos, sin, -sin, cos, 0, 0];
      }
      return [1, 0, 0, 1, 0, 0];
    },

    /*
     * calculate trasform Matrix that represent current transformation from
     * object properties.
     * @return {Array} matrix Transform Matrix for the object
     */
    calcTransformMatrix: function() {
      var center = this.getCenterPoint(),
          translateMatrix = [1, 0, 0, 1, center.x, center.y],
          rotateMatrix = this._calcRotateMatrix(),
          dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true),
          matrix = this.group ? this.group.calcTransformMatrix() : [1, 0, 0, 1, 0, 0];
      matrix = multiplyMatrices(matrix, translateMatrix);
      matrix = multiplyMatrices(matrix, rotateMatrix);
      matrix = multiplyMatrices(matrix, dimensionMatrix);
      return matrix;
    },

    _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) {
      var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1],
          skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1],
          scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1),
          scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1),
          scaleMatrix = [scaleX, 0, 0, scaleY],
          m = multiplyMatrices(scaleMatrix, skewMatrixX, true);
      return multiplyMatrices(m, skewMatrixY, true);
    }
  });
})();
<commit_msg>add options to bounding rect<commit_after>(function() {

  function getCoords(oCoords) {
    return [
      new fabric.Point(oCoords.tl.x, oCoords.tl.y),
      new fabric.Point(oCoords.tr.x, oCoords.tr.y),
      new fabric.Point(oCoords.br.x, oCoords.br.y),
      new fabric.Point(oCoords.bl.x, oCoords.bl.y)
    ];
  }

  var degreesToRadians = fabric.util.degreesToRadians,
      multiplyMatrices = fabric.util.multiplyTransformMatrices;

  fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {

    /**
     * Object containing coordinates of object's controls
     * @type Object
     * @default
     */
    oCoords: null,

    /**
     * Checks if object intersects with an area formed by 2 points
     * @param {Object} pointTL top-left point of area
     * @param {Object} pointBR bottom-right point of area
     * @return {Boolean} true if object intersects with an area formed by 2 points
     */
    intersectsWithRect: function(pointTL, pointBR) {
      var oCoords = getCoords(this.oCoords),
          intersection = fabric.Intersection.intersectPolygonRectangle(
            oCoords,
            pointTL,
            pointBR
          );
      return intersection.status === 'Intersection';
    },

    /**
     * Checks if object intersects with another object
     * @param {Object} other Object to test
     * @return {Boolean} true if object intersects with another object
     */
    intersectsWithObject: function(other) {
      var intersection = fabric.Intersection.intersectPolygonPolygon(
            getCoords(this.oCoords),
            getCoords(other.oCoords)
          );

      return intersection.status === 'Intersection'
        || other.isContainedWithinObject(this)
        || this.isContainedWithinObject(other);
    },

    /**
     * Checks if object is fully contained within area of another object
     * @param {Object} other Object to test
     * @return {Boolean} true if object is fully contained within area of another object
     */
    isContainedWithinObject: function(other) {
      var points = getCoords(this.oCoords),
          i = 0;
      for (; i < 4; i++) {
        if (!other.containsPoint(points[i])) {
          return false;
        }
      }
      return true;
    },

    /**
     * Checks if object is fully contained within area formed by 2 points
     * @param {Object} pointTL top-left point of area
     * @param {Object} pointBR bottom-right point of area
     * @return {Boolean} true if object is fully contained within area formed by 2 points
     */
    isContainedWithinRect: function(pointTL, pointBR) {
      var boundingRect = this.getBoundingRect();

      return (
        boundingRect.left >= pointTL.x &&
        boundingRect.left + boundingRect.width <= pointBR.x &&
        boundingRect.top >= pointTL.y &&
        boundingRect.top + boundingRect.height <= pointBR.y
      );
    },

    /**
     * Checks if point is inside the object
     * @param {fabric.Point} point Point to check against
     * @return {Boolean} true if point is inside the object
     */
    containsPoint: function(point) {
      if (!this.oCoords) {
        this.setCoords();
      }
      var lines = this._getImageLines(this.oCoords),
          xPoints = this._findCrossPoints(point, lines);

      // if xPoints is odd then point is inside the object
      return (xPoints !== 0 && xPoints % 2 === 1);
    },

    /**
     * Method that returns an object with the object edges in it, given the coordinates of the corners
     * @private
     * @param {Object} oCoords Coordinates of the object corners
     */
    _getImageLines: function(oCoords) {
      return {
        topline: {
          o: oCoords.tl,
          d: oCoords.tr
        },
        rightline: {
          o: oCoords.tr,
          d: oCoords.br
        },
        bottomline: {
          o: oCoords.br,
          d: oCoords.bl
        },
        leftline: {
          o: oCoords.bl,
          d: oCoords.tl
        }
      };
    },

    /**
     * Helper method to determine how many cross points are between the 4 object edges
     * and the horizontal line determined by a point on canvas
     * @private
     * @param {fabric.Point} point Point to check
     * @param {Object} oCoords Coordinates of the object being evaluated
     */
     // remove yi, not used but left code here just in case.
    _findCrossPoints: function(point, oCoords) {
      var b1, b2, a1, a2, xi, // yi,
          xcount = 0,
          iLine;

      for (var lineKey in oCoords) {
        iLine = oCoords[lineKey];
        // optimisation 1: line below point. no cross
        if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) {
          continue;
        }
        // optimisation 2: line above point. no cross
        if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) {
          continue;
        }
        // optimisation 3: vertical line case
        if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) {
          xi = iLine.o.x;
          // yi = point.y;
        }
        // calculate the intersection point
        else {
          b1 = 0;
          b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x);
          a1 = point.y - b1 * point.x;
          a2 = iLine.o.y - b2 * iLine.o.x;

          xi = -(a1 - a2) / (b1 - b2);
          // yi = a1 + b1 * xi;
        }
        // dont count xi < point.x cases
        if (xi >= point.x) {
          xcount += 1;
        }
        // optimisation 4: specific for square images
        if (xcount === 2) {
          break;
        }
      }
      return xcount;
    },

    /**
     * Returns width of an object's bounding rectangle
     * @deprecated since 1.0.4
     * @return {Number} width value
     */
    getBoundingRectWidth: function() {
      return this.getBoundingRect().width;
    },

    /**
     * Returns height of an object's bounding rectangle
     * @deprecated since 1.0.4
     * @return {Number} height value
     */
    getBoundingRectHeight: function() {
      return this.getBoundingRect().height;
    },

    /**
     * Returns coordinates of object's bounding rectangle (left, top, width, height)
     * the box is intented as aligned to axis of canvas.
     * @param {Boolean} ignoreVpt bounding box will not be affected by viewportTransform
     * @return {Object} Object with left, top, width, height properties
     */
    getBoundingRect: function(ignoreVpt) {
      var coords = this.calcCoords(ignoreVpt);
      return fabric.util.makeBoundingBoxFromPoints([
        coords.tl,
        coords.tr,
        coords.br,
        coords.bl
      ]);
    },

    /**
     * Returns width of an object bounding box counting transformations
     * @return {Number} width value
     */
    getWidth: function() {
      return this._getTransformedDimensions().x;
    },

    /**
     * Returns height of an object bounding box counting transformations
     * to be renamed in 2.0
     * @return {Number} height value
     */
    getHeight: function() {
      return this._getTransformedDimensions().y;
    },

    /**
     * Makes sure the scale is valid and modifies it if necessary
     * @private
     * @param {Number} value
     * @return {Number}
     */
    _constrainScale: function(value) {
      if (Math.abs(value) < this.minScaleLimit) {
        if (value < 0) {
          return -this.minScaleLimit;
        }
        else {
          return this.minScaleLimit;
        }
      }
      return value;
    },

    /**
     * Scales an object (equally by x and y)
     * @param {Number} value Scale factor
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scale: function(value) {
      value = this._constrainScale(value);

      if (value < 0) {
        this.flipX = !this.flipX;
        this.flipY = !this.flipY;
        value *= -1;
      }

      this.scaleX = value;
      this.scaleY = value;
      return this.setCoords();
    },

    /**
     * Scales an object to a given width, with respect to bounding box (scaling by x/y equally)
     * @param {Number} value New width value
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scaleToWidth: function(value) {
      // adjust to bounding rect factor so that rotated shapes would fit as well
      var boundingRectFactor = this.getBoundingRect().width / this.getWidth();
      return this.scale(value / this.width / boundingRectFactor);
    },

    /**
     * Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
     * @param {Number} value New height value
     * @return {fabric.Object} thisArg
     * @chainable
     */
    scaleToHeight: function(value) {
      // adjust to bounding rect factor so that rotated shapes would fit as well
      var boundingRectFactor = this.getBoundingRect().height / this.getHeight();
      return this.scale(value / this.height / boundingRectFactor);
    },

    /**
     * Calculate and returns the .coords of an object.
     * @return {Object} Object with tl, tr, br, bl ....
     * @chainable
     */
    calcCoords: function(ignoreVpt) {
      var theta = degreesToRadians(this.angle),
          vpt = this.getViewportTransform(),
          dim = ignoreVpt ? this._getTransformedDimensions() : this._calculateCurrentDimensions(),
          currentWidth = dim.x, currentHeight = dim.y,
          sinTh = Math.sin(theta),
          cosTh = Math.cos(theta),
          _angle = currentWidth > 0 ? Math.atan(currentHeight / currentWidth) : 0,
          _hypotenuse = (currentWidth / Math.cos(_angle)) / 2,
          offsetX = Math.cos(_angle + theta) * _hypotenuse,
          offsetY = Math.sin(_angle + theta) * _hypotenuse,
          center = this.getCenterPoint(),
          // offset added for rotate and scale actions
          coords = ignoreVpt ? center : fabric.util.transformPoint(center, vpt),
          tl  = new fabric.Point(coords.x - offsetX, coords.y - offsetY),
          tr  = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)),
          bl  = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)),
          br  = new fabric.Point(coords.x + offsetX, coords.y + offsetY),
          ml  = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
          mt  = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
          mr  = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
          mb  = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),
          mtr = new fabric.Point(mt.x + sinTh * this.rotatingPointOffset, mt.y - cosTh * this.rotatingPointOffset);
      // debugging

      /* setTimeout(function() {
         canvas.contextTop.fillStyle = 'green';
         canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
         canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
         canvas.contextTop.fillRect(br.x, br.y, 3, 3);
         canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
         canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
         canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
         canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
         canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
         canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3);
       }, 50); */

      return {
        // corners
        tl: tl, tr: tr, br: br, bl: bl,
        // middle
        ml: ml, mt: mt, mr: mr, mb: mb,
        // rotating point
        mtr: mtr
      };
    },

    /**
     * Sets corner position coordinates based on current angle, width and height
     * See https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords
     * @return {fabric.Object} thisArg
     * @chainable
     */
    setCoords: function(ignoreZoom) {
      this.oCoords = this.calcCoords(ignoreZoom);

      // set coordinates of the draggable boxes in the corners used to scale/rotate the image
      ignoreZoom || this._setCornerCoords && this._setCornerCoords();

      return this;
    },

    /*
     * calculate rotation matrix of an object
     * @return {Array} rotation matrix for the object
     */
    _calcRotateMatrix: function() {
      if (this.angle) {
        var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta);
        return [cos, sin, -sin, cos, 0, 0];
      }
      return [1, 0, 0, 1, 0, 0];
    },

    /*
     * calculate trasform Matrix that represent current transformation from
     * object properties.
     * @return {Array} matrix Transform Matrix for the object
     */
    calcTransformMatrix: function() {
      var center = this.getCenterPoint(),
          translateMatrix = [1, 0, 0, 1, center.x, center.y],
          rotateMatrix = this._calcRotateMatrix(),
          dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true),
          matrix = this.group ? this.group.calcTransformMatrix() : [1, 0, 0, 1, 0, 0];
      matrix = multiplyMatrices(matrix, translateMatrix);
      matrix = multiplyMatrices(matrix, rotateMatrix);
      matrix = multiplyMatrices(matrix, dimensionMatrix);
      return matrix;
    },

    _calcDimensionsTransformMatrix: function(skewX, skewY, flipping) {
      var skewMatrixX = [1, 0, Math.tan(degreesToRadians(skewX)), 1],
          skewMatrixY = [1, Math.tan(degreesToRadians(skewY)), 0, 1],
          scaleX = this.scaleX * (flipping && this.flipX ? -1 : 1),
          scaleY = this.scaleY * (flipping && this.flipY ? -1 : 1),
          scaleMatrix = [scaleX, 0, 0, scaleY],
          m = multiplyMatrices(scaleMatrix, skewMatrixX, true);
      return multiplyMatrices(m, skewMatrixY, true);
    }
  });
})();
<|endoftext|>