import Circle from '../../../../../src/ol/geom/Circle.js';
import Draw, {
  createBox,
  createRegularPolygon,
} from '../../../../../src/ol/interaction/Draw.js';
import Feature from '../../../../../src/ol/Feature.js';
import GeometryType from '../../../../../src/ol/geom/GeometryType.js';
import Interaction from '../../../../../src/ol/interaction/Interaction.js';
import LineString from '../../../../../src/ol/geom/LineString.js';
import Map from '../../../../../src/ol/Map.js';
import MapBrowserEvent from '../../../../../src/ol/MapBrowserEvent.js';
import MultiLineString from '../../../../../src/ol/geom/MultiLineString.js';
import MultiPoint from '../../../../../src/ol/geom/MultiPoint.js';
import MultiPolygon from '../../../../../src/ol/geom/MultiPolygon.js';
import Point from '../../../../../src/ol/geom/Point.js';
import Polygon from '../../../../../src/ol/geom/Polygon.js';
import VectorLayer from '../../../../../src/ol/layer/Vector.js';
import VectorSource from '../../../../../src/ol/source/Vector.js';
import View from '../../../../../src/ol/View.js';
import proj4 from 'proj4';
import {
  altKeyOnly,
  always,
  shiftKeyOnly,
} from '../../../../../src/ol/events/condition.js';
import {
  clearUserProjection,
  setUserProjection,
  transform,
} from '../../../../../src/ol/proj.js';
import {equals} from '../../../../../src/ol/array.js';
import {listen} from '../../../../../src/ol/events.js';
import {register} from '../../../../../src/ol/proj/proj4.js';
import {unByKey} from '../../../../../src/ol/Observable.js';

describe('ol.interaction.Draw', function () {
  let target, map, source;

  const width = 360;
  const height = 180;

  beforeEach(function (done) {
    target = document.createElement('div');
    const style = target.style;
    style.position = 'absolute';
    style.left = '-1000px';
    style.top = '-1000px';
    style.width = width + 'px';
    style.height = height + 'px';
    document.body.appendChild(target);
    source = new VectorSource();
    const layer = new VectorLayer({source: source});
    map = new Map({
      target: target,
      layers: [layer],
      view: new View({
        projection: 'EPSG:4326',
        center: [0, 0],
        resolution: 1,
      }),
    });
    map.once('postrender', function () {
      done();
    });
  });

  afterEach(function () {
    map.dispose();
    document.body.removeChild(target);
    clearUserProjection();
  });

  /**
   * Simulates a browser event on the map viewport.  The client x/y location
   * will be adjusted as if the map were centered at 0,0.
   * @param {string} type Event type.
   * @param {number} x Horizontal offset from map center.
   * @param {number} y Vertical offset from map center.
   * @param {boolean} [opt_shiftKey] Shift key is pressed.
   * @param {boolean} [opt_pointerId] Pointer id.
   * @return {module:ol/MapBrowserEvent} The simulated event.
   */
  function simulateEvent(type, x, y, opt_shiftKey, opt_pointerId = 0) {
    const viewport = map.getViewport();
    // calculated in case body has top < 0 (test runner with small window)
    const position = viewport.getBoundingClientRect();
    const shiftKey = opt_shiftKey !== undefined ? opt_shiftKey : false;
    const event = {};
    event.type = type;
    event.target = viewport.firstChild;
    event.clientX = position.left + x + width / 2;
    event.clientY = position.top + y + height / 2;
    event.shiftKey = shiftKey;
    event.preventDefault = function () {};
    event.pointerType = 'mouse';
    event.pointerId = opt_pointerId;
    const simulatedEvent = new MapBrowserEvent(type, map, event);
    map.handleMapBrowserEvent(simulatedEvent);
    return simulatedEvent;
  }

  function simulateBrowserEvent(type, x, y) {
    const viewport = map.getViewport();
    // calculated in case body has top < 0 (test runner with small window)
    const position = viewport.getBoundingClientRect();
    const evt = new PointerEvent(type, {
      clientX: position.left + x + width / 2,
      clientY: position.top + y + height / 2,
    });
    (type === 'pointerup' ? document : map.getViewport()).dispatchEvent(evt);
  }

  describe('constructor', function () {
    it('creates a new interaction', function () {
      const draw = new Draw({
        source: source,
        type: 'Point',
      });
      expect(draw).to.be.a(Draw);
      expect(draw).to.be.a(Interaction);
    });

    it('accepts a freehand option', function () {
      const draw = new Draw({
        source: source,
        type: 'LineString',
        freehand: true,
      });

      const event = new PointerEvent('pointerdown', {
        clientX: 0,
        clientY: 0,
        shiftKey: false,
      });

      expect(draw.freehandCondition_(event)).to.be(true);
    });

    it('accepts a dragVertexDelay option', function () {
      const draw = new Draw({
        source: source,
        type: 'LineString',
        dragVertexDelay: 42,
      });
      expect(draw.dragVertexDelay_).to.be(42);
    });

    it('accepts a stopClick option', function () {
      const draw = new Draw({
        source: source,
        type: 'Point',
        stopClick: true,
      });
      map.addInteraction(draw);
      let clicked = false;
      const clickKey = map.on('click', () => (clicked = true));
      simulateBrowserEvent('pointermove', 10, 20);
      simulateBrowserEvent('pointerdown', 10, 20);
      simulateBrowserEvent('pointerup', 10, 20);
      //setTimeout(() => {
      expect(clicked).to.be(false);
      unByKey(clickKey);
      //}, 300);
    });
  });

  describe('specifying a geometryName', function () {
    beforeEach(function () {
      const draw = new Draw({
        source: source,
        geometryName: 'the_geom',
        type: 'Point',
      });
      map.addInteraction(draw);
    });

    it('creates a feature with the correct geometryName', function () {
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(features[0].getGeometryName()).to.equal('the_geom');
      expect(geometry).to.be.a(Point);
    });
  });

  describe('specifying a clickTolerance', function () {
    beforeEach(function () {
      const draw = new Draw({
        source: source,
        type: 'Point',
        clickTolerance: 6,
      });
      map.addInteraction(draw);
    });

    it('adds a point when below the tolerance', function () {
      let features;

      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 15, 25);
      features = source.getFeatures();
      expect(features).to.length(0);

      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 14, 24);
      features = source.getFeatures();
      expect(features).to.length(1);
    });
  });

  describe('drawing points', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'Point',
      });
      map.addInteraction(draw);
    });

    it('draws a point on click', function () {
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Point);
      expect(geometry.getCoordinates()).to.eql([10, -20]);
    });

    it('does not draw a point with a significant drag', function () {
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointermove', 18, 20);
      simulateEvent('pointerup', 18, 20);
      const features = source.getFeatures();
      expect(features).to.have.length(0);
    });

    it('does not draw a point when modifier key is pressed', function () {
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20, true);
      simulateEvent('pointerup', 10, 20);
      const features = source.getFeatures();
      expect(features).to.have.length(0);
    });

    it('does not draw a point when multiple pointers are involved', function () {
      simulateEvent('pointerdown', 10, 20, false, 1);
      simulateEvent('pointerdown', 10, 20, false, 2);
      simulateEvent('pointermove', 10, 30, false, 1);
      simulateEvent('pointermove', 10, 10, false, 2);
      simulateEvent('pointerup', 10, 30, false, 1);
      simulateEvent('pointerup', 10, 10, false, 2);
      const features = source.getFeatures();
      expect(features).to.have.length(0);
    });

    it('triggers draw events', function () {
      const ds = sinon.spy();
      const de = sinon.spy();
      const da = sinon.spy();
      listen(draw, 'drawstart', ds);
      listen(draw, 'drawend', de);
      listen(draw, 'drawabort', da);
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      expect(ds.called).to.be(true);
      expect(de.called).to.be(true);
      expect(da.called).to.be(false);
      simulateEvent('pointermove', 20, 20);
      expect(ds.callCount).to.be(1);
      expect(de.callCount).to.be(1);
      expect(da.callCount).to.be(0);
    });

    it('triggers drawend event before inserting the feature', function () {
      const receivedEvents = {
        end: 0,
        addfeature: 0,
      };
      listen(draw, 'drawend', function () {
        expect(receivedEvents.end).to.be(0);
        expect(receivedEvents.addfeature).to.be(0);
        ++receivedEvents.end;
      });
      source.on('addfeature', function () {
        expect(receivedEvents.end).to.be(1);
        expect(receivedEvents.addfeature).to.be(0);
        receivedEvents.addfeature++;
      });
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      simulateEvent('pointermove', 20, 20);
      expect(receivedEvents.end).to.be(1);
      expect(receivedEvents.addfeature).to.be(1);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing multipoints', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'MultiPoint',
      });
      map.addInteraction(draw);
    });

    it('draws multipoint on click', function () {
      simulateEvent('pointermove', 30, 15);
      simulateEvent('pointerdown', 30, 15);
      simulateEvent('pointerup', 30, 15);
      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(MultiPoint);
      expect(geometry.getCoordinates()).to.eql([[30, -15]]);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing linestrings', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'LineString',
      });
      map.addInteraction(draw);
    });

    it('draws linestring with clicks, finishing on last point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // finish on second point
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(LineString);
      expect(geometry.getCoordinates()).to.eql([
        [10, -20],
        [30, -20],
      ]);
    });

    it('supports removeLastPoint while drawing', function () {
      draw.removeLastPoint();

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 40, 30);
      simulateEvent('pointerdown', 40, 30);
      simulateEvent('pointerup', 40, 30);

      simulateEvent('pointermove', 100, 100);
      draw.removeLastPoint();

      // click near the removed point
      simulateEvent('pointermove', 39, 31);
      simulateEvent('pointerdown', 38, 31);
      simulateEvent('pointerup', 38, 31);

      expect(source.getFeatures()).to.have.length(0);
    });

    it('supports freehand drawing for linestrings', function () {
      // freehand sequence
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20, true);
      simulateEvent('pointermove', 20, 30, true);
      simulateEvent('pointerdrag', 20, 30, true);
      simulateEvent('pointermove', 20, 40, true);
      simulateEvent('pointerdrag', 20, 40, true);
      simulateEvent('pointerup', 20, 40, true);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(LineString);
      expect(geometry.getCoordinates()).to.eql([
        [10, -20],
        [20, -30],
        [20, -40],
      ]);
    });

    it('allows freehand mode for part of the drawing', function () {
      // non-freehand
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      simulateEvent('pointermove', 20, 30);

      // freehand
      simulateEvent('pointermove', 20, 30, true);
      simulateEvent('pointerdrag', 20, 30, true);
      simulateEvent('pointermove', 30, 40, true);
      simulateEvent('pointerdrag', 30, 40, true);
      simulateEvent('pointermove', 40, 50, true);
      simulateEvent('pointerdrag', 40, 50, true);

      // non-freehand
      simulateEvent('pointerup', 40, 50);
      simulateEvent('pointermove', 50, 60);
      simulateEvent('pointerdown', 50, 60);
      simulateEvent('pointerup', 50, 60);
      simulateEvent('pointermove', 60, 70);
      simulateEvent('pointerdown', 60, 70);
      simulateEvent('pointerup', 60, 70);

      // finish
      simulateEvent('pointerdown', 60, 70);
      simulateEvent('pointerup', 60, 70);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(LineString);
      expect(geometry.getCoordinates()).to.eql([
        [10, -20],
        [20, -30],
        [30, -40],
        [40, -50],
        [50, -60],
        [60, -70],
      ]);
    });

    it('does not add a point with a significant drag', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // drag map
      simulateEvent('pointermove', 15, 20);
      simulateEvent('pointerdown', 15, 20);
      simulateEvent('pointermove', 23, 20);
      simulateEvent('pointerup', 23, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // finish on second point
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(LineString);
      expect(geometry.getCoordinates()).to.eql([
        [10, -20],
        [30, -20],
      ]);
    });

    it('allows dragging of the vertex after dragVertexDelay', function (done) {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point, drag vertex
      simulateEvent('pointermove', 15, 20);
      simulateEvent('pointerdown', 15, 20);
      setTimeout(function () {
        simulateEvent('pointermove', 20, 10);
        simulateEvent('pointerdrag', 20, 10);
        simulateEvent('pointerup', 20, 10);
        // third point
        simulateEvent('pointermove', 30, 20);
        simulateEvent('pointerdown', 30, 20);
        simulateEvent('pointerup', 30, 20);

        // finish on third point
        simulateEvent('pointerdown', 30, 20);
        simulateEvent('pointerup', 30, 20);

        const features = source.getFeatures();
        expect(features).to.have.length(1);
        const geometry = features[0].getGeometry();
        expect(geometry).to.be.a(LineString);
        expect(geometry.getCoordinates()).to.eql([
          [10, -20],
          [20, -10],
          [30, -20],
        ]);

        done();
      }, 600);
    });

    it('triggers draw events', function () {
      const ds = sinon.spy();
      const de = sinon.spy();
      const da = sinon.spy();
      listen(draw, 'drawstart', ds);
      listen(draw, 'drawend', de);
      listen(draw, 'drawabort', da);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // finish on second point
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);
      simulateEvent('pointermove', 10, 20);

      expect(ds.called).to.be(true);
      expect(ds.callCount).to.be(1);
      expect(de.called).to.be(true);
      expect(de.callCount).to.be(1);
      expect(da.called).to.be(false);
      expect(da.callCount).to.be(0);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing with a condition', function () {
    let draw;
    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'LineString',
        condition: shiftKeyOnly,
        freehandCondition: altKeyOnly,
      });
      map.addInteraction(draw);
    });

    it('finishes draw sequence correctly', function () {
      // first point
      simulateEvent('pointermove', 10, 20, true);
      simulateEvent('pointerdown', 10, 20, true);
      simulateEvent('pointerup', 10, 20, true);

      // second point
      simulateEvent('pointermove', 30, 20, true);
      simulateEvent('pointerdown', 30, 20, true);
      simulateEvent('pointerup', 30, 20, true);

      // finish on second point
      simulateEvent('pointerdown', 30, 20, true);
      simulateEvent('pointerup', 30, 20, true);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(LineString);
      expect(geometry.getCoordinates()).to.eql([
        [10, -20],
        [30, -20],
      ]);

      // without modifier, to be handled by the map's DragPan interaction
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointermove', 10, 30);
      expect(draw.lastDragTime_).to.be(undefined);
    });
  });

  describe('finishCondition called for each type', function () {
    let draw;
    function createDrawInteraction(type, finishCondition) {
      draw = new Draw({
        source: source,
        type: type,
        finishCondition: finishCondition,
      });
      draw.atFinish_ = sinon.spy(Draw.prototype.atFinish_);
      draw.finishDrawing = sinon.spy(Draw.prototype.finishDrawing);
      map.addInteraction(draw);
    }

    const testCoordinates = [
      [0, 0],
      [10, 0],
      [10, 10],
    ];

    function drawType(type, amount, finishCondition) {
      createDrawInteraction(type, finishCondition);

      for (let i = 0; i < amount; ++i) {
        const [x, y] = testCoordinates[i];
        simulateEvent('pointermove', x, y);
        simulateEvent('pointerdown', x, y);
        simulateEvent('pointerup', x, y);
      }
      if (amount > 1 && type !== GeometryType.CIRCLE) {
        const [x, y] = testCoordinates[amount - 1];
        simulateEvent('pointerdown', x, y);
        simulateEvent('pointerup', x, y);
      }
    }

    function testFinishConditionTrue(type, amount) {
      const finishCondition = sinon.spy(() => true);
      drawType(type, amount, finishCondition);
      expect(draw.atFinish_.called).to.be(true);
      expect(finishCondition.callCount).to.be(1);
      expect(draw.finishDrawing.callCount).to.be(1);
      expect(source.getFeatures()).to.have.length(1);
    }
    it('calls finishCondition:true for POINT type', function () {
      testFinishConditionTrue(GeometryType.POINT, 1);
    });
    it('calls finishCondition:true for MULTI_POINT type', function () {
      testFinishConditionTrue(GeometryType.MULTI_POINT, 1);
    });
    it('calls finishCondition:true for LINE_STRING type', function () {
      testFinishConditionTrue(GeometryType.LINE_STRING, 2);
    });
    it('calls finishCondition:true for MULTI_LINE_STRING type', function () {
      testFinishConditionTrue(GeometryType.MULTI_LINE_STRING, 2);
    });
    it('calls finishCondition:true for CIRCLE type', function () {
      testFinishConditionTrue(GeometryType.CIRCLE, 2);
    });
    it('calls finishCondition:true for POLYGON type', function () {
      testFinishConditionTrue(GeometryType.POLYGON, 3);
    });
    it('calls finishCondition:true for MULTI_POLYGON type', function () {
      testFinishConditionTrue(GeometryType.MULTI_POLYGON, 3);
    });

    function testFinishConditionFalse(type, amount) {
      const finishCondition = sinon.spy(() => false);
      drawType(type, amount, finishCondition);
      expect(draw.atFinish_.called).to.be(true);
      expect(finishCondition.callCount).to.be(1);
      expect(draw.finishDrawing.called).to.be(false);
      expect(source.getFeatures()).to.have.length(0);
    }
    it('calls finishCondition:false for POINT type', function () {
      testFinishConditionFalse(GeometryType.POINT, 1);
    });
    it('calls finishCondition:false for MULTI_POINT type', function () {
      testFinishConditionFalse(GeometryType.MULTI_POINT, 1);
    });
    it('calls finishCondition:false for LINE_STRING type', function () {
      testFinishConditionFalse(GeometryType.LINE_STRING, 2);
    });
    it('calls finishCondition:false for MULTI_LINE_STRING type', function () {
      testFinishConditionFalse(GeometryType.MULTI_LINE_STRING, 2);
    });
    it('calls finishCondition:false for CIRCLE type', function () {
      testFinishConditionFalse(GeometryType.CIRCLE, 2);
    });
    it('calls finishCondition:false for POLYGON type', function () {
      testFinishConditionFalse(GeometryType.POLYGON, 3);
    });
    it('calls finishCondition:false for MULTI_POLYGON type', function () {
      testFinishConditionFalse(GeometryType.MULTI_POLYGON, 3);
    });
  });

  describe('drawing with a finishCondition', function () {
    beforeEach(function () {
      const draw = new Draw({
        source: source,
        type: 'LineString',
        finishCondition: function (event) {
          return equals(event.coordinate, [30, -20]);
        },
      });
      map.addInteraction(draw);
    });

    it('draws a linestring failing to finish it first, then finishes it', function () {
      let features;

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 40, 30);
      simulateEvent('pointerdown', 40, 30);
      simulateEvent('pointerup', 40, 30);

      // try to finish on this point
      simulateEvent('pointerdown', 40, 30);
      simulateEvent('pointerup', 40, 30);

      features = source.getFeatures();
      expect(features).to.have.length(0);

      // third point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      //  finish on this point
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      features = source.getFeatures();
      expect(features).to.have.length(1);
    });
  });

  describe('drawing multi-linestrings', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'MultiLineString',
      });
      map.addInteraction(draw);
    });

    it('draws multi with clicks, finishing on last point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // finish on second point
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(MultiLineString);
      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [30, -20],
        ],
      ]);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing polygons', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'Polygon',
      });
      map.addInteraction(draw);
    });

    function isClosed(polygon) {
      const first = polygon.getFirstCoordinate();
      const last = polygon.getLastCoordinate();
      expect(first).to.eql(last);
    }

    it('draws polygon with clicks, finishing on first point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      isClosed(draw.sketchFeature_.getGeometry());

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);
      isClosed(draw.sketchFeature_.getGeometry());

      // third point
      simulateEvent('pointermove', 40, 10);
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);
      isClosed(draw.sketchFeature_.getGeometry());

      // finish on first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [30, -20],
          [40, -10],
          [10, -20],
        ],
      ]);
    });

    it('supports removeLastPoint while drawing', function () {
      draw.removeLastPoint();

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 40, 30);
      simulateEvent('pointerdown', 40, 30);
      simulateEvent('pointerup', 40, 30);

      simulateEvent('pointermove', 100, 100);
      draw.removeLastPoint();

      // click near the removed point
      simulateEvent('pointermove', 39, 31);
      simulateEvent('pointerdown', 39, 31);
      simulateEvent('pointerup', 39, 31);

      expect(source.getFeatures()).to.have.length(0);
    });

    it('will tolerate removeLastPoint being called when no coordinates', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 40, 30);
      simulateEvent('pointerdown', 40, 30);
      simulateEvent('pointerup', 40, 30);

      simulateEvent('pointermove', 100, 100);

      expect(function () {
        draw.removeLastPoint();
        draw.removeLastPoint();
        draw.removeLastPoint();
      }).to.not.throwException();
    });

    it('draws polygon with clicks, finishing on last point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // third point
      simulateEvent('pointermove', 40, 10);
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      // finish on last point
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [30, -20],
          [40, -10],
          [10, -20],
        ],
      ]);
    });

    it('supports freehand drawing for polygons', function () {
      // freehand sequence
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20, true);
      simulateEvent('pointermove', 30, 20, true);
      simulateEvent('pointerdrag', 30, 20, true);
      simulateEvent('pointermove', 40, 10, true);
      simulateEvent('pointerdrag', 40, 10, true);
      simulateEvent('pointerup', 40, 10, true);

      // finish on last point
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [30, -20],
          [40, -10],
          [10, -20],
        ],
      ]);
    });

    it('triggers draw events', function () {
      const ds = sinon.spy();
      const de = sinon.spy();
      const da = sinon.spy();
      listen(draw, 'drawstart', ds);
      listen(draw, 'drawend', de);
      listen(draw, 'drawabort', da);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // third point
      simulateEvent('pointermove', 30, 10);
      simulateEvent('pointerdown', 30, 10);
      simulateEvent('pointerup', 30, 10);

      // finish on first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      expect(ds.called).to.be(true);
      expect(ds.callCount).to.be(1);
      expect(de.called).to.be(true);
      expect(de.callCount).to.be(1);
      expect(da.called).to.be(false);
      expect(da.callCount).to.be(0);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing multi-polygons', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'MultiPolygon',
      });
      map.addInteraction(draw);
    });

    it('draws multi with clicks, finishing on first point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // third point
      simulateEvent('pointermove', 40, 10);
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      // finish on first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(MultiPolygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates).to.have.length(1);

      expect(coordinates[0]).to.eql([
        [
          [10, -20],
          [30, -20],
          [40, -10],
          [10, -20],
        ],
      ]);
    });

    it('draws multi with clicks, finishing on last point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // third point
      simulateEvent('pointermove', 40, 10);
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      // finish on last point
      simulateEvent('pointerdown', 40, 10);
      simulateEvent('pointerup', 40, 10);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(MultiPolygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates).to.have.length(1);

      expect(coordinates[0]).to.eql([
        [
          [10, -20],
          [30, -20],
          [40, -10],
          [10, -20],
        ],
      ]);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('drawing circles', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'Circle',
      });
      map.addInteraction(draw);
    });

    it('draws circle with clicks, finishing on second point along x axis', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // finish on second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Circle);
      expect(geometry.getCenter()).to.eql([10, -20]);
      expect(geometry.getRadius()).to.eql(20);
    });

    it('draws circle with clicks, finishing on second point along y axis', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // finish on second point
      simulateEvent('pointermove', 10, 40);
      simulateEvent('pointerdown', 10, 40);
      simulateEvent('pointerup', 10, 40);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Circle);
      expect(geometry.getCenter()).to.eql([10, -20]);
      expect(geometry.getRadius()).to.eql(20);
    });

    it('draws circle with clicks in a user projection, finishing on second point along x axis', function () {
      const userProjection = 'EPSG:3857';
      setUserProjection(userProjection);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // finish on second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Circle);
      const viewProjection = map.getView().getProjection();
      expect(geometry.getCenter()).to.eql(
        transform([10, -20], viewProjection, userProjection)
      );
      const radius = geometry
        .clone()
        .transform(userProjection, viewProjection)
        .getRadius();
      expect(radius).to.roughlyEqual(20, 1e-9);
    });

    it('draws circle with clicks in a user projection, finishing on second point along y axis', function () {
      const userProjection = 'EPSG:3857';
      setUserProjection(userProjection);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // finish on second point
      simulateEvent('pointermove', 10, 40);
      simulateEvent('pointerdown', 10, 40);
      simulateEvent('pointerup', 10, 40);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Circle);
      const viewProjection = map.getView().getProjection();
      expect(geometry.getCenter()).to.eql(
        transform([10, -20], viewProjection, userProjection)
      );
      const radius = geometry
        .clone()
        .transform(userProjection, viewProjection)
        .getRadius();
      expect(radius).to.roughlyEqual(20, 1e-9);
    });

    it('supports freehand drawing for circles', function () {
      draw.freehand_ = true;
      draw.freehandCondition_ = always;

      // no feature created when not moved
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      expect(source.getFeatures()).to.have.length(0);

      // feature created when moved
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerup', 30, 20);
      expect(source.getFeatures()).to.have.length(1);
    });

    it('triggers draw events', function () {
      const ds = sinon.spy();
      const de = sinon.spy();
      const da = sinon.spy();
      listen(draw, 'drawstart', ds);
      listen(draw, 'drawend', de);
      listen(draw, 'drawabort', da);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // finish on second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      expect(ds.called).to.be(true);
      expect(ds.callCount).to.be(1);
      expect(de.called).to.be(true);
      expect(de.callCount).to.be(1);
      expect(da.called).to.be(false);
      expect(da.callCount).to.be(0);
    });
  });

  describe('#abortDrawing()', function () {
    let draw;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'LineString',
      });
      map.addInteraction(draw);
    });

    it('aborts the current drawing', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      draw.abortDrawing();

      expect(source.getFeatures()).to.have.length(0);
      expect(draw.sketchFeature_).to.be(null);
    });

    it('triggers draw events', function () {
      const ds = sinon.spy();
      const de = sinon.spy();
      const da = sinon.spy();
      listen(draw, 'drawstart', ds);
      listen(draw, 'drawend', de);
      listen(draw, 'drawabort', da);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      draw.abortDrawing();

      expect(ds.called).to.be(true);
      expect(ds.callCount).to.be(1);
      expect(de.called).to.be(false);
      expect(de.callCount).to.be(0);
      expect(da.called).to.be(true);
      expect(da.callCount).to.be(1);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      // second point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      draw.removeLastPoint();
      draw.removeLastPoint();
      draw.removeLastPoint();

      expect(ds.called).to.be(true);
      expect(ds.callCount).to.be(2);
      expect(de.called).to.be(false);
      expect(de.callCount).to.be(0);
      expect(da.called).to.be(true);
      expect(da.callCount).to.be(2);
    });

    it('works if finishDrawing is called when the sketch feature is not defined', function () {
      expect(function () {
        draw.finishDrawing();
      }).to.not.throwException();
    });
  });

  describe('#setActive()', function () {
    let interaction;

    beforeEach(function () {
      interaction = new Draw({
        type: 'LineString',
      });

      expect(interaction.getActive()).to.be(true);

      map.addInteraction(interaction);

      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      expect(interaction.sketchFeature_).not.to.be(null);
    });

    afterEach(function () {
      map.removeInteraction(interaction);
    });

    describe('#setActive(false)', function () {
      it('unsets the map from the feature overlay', function () {
        const spy = sinon.spy(interaction.overlay_, 'setMap');
        interaction.setActive(false);
        expect(spy.getCall(0).args[0]).to.be(null);
      });
      it('aborts the drawing', function () {
        interaction.setActive(false);
        expect(interaction.sketchFeature_).to.be(null);
      });
      it('fires change:active', function () {
        const spy = sinon.spy(interaction.overlay_, 'setMap');
        const listenerSpy = sinon.spy(function () {
          // test that the interaction's change:active listener is called first
          expect(spy.getCall(0).args[0]).to.be(null);
        });
        interaction.on('change:active', listenerSpy);
        interaction.setActive(false);
        expect(listenerSpy.callCount).to.be(1);
      });
    });

    describe('#setActive(true)', function () {
      beforeEach(function () {
        interaction.setActive(false);
      });
      it('sets the map into the feature overlay', function () {
        const spy = sinon.spy(interaction.overlay_, 'setMap');
        interaction.setActive(true);
        expect(spy.getCall(0).args[0]).to.be(map);
      });
      it('fires change:active', function () {
        const spy = sinon.spy(interaction.overlay_, 'setMap');
        const listenerSpy = sinon.spy(function () {
          // test that the interaction's change:active listener is called first
          expect(spy.getCall(0).args[0]).to.be(map);
        });
        interaction.on('change:active', listenerSpy);
        interaction.setActive(true);
        expect(listenerSpy.callCount).to.be(1);
      });
    });
  });

  describe('#setMap()', function () {
    let interaction;

    beforeEach(function () {
      interaction = new Draw({
        type: 'LineString',
      });
      expect(interaction.getActive()).to.be(true);
    });

    describe('#setMap(null)', function () {
      beforeEach(function () {
        map.addInteraction(interaction);
        // first point
        simulateEvent('pointermove', 10, 20);
        simulateEvent('pointerdown', 10, 20);
        simulateEvent('pointerup', 10, 20);
        expect(interaction.sketchFeature_).not.to.be(null);
      });
      afterEach(function () {
        map.removeInteraction(interaction);
      });
      describe('#setMap(null) when interaction is active', function () {
        it('unsets the map from the feature overlay', function () {
          const spy = sinon.spy(interaction.overlay_, 'setMap');
          interaction.setMap(null);
          expect(spy.getCall(0).args[0]).to.be(null);
        });
        it('aborts the drawing', function () {
          interaction.setMap(null);
          expect(interaction.sketchFeature_).to.be(null);
        });
      });
    });

    describe('#setMap(null) when no drawing is in progress', function () {
      beforeEach(function () {
        map.addInteraction(interaction);
        simulateEvent('pointermove', 10, 20);
        expect(interaction.sketchFeature_).to.be(null);
        expect(interaction.sketchPoint_).not.to.be(null);
      });
      afterEach(function () {
        map.removeInteraction(interaction);
      });
      it('clears the sketch features', function () {
        interaction.setMap(null);
        expect(interaction.sketchFeature_).to.be(null);
        expect(interaction.sketchPoint_).to.be(null);
        expect(interaction.sketchLine_).to.be(null);
      });
    });

    describe('#setMap(map)', function () {
      describe('#setMap(map) when interaction is active', function () {
        it('sets the map into the feature overlay', function () {
          const spy = sinon.spy(interaction.overlay_, 'setMap');
          interaction.setMap(map);
          expect(spy.getCall(0).args[0]).to.be(map);
        });
      });
      describe('#setMap(map) when interaction is not active', function () {
        it('does not set the map into the feature overlay', function () {
          interaction.setActive(false);
          const spy = sinon.spy(interaction.overlay_, 'setMap');
          interaction.setMap(map);
          expect(spy.getCall(0).args[0]).to.be(null);
        });
      });
    });
  });

  describe('#getOverlay', function () {
    it('returns the feature overlay layer', function () {
      const draw = new Draw({type: 'Point'});
      expect(draw.getOverlay()).to.eql(draw.overlay_);
    });
  });

  describe('createRegularPolygon', function () {
    it('creates a regular polygon in Circle mode', function () {
      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createRegularPolygon(4),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0].length).to.eql(5);
      expect(coordinates[0][0][0]).to.roughlyEqual(20, 1e-9);
      expect(coordinates[0][0][1]).to.roughlyEqual(-20, 1e-9);
    });

    it('creates a regular polygon at specified angle', function () {
      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createRegularPolygon(4, Math.PI / 4),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0].length).to.eql(5);
      expect(coordinates[0][0][0]).to.roughlyEqual(20, 1e-9);
      expect(coordinates[0][0][1]).to.roughlyEqual(20, 1e-9);
    });

    it('creates a regular polygon at specified 0 angle', function () {
      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createRegularPolygon(4, 0),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0].length).to.eql(5);
      expect(coordinates[0][0][0]).to.roughlyEqual(28.2842712474619, 1e-9);
      expect(coordinates[0][0][1]).to.roughlyEqual(0, 1e-9);
    });

    it('creates a regular polygon in Circle mode in a user projection', function () {
      const userProjection = 'EPSG:3857';
      setUserProjection(userProjection);

      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createRegularPolygon(4, Math.PI / 4),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0].length).to.eql(5);
      const viewProjection = map.getView().getProjection();
      const coordinate = transform([20, 20], viewProjection, userProjection);
      expect(coordinates[0][0][0]).to.roughlyEqual(coordinate[0], 1e-9);
      expect(coordinates[0][0][1]).to.roughlyEqual(coordinate[1], 1e-9);
    });

    it('sketch start point always matches the mouse point', function () {
      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createRegularPolygon(3),
      });
      map.addInteraction(draw);

      // regular polygon center point
      simulateEvent('pointermove', 60, 60);
      simulateEvent('pointerdown', 60, 60);
      simulateEvent('pointerup', 60, 60);

      // move to first quadrant
      simulateEvent('pointermove', 79, 80);
      let event = simulateEvent('pointermove', 80, 80);
      let coordinate = event.coordinate;
      const firstQuadrantCoordinate = draw.sketchFeature_
        .getGeometry()
        .getFirstCoordinate();
      expect(firstQuadrantCoordinate[0]).to.roughlyEqual(coordinate[0], 1e-9);
      expect(firstQuadrantCoordinate[1]).to.roughlyEqual(coordinate[1], 1e-9);

      // move to second quadrant
      simulateEvent('pointermove', 41, 80);
      event = simulateEvent('pointermove', 40, 80);
      coordinate = event.coordinate;
      const secondQuadrantCoordinate = draw.sketchFeature_
        .getGeometry()
        .getFirstCoordinate();
      expect(secondQuadrantCoordinate[0]).to.roughlyEqual(coordinate[0], 1e-9);
      expect(secondQuadrantCoordinate[1]).to.roughlyEqual(coordinate[1], 1e-9);

      // move to third quadrant
      simulateEvent('pointermove', 40, 41);
      event = simulateEvent('pointermove', 40, 40);
      coordinate = event.coordinate;
      const thirdQuadrantCoordinate = draw.sketchFeature_
        .getGeometry()
        .getFirstCoordinate();
      expect(thirdQuadrantCoordinate[0]).to.roughlyEqual(coordinate[0], 1e-9);
      expect(thirdQuadrantCoordinate[1]).to.roughlyEqual(coordinate[1], 1e-9);

      // move to fourth quadrant
      simulateEvent('pointermove', 79, 40);
      event = simulateEvent('pointermove', 80, 40);
      coordinate = event.coordinate;
      const fourthQuadrantCoordinate = draw.sketchFeature_
        .getGeometry()
        .getFirstCoordinate();
      expect(fourthQuadrantCoordinate[0]).to.roughlyEqual(coordinate[0], 1e-9);
      expect(fourthQuadrantCoordinate[1]).to.roughlyEqual(coordinate[1], 1e-9);
    });
  });

  describe('createBox', function () {
    it('creates a box-shaped polygon in Circle mode', function () {
      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createBox(),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0]).to.have.length(5);
      expect(geometry.getArea()).to.equal(400);
      expect(geometry.getExtent()).to.eql([0, -20, 20, 0]);
    });

    it('creates a box-shaped polygon in Circle mode in a user projection', function () {
      proj4.defs(
        'ESRI:54009',
        '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'
      );
      register(proj4);
      const userProjection = 'ESRI:54009';
      setUserProjection(userProjection);

      const draw = new Draw({
        source: source,
        type: 'Circle',
        geometryFunction: createBox(),
      });
      map.addInteraction(draw);

      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      // finish on second point
      simulateEvent('pointermove', 20, 20);
      simulateEvent('pointerdown', 20, 20);
      simulateEvent('pointerup', 20, 20);

      const features = source.getFeatures();
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);
      const coordinates = geometry.getCoordinates();
      expect(coordinates[0]).to.have.length(5);
      const viewProjection = map.getView().getProjection();
      const area = geometry
        .clone()
        .transform(userProjection, viewProjection)
        .getArea();
      expect(area).to.roughlyEqual(400, 1e-9);
      const extent = geometry
        .clone()
        .transform(userProjection, viewProjection)
        .getExtent();
      expect(extent[0]).to.roughlyEqual(0, 1e-9);
      expect(extent[1]).to.roughlyEqual(-20, 1e-9);
      expect(extent[2]).to.roughlyEqual(20, 1e-9);
      expect(extent[3]).to.roughlyEqual(0, 1e-9);
    });
  });

  describe('extend an existing feature', function () {
    let draw;
    let feature;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'LineString',
      });
      map.addInteraction(draw);
      feature = new Feature(
        new LineString([
          [0, 0],
          [1, 1],
          [2, 0],
        ])
      );
    });

    it('sets the initial state', function () {
      draw.extend(feature);
      expect(draw.sketchCoords_).to.have.length(4);
      expect(draw.sketchCoords_).to.eql([
        [0, 0],
        [1, 1],
        [2, 0],
        [2, 0],
      ]);
      expect(draw.finishCoordinate_).to.eql([2, 0]);
    });

    it('dispatches a drawstart event', function () {
      const spy = sinon.spy();
      listen(draw, 'drawstart', spy);
      draw.extend(feature);
      expect(spy.callCount).to.be(1);
    });
  });

  describe('append coordinates when drawing a Polygon feature', function () {
    let draw;
    let coordinates;
    let coordinates2;

    beforeEach(function () {
      draw = new Draw({
        source: source,
        type: 'Polygon',
      });
      map.addInteraction(draw);
      coordinates = [
        [0, 0],
        [1, 1],
        [2, 0],
        [0, 3],
        [3, 2],
        [4, 4],
      ];
      coordinates2 = [
        [10, 10],
        [11, 11],
        [12, 10],
        [10, 13],
        [13, 12],
        [14, 14],
      ];
    });

    function isClosed(polygon) {
      const first = polygon.getFirstCoordinate();
      const last = polygon.getLastCoordinate();
      expect(first).to.eql(last);
    }

    it('draws polygon with clicks, adds coordinates to drawing, finishing on first point', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      isClosed(draw.sketchFeature_.getGeometry());

      // add coordinates
      draw.appendCoordinates(coordinates);

      // finish on first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [0, 0],
          [1, 1],
          [2, 0],
          [0, 3],
          [3, 2],
          [4, 4],
          [10, -20],
        ],
      ]);
    });

    it('adds coordinates to empty drawing', function () {
      // first point
      simulateEvent('pointermove', 0, 0);
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);
      draw.removeLastPoint();
      draw.appendCoordinates(coordinates);
      isClosed(draw.sketchFeature_.getGeometry());

      // finish drawing
      simulateEvent('pointerdown', 0, 0);
      simulateEvent('pointerup', 0, 0);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [0, 0],
          [1, 1],
          [2, 0],
          [0, 3],
          [3, 2],
          [4, 4],
          [0, 0],
        ],
      ]);
    });

    it('keeps updating the sketch feature after appending coordinates', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      isClosed(draw.sketchFeature_.getGeometry());

      // add coordinates
      draw.appendCoordinates(coordinates);

      // add another point
      simulateEvent('pointermove', 30, 20);
      simulateEvent('pointerdown', 30, 20);
      simulateEvent('pointerup', 30, 20);

      // sketchGeom should have a complete ring, with a double coordinate for cursor
      const sketchGeom = draw.sketchFeature_.getGeometry();
      expect(sketchGeom.getCoordinates()).to.eql([
        [
          [10, -20],
          [0, 0],
          [1, 1],
          [2, 0],
          [0, 3],
          [3, 2],
          [4, 4],
          [30, -20],
          [30, -20],
          [10, -20],
        ],
      ]);
    });

    it('keeps updating the sketch feature after multiple appendiges', function () {
      // first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);
      isClosed(draw.sketchFeature_.getGeometry());

      // add coordinates
      draw.appendCoordinates(coordinates);

      // another point
      simulateEvent('pointermove', 100, 100);
      simulateEvent('pointerdown', 100, 100);
      simulateEvent('pointerup', 100, 100);

      // add another array of coordinates
      draw.appendCoordinates(coordinates2);

      // finish on first point
      simulateEvent('pointermove', 10, 20);
      simulateEvent('pointerdown', 10, 20);
      simulateEvent('pointerup', 10, 20);

      const features = source.getFeatures();
      expect(features).to.have.length(1);
      const geometry = features[0].getGeometry();
      expect(geometry).to.be.a(Polygon);

      expect(geometry.getCoordinates()).to.eql([
        [
          [10, -20],
          [0, 0],
          [1, 1],
          [2, 0],
          [0, 3],
          [3, 2],
          [4, 4],
          [100, -100],
          [10, 10],
          [11, 11],
          [12, 10],
          [10, 13],
          [13, 12],
          [14, 14],
          [10, -20],
        ],
      ]);
    });
  });
});
