<commit_before>"use strict";

!function (name, context, definition) {
  if (typeof define == 'function') define(definition);
  else if (typeof module != 'undefined') module.exports = definition();
  else context[name] = definition();
}('mandlebrot', this, () => {
  // INIT CANVAS
  const canvas = document.getElementById("canvas");
  const canvasOverlay = document.getElementById("canvasOverlay");

  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
  canvasOverlay.width  = window.innerWidth;
  canvasOverlay.height = window.innerHeight;

  const ratio = canvas.width / canvas.height;

  const yMax = 1.2;
  const width = yMax * (canvas.width / canvas.height); // -2 to 2

  const overlayCtx = canvasOverlay.getContext('2d');
  overlayCtx.lineWidth = 3;
  overlayCtx.strokeStyle = '#FF00FF';
  const ctx = canvas.getContext('2d');
  const imgData = ctx.createImageData(canvas.width, 1);

  const x_interval = width * 2 / canvas.width; // -2 to 2
  const y_interval = yMax * 2 / canvas.height; // -2 to 2

  const helpers = {
    getEl(id) {
      return document.getElementById(id);
    },

    getParams() {
      let params = window.location.search;
      const f = {};
      params.replace("?", "")
            .split("&")
            .map((p) => p.split("="))
            .forEach((p) => f[p[0]] = p[1]);
      return f;
    },

    getValue(param, value, fallback) {
      const params = helpers.getParams();
      return params[param] || value || fallback;
    },

    getBoolean(value) {
      if (typeof value === "string") return value == "true";
      return value;
    }
  }

  const m = {
    maxIterations: parseInt(helpers.getValue("mi", helpers.getEl("maxIterations").value, 50)),
    escapeRadius: parseInt(helpers.getValue("er", helpers.getEl("escapeRadius").value, 5)),
    y_interval,
    x_interval,
    boundaries: {
      top: parseFloat(helpers.getValue("t", yMax)),
      right: parseFloat(helpers.getValue("r", width)),
      bottom: parseFloat(helpers.getValue("b", -yMax)),
      left: parseFloat(helpers.getValue("l", -width))
    },
    ctx,
    overlayCtx,
    imgData,
    yPixel: 0,
    y: parseFloat(helpers.getValue("t", yMax)),
    c: [-0.123, 0.745], // TODO update this
    color: helpers.getBoolean(helpers.getValue("co", helpers.getEl("color").checked, false)),
    julia: helpers.getBoolean(helpers.getValue("j", helpers.getEl("julia").checked, false)),
    lastUpdatedAt: 0,
    totalTime: document.getElementById("totalTime"),

    // https://gist.github.com/mjackson/5311256#file-color-conversion-algorithms-js-L119
    hsvToRgb(h, s, v) {
      let r, g, b;

      const i = Math.floor(h * 6);
      const f = h * 6 - i;
      const p = v * (1 - s);
      const q = v * (1 - f * s);
      const t = v * (1 - (1 - f) * s);

      switch (i % 6) {
        case 0: r = v, g = t, b = p; break;
        case 1: r = q, g = v, b = p; break;
        case 2: r = p, g = v, b = t; break;
        case 3: r = p, g = q, b = v; break;
        case 4: r = t, g = p, b = v; break;
        case 5: r = v, g = p, b = q; break;
      }

      return [ r * 255, g * 255, b * 255 ];
    },

    getColor(iterations) {
      // smooth color by adjusting iteration count
      const n = iterations.number - Math.log2(Math.log2(iterations.absoluteValue));

      // grayscale
      if (!m.color) {
        const value = 255 - (n / m.maxIterations * 255)
        return [value, value, value];
      }

      // color
      if (n <= 1) return [255, 255, 255];
      if (n >= m.maxIterations - 1) return [0, 0, 0];

      const value = n / m.maxIterations;

      // adjusting hue and value to make colors look better (blue only)
      return m.hsvToRgb(.5 + value / 2, 1, 1 - value);
    },

    getIterations(real, imaginary) {
      let Zr = m.julia ? real : 0,
          Zi = m.julia ? imaginary : 0,
          tempR,
          tempI,
          n  = 0,
          abs = 0,
          max = m.maxIterations,
          escape = m.escapeRadius,
          Cr = m.julia ? m.c[0] : real,
          Ci = m.julia ? m.c[1] : imaginary;

      for ( ; n < max && abs <= escape; n++) {
        tempR = Math.pow(Zr, 2) - Math.pow(Zi, 2) + Cr;
        tempI = 2 * Zr * Zi + Ci;

        Zr = tempR;
        Zi = tempI;
        abs = Math.sqrt(Math.pow(Zr, 2) + Math.pow(Zi, 2));
      }

      return {number: n, absoluteValue: abs};
    },

    updateProgressLine(yPixel) {
      m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);

      if (yPixel) {
        m.overlayCtx.beginPath();
        m.overlayCtx.moveTo(0, yPixel);
        m.overlayCtx.lineTo(canvasOverlay.width, yPixel);
        m.overlayCtx.stroke();
      }

    },

    drawSingleLine(x) {
      let offset = 0;
      // build line of pixel data to render
      for(; x <= m.boundaries.right; x += m.x_interval) {
        const iterations = m.getIterations(x, m.y);
        const color = m.getColor(iterations);

        m.imgData.data[offset++] = color[0];
        m.imgData.data[offset++] = color[1];
        m.imgData.data[offset++] = color[2];
        m.imgData.data[offset++] = 255;
      }
    },

    draw() {
      if (!m.startTime) m.startTime = Date.now();
      m.yPixel++;

      m.drawSingleLine(m.boundaries.left);
      m.ctx.putImageData(imgData, 0, m.yPixel);
      m.y -= m.y_interval

      if (m.yPixel <= window.innerHeight) {
        // not done, keep drawing
        if (Date.now() - m.lastUpdatedAt > 200) {
          // throttle updating DOM
          m.lastUpdatedAt = Date.now();
          m.updateProgressLine(m.yPixel);
          // go to next tick so DOM can update
          setTimeout(m.draw, 0);
        } else {
          m.draw();
        }
      } else {
        // done drawing, reset
        m.y = m.boundaries.top;
        m.lastUpdatedAt = 0;
        m.yPixel = 0;
        m.updateProgressLine();
        m.totalTime.textContent = `${(Date.now() - m.startTime) / 1000}s`;
        m.startTime = null;
      }
    },

    getC(value) {
      let values = value.trim().match(/-?(?:\d*\.)?\d+/g);
      return [parseFloat(values[0]), parseFloat(values[1])];
    },

    pushState(replace) {
      const params = [
        `mi=${m.maxIterations}`,
        `er=${m.escapeRadius}`,
        `co=${m.color}`,
        `j=${m.julia}`,
        `c=${m.c}`,
        `l=${m.boundaries.left}`,
        `t=${m.boundaries.top}`,
        `r=${m.boundaries.right}`,
        `b=${m.boundaries.bottom}`
      ];
      const state = {
        mi: m.maxIterations,
        er: m.escapeRadius,
        co: m.color,
        j: m.julia,
        c: m.c,
        l: m.boundaries.left,
        t: m.boundaries.top,
        r: m.boundaries.right,
        b: m.boundaries.bottom
      };

      if (replace) {
        history.replaceState(state, "Mandelbrot",  `?${params.join("&")}`);
      } else {
        history.pushState(state, "Mandelbrot", `?${params.join("&")}`);
      }
    },

    onPopState(e) {
      m.updateState(e.state);
    },

    bindListeners() {
      window.addEventListener('popstate', m.onPopState);

      // INPUT CONTROLS
      helpers.getEl("maxIterations").addEventListener("change", (e) => {
        m.maxIterations = parseInt(e.target.value);
        m.draw();
        m.pushState();
      });

      helpers.getEl("escapeRadius").addEventListener("change", (e) => {
        m.escapeRadius = parseInt(e.target.value);
        m.draw();
        m.pushState();
      });

      helpers.getEl("color").addEventListener("change", (e) => {
        m.color = e.target.checked;
        m.draw();
        m.pushState();
      });

      helpers.getEl("julia").addEventListener("change", (e) => {
        m.julia = e.target.checked;
        m.draw();
        m.pushState();
      });

      helpers.getEl("c").addEventListener("change", (e) => {
        m.c = m.getC(e.target.value);
        m.draw();
        m.pushState();
      });

      // DRAG ZOOMING
      let zoomBox = null;

      canvasOverlay.addEventListener("mousedown", (e) => {
        zoomBox = [e.clientX, e.clientY, 0, 0];
      });

      canvasOverlay.addEventListener("mousemove", (e) => {
        if (zoomBox) {
          // clear out old box first
          m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);

          // draw new box keeping aspect ratio
          zoomBox[2] = e.clientX;
          zoomBox[3] = zoomBox[1] + ((e.clientX - zoomBox[0]) / ratio);

          m.overlayCtx.strokeRect(zoomBox[0], zoomBox[1], zoomBox[2] - zoomBox[0], zoomBox[3] - zoomBox[1]);
        }
      });

      canvasOverlay.addEventListener("mouseup", (e) => {
        m.boundaries = {
          left: m.boundaries.left + m.x_interval * zoomBox[0],
          right: m.boundaries.left + m.x_interval * zoomBox[2],
          top: m.boundaries.top - m.y_interval * zoomBox[1],
          bottom: m.boundaries.top - m.y_interval * zoomBox[3]
        };
        m.y = m.boundaries.top;
        m.x_interval = Math.abs(m.boundaries.left - m.boundaries.right) / canvasOverlay.width;
        m.y_interval = Math.abs(m.boundaries.top - m.boundaries.bottom) / canvasOverlay.height;

        zoomBox = null;
        m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);
        m.draw();
      });
    },

    updateState(obj = {}) {
      console.log(obj);
      const color = obj.co;
      helpers.getEl("color").checked = color;
      m.color = color;

      const julia = obj.j;
      helpers.getEl("julia").checked = julia;
      m.julia = julia;

      const maxIterations = obj.mi;
      helpers.getEl("maxIterations").value = maxIterations;
      m.maxIterations = maxIterations;

      const escapeRadius = obj.er;
      helpers.getEl("escapeRadius").value = escapeRadius;

      const c = obj.c;
      helpers.getEl("c").value = c; // need to format this first
      m.c = c;

      m.draw();
    },

    init() {
      m.bindListeners();
      m.draw();
      m.pushState(true);
    }
  };

  return {
    helpers,
    m
  };
});
<commit_msg>start using state<commit_after>"use strict";

!function (name, context, definition) {
  if (typeof define == 'function') define(definition);
  else if (typeof module != 'undefined') module.exports = definition();
  else context[name] = definition();
}('mandlebrot', this, () => {
  // INIT CANVAS
  const canvas = document.getElementById("canvas");
  const canvasOverlay = document.getElementById("canvasOverlay");

  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
  canvasOverlay.width  = window.innerWidth;
  canvasOverlay.height = window.innerHeight;

  const ratio = canvas.width / canvas.height;

  const yMax = 1.2;
  const width = yMax * (canvas.width / canvas.height); // -2 to 2

  const overlayCtx = canvasOverlay.getContext('2d');
  overlayCtx.lineWidth = 3;
  overlayCtx.strokeStyle = '#FF00FF';
  const ctx = canvas.getContext('2d');
  const imgData = ctx.createImageData(canvas.width, 1);

  const x_interval = width * 2 / canvas.width; // -2 to 2
  const y_interval = yMax * 2 / canvas.height; // -2 to 2

  const helpers = {
    getEl(id) {
      return document.getElementById(id);
    },

    getParams() {
      let params = window.location.search;
      const f = {};
      params.replace("?", "")
            .split("&")
            .map((p) => p.split("="))
            .forEach((p) => f[p[0]] = p[1]);
      return f;
    },

    getValue(param, value, fallback) {
      const params = helpers.getParams();
      return params[param] || value || fallback;
    },

    getBoolean(value) {
      if (typeof value === "string") return value == "true";
      return value;
    }
  }

  const m = {
    maxIterations: parseInt(helpers.getValue("mi", helpers.getEl("maxIterations").value, 50)),
    escapeRadius: parseInt(helpers.getValue("er", helpers.getEl("escapeRadius").value, 5)),
    y_interval,
    x_interval,
    boundaries: {
      top: parseFloat(helpers.getValue("t", yMax)),
      right: parseFloat(helpers.getValue("r", width)),
      bottom: parseFloat(helpers.getValue("b", -yMax)),
      left: parseFloat(helpers.getValue("l", -width))
    },
    ctx,
    overlayCtx,
    imgData,
    yPixel: 0,
    y: parseFloat(helpers.getValue("t", yMax)),
    c: [-0.123, 0.745], // TODO update this
    color: helpers.getBoolean(helpers.getValue("co", helpers.getEl("color").checked, false)),
    julia: helpers.getBoolean(helpers.getValue("j", helpers.getEl("julia").checked, false)),
    lastUpdatedAt: 0,
    totalTime: document.getElementById("totalTime"),

    // https://gist.github.com/mjackson/5311256#file-color-conversion-algorithms-js-L119
    hsvToRgb(h, s, v) {
      let r, g, b;

      const i = Math.floor(h * 6);
      const f = h * 6 - i;
      const p = v * (1 - s);
      const q = v * (1 - f * s);
      const t = v * (1 - (1 - f) * s);

      switch (i % 6) {
        case 0: r = v, g = t, b = p; break;
        case 1: r = q, g = v, b = p; break;
        case 2: r = p, g = v, b = t; break;
        case 3: r = p, g = q, b = v; break;
        case 4: r = t, g = p, b = v; break;
        case 5: r = v, g = p, b = q; break;
      }

      return [ r * 255, g * 255, b * 255 ];
    },

    getColor(iterations) {
      // smooth color by adjusting iteration count
      const n = iterations.number - Math.log2(Math.log2(iterations.absoluteValue));

      // grayscale
      if (!m.color) {
        const value = 255 - (n / m.maxIterations * 255)
        return [value, value, value];
      }

      // color
      if (n <= 1) return [255, 255, 255];
      if (n >= m.maxIterations - 1) return [0, 0, 0];

      const value = n / m.maxIterations;

      // adjusting hue and value to make colors look better (blue only)
      return m.hsvToRgb(.5 + value / 2, 1, 1 - value);
    },

    getIterations(real, imaginary) {
      let Zr = m.julia ? real : 0,
          Zi = m.julia ? imaginary : 0,
          tempR,
          tempI,
          n  = 0,
          abs = 0,
          max = m.maxIterations,
          escape = m.escapeRadius,
          Cr = m.julia ? m.c[0] : real,
          Ci = m.julia ? m.c[1] : imaginary;

      for ( ; n < max && abs <= escape; n++) {
        tempR = Math.pow(Zr, 2) - Math.pow(Zi, 2) + Cr;
        tempI = 2 * Zr * Zi + Ci;

        Zr = tempR;
        Zi = tempI;
        abs = Math.sqrt(Math.pow(Zr, 2) + Math.pow(Zi, 2));
      }

      return {number: n, absoluteValue: abs};
    },

    updateProgressLine(yPixel) {
      m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);

      if (yPixel) {
        m.overlayCtx.beginPath();
        m.overlayCtx.moveTo(0, yPixel);
        m.overlayCtx.lineTo(canvasOverlay.width, yPixel);
        m.overlayCtx.stroke();
      }

    },

    drawSingleLine(x) {
      let offset = 0;
      // build line of pixel data to render
      for(; x <= m.boundaries.right; x += m.x_interval) {
        const iterations = m.getIterations(x, m.y);
        const color = m.getColor(iterations);

        m.imgData.data[offset++] = color[0];
        m.imgData.data[offset++] = color[1];
        m.imgData.data[offset++] = color[2];
        m.imgData.data[offset++] = 255;
      }
    },

    draw() {
      if (!m.startTime) m.startTime = Date.now();
      m.yPixel++;

      m.drawSingleLine(m.boundaries.left);
      m.ctx.putImageData(imgData, 0, m.yPixel);
      m.y -= m.y_interval

      if (m.yPixel <= window.innerHeight) {
        // not done, keep drawing
        if (Date.now() - m.lastUpdatedAt > 200) {
          // throttle updating DOM
          m.lastUpdatedAt = Date.now();
          m.updateProgressLine(m.yPixel);
          // go to next tick so DOM can update
          setTimeout(m.draw, 0);
        } else {
          m.draw();
        }
      } else {
        // done drawing, reset
        m.y = m.boundaries.top;
        m.lastUpdatedAt = 0;
        m.yPixel = 0;
        m.updateProgressLine();
        m.totalTime.textContent = `${(Date.now() - m.startTime) / 1000}s`;
        m.startTime = null;
      }
    },

    getC(value) {
      let values = value.trim().match(/-?(?:\d*\.)?\d+/g);
      return [parseFloat(values[0]), parseFloat(values[1])];
    },

    pushState(replace) {
      const params = [
        `mi=${m.maxIterations}`,
        `er=${m.escapeRadius}`,
        `co=${m.color}`,
        `j=${m.julia}`,
        `c=${m.c}`,
        `l=${m.boundaries.left}`,
        `t=${m.boundaries.top}`,
        `r=${m.boundaries.right}`,
        `b=${m.boundaries.bottom}`
      ];
      // TODO add getState method, maybe with a toParams method too
      const state = {
        mi: m.maxIterations,
        er: m.escapeRadius,
        co: m.color,
        j: m.julia,
        c: m.c,
        l: m.boundaries.left,
        t: m.boundaries.top,
        r: m.boundaries.right,
        b: m.boundaries.bottom
      };

      if (replace) {
        history.replaceState(state, "Mandelbrot",  `?${params.join("&")}`);
      } else {
        history.pushState(state, "Mandelbrot", `?${params.join("&")}`);
      }
    },

    onPopState(e) {
      m.updateState(e.state);
    },

    setState(id, value) {
      m[id] = value;
      const el = helpers.getEl(id);
      if (typeof value === "boolean") {
        el.checked = value;
      } else {
        el.value = value;
      }

      m.draw();
      m.pushState();
    },

    bindListeners() {
      window.addEventListener('popstate', m.onPopState);

      // INPUT CONTROLS
      helpers.getEl("maxIterations").addEventListener("change", (e) => m.setState("maxIterations", parseInt(e.target.value)));
      helpers.getEl("escapeRadius").addEventListener("change", (e) => m.setState("escapeRadius", parseInt(e.target.value)));
      helpers.getEl("color").addEventListener("change", (e) => m.setState("color", e.target.checked));
      helpers.getEl("julia").addEventListener("change", (e) => m.setState("julia", e.target.checked));
      helpers.getEl("c").addEventListener("change", (e) => m.setState("c", m.getC(e.target.value)));

      // DRAG ZOOMING
      let zoomBox = null;

      canvasOverlay.addEventListener("mousedown", (e) => {
        zoomBox = [e.clientX, e.clientY, 0, 0];
      });

      canvasOverlay.addEventListener("mousemove", (e) => {
        if (zoomBox) {
          // clear out old box first
          m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);

          // draw new box keeping aspect ratio
          zoomBox[2] = e.clientX;
          zoomBox[3] = zoomBox[1] + ((e.clientX - zoomBox[0]) / ratio);

          m.overlayCtx.strokeRect(zoomBox[0], zoomBox[1], zoomBox[2] - zoomBox[0], zoomBox[3] - zoomBox[1]);
        }
      });

      canvasOverlay.addEventListener("mouseup", (e) => {
        // TODO this needs state change logic
        m.boundaries = {
          left: m.boundaries.left + m.x_interval * zoomBox[0],
          right: m.boundaries.left + m.x_interval * zoomBox[2],
          top: m.boundaries.top - m.y_interval * zoomBox[1],
          bottom: m.boundaries.top - m.y_interval * zoomBox[3]
        };
        m.y = m.boundaries.top;
        m.x_interval = Math.abs(m.boundaries.left - m.boundaries.right) / canvasOverlay.width;
        m.y_interval = Math.abs(m.boundaries.top - m.boundaries.bottom) / canvasOverlay.height;

        zoomBox = null;
        m.overlayCtx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);
        m.draw();
      });
    },

    updateState(obj = {}) {
      // TODO use setState with dirty logic so it only draws once
      helpers.getEl("color").checked = m.color = obj.co;
      helpers.getEl("julia").checked = m.julia = obj.j;
      helpers.getEl("maxIterations").value = m.maxIterations = obj.mi;
      helpers.getEl("escapeRadius").value = m.escapeRadius = obj.er;

      helpers.getEl("c").value = obj.c; // need to format this first
      m.c = obj.c;

      m.draw();
    },

    init() {
      m.bindListeners();
      m.draw();
      m.pushState(true);
    }
  };

  return {
    helpers,
    m
  };
});
<|endoftext|>


<commit_before>import { Record } from "immutable";
import {
  shouldShowPrettyPrint,
  shouldShowFooter,
  traverseResults,
  toEditorLine,
  toEditorPosition,
  toEditorRange,
  toSourceLine,
  toSourceLocation,
  scrollToColumn,
  markText,
  lineAtHeight,
  getSourceLocationFromMouseEvent,
  forEachLine,
  removeLineClass,
  clearLineClass,
  getTextForLine,
  getCursorLine
} from "../index";

const makeSource = Record({
  id: "test-id-123/originalSource",
  url: "http://example.com/index.js",
  sourceMapURL: "http://example.com/index.source.map",
  isBlackBoxed: false,
  isPrettyPrinted: false,
  isWasm: false,
  text: "some text here",
  contentType: "text/javascript",
  loadedState: "loaded"
});

describe("shouldShowPrettyPrint", () => {
  it("no pretty print with no source", () => {
    expect(shouldShowPrettyPrint(null)).toEqual(false);
  });

  it("shows pretty print for a source", () => {
    let source = makeSource();
    source = source.merge({
      id: "test-id-123",
      sourceMapURL: null
    });
    expect(shouldShowPrettyPrint(source)).toEqual(true);
  });
});

describe("shouldShowFooter", () => {
  it("shows footer when not horizontal", () => {
    expect(shouldShowFooter(makeSource(), false)).toEqual(true);
  });

  it("does not show footer when no source is selected", () => {
    expect(shouldShowFooter(null, true)).toEqual(false);
  });

  it("shows if pretty print should show", () => {
    expect(shouldShowFooter(makeSource(), true)).toEqual(true);
  });
});

describe("traverseResults", () => {
  const e = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
  const ctx = {};
  const query = "Awesome books";
  const modifiers = {
    caseSensitive: false,
    regexMatch: false,
    wholeWord: false
  };
  it("traverses next", () => {
    traverseResults(e, ctx, query, "next", modifiers);
    expect(e.stopPropagation).toHaveBeenCalled();
    expect(e.preventDefault).toHaveBeenCalled();
    // expect(findNext).toHaveBeenCalledWith(ctx, query, true, modifiers);
  });

  it("traverses previous", () => {
    e.stopPropagation.mockClear();
    e.preventDefault.mockClear();
    traverseResults(e, ctx, query, "prev", modifiers);
    expect(e.stopPropagation).toHaveBeenCalled();
    expect(e.preventDefault).toHaveBeenCalled();
    // expect(findPrev).toHaveBeenCalledWith(ctx, query, true, modifiers);
  });
});

describe("toEditorLine", () => {
  it("returns an editor line", () => {
    const testId = "test-123";
    const line = 30;
    expect(toEditorLine(testId, line)).toEqual(29);
  });
});

describe("toEditorPosition", () => {
  it("returns an editor position", () => {
    const loc = { line: 100, column: 25 };
    expect(toEditorPosition(loc)).toEqual({
      line: 99,
      column: 25
    });
  });
});

describe("toEditorRange", () => {
  it("returns an editor range", () => {
    const testId = "test-123";
    const loc = { start: { line: 100, column: 25 }, end: { line: 200 } };
    expect(toEditorRange(testId, loc)).toEqual({
      start: { line: 99, column: 25 },
      end: { line: 199, column: 0 }
    });
  });
});

describe("toSourceLine", () => {
  it("returns a source line", () => {
    const testId = "test-123";
    const line = 30;
    expect(toSourceLine(testId, line)).toEqual(31);
  });
});

describe("toSourceLocation", () => {
  const testId = "test-123";
  const loc = { line: 100, column: 30 };
  it("returns a source location", () => {
    expect(toSourceLocation(testId, loc)).toEqual({
      line: 101,
      column: 30
    });
  });
});

const codeMirror = {
  doc: {
    iter: jest.fn((_, __, cb) => cb())
  },
  lineCount: jest.fn(() => 100),
  getLine: jest.fn(() => "something"),
  getCursor: jest.fn(() => ({ line: 3 })),
  getScrollerElement: jest.fn(() => ({
    offsetWidth: 100,
    offsetHeight: 100
  })),
  getScrollInfo: () => {},
  removeLineClass: jest.fn(),
  operation: jest.fn(cb => cb()),
  charCoords: jest.fn(() => ({
    top: 100,
    right: 50,
    bottom: 100,
    left: 50
  })),
  coordsChar: jest.fn(() => ({ line: 6, ch: 30 })),
  lineAtHeight: jest.fn(() => 300),
  markText: jest.fn(),
  scrollTo: jest.fn()
};

const editor = { codeMirror };

describe("scrollToColumn", () => {
  it("calls codemirror APIs charCoords, getScrollerElement, scrollTo", () => {
    scrollToColumn(codeMirror, 60, 123);
    expect(codeMirror.charCoords).toHaveBeenCalledWith(
      { line: 60, ch: 123 },
      "local"
    );
    expect(codeMirror.scrollTo).toHaveBeenCalledWith(0, 50);
  });
});

describe("markText", () => {
  it("calls codemirror API markText & returns marker", () => {
    const loc = {
      start: { line: 10, column: 0 },
      end: { line: 30, column: 50 }
    };
    markText(editor, "test-123", loc);
    expect(codeMirror.markText).toHaveBeenCalledWith(
      { ch: loc.start.column, line: loc.start.line },
      { ch: loc.end.column, line: loc.end.line },
      { className: "test-123" }
    );
  });
});

describe("lineAtHeight", () => {
  it("calls codemirror API lineAtHeight", () => {
    const e = { clientX: 30, clientY: 60 };
    expect(lineAtHeight(editor, "test-123", e)).toEqual(301);
    expect(editor.codeMirror.lineAtHeight).toHaveBeenCalledWith(e.clientY);
  });
});

describe("getSourceLocationFromMouseEvent", () => {
  it("calls codemirror API coordsChar & returns location", () => {
    const loc = { sourceId: "test-123" };
    const e = { clientX: 30, clientY: 60 };
    expect(getSourceLocationFromMouseEvent(editor, loc, e)).toEqual({
      sourceId: "test-123",
      line: 7,
      column: 31
    });
    expect(editor.codeMirror.coordsChar).toHaveBeenCalledWith({
      left: 30,
      top: 60
    });
  });
});

describe("forEachLine", () => {
  it("calls codemirror API operation && doc.iter across a doc", () => {
    const test = jest.fn();
    forEachLine(codeMirror, test);
    expect(codeMirror.operation).toHaveBeenCalled();
    expect(codeMirror.doc.iter).toHaveBeenCalledWith(0, 100, test);
  });
});

describe("removeLineClass", () => {
  it("calls codemirror API removeLineClass", () => {
    const line = 3;
    const className = "test-class";
    removeLineClass(codeMirror, line, className);
    expect(codeMirror.removeLineClass).toHaveBeenCalledWith(
      line,
      "line",
      className
    );
  });
});

describe("clearLineClass", () => {
  it("Uses forEachLine & removeLineClass to clear class on all lines", () => {
    codeMirror.operation.mockClear();
    codeMirror.doc.iter.mockClear();
    codeMirror.removeLineClass.mockClear();
    clearLineClass(codeMirror, "test-class");
    expect(codeMirror.operation).toHaveBeenCalled();
    expect(codeMirror.doc.iter).toHaveBeenCalledWith(
      0,
      100,
      expect.any(Function)
    );
    expect(codeMirror.removeLineClass).toHaveBeenCalled();
  });
});

describe("getTextForLine", () => {
  it("calls codemirror API getLine & returns line text", () => {
    getTextForLine(codeMirror, 3);
    expect(codeMirror.getLine).toHaveBeenCalledWith(2);
  });
});
describe("getCursorLine", () => {
  it("calls codemirror API getCursor & returns line number", () => {
    getCursorLine(codeMirror);
    expect(codeMirror.getCursor).toHaveBeenCalled();
  });
});
<commit_msg>🐛: Fix broken test by adding appropriate mocks (#5242)<commit_after>import { Record } from "immutable";
import {
  shouldShowPrettyPrint,
  shouldShowFooter,
  traverseResults,
  toEditorLine,
  toEditorPosition,
  toEditorRange,
  toSourceLine,
  toSourceLocation,
  scrollToColumn,
  markText,
  lineAtHeight,
  getSourceLocationFromMouseEvent,
  forEachLine,
  removeLineClass,
  clearLineClass,
  getTextForLine,
  getCursorLine
} from "../index";

const makeSource = Record({
  id: "test-id-123/originalSource",
  url: "http://example.com/index.js",
  sourceMapURL: "http://example.com/index.source.map",
  isBlackBoxed: false,
  isPrettyPrinted: false,
  isWasm: false,
  text: "some text here",
  contentType: "text/javascript",
  loadedState: "loaded"
});

describe("shouldShowPrettyPrint", () => {
  it("no pretty print with no source", () => {
    expect(shouldShowPrettyPrint(null)).toEqual(false);
  });

  it("shows pretty print for a source", () => {
    let source = makeSource();
    source = source.merge({
      id: "test-id-123",
      sourceMapURL: null
    });
    expect(shouldShowPrettyPrint(source)).toEqual(true);
  });
});

describe("shouldShowFooter", () => {
  it("shows footer when not horizontal", () => {
    expect(shouldShowFooter(makeSource(), false)).toEqual(true);
  });

  it("does not show footer when no source is selected", () => {
    expect(shouldShowFooter(null, true)).toEqual(false);
  });

  it("shows if pretty print should show", () => {
    expect(shouldShowFooter(makeSource(), true)).toEqual(true);
  });
});

describe("traverseResults", () => {
  const e = { stopPropagation: jest.fn(), preventDefault: jest.fn() };
  const ctx = {};
  const query = "Awesome books";
  const modifiers = {
    caseSensitive: false,
    regexMatch: false,
    wholeWord: false
  };
  it("traverses next", () => {
    traverseResults(e, ctx, query, "next", modifiers);
    expect(e.stopPropagation).toHaveBeenCalled();
    expect(e.preventDefault).toHaveBeenCalled();
    // expect(findNext).toHaveBeenCalledWith(ctx, query, true, modifiers);
  });

  it("traverses previous", () => {
    e.stopPropagation.mockClear();
    e.preventDefault.mockClear();
    traverseResults(e, ctx, query, "prev", modifiers);
    expect(e.stopPropagation).toHaveBeenCalled();
    expect(e.preventDefault).toHaveBeenCalled();
    // expect(findPrev).toHaveBeenCalledWith(ctx, query, true, modifiers);
  });
});

describe("toEditorLine", () => {
  it("returns an editor line", () => {
    const testId = "test-123";
    const line = 30;
    expect(toEditorLine(testId, line)).toEqual(29);
  });
});

describe("toEditorPosition", () => {
  it("returns an editor position", () => {
    const loc = { line: 100, column: 25 };
    expect(toEditorPosition(loc)).toEqual({
      line: 99,
      column: 25
    });
  });
});

describe("toEditorRange", () => {
  it("returns an editor range", () => {
    const testId = "test-123";
    const loc = { start: { line: 100, column: 25 }, end: { line: 200 } };
    expect(toEditorRange(testId, loc)).toEqual({
      start: { line: 99, column: 25 },
      end: { line: 199, column: 0 }
    });
  });
});

describe("toSourceLine", () => {
  it("returns a source line", () => {
    const testId = "test-123";
    const line = 30;
    expect(toSourceLine(testId, line)).toEqual(31);
  });
});

describe("toSourceLocation", () => {
  const testId = "test-123";
  const loc = { line: 100, column: 30 };
  it("returns a source location", () => {
    expect(toSourceLocation(testId, loc)).toEqual({
      line: 101,
      column: 30
    });
  });
});

const codeMirror = {
  doc: {
    iter: jest.fn((_, __, cb) => cb())
  },
  lineCount: jest.fn(() => 100),
  getLine: jest.fn(() => "something"),
  getCursor: jest.fn(() => ({ line: 3 })),
  getScrollerElement: jest.fn(() => ({
    offsetWidth: 100,
    offsetHeight: 100
  })),
  getScrollInfo: () => ({
    top: 0,
    right: 0,
    bottom: 0,
    left: 0,
    clientHeight: 100,
    clientWidth: 100
  }),
  removeLineClass: jest.fn(),
  operation: jest.fn(cb => cb()),
  charCoords: jest.fn(() => ({
    top: 100,
    right: 50,
    bottom: 100,
    left: 50
  })),
  coordsChar: jest.fn(() => ({ line: 6, ch: 30 })),
  lineAtHeight: jest.fn(() => 300),
  markText: jest.fn(),
  scrollTo: jest.fn(),
  defaultCharWidth: jest.fn(() => 8),
  defaultTextHeight: jest.fn(() => 16)
};

const editor = { codeMirror };

describe("scrollToColumn", () => {
  it("calls codemirror APIs charCoords, getScrollerElement, scrollTo", () => {
    scrollToColumn(codeMirror, 60, 123);
    expect(codeMirror.charCoords).toHaveBeenCalledWith(
      { line: 60, ch: 123 },
      "local"
    );
    expect(codeMirror.scrollTo).toHaveBeenCalledWith(0, 50);
  });
});

describe("markText", () => {
  it("calls codemirror API markText & returns marker", () => {
    const loc = {
      start: { line: 10, column: 0 },
      end: { line: 30, column: 50 }
    };
    markText(editor, "test-123", loc);
    expect(codeMirror.markText).toHaveBeenCalledWith(
      { ch: loc.start.column, line: loc.start.line },
      { ch: loc.end.column, line: loc.end.line },
      { className: "test-123" }
    );
  });
});

describe("lineAtHeight", () => {
  it("calls codemirror API lineAtHeight", () => {
    const e = { clientX: 30, clientY: 60 };
    expect(lineAtHeight(editor, "test-123", e)).toEqual(301);
    expect(editor.codeMirror.lineAtHeight).toHaveBeenCalledWith(e.clientY);
  });
});

describe("getSourceLocationFromMouseEvent", () => {
  it("calls codemirror API coordsChar & returns location", () => {
    const loc = { sourceId: "test-123" };
    const e = { clientX: 30, clientY: 60 };
    expect(getSourceLocationFromMouseEvent(editor, loc, e)).toEqual({
      sourceId: "test-123",
      line: 7,
      column: 31
    });
    expect(editor.codeMirror.coordsChar).toHaveBeenCalledWith({
      left: 30,
      top: 60
    });
  });
});

describe("forEachLine", () => {
  it("calls codemirror API operation && doc.iter across a doc", () => {
    const test = jest.fn();
    forEachLine(codeMirror, test);
    expect(codeMirror.operation).toHaveBeenCalled();
    expect(codeMirror.doc.iter).toHaveBeenCalledWith(0, 100, test);
  });
});

describe("removeLineClass", () => {
  it("calls codemirror API removeLineClass", () => {
    const line = 3;
    const className = "test-class";
    removeLineClass(codeMirror, line, className);
    expect(codeMirror.removeLineClass).toHaveBeenCalledWith(
      line,
      "line",
      className
    );
  });
});

describe("clearLineClass", () => {
  it("Uses forEachLine & removeLineClass to clear class on all lines", () => {
    codeMirror.operation.mockClear();
    codeMirror.doc.iter.mockClear();
    codeMirror.removeLineClass.mockClear();
    clearLineClass(codeMirror, "test-class");
    expect(codeMirror.operation).toHaveBeenCalled();
    expect(codeMirror.doc.iter).toHaveBeenCalledWith(
      0,
      100,
      expect.any(Function)
    );
    expect(codeMirror.removeLineClass).toHaveBeenCalled();
  });
});

describe("getTextForLine", () => {
  it("calls codemirror API getLine & returns line text", () => {
    getTextForLine(codeMirror, 3);
    expect(codeMirror.getLine).toHaveBeenCalledWith(2);
  });
});
describe("getCursorLine", () => {
  it("calls codemirror API getCursor & returns line number", () => {
    getCursorLine(codeMirror);
    expect(codeMirror.getCursor).toHaveBeenCalled();
  });
});
<|endoftext|>


<commit_before>(function(){

  var polyProto = google.maps.Polygon.prototype;

  // Default lineSize to 100
  polyProto.lineSize = 100;

  // Default to normal non-editable state
  polyProto.editable = false;

  // Default highlight color for edit lines
  polyProto.highlightColor = '#cccccc';

  polyProto.editLines = [];

  /**
   * Override setOptions so that we can intercept
   * the editable and lineSize options.
   */
  var _setOptions = polyProto.setOptions;
  polyProto.setOptions = function(options){
    if(_isObject(options)){

      // Intercept the editable option
      var editable = this.getEditable();
      if(!_isUndefined(options.editable)){
        editable = options.editable;
        delete options.editable;
      }

      // Intercept the lineSize option
      var lineSize = this.lineSize;
      if(!_isUndefined(options.lineSize)){
        lineSize = options.lineSize;
        delete options.lineSize;
      }

      // Intercept the highlightColor option
      var highlightColor = this.highlightColor;
      if(!_isUndefined(options.highlightColor)){
        highlightColor = options.highlightColor;
        delete options.highlightColor;
      }

      // Pass other options on to the Polygon
      _setOptions.call(this, options);

      // Set the new properties after setting
      // normal Polygon properties so that we
      // can be sure to get the last say on
      // any settings or behavior
      this.lineSize = lineSize;
      this.setEditable(editable);
      this.highlightColor = highlightColor;
    }
  }

  /**
   * Override the setEditable function so that
   * we know when to generate the polylines
   */
  polyProto.setEditable = function(editable){
    if(_isBoolean(editable)){
      this.editable = editable;
      if(editable){
        this.generateEditLines();
      }
    }
  };

  /**
   * Override getEditable since we don't
   * actually make the underlying polygon
   * editable
   */
  polyProto.getEditable = function(){
    return this.editable;
  };

  /**
   * Create the edit lines
   */
  polyProto.generateEditLines = function(){

    var self = this,
        map = self.getMap(),
        lineStyle = {
          strokeColor: self.get('strokeColor'),
          strokeOpacity: self.get('strokeOpacity'),
          strokeWeight: self.get('strokeWeight')
        };

    // Process each path independently
    this.getPaths().forEach(function(path, pathIndex){

      var lineLengths = _lineLengths(path.getLength(), self.lineSize),
          lines = [];

      // Iterate over each edit line
      var position = 0;
      for(var i = 0; i < lineLengths.length; i++){
        var newLinePath = [],
            end = Math.min(position + lineLengths[i], path.getLength());

        // Add the relevent points to the line's path
        for(; position < end; position++){
          newLinePath.push(path.getAt(position));
        }

        // Decrement position by 1 so that the next
        // line duplicates the last point of this line
        position--;

        // Create the google maps line object
        // and style it to match the polygon's borders
        var line = new google.maps.Polyline();
        line.setPath(newLinePath);
        line.setOptions(lineStyle);
        line.setMap(map);

        // Add events
        line.addListener('click', function(){
          this.setEditable(true);
        });
        line.addListener('mouseover', function(){
          this.set('strokeColor', self.highlightColor);
        });
        line.addListener('mouseout', function(){
          this.setOptions(lineStyle);
        });

        // Save the line
        lines.push(line);
      }

      // Add the first point of the path to the end of the last line
      lines[lines.length-1].getPath().push(path.getAt(0));

      // Keep track of whether a point event was fired
      // by the user or by us updating a neighbor point
      var updatingNeighbor = false;

      // Setup event listeners on the line's paths so that
      // we can update the underlying shape when the lines change
      $.each(lines, function(i, line){

        // New point
        line.getPath().addListener('insert_at', function(){
          self.updateFromLines(pathIndex, lines);
        });

        // Point removed
        line.getPath().addListener('remove_at', function(){
          self.updateFromLines(pathIndex, lines);
        });

        // Point moved
        line.getPath().addListener('set_at', function(vertex){

          // When either the first or last point was moved we need
          // to move the matching point of the neighboring line.
          // Because lines may be deleted (unlikely, but possible)
          // we have to figure out the line's position and neighbors
          // at runtime.
          if((vertex === 0 || vertex === this.getLength() - 1) && !updatingNeighbor){
            var neighbor = findNeighbor(lines, line, vertex);
            updatingNeighbor = true;
            neighbor.path.setAt(neighbor.index, this.getAt(vertex));
          }

          // Update underlying shape with changes only if we're moving
          // a point in the middle of the line or we're updating a neighbor
          // point after the user moved a first/last point
          else {
            updatingNeighbor = false;
            self.updateFromLines(pathIndex, lines);
          }
        });

        // Deleting a point
        // http://stackoverflow.com/a/14441786
        line.addListener('rightclick', function(event){
          var vertex = event.vertex;

          if(vertex != null){

            // Remove the point
            this.getPath().removeAt(event.vertex);

            // If the point being removed is the first or last
            // point in the line then we need to find and move
            // the neigbor's point to match our new end point.
            if(vertex === 0 || vertex === this.getPath().getLength()){
              var neighbor = findNeighbor(lines, this, vertex),
                  newEndIndex = vertex === 0 ? 0 : this.getPath().getLength() - 1;
              neighbor.path.setAt(neighbor.index, this.getPath().getAt(newEndIndex));
            }

            // If there if only one point left then delete the line.
            // It's neghbors should both be overlapping the last remaining point.
            if(this.getPath().getLength() === 1){
              this.setMap(null);
              var linePos = lines.indexOf(this);
              lines.splice(linePos, 1);
            }

            // TODO: handle deleting entire paths
          }
        });
      });

      self.editLines = self.editLines.concat(lines);

    });
  };

  /**
   * Called whenever lines change; updates the specified
   * path of the shape based on the lines that were
   * derived from it
   */
  polyProto.updateFromLines = function(pathIndex, lines){
    var newPoints = [];
    for(var i = 0; i < lines.length; i++){
      var linePath = lines[i].getPath(),
          lineLength = linePath.getLength();
      linePath.forEach(function(point, j){
        // Don't add the last point of each line
        // because those are duplicated
        if(j !== lineLength - 1){
          newPoints.push(point);
        }
      });
    }
    this.getPaths().setAt(pathIndex, new google.maps.MVCArray(newPoints));
    // TODO: Fire changed event
  };

  /**
   * Calculate length of each edit line.
   * Returns an array of numbers which represent
   * the length. Tries to even out the length of
   * the lines as best it can.
   */
  function _lineLengths(pathLength, lineSizeLimit){

    // Short circuit if there will only be one edit line
    if(lineSizeLimit >= pathLength){
      return [pathLength];
    }

    var rawNum = _numLines(pathLength, lineSizeLimit),
        numLines = Math.floor(rawNum),
        remainder = rawNum - numLines,
        lineLengths = [];

    // If lineSizeLimit divides into pathLenght evenly
    // then just return an array of length numLines
    // filled with lineSizeLimit.
    lineLengths = _numberArray(lineSizeLimit, numLines);

    // If lineSize doesn't divide into the pathLength evently
    // then try to even out the lengths of the lines.
    if(remainder) {
      var lastLength = Math.ceil(lineSizeLimit * remainder);

      // The last line cannot have 1 point
      if(lastLength < 2){
        lastLength = 2;
      }

      lineLengths.push(lastLength);

      // Starting at the second to last number (skip the
      // number we just pushed on), iterate backwards
      // over the array and decrement the numbers by one
      // until we make up the difference between the
      // lastLength and the LineSizeLimit

      var difference = lineSizeLimit - lastLength - 1,
          i = lineLengths.length - 2,
          last = lineLengths.length - 1;

      while(difference > 0){
        // Allow i to wrap back to the end
        if(i < 0){
          i = lineLengths.length - 1;
        }
        lineLengths[i] = lineLengths[i] - 1;
        difference--;
        i--;
        lineLengths[last]++;
      }
    }

    return lineLengths;
  };

  /**
   * Generate an array filled with a given number
   */
  function _numberArray(number, length){
    var array = [];
    for(var i = 0; i < length; i++){
      array[i] = number;
    }
    return array;
  };

  /**
   * Calculate number of edit lines
   */
  function _numLines(pathLength, lineSizeLimit){
    // Subtract one frome lineSizeLimit to account for
    // overlapping points at the edges.
    return pathLength / (lineSizeLimit - 1);
  };

  /**
   * Utility functions lifted from underscore
   */

  _isBoolean = function(obj) {
    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
  };

  _isUndefined = function(obj) {
    return obj === void 0;
  };

  _isString = function(obj){
    return toString.call(obj) == '[object String]';
  };

  _isObject = function(obj) {
    return obj === Object(obj);
  };

  _isArray = Array.isArray || function(obj) {
    return toString.call(obj) == '[object Array]';
  };

  /**
   * Expose some internal functions when testing
   */
  if(window.GMAPS_LARGE_POLYGON_TESTING){
    window.gmlp = {
      _lineLengths: _lineLengths,
      _numLines: _numLines
    };
  }

}());<commit_msg>Add findNeighbor and handle deleting paths<commit_after>(function(){

  var polyProto = google.maps.Polygon.prototype;

  // Default lineSize to 100
  polyProto.lineSize = 100;

  // Default to normal non-editable state
  polyProto.editable = false;

  // Default highlight color for edit lines
  polyProto.highlightColor = '#cccccc';

  polyProto.editLines = [];

  /**
   * Override setOptions so that we can intercept
   * the editable and lineSize options.
   */
  var _setOptions = polyProto.setOptions;
  polyProto.setOptions = function(options){
    if(_isObject(options)){

      // Intercept the editable option
      var editable = this.getEditable();
      if(!_isUndefined(options.editable)){
        editable = options.editable;
        delete options.editable;
      }

      // Intercept the lineSize option
      var lineSize = this.lineSize;
      if(!_isUndefined(options.lineSize)){
        lineSize = options.lineSize;
        delete options.lineSize;
      }

      // Intercept the highlightColor option
      var highlightColor = this.highlightColor;
      if(!_isUndefined(options.highlightColor)){
        highlightColor = options.highlightColor;
        delete options.highlightColor;
      }

      // Pass other options on to the Polygon
      _setOptions.call(this, options);

      // Set the new properties after setting
      // normal Polygon properties so that we
      // can be sure to get the last say on
      // any settings or behavior
      this.lineSize = lineSize;
      this.setEditable(editable);
      this.highlightColor = highlightColor;
    }
  }

  /**
   * Override the setEditable function so that
   * we know when to generate the polylines
   */
  polyProto.setEditable = function(editable){
    if(_isBoolean(editable)){
      this.editable = editable;
      if(editable){
        this.generateEditLines();
      }
    }
  };

  /**
   * Override getEditable since we don't
   * actually make the underlying polygon
   * editable
   */
  polyProto.getEditable = function(){
    return this.editable;
  };

  /**
   * Create the edit lines
   */
  polyProto.generateEditLines = function(){

    var self = this,
        map = self.getMap(),
        lineStyle = {
          strokeColor: self.get('strokeColor'),
          strokeOpacity: self.get('strokeOpacity'),
          strokeWeight: self.get('strokeWeight')
        };

    // Process each path independently
    this.getPaths().forEach(function(path, pathIndex){

      var lineLengths = _lineLengths(path.getLength(), self.lineSize),
          lines = [];

      // Iterate over each edit line
      var position = 0;
      for(var i = 0; i < lineLengths.length; i++){
        var newLinePath = [],
            end = Math.min(position + lineLengths[i], path.getLength());

        // Add the relevent points to the line's path
        for(; position < end; position++){
          newLinePath.push(path.getAt(position));
        }

        // Decrement position by 1 so that the next
        // line duplicates the last point of this line
        position--;

        // Create the google maps line object
        // and style it to match the polygon's borders
        var line = new google.maps.Polyline();
        line.setPath(newLinePath);
        line.setOptions(lineStyle);
        line.setMap(map);

        // Add events
        line.addListener('click', function(){
          this.setEditable(true);
        });
        line.addListener('mouseover', function(){
          this.set('strokeColor', self.highlightColor);
        });
        line.addListener('mouseout', function(){
          this.setOptions(lineStyle);
        });

        // Save the line
        lines.push(line);
      }

      // Add the first point of the path to the end of the last line
      lines[lines.length-1].getPath().push(path.getAt(0));

      // Keep track of whether a point event was fired
      // by the user or by us updating a neighbor point
      var updatingNeighbor = false;

      // Setup event listeners on the line's paths so that
      // we can update the underlying shape when the lines change
      $.each(lines, function(i, line){

        // New point
        line.getPath().addListener('insert_at', function(){
          self.updateFromLines(pathIndex, lines);
        });

        // Point removed
        line.getPath().addListener('remove_at', function(){
          self.updateFromLines(pathIndex, lines);
        });

        // Point moved
        line.getPath().addListener('set_at', function(vertex){

          // When either the first or last point was moved we need
          // to move the matching point of the neighboring line.
          // Because lines may be deleted (unlikely, but possible)
          // we have to figure out the line's position and neighbors
          // at runtime.
          if((vertex === 0 || vertex === this.getLength() - 1) && !updatingNeighbor){
            var neighbor = findNeighbor(lines, line, vertex);
            updatingNeighbor = true;
            neighbor.path.setAt(neighbor.index, this.getAt(vertex));
          }

          // Update underlying shape with changes only if we're moving
          // a point in the middle of the line or we're updating a neighbor
          // point after the user moved a first/last point
          else {
            updatingNeighbor = false;
            self.updateFromLines(pathIndex, lines);
          }
        });

        // Deleting a point when rightclicked.
        // updateFromLines is not called here to update the polygon
        // because we modify the lines which fire the event listeners
        // bound above which then call updateFromLines
        // Inspired by http://stackoverflow.com/a/14441786
        line.addListener('rightclick', function(event){
          var vertex = event.vertex;

          if(vertex != null){

            // Remove the point
            this.getPath().removeAt(event.vertex);

            // If the point being removed is the first or last
            // point in the line then we need to find and move
            // the neigbor's point to match our new end point.
            if(vertex === 0 || vertex === this.getPath().getLength()){
              var neighbor = findNeighbor(lines, this, vertex),
                  newEndIndex = vertex === 0 ? 0 : this.getPath().getLength() - 1;
              neighbor.path.setAt(neighbor.index, this.getPath().getAt(newEndIndex));
            }

            // If there is only one point left then delete the line.
            // It's neghbors should both be overlapping now.
            if(this.getPath().getLength() === 1){
              this.setMap(null);
              var linePos = lines.indexOf(this);
              lines.splice(linePos, 1);
            }

            // Delete the entire path if there is less than
            // three points left; with only one or two points
            // you don't have a polygon
            if(self.getPaths().getAt(pathIndex).getLength() < 3){
              self.getPaths().removeAt(pathIndex);
              // If this path isn't the last one then it will change
              // the ordering and make pathIndex irrelevent and break
              // a lot of code
            }

            // TODO: fire changed event
          }
        });
      });

      self.editLines = self.editLines.concat(lines);

    });
  };

  /**
   * Called whenever lines change; updates the specified
   * path of the shape based on the lines that were
   * derived from it
   */
  polyProto.updateFromLines = function(pathIndex, lines){
    var newPoints = [];
    for(var i = 0; i < lines.length; i++){
      var linePath = lines[i].getPath(),
          lineLength = linePath.getLength();
      linePath.forEach(function(point, j){
        // Don't add the last point of each line
        // because those are duplicated
        if(j !== lineLength - 1){
          newPoints.push(point);
        }
      });
    }
    this.getPaths().setAt(pathIndex, new google.maps.MVCArray(newPoints));
    // TODO: Fire changed event
  };

  /**
   * Finds the neighboring path and point index for
   * updating a neighbor's matching end point.
   */
  function findNeighbor(lines, line, pointIndex){
    var numLines = lines.length,
        lineIndex = lines.indexOf(line),
        neighborLineIndex, neighborPath, neighborPointIndex;

    // First point in line
    if(pointIndex === 0){
      neighborLineIndex = lineIndex === 0 ? numLines - 1 : lineIndex - 1;
      neighborPath = lines[neighborLineIndex].getPath();
      neighborPointIndex = neighborPath.getLength() - 1;
    }

    // Last point in line
    else {
      neighborLineIndex = lineIndex === numLines - 1 ? 0 : lineIndex + 1;
      neighborPath = lines[neighborLineIndex].getPath();
      neighborPointIndex = 0;
    }

    return {
      path: neighborPath,
      index: neighborPointIndex
    };
  };

  /**
   * Calculate length of each edit line.
   * Returns an array of numbers which represent
   * the length. Tries to even out the length of
   * the lines as best it can.
   */
  function _lineLengths(pathLength, lineSizeLimit){

    // Short circuit if there will only be one edit line
    if(lineSizeLimit >= pathLength){
      return [pathLength];
    }

    var rawNum = _numLines(pathLength, lineSizeLimit),
        numLines = Math.floor(rawNum),
        remainder = rawNum - numLines,
        lineLengths = [];

    // If lineSizeLimit divides into pathLenght evenly
    // then just return an array of length numLines
    // filled with lineSizeLimit.
    lineLengths = _numberArray(lineSizeLimit, numLines);

    // If lineSize doesn't divide into the pathLength evently
    // then try to even out the lengths of the lines.
    if(remainder) {
      var lastLength = Math.ceil(lineSizeLimit * remainder);

      // The last line cannot have 1 point
      if(lastLength < 2){
        lastLength = 2;
      }

      lineLengths.push(lastLength);

      // Starting at the second to last number (skip the
      // number we just pushed on), iterate backwards
      // over the array and decrement the numbers by one
      // until we make up the difference between the
      // lastLength and the LineSizeLimit

      var difference = lineSizeLimit - lastLength - 1,
          i = lineLengths.length - 2,
          last = lineLengths.length - 1;

      while(difference > 0){
        // Allow i to wrap back to the end
        if(i < 0){
          i = lineLengths.length - 1;
        }
        lineLengths[i] = lineLengths[i] - 1;
        difference--;
        i--;
        lineLengths[last]++;
      }
    }

    return lineLengths;
  };

  /**
   * Generate an array filled with a given number
   */
  function _numberArray(number, length){
    var array = [];
    for(var i = 0; i < length; i++){
      array[i] = number;
    }
    return array;
  };

  /**
   * Calculate number of edit lines
   */
  function _numLines(pathLength, lineSizeLimit){
    // Subtract one frome lineSizeLimit to account for
    // overlapping points at the edges.
    return pathLength / (lineSizeLimit - 1);
  };

  /**
   * Utility functions lifted from underscore
   */

  _isBoolean = function(obj) {
    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
  };

  _isUndefined = function(obj) {
    return obj === void 0;
  };

  _isString = function(obj){
    return toString.call(obj) == '[object String]';
  };

  _isObject = function(obj) {
    return obj === Object(obj);
  };

  _isArray = Array.isArray || function(obj) {
    return toString.call(obj) == '[object Array]';
  };

  /**
   * Expose some internal functions when testing
   */
  if(window.GMAPS_LARGE_POLYGON_TESTING){
    window.gmlp = {
      _lineLengths: _lineLengths,
      _numLines: _numLines
    };
  }

}());<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import { flatten, prop, range, splitEvery } from 'ramda';

/* eslint-disable padding-line-between-statements, newline-after-var */
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_LARGE';
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
/* eslint-enable padding-line-between-statements, newline-after-var */

export const shortUrlVisitsType = PropTypes.shape({
  visits: PropTypes.array,
  loading: PropTypes.bool,
  error: PropTypes.bool,
});

const initialState = {
  visits: [],
  loading: false,
  loadingLarge: false,
  error: false,
  cancelLoad: false,
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case GET_SHORT_URL_VISITS_START:
      return {
        ...state,
        loading: true,
        loadingLarge: false,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS_ERROR:
      return {
        ...state,
        loading: false,
        loadingLarge: false,
        error: true,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS:
      return {
        visits: action.visits,
        loading: false,
        loadingLarge: false,
        error: false,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS_LARGE:
      return {
        ...state,
        loadingLarge: true,
      };
    case GET_SHORT_URL_VISITS_CANCEL:
      return {
        ...state,
        cancelLoad: true,
      };
    default:
      return state;
  }
}

export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) => async (dispatch, getState) => {
  dispatch({ type: GET_SHORT_URL_VISITS_START });

  const { selectedServer } = getState();
  const { getShortUrlVisits } = buildShlinkApiClient(selectedServer);
  const itemsPerPage = 5000;
  const isLastPage = ({ currentPage, pagesCount }) => currentPage >= pagesCount;

  const loadVisits = async (page = 1) => {
    const { pagination, data } = await getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage });

    // If pagination was not returned, then this is an older shlink version. Just return data
    if (!pagination || isLastPage(pagination)) {
      return data;
    }

    // If there are more pages, make requests in blocks of 4
    const parallelRequestsCount = 4;
    const parallelStartingPage = 2;
    const pagesRange = range(parallelStartingPage, pagination.pagesCount + 1);
    const pagesBlocks = splitEvery(parallelRequestsCount, pagesRange);

    if (pagination.pagesCount - 1 > parallelRequestsCount) {
      dispatch({ type: GET_SHORT_URL_VISITS_LARGE });
    }

    return data.concat(await loadPagesBlocks(pagesBlocks));
  };

  const loadPagesBlocks = async (pagesBlocks, index = 0) => {
    const { shortUrlVisits: { cancelLoad } } = getState();

    if (cancelLoad) {
      return [];
    }

    const data = await loadVisitsInParallel(pagesBlocks[index]);

    if (index < pagesBlocks.length - 1) {
      return data.concat(await loadPagesBlocks(pagesBlocks, index + 1));
    }

    return data;
  };

  const loadVisitsInParallel = (pages) =>
    Promise.all(pages.map(
      (page) =>
        getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage })
          .then(prop('data'))
    ))
      .then(flatten);

  try {
    const visits = await loadVisits();

    dispatch({ visits, type: GET_SHORT_URL_VISITS });
  } catch (e) {
    dispatch({ type: GET_SHORT_URL_VISITS_ERROR });
  }
};

export const cancelGetShortUrlVisits = () => ({ type: GET_SHORT_URL_VISITS_CANCEL });
<commit_msg>Removed empty line<commit_after>import PropTypes from 'prop-types';
import { flatten, prop, range, splitEvery } from 'ramda';

/* eslint-disable padding-line-between-statements, newline-after-var */
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR';
export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS';
export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_LARGE';
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
/* eslint-enable padding-line-between-statements, newline-after-var */

export const shortUrlVisitsType = PropTypes.shape({
  visits: PropTypes.array,
  loading: PropTypes.bool,
  error: PropTypes.bool,
});

const initialState = {
  visits: [],
  loading: false,
  loadingLarge: false,
  error: false,
  cancelLoad: false,
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case GET_SHORT_URL_VISITS_START:
      return {
        ...state,
        loading: true,
        loadingLarge: false,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS_ERROR:
      return {
        ...state,
        loading: false,
        loadingLarge: false,
        error: true,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS:
      return {
        visits: action.visits,
        loading: false,
        loadingLarge: false,
        error: false,
        cancelLoad: false,
      };
    case GET_SHORT_URL_VISITS_LARGE:
      return {
        ...state,
        loadingLarge: true,
      };
    case GET_SHORT_URL_VISITS_CANCEL:
      return {
        ...state,
        cancelLoad: true,
      };
    default:
      return state;
  }
}

export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) => async (dispatch, getState) => {
  dispatch({ type: GET_SHORT_URL_VISITS_START });

  const { selectedServer } = getState();
  const { getShortUrlVisits } = buildShlinkApiClient(selectedServer);
  const itemsPerPage = 5000;
  const isLastPage = ({ currentPage, pagesCount }) => currentPage >= pagesCount;

  const loadVisits = async (page = 1) => {
    const { pagination, data } = await getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage });

    // If pagination was not returned, then this is an older shlink version. Just return data
    if (!pagination || isLastPage(pagination)) {
      return data;
    }

    // If there are more pages, make requests in blocks of 4
    const parallelRequestsCount = 4;
    const parallelStartingPage = 2;
    const pagesRange = range(parallelStartingPage, pagination.pagesCount + 1);
    const pagesBlocks = splitEvery(parallelRequestsCount, pagesRange);

    if (pagination.pagesCount - 1 > parallelRequestsCount) {
      dispatch({ type: GET_SHORT_URL_VISITS_LARGE });
    }

    return data.concat(await loadPagesBlocks(pagesBlocks));
  };

  const loadPagesBlocks = async (pagesBlocks, index = 0) => {
    const { shortUrlVisits: { cancelLoad } } = getState();

    if (cancelLoad) {
      return [];
    }

    const data = await loadVisitsInParallel(pagesBlocks[index]);

    if (index < pagesBlocks.length - 1) {
      return data.concat(await loadPagesBlocks(pagesBlocks, index + 1));
    }

    return data;
  };

  const loadVisitsInParallel = (pages) =>
    Promise.all(pages.map(
      (page) =>
        getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage })
          .then(prop('data'))
    )).then(flatten);

  try {
    const visits = await loadVisits();

    dispatch({ visits, type: GET_SHORT_URL_VISITS });
  } catch (e) {
    dispatch({ type: GET_SHORT_URL_VISITS_ERROR });
  }
};

export const cancelGetShortUrlVisits = () => ({ type: GET_SHORT_URL_VISITS_CANCEL });
<|endoftext|>


<commit_before>'use strict'

// Create objects for all 9 goats images after user press 'Submit' button
var goatImages= [
 new goatImagesFunc('goat_1', 'Goat_1.jpg'),
 new goatImagesFunc('goat_2', 'Goat_2.jpg'),
 new goatImagesFunc('goat_3', 'Goat_3.jpg'),
 new goatImagesFunc('goat_4', 'Goat_4.jpg'),
 new goatImagesFunc('goat_5', 'Goat_5.jpg'),
 new goatImagesFunc('goat_6', 'Goat_6.jpg'),
 new goatImagesFunc('goat_7', 'Goat_7.jpg'),
 new goatImagesFunc('goat_8', 'Goat_8.jpg'),
 new goatImagesFunc('goat_9', 'Goat_9.jpg')
];

// constructor for the goatImages
function goatImagesFunc(name, filename) {
 this.name = name;
 this.filename = 'img/goat_images/' + filename;
}

// access
, create
 element in html, and append it to

var mouseoverImages = document.getElementById('mouseover_images');
var imageUL = document.createElement('ul');
mouseoverImages.appendChild(imageUL);

// create a for loop to display all images in img folder onto the page
var imageElement;
var imageLI;
for(var i=0; i<goatImages.length; i++) {
 imageElement = document.createElement('img');
 imageLI = document.createElement('li');
 imageElement.src = goatImages[i].filename;
 imageLI.appendChild(imageElement);
 imageUL.appendChild(imageLI);
}
<commit_msg>changed goat.js<commit_after>'use strict';

var goats = [];

try {
  goats = JSON.parse(localStorage.goats);
} catch(error){
  console.log('problem getting info', error);
}
// Create objects for all 9 goats images after user press 'Submit' button
var goats = [
  new goats('goat_1', 'Goat_1.jpg'),
  new goats('goat_2', 'Goat_2.jpg'),
  new goats('goat_3', 'Goat_3.jpg'),
  new goats('goat_4', 'Goat_4.jpg'),
  new goats('goat_5', 'Goat_5.jpg'),
  new goats('goat_6', 'Goat_6.jpg'),
  new goats('goat_7', 'Goat_7.jpg'),
  new goats('goat_8', 'Goat_8.jpg'),
  new goats('goat_9', 'Goat_9.jpg')
];

// constructor for the goatImages
function goats(name, filename) {
  this.name = name;
  this.filename = 'img/goat_images/' + filename;
}

// access
, create
 element in html, and append it to

var mouseoverImages = document.getElementById('mouseover_images');
var imageUL = document.createElement('ul');
mouseoverImages.appendChild(imageUL);

// create a for loop to display all images in img folder onto the page
var imageElement;
var imageLI;
for(var i=0; i<goats.length; i++) {
  imageElement = document.createElement('img');
  imageLI = document.createElement('li');
  imageElement.src = goats[i].filename;
  imageLI.appendChild(imageElement);
  imageUL.appendChild(imageLI);
}
<|endoftext|>


<commit_before>import nodeResolve from 'rollup-plugin-node-resolve';
import convertCJS from 'rollup-plugin-commonjs';

const packageInfo = require( '../../package.json' );

export default {
	entry: 'src/index.js',
	format: 'es6',
	sourceMap: true,
	plugins: [
		nodeResolve( {
			jsnext: true,
			main: false
		} ),
		convertCJS()
	],
	banner: `/*! ${packageInfo.name} v${packageInfo.version} | (c) ${new Date().getFullYear()} ${packageInfo.author.name} | ${packageInfo.license} license (see LICENSE) */`,
	dest: `dist/${packageInfo.name}.js`
};
<commit_msg>[ci skip] Updated rollup configuration.<commit_after>import nodeResolve from 'rollup-plugin-node-resolve';
import convertCJS from 'rollup-plugin-commonjs';

const packageInfo = require( '../../package.json' );

export default {
	entry: 'src/index.js',
	format: 'es',
	sourceMap: true,
	plugins: [
		nodeResolve( {
			jsnext: true,
			main: false
		} ),
		convertCJS()
	],
	banner: `/*! ${packageInfo.name} v${packageInfo.version} | (c) ${new Date().getFullYear()} ${packageInfo.author.name} | ${packageInfo.license} license (see LICENSE) */`,
	dest: `dist/${packageInfo.name}.js`
};
<|endoftext|>


<commit_before>/**
 * DenHub-Device
 * https://github.com/odentools/denhub-device
 * (C) 2016 - OdenTools; Released under MIT License.
 */

'use strict';

var WebSocket = require('ws');

var HandlerHelper = require(__dirname + '/models/handler-helper'),
	HandlerCallbackRunner = require(__dirname + '/models/handler-callback-runner'),
	HandlerLogger = require(__dirname + '/models/handler-logger'),
	helper = require(__dirname + '/models/helper');

/**
 * Constructor
 * @param {Object} CommandsHandlerModel  Class of CommandsHandler
 * @param {Object} opt_config            Configuration data
 */
var DenHubDevice = function (CommandsHandlerModel, opt_config) {

	this.config = opt_config || helper.getConfig(false);
	this.webSocket = null;

	// Instances of the plugins
	this.pluginInstances = {};
	this._initPlugins();

	// Make an instance of the CommandsHandler
	var ch_logger = new HandlerLogger(this);
	var handler_helper = new HandlerHelper(this);
	this.commandsHandler = new CommandsHandlerModel(this.config, ch_logger, handler_helper);

	// Check the command arguments
	if (process.argv.indexOf('--development') != -1) {
		this.config.isDebugMode = true;
	}

	// Check the options
	this.isDebugMode = this.config.isDebugMode || false;
	this.isSuppressLog = this.config.isSuppressLog || false;

	// Check the configuration
	if (this.config.denhubServerHost == null){
		throw new Error('A required configuration undefined: denhubServerHost');
	} else if (this.config.deviceName == null) {
		throw new Error('A required configuration undefined: deviceName');
	} else if (this.config.deviceType == null) {
		throw new Error('A required configuration undefined: deviceType');
	}

	// Interval time (millisec) for reconnecting to server
	this.RECONNECT_DELAY_TIME_MSEC = 3000;

};

/**
 * Start the daemon
 * @param  {Function} opt_callback Callback - function (error, websocket_connection)
 */
DenHubDevice.prototype.start = function (opt_callback) {

	var self = this;

	// Set the fatal exception handler
	if (!self.isDebugMode) {
		process.on('uncaughtException', function (err) {
			self.logError('uncaughtException', err.stack.toString(), true);
			self.restart();
		});
	}

	// Connect to the WebSocket Server
	var url = self.config.denhubServerHost + '?name=' + self.config.deviceName + '&token=' + self.config.deviceToken;
	try {
		self.webSocket = new WebSocket(url);
	} catch (e) {
		if (opt_callback) opt_callback(e, null);
		if (self.isDebugMode) throw e;
		self.logWarn('ws', 'Could not connect to server; Reconnecting...');
		setTimeout(self.start, self.RECONNECT_DELAY_TIME_MSEC);
	}

	// Set the event listener - Connection Opened
	self.webSocket.on('open', function () {

		self.logDebug('ws', 'Connected to ' + self.config.denhubServerHost + ' :)');

		if (opt_callback) opt_callback(null, self.webSocket);

		// Send the manifest
		self._sendManifest();

	});

	// Set the event listener - Connection Closed
	self.webSocket.on('close', function() {

		if (opt_callback) opt_callback(new Error('Connection closed'), null);
		if (self.isDebugMode) throw new Error('Connection closed');

		// Re-connect
		self.logWarn('ws', 'Disconnected from server; Reconnecting...');
		setTimeout(self.start, self.RECONNECT_DELAY_TIME_MSEC);

	});

	// Set the event listener - Message Received
	self.webSocket.on('message', function (message, flags) {

		var data = {};
		try {
			data = JSON.parse(message);
		} catch (e) {
			return;
		}

		if (!data.cmd) return;

		self._onCmdMessage(data);

	});

};

/**
 * Exit the daemon
 */
DenHubDevice.prototype.exit = function () {

	var self = this;

	process.exit(0);

};

/**
 * Restart the daemon
 */
DenHubDevice.prototype.restart = function () {

	var self = this;

	// Get the command and parameters of myself
	var argv = process.argv.concat(); // Copy the arguments array
	var cmd = argv.shift();

	// Start the new app
	console.log('Restarting myself...');
	var child = null;
	try {
		child = require('child_process').spawn(cmd, argv, {
			detached: true,
			stdio: [ 'ignore', 'ignore', 'ignore' ]
		});
		child.unref();
	} catch (e) {
		console.log('Could not restart myself - ' + e.toString());
		return;
	}

	// Exit myself
	var timer = setTimeout(function() {
		process.exit(0);
	}, 500);

};

/**
 * Get an instance of the denhub device plugin
 * @param  {String} plugin_name  Name of the plugin
 * @return  {Object} Initialized instance of the plugin
 */
DenHubDevice.prototype.getPlugin = function (plugin_name) {

	var self = this;

	var plugin_type = 'local'; // 'core' or 'local'
	if (plugin_name.match(/^\_(.+)$/)) {
		plugin_name = RegExp.$1;
		plugin_type = 'core';
	}

	// Return an instance from the plugins array
	var instance = self.pluginInstances[plugin_type + ':' + plugin_name] || null;
	return instance;

};

/**
 * Get the websocket instance
 * @return {WebSocket} WebSocket instance
 */
DenHubDevice.prototype.getWsInstance = function () {

	var self = this;

	return self.webSocket;

};

/**
 * Print the message to the debug log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logDebug = function (tag_text, log_obj, is_dont_send) {

	this._log('debug', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the information log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logInfo = function (tag_text, log_obj, is_dont_send) {

	this._log('info', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the warning log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logWarn = function (tag_text, log_obj, is_dont_send) {

	this._log('warn', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the error log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logError = function (tag_text, log_obj, is_dont_send) {

	this._log('error', tag_text, log_obj, is_dont_send);

};

/**
 * Method for handle the command message
 * @param  {Object} data Received command message
 */
DenHubDevice.prototype._onCmdMessage = function (data) {

	var self = this;

	// Get the command parameters
	var cmd_name = data.cmd;
	if (cmd_name.match(/^\_/)) return;

	var cmd_exec_id = data.cmdExecId || -1;

	var cmd_args = data.args || {};

	// Call a hook of the plugins
	for (var plugin_name in self.pluginInstances) {
		try {
			self.pluginInstances[plugin_name].onCmdReceive(cmd_name, cmd_args, cmd_exec_id);
		} catch (e) {
			self.logDebug('_onCmdMessage', 'Could not call onCmdReceive hook of the plugin: ' + plugin_name);
		}
	}

	// Make a callback runner
	var callback_runner = new HandlerCallbackRunner(self, cmd_name, cmd_exec_id);

	// Find the handler method from an instance of CommandsHandler
	var commands_list = self.config.commands || self.config.cmds;
	var is_executed_handler = false, is_wont_response = false;
	for (var key in commands_list) {

		if (key == cmd_name) {

			try {
				// Execute the handler method
				is_wont_response = self.commandsHandler[key](cmd_args, callback_runner);
				is_executed_handler = true;
			} catch (e) {
				self.logWarn('_onCmdMessage', 'Error occurred in the command handler: ' + cmd_name + '\n' + e.stack.toString());
			}

			break;
		}

	}

	if (!is_executed_handler) {
		self.logDebug('_onCmdMessage', 'Not found the matched command handler to ' + cmd_name);
	} else if (is_wont_response) {
		callback_runner.send(null, 'Command executed');
	}

};

/**
 * Initialize an instance of the each plugins
 */
DenHubDevice.prototype._initPlugins = function (plugin_name, callback) {

	var self = this;

	var fs = require('fs');
	var plugin_dir_base_path = __dirname + '/plugins';

	self.pluginInstances = {};

	var PLUGIN_TYPES = ['core', 'local'];

	// Initialize the plugins
	PLUGIN_TYPES.forEach(function (plugin_type, i) {

		var file_list = [];
		try {
			file_list = fs.readdirSync(plugin_dir_base_path + '/' + plugin_type);
		} catch (e) {
			return;
		}

		file_list.forEach(function (plugin_filename, i) {
			if (plugin_filename.match(/^([a-zA-Z0-9_\-]+)\.js$/)) {

				var plugin_name = RegExp.$1;
				if (plugin_type == 'core' && plugin_name == 'base') return; // /plugins/core/base.js should be ignored

				// Initialize an instance of the plugin
				var instance = null;
				self.logDebug('_initPlugins', 'Initialize the plugin: ' + plugin_dir_base_path + '/' + plugin_type + '/' + plugin_name);
				try {
					var PluginClass = require(plugin_dir_base_path + '/' + plugin_type + '/' + plugin_name);
					instance = new PluginClass(self);
				} catch (e) {
					self.logWarn('_initPlugins', e.stack.toString());
				}

				// Save the instance to plugins array
				self.pluginInstances[plugin_type + ':' + plugin_name] = instance || null;

			}
		});

	});

};

/**
 * Send a device manifest to the server
 */
DenHubDevice.prototype._sendManifest = function () {

	var self = this;

	var manifest = self.config;

	// Call a hook of the plugins
	for (var plugin_name in self.pluginInstances) {
		try {
			var append_items = self.pluginInstances[plugin_name].onSendManifest(manifest);
			for (var key in append_items) {
				manifest[key] = append_items[key];
			}
		} catch (e) {
			self.logDebug('_sendManifest', 'Could not call onSendManifest hook of the plugin: ' + plugin_name);
		}
	}

	// Add a package name and version from the package.json
	manifest.deviceDaemon = helper.getPackageInfo().name + '/' + helper.getPackageInfo().version;

	// Send to server
	self.webSocket.send(JSON.stringify(manifest));

};

/**
 * Print the message to the log
 * @param  {String} type_str    Type string of the item - 'debug', 'info', 'warn', 'error'
 * @param  {String} tag_str     Tag string of the item
 * @param  {Object} log_obj     Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype._log = function (type_str, tag_str, log_obj, is_dont_send) {

	var self = this;

	var log_text = new String();
	if (log_obj != null) {
		var text_class = Object.prototype.toString.call(log_obj).slice(8, -1);
		if (text_class == 'Object' || text_class == 'Array') { // Map or Array
			log_text = JSON.stringify(log_obj, null, '\t');
		} else if (text_class == 'Error' && log_obj.stack != null) {
			log_text = log_obj.stack.toString();
		} else {
			log_text = log_obj;
		}
	}

	console.log('[' + helper.toUpperCase(type_str) + '] ' + tag_str + ' / ' + log_text);

	if (is_dont_send) return;

	try {
		self.webSocket.send(JSON.stringify({
			cmd: '_sendLog',
			args: {
				type: type_str,
				text: log_text,
				tag: tag_str
			},
			sentAt: new Date().getTime()
		}));
	} catch (e) {
		return;
	}

};

// ----

module.exports = DenHubDevice;
<commit_msg>Change: DenHubDevice - Improve restart process for fatal error<commit_after>/**
 * DenHub-Device
 * https://github.com/odentools/denhub-device
 * (C) 2016 - OdenTools; Released under MIT License.
 */

'use strict';

var WebSocket = require('ws');

var HandlerHelper = require(__dirname + '/models/handler-helper'),
	HandlerCallbackRunner = require(__dirname + '/models/handler-callback-runner'),
	HandlerLogger = require(__dirname + '/models/handler-logger'),
	helper = require(__dirname + '/models/helper');

/**
 * Constructor
 * @param {Object} CommandsHandlerModel  Class of CommandsHandler
 * @param {Object} opt_config            Configuration data
 */
var DenHubDevice = function (CommandsHandlerModel, opt_config) {

	this.config = opt_config || helper.getConfig(false);
	this.webSocket = null;

	// Instances of the plugins
	this.pluginInstances = {};
	this._initPlugins();

	// Make an instance of the CommandsHandler
	var ch_logger = new HandlerLogger(this);
	var handler_helper = new HandlerHelper(this);
	this.commandsHandler = new CommandsHandlerModel(this.config, ch_logger, handler_helper);

	// Check the command arguments
	if (process.argv.indexOf('--development') != -1) {
		this.config.isDebugMode = true;
	}

	// Check the options
	this.isDebugMode = this.config.isDebugMode || false;
	this.isSuppressLog = this.config.isSuppressLog || false;

	// Check the configuration
	if (this.config.denhubServerHost == null){
		throw new Error('A required configuration undefined: denhubServerHost');
	} else if (this.config.deviceName == null) {
		throw new Error('A required configuration undefined: deviceName');
	} else if (this.config.deviceType == null) {
		throw new Error('A required configuration undefined: deviceType');
	}

	// Interval time (millisec) for reconnecting to server
	this.config.reconnectDelayTimeMsec = this.config.reconnectDelayTimeMsec || 5000;

	// Interval time (millisec) for restarting when the fatal error occurred
	this.config.restartDelayTime = this.config.restartDelayTime || 6000;

};

/**
 * Start the daemon
 * @param  {Function} opt_callback Callback - function (error, websocket_connection)
 */
DenHubDevice.prototype.start = function (opt_callback) {

	var self = this;

	// Set the fatal exception handler
	if (!self.isDebugMode) {
		process.on('uncaughtException', function (err) {
			self.logError('uncaughtException', err.stack.toString(), true);
			self.restart();
		});
	}

	// Connect to the WebSocket Server
	var url = self.config.denhubServerHost + '?name=' + self.config.deviceName + '&token=' + self.config.deviceToken;
	try {
		self.webSocket = new WebSocket(url);
	} catch (e) {
		if (opt_callback) opt_callback(e, null);
		if (self.isDebugMode) throw e;
		self.logWarn('ws', 'Could not connect to server; Reconnecting...');
		setTimeout(self.start, self.config.reconnectDelayTimeMsec);
	}

	// Set the event listener - Connection Opened
	self.webSocket.on('open', function () {

		self.logDebug('ws', 'Connected to ' + self.config.denhubServerHost + ' :)');

		if (opt_callback) opt_callback(null, self.webSocket);

		// Send the manifest
		self._sendManifest();

	});

	// Set the event listener - Connection Closed
	self.webSocket.on('close', function() {

		if (opt_callback) opt_callback(new Error('Connection closed'), null);
		if (self.isDebugMode) throw new Error('Connection closed');

		// Re-connect
		self.logWarn('ws', 'Disconnected from server; Reconnecting...');
		setTimeout(self.start, self.config.reconnectDelayTimeMsec);

	});

	// Set the event listener - Message Received
	self.webSocket.on('message', function (message, flags) {

		var data = {};
		try {
			data = JSON.parse(message);
		} catch (e) {
			return;
		}

		if (!data.cmd) return;

		self._onCmdMessage(data);

	});

};

/**
 * Exit the daemon
 */
DenHubDevice.prototype.exit = function () {

	var self = this;

	process.exit(0);

};

/**
 * Restart the daemon
 */
DenHubDevice.prototype.restart = function () {

	var self = this;

	self.logInfo('restart', 'The daemon will be restart soon...\n\
If you need cancel the restarting, try the command: $ kill -9 ' + process.pid, true);

	var timer = setTimeout(function () {

		// Get the command and parameters of myself
		var argv = process.argv.concat(); // Copy the arguments array
		var cmd = argv.shift();

		// Start the new app
		console.log('Restarting...');
		var child = null;
		try {
			child = require('child_process').spawn(cmd, argv, {
				cwd: process.cwd,
				detached: true,
				env: process.env,
				stdio: [process.stdin, process.stdout, process.stderr]
			});
			child.unref();
		} catch (e) {
			console.log('Could not restart myself - ' + e.toString());
			return;
		}

		// Exit myself
		var timer_ = setTimeout(function() {
			process.exit(0);
		}, 500);

	}, self.config.restartDelayTime);

};

/**
 * Get an instance of the denhub device plugin
 * @param  {String} plugin_name  Name of the plugin
 * @return  {Object} Initialized instance of the plugin
 */
DenHubDevice.prototype.getPlugin = function (plugin_name) {

	var self = this;

	var plugin_type = 'local'; // 'core' or 'local'
	if (plugin_name.match(/^\_(.+)$/)) {
		plugin_name = RegExp.$1;
		plugin_type = 'core';
	}

	// Return an instance from the plugins array
	var instance = self.pluginInstances[plugin_type + ':' + plugin_name] || null;
	return instance;

};

/**
 * Get the websocket instance
 * @return {WebSocket} WebSocket instance
 */
DenHubDevice.prototype.getWsInstance = function () {

	var self = this;

	return self.webSocket;

};

/**
 * Print the message to the debug log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logDebug = function (tag_text, log_obj, is_dont_send) {

	this._log('debug', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the information log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logInfo = function (tag_text, log_obj, is_dont_send) {

	this._log('info', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the warning log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logWarn = function (tag_text, log_obj, is_dont_send) {

	this._log('warn', tag_text, log_obj, is_dont_send);

};

/**
 * Print the message to the error log
 * @param  {String} tag_text     Tag string of the item
 * @param  {Object} log_obj      Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype.logError = function (tag_text, log_obj, is_dont_send) {

	this._log('error', tag_text, log_obj, is_dont_send);

};

/**
 * Method for handle the command message
 * @param  {Object} data Received command message
 */
DenHubDevice.prototype._onCmdMessage = function (data) {

	var self = this;

	// Get the command parameters
	var cmd_name = data.cmd;
	if (cmd_name.match(/^\_/)) return;

	var cmd_exec_id = data.cmdExecId || -1;

	var cmd_args = data.args || {};

	// Call a hook of the plugins
	for (var plugin_name in self.pluginInstances) {
		try {
			self.pluginInstances[plugin_name].onCmdReceive(cmd_name, cmd_args, cmd_exec_id);
		} catch (e) {
			self.logDebug('_onCmdMessage', 'Could not call onCmdReceive hook of the plugin: ' + plugin_name);
		}
	}

	// Make a callback runner
	var callback_runner = new HandlerCallbackRunner(self, cmd_name, cmd_exec_id);

	// Find the handler method from an instance of CommandsHandler
	var commands_list = self.config.commands || self.config.cmds;
	var is_executed_handler = false, is_wont_response = false;
	for (var key in commands_list) {

		if (key == cmd_name) {

			try {
				// Execute the handler method
				is_wont_response = self.commandsHandler[key](cmd_args, callback_runner);
				is_executed_handler = true;
			} catch (e) {
				self.logWarn('_onCmdMessage', 'Error occurred in the command handler: ' + cmd_name + '\n' + e.stack.toString());
			}

			break;
		}

	}

	if (!is_executed_handler) {
		self.logDebug('_onCmdMessage', 'Not found the matched command handler to ' + cmd_name);
	} else if (is_wont_response) {
		callback_runner.send(null, 'Command executed');
	}

};

/**
 * Initialize an instance of the each plugins
 */
DenHubDevice.prototype._initPlugins = function (plugin_name, callback) {

	var self = this;

	var fs = require('fs');
	var plugin_dir_base_path = __dirname + '/plugins';

	self.pluginInstances = {};

	var PLUGIN_TYPES = ['core', 'local'];

	// Initialize the plugins
	PLUGIN_TYPES.forEach(function (plugin_type, i) {

		var file_list = [];
		try {
			file_list = fs.readdirSync(plugin_dir_base_path + '/' + plugin_type);
		} catch (e) {
			return;
		}

		file_list.forEach(function (plugin_filename, i) {
			if (plugin_filename.match(/^([a-zA-Z0-9_\-]+)\.js$/)) {

				var plugin_name = RegExp.$1;
				if (plugin_type == 'core' && plugin_name == 'base') return; // /plugins/core/base.js should be ignored

				// Initialize an instance of the plugin
				var instance = null;
				self.logDebug('_initPlugins', 'Initialize the plugin: ' + plugin_dir_base_path + '/' + plugin_type + '/' + plugin_name);
				try {
					var PluginClass = require(plugin_dir_base_path + '/' + plugin_type + '/' + plugin_name);
					instance = new PluginClass(self);
				} catch (e) {
					self.logWarn('_initPlugins', e.stack.toString());
				}

				// Save the instance to plugins array
				self.pluginInstances[plugin_type + ':' + plugin_name] = instance || null;

			}
		});

	});

};

/**
 * Send a device manifest to the server
 */
DenHubDevice.prototype._sendManifest = function () {

	var self = this;

	var manifest = self.config;

	// Call a hook of the plugins
	for (var plugin_name in self.pluginInstances) {
		try {
			var append_items = self.pluginInstances[plugin_name].onSendManifest(manifest);
			for (var key in append_items) {
				manifest[key] = append_items[key];
			}
		} catch (e) {
			self.logDebug('_sendManifest', 'Could not call onSendManifest hook of the plugin: ' + plugin_name);
		}
	}

	// Add a package name and version from the package.json
	manifest.deviceDaemon = helper.getPackageInfo().name + '/' + helper.getPackageInfo().version;

	// Send to server
	self.webSocket.send(JSON.stringify(manifest));

};

/**
 * Print the message to the log
 * @param  {String} type_str    Type string of the item - 'debug', 'info', 'warn', 'error'
 * @param  {String} tag_str     Tag string of the item
 * @param  {Object} log_obj     Log object of the item
 * @param  {Boolean} is_dont_send Whether the log to send to server
 */
DenHubDevice.prototype._log = function (type_str, tag_str, log_obj, is_dont_send) {

	var self = this;

	var log_text = new String();
	if (log_obj != null) {
		var text_class = Object.prototype.toString.call(log_obj).slice(8, -1);
		if (text_class == 'Object' || text_class == 'Array') { // Map or Array
			log_text = JSON.stringify(log_obj, null, '\t');
		} else if (text_class == 'Error' && log_obj.stack != null) {
			log_text = log_obj.stack.toString();
		} else {
			log_text = log_obj;
		}
	}

	console.log('[' + helper.toUpperCase(type_str) + '] ' + tag_str + ' / ' + log_text);

	if (is_dont_send) return;

	try {
		self.webSocket.send(JSON.stringify({
			cmd: '_sendLog',
			args: {
				type: type_str,
				text: log_text,
				tag: tag_str
			},
			sentAt: new Date().getTime()
		}));
	} catch (e) {
		return;
	}

};

// ----

module.exports = DenHubDevice;
<|endoftext|>