Source: bemap-ol/bemap-ol-v10/bemap-map-ol-draw.js

/**
 * BeNomad BeMap JavaScript API - OpenLayers v10 - Draw Interaction
 */

bemap.OlMap.prototype.drawPolygon = function(options, callback) {
  return this._startDraw('Polygon', options, callback);
};

bemap.OlMap.prototype.drawPolyline = function(options, callback) {
  return this._startDraw('LineString', options, callback);
};

bemap.OlMap.prototype.drawCircle = function(options, callback) {
  return this._startDraw('Circle', options, callback);
};

bemap.OlMap.prototype.drawRectangle = function(options, callback) {
  return this._startDraw('Circle', options, callback, true); // geometryFunction makes it a box
};

bemap.OlMap.prototype.drawMarker = function(options, callback) {
  return this._startDraw('Point', options, callback);
};

bemap.OlMap.prototype._startDraw = function(type, options, callback, isBox) {
  if (typeof ol === 'undefined' || !ol.interaction || !ol.interaction.Draw) {
    console.warn('ol.interaction.Draw not available');
    return new bemap.Listener();
  }
  var opts = options || {};
  var _this = this;

  // Cancel any existing draw
  this.cancelDraw();

  // Create a temporary vector source for drawing
  var drawSource = new ol.source.Vector({ features: [] });
  var drawLayer = new ol.layer.Vector({ source: drawSource });
  this.native.addLayer(drawLayer);

  // Build draw interaction options
  var drawOpts = { type: type, source: drawSource };

  if (isBox) {
    drawOpts.type = 'Circle';
    if (ol.interaction.Draw.createBox) drawOpts.geometryFunction = ol.interaction.Draw.createBox();
  }

  var interaction = new ol.interaction.Draw(drawOpts);

  interaction.on('drawend', function(evt) {
    var feature = evt.feature;
    var geom = feature.getGeometry();

    // Transform back to WGS84
    var cloned = geom.clone().transform(_this.native.getView().getProjection(), bemap.Map.PROJ.EPSG_WGS84);

    var defaultPolyStyle = opts.style || new bemap.PolygonStyle({
      fillColor: new bemap.Color(0, 102, 204, 0.2),
      borderColor: new bemap.Color(0, 102, 204, 1),
      borderWidth: 2
    });
    var bemapObj = null;

    if (type === 'Polygon' || isBox) {
      var ringCoords = cloned.getCoordinates()[0];
      var bemapCoords = [];
      for (var i = 0; i < ringCoords.length - 1; i++) { // skip closing coord
        bemapCoords.push(new bemap.Coordinate(ringCoords[i][0], ringCoords[i][1]));
      }
      bemapObj = new bemap.Polygon(bemapCoords, { style: defaultPolyStyle });

    } else if (type === 'LineString') {
      var lineCoords = cloned.getCoordinates();
      var bemapLineCoords = [];
      for (var j = 0; j < lineCoords.length; j++) {
        bemapLineCoords.push(new bemap.Coordinate(lineCoords[j][0], lineCoords[j][1]));
      }
      var defaultLineStyle = opts.style || new bemap.LineStyle({ color: new bemap.Color(230, 57, 70, 1), width: 3 });
      bemapObj = new bemap.Polyline(bemapLineCoords, { style: defaultLineStyle });

    } else if (type === 'Circle') {
      var center = cloned.getCenter();
      var centerCoord = new bemap.Coordinate(center[0], center[1]);
      // Compute real-Earth radius in meters via Haversine from center to edge point
      var mercCenter = geom.getCenter();
      var mercRadius = geom.getRadius();
      var edgeMerc = [mercCenter[0] + mercRadius, mercCenter[1]];
      var edgeWgs = ol.proj.transform(edgeMerc, _this.native.getView().getProjection(), bemap.Map.PROJ.EPSG_WGS84);
      var R = 6371000;
      var lat1 = center[1] * Math.PI / 180, lat2 = edgeWgs[1] * Math.PI / 180;
      var dLat = (edgeWgs[1] - center[1]) * Math.PI / 180;
      var dLon = (edgeWgs[0] - center[0]) * Math.PI / 180;
      var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon/2) * Math.sin(dLon/2);
      var radiusM = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
      var circleStyle = opts.style || new bemap.CircleStyle({
        fillColor: new bemap.Color(0, 102, 204, 0.2),
        borderColor: new bemap.Color(0, 102, 204, 1),
        borderWidth: 2
      });
      bemapObj = new bemap.Circle(centerCoord, radiusM, { style: circleStyle });

    } else if (type === 'Point') {
      var pt = cloned.getCoordinates();
      bemapObj = new bemap.Marker(new bemap.Coordinate(pt[0], pt[1]));
    }

    // Clean up
    _this.native.removeInteraction(interaction);
    _this.native.removeLayer(drawLayer);
    _this._drawInteraction = null;

    if (bemapObj) {
      if (callback) {
        callback(new bemap.MapEvent({
          map: _this,
          bemapObject: bemapObj,
          coordinate: bemapObj.coordinate || (bemapObj.getCoordinates ? bemapObj.getCoordinates()[0] : null)
        }));
      }

      // Auto-add to map
      if (opts.addToMap !== false) {
        if (bemap.inheritsof(bemapObj, bemap.Polygon)) _this.addPolygon(bemapObj);
        else if (bemap.inheritsof(bemapObj, bemap.Polyline)) _this.addPolyline(bemapObj);
        else if (bemap.inheritsof(bemapObj, bemap.Circle)) _this.addCircle(bemapObj);
        else if (bemap.inheritsof(bemapObj, bemap.Marker)) _this.addMarker(bemapObj);
      }

      // Auto-enable vertex editing
      if (opts.editable !== false && bemap.inheritsof(bemapObj, bemap.Polygon)) {
        _this.editPolygon(bemapObj, callback);
      }
    }
  });

  this.native.addInteraction(interaction);
  this._drawInteraction = interaction;
  this._drawLayer = drawLayer;

  // Set crosshair cursor
  var container = document.getElementById(this.target);
  if (container) container.classList.add('bemap-drawing-active');

  return new bemap.Listener({ native: interaction, bemapObject: null, key: 'draw' });
};

bemap.OlMap.prototype.cancelDraw = function() {
  if (this._drawInteraction) {
    this.native.removeInteraction(this._drawInteraction);
    this._drawInteraction = null;
  }
  if (this._editInteraction) {
    this.native.removeInteraction(this._editInteraction);
    this._editInteraction = null;
  }
  if (this._drawLayer) {
    this.native.removeLayer(this._drawLayer);
    this._drawLayer = null;
  }
  var container = document.getElementById(this.target);
  if (container) container.classList.remove('bemap-drawing-active');
  return this;
};

bemap.OlMap.prototype.editPolygon = function(polygon, callback) {
  if (!polygon || !polygon.native) return new bemap.Listener();
  if (typeof ol === 'undefined' || !ol.interaction || !ol.interaction.Modify) return new bemap.Listener();
  var _this = this;

  // Remove previous edit interaction if any
  if (this._editInteraction) {
    this.native.removeInteraction(this._editInteraction);
    this._editInteraction = null;
  }

  var modify = new ol.interaction.Modify({
    features: new ol.Collection([polygon.native])
  });

  var MERGE_PX = 15;

  modify.on('modifyend', function() {
    var geom = polygon.native.getGeometry().clone().transform(
      _this.native.getView().getProjection(), bemap.Map.PROJ.EPSG_WGS84
    );
    var ringCoords = geom.getCoordinates()[0];
    var updatedCoords = [];
    for (var i = 0; i < ringCoords.length - 1; i++) {
      updatedCoords.push(new bemap.Coordinate(ringCoords[i][0], ringCoords[i][1]));
    }

    // Merge: remove near-duplicate vertices
    if (updatedCoords.length > 3) {
      var merged = [updatedCoords[0]];
      for (var m = 1; m < updatedCoords.length; m++) {
        var pxA = _this.native.getPixelFromCoordinate(ol.proj.transform(
          [updatedCoords[m].getLon(), updatedCoords[m].getLat()], bemap.Map.PROJ.EPSG_WGS84, _this.native.getView().getProjection()
        ));
        var tooClose = false;
        for (var n = 0; n < merged.length; n++) {
          var pxB = _this.native.getPixelFromCoordinate(ol.proj.transform(
            [merged[n].getLon(), merged[n].getLat()], bemap.Map.PROJ.EPSG_WGS84, _this.native.getView().getProjection()
          ));
          if (pxA && pxB && Math.sqrt(Math.pow(pxA[0] - pxB[0], 2) + Math.pow(pxA[1] - pxB[1], 2)) < MERGE_PX) {
            tooClose = true; break;
          }
        }
        if (!tooClose) merged.push(updatedCoords[m]);
      }
      updatedCoords = merged;
    }

    polygon.coords = updatedCoords;
    if (callback) {
      callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
    }
  });

  // Right-click to delete nearest vertex
  var contextHandler = function(evt) {
    evt.preventDefault();
    var geom = polygon.native.getGeometry();
    var ringCoords = geom.getCoordinates()[0];
    if (ringCoords.length - 1 <= 3) return; // min 3 vertices (ring has closing coord)

    var pixel = _this.native.getEventPixel(evt);
    var coord = _this.native.getCoordinateFromPixel(pixel);
    var bestIdx = -1, bestDist = 20; // max 20px

    for (var v = 0; v < ringCoords.length - 1; v++) {
      var vPx = _this.native.getPixelFromCoordinate(ringCoords[v]);
      if (vPx) {
        var d = Math.sqrt(Math.pow(pixel[0] - vPx[0], 2) + Math.pow(pixel[1] - vPx[1], 2));
        if (d < bestDist) { bestDist = d; bestIdx = v; }
      }
    }

    if (bestIdx >= 0) {
      ringCoords.splice(bestIdx, 1);
      ringCoords[ringCoords.length - 1] = ringCoords[0]; // re-close ring
      geom.setCoordinates([ringCoords]);

      var wgs = geom.clone().transform(_this.native.getView().getProjection(), bemap.Map.PROJ.EPSG_WGS84).getCoordinates()[0];
      var newCoords = [];
      for (var nc = 0; nc < wgs.length - 1; nc++) {
        newCoords.push(new bemap.Coordinate(wgs[nc][0], wgs[nc][1]));
      }
      polygon.coords = newCoords;
      if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
    }
  };
  var mapEl = document.getElementById(_this.target);
  if (mapEl) mapEl.addEventListener('contextmenu', contextHandler);
  polygon._contextHandler = contextHandler;

  this.native.addInteraction(modify);
  this._editInteraction = modify;
  return new bemap.Listener({ native: modify, bemapObject: polygon, key: 'editPolygon' });
};