Source: bemap-leaflet/bemap-map-leaflet-draw.js

/**
 * BeNomad BeMap JavaScript API - Leaflet - Draw Interaction
 * Uses L.Draw if available, otherwise falls back to click-based drawing.
 */

bemap.LeafletMap.prototype.drawPolygon = function(options, callback) {
  var opts = options || {};
  var _this = this;
  this.cancelDraw();

  // If Leaflet.Draw is available, use it
  if (typeof L !== 'undefined' && L.Draw && L.Draw.Polygon) {
    var shapeOpts = {};
    if (opts.style) {
      if (opts.style.borderColor) shapeOpts.color = _this._colorToHex(opts.style.borderColor);
      if (opts.style.borderWidth) shapeOpts.weight = opts.style.borderWidth;
      if (opts.style.fillColor) shapeOpts.fillColor = _this._colorToHex(opts.style.fillColor);
      if (opts.style.fillColor) shapeOpts.fillOpacity = opts.style.fillColor.getAlpha ? opts.style.fillColor.getAlpha() : 0.3;
    }

    var drawer = new L.Draw.Polygon(this.native, { shapeOptions: shapeOpts });
    drawer.enable();

    var handler = function(e) {
      _this.native.off(L.Draw.Event.CREATED, handler);
      _this._drawHandler = null;

      var latlngs = e.layer.getLatLngs()[0];
      var coords = [];
      for (var i = 0; i < latlngs.length; i++) {
        coords.push(new bemap.Coordinate(latlngs[i].lng, latlngs[i].lat));
      }
      var drawStyle = 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 polygon = new bemap.Polygon(coords, { style: drawStyle });

      var container = _this.native.getContainer();
      if (container) container.classList.remove('bemap-drawing-active');

      if (callback) {
        callback(new bemap.MapEvent({ map: _this, bemapObject: polygon, coordinate: coords[0] }));
      }

      // Add polygon to map using bemap's addPolygon (creates proper L.polygon)
      if (opts.addToMap !== false) {
        _this.addPolygon(polygon);
      }
      // Auto-enable vertex editing on the bemap-created native polygon
      if (opts.editable !== false) {
        _this.editPolygon(polygon, callback);
      }
    };
    this.native.on(L.Draw.Event.CREATED, handler);
    this._drawHandler = { drawer: drawer, handler: handler };

    var container = this.native.getContainer();
    if (container) container.classList.add('bemap-drawing-active');

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

  // Fallback: click-based drawing
  return this._clickDraw('Polygon', opts, callback);
};

bemap.LeafletMap.prototype.drawPolyline = function(options, callback) {
  var opts = options || {};
  var _this = this;
  this.cancelDraw();

  if (typeof L !== 'undefined' && L.Draw && L.Draw.Polyline) {
    var shapeOpts = {};
    if (opts.style) {
      if (opts.style.color) shapeOpts.color = _this._colorToHex(opts.style.color);
      if (opts.style.width) shapeOpts.weight = opts.style.width;
    }
    var drawer = new L.Draw.Polyline(this.native, { shapeOptions: shapeOpts });
    drawer.enable();

    var handler = function(e) {
      _this.native.off(L.Draw.Event.CREATED, handler);
      _this._drawHandler = null;
      var latlngs = e.layer.getLatLngs();
      var coords = [];
      for (var i = 0; i < latlngs.length; i++) {
        coords.push(new bemap.Coordinate(latlngs[i].lng, latlngs[i].lat));
      }
      var lineStyle = opts.style || new bemap.LineStyle({
        color: new bemap.Color(230, 57, 70, 1),
        width: 3
      });
      var polyline = new bemap.Polyline(coords, { style: lineStyle });
      var container = _this.native.getContainer();
      if (container) container.classList.remove('bemap-drawing-active');
      if (callback) {
        callback(new bemap.MapEvent({ map: _this, bemapObject: polyline, coordinate: coords[0] }));
      }
      if (opts.addToMap !== false) {
        _this.addPolyline(polyline);
      }
    };
    this.native.on(L.Draw.Event.CREATED, handler);
    this._drawHandler = { drawer: drawer, handler: handler };
    var container = this.native.getContainer();
    if (container) container.classList.add('bemap-drawing-active');
    return new bemap.Listener({ native: drawer, bemapObject: null, key: 'drawPolyline' });
  }

  return this._clickDraw('Polyline', opts, callback);
};

bemap.LeafletMap.prototype.drawRectangle = function(options, callback) {
  var opts = options || {};
  var _this = this;
  this.cancelDraw();

  if (typeof L !== 'undefined' && L.Draw && L.Draw.Rectangle) {
    var drawer = new L.Draw.Rectangle(this.native, {});
    drawer.enable();
    var handler = function(e) {
      _this.native.off(L.Draw.Event.CREATED, handler);
      _this._drawHandler = null;
      var latlngs = e.layer.getLatLngs()[0];
      var coords = [];
      for (var i = 0; i < latlngs.length; i++) {
        coords.push(new bemap.Coordinate(latlngs[i].lng, latlngs[i].lat));
      }
      var rectStyle = 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 polygon = new bemap.Polygon(coords, { style: rectStyle });
      var container = _this.native.getContainer();
      if (container) container.classList.remove('bemap-drawing-active');
      if (callback) {
        callback(new bemap.MapEvent({ map: _this, bemapObject: polygon, coordinate: coords[0] }));
      }
      if (opts.addToMap !== false) _this.addPolygon(polygon);
      if (opts.editable !== false) _this.editPolygon(polygon, callback);
    };
    this.native.on(L.Draw.Event.CREATED, handler);
    this._drawHandler = { drawer: drawer, handler: handler };
    var containerR = this.native.getContainer();
    if (containerR) containerR.classList.add('bemap-drawing-active');
    return new bemap.Listener({ native: drawer, bemapObject: null, key: 'drawRectangle' });
  }

  return new bemap.Listener();
};

bemap.LeafletMap.prototype.drawCircle = function(options, callback) {
  var opts = options || {};
  var _this = this;
  this.cancelDraw();

  if (typeof L !== 'undefined' && L.Draw && L.Draw.Circle) {
    var drawer = new L.Draw.Circle(this.native, {});
    drawer.enable();
    var handler = function(e) {
      _this.native.off(L.Draw.Event.CREATED, handler);
      _this._drawHandler = null;
      var center = new bemap.Coordinate(e.layer.getLatLng().lng, e.layer.getLatLng().lat);
      var radius = e.layer.getRadius();
      var circle = new bemap.Circle(center, radius, { style: opts.style });
      var container = _this.native.getContainer();
      if (container) container.classList.remove('bemap-drawing-active');
      if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: circle, coordinate: center }));
      if (opts.addToMap !== false) _this.addCircle(circle);
    };
    this.native.on(L.Draw.Event.CREATED, handler);
    this._drawHandler = { drawer: drawer, handler: handler };
    var container = this.native.getContainer();
    if (container) container.classList.add('bemap-drawing-active');
    return new bemap.Listener({ native: drawer, bemapObject: null, key: 'drawCircle' });
  }
  return new bemap.Listener();
};

bemap.LeafletMap.prototype.drawMarker = function(options, callback) {
  var _this = this;
  this.cancelDraw();
  var clickHandler = function(e) {
    _this.native.off('click', clickHandler);
    var coord = new bemap.Coordinate(e.latlng.lng, e.latlng.lat);
    var marker = new bemap.Marker(coord, { icon: (options || {}).icon });
    if (callback) {
      callback(new bemap.MapEvent({ map: _this, bemapObject: marker, coordinate: coord }));
    }
    if ((options || {}).addToMap !== false) {
      _this.addMarker(marker);
    }
  };
  this.native.on('click', clickHandler);
  this._drawHandler = { clickHandler: clickHandler };
  return new bemap.Listener();
};

bemap.LeafletMap.prototype.cancelDraw = function() {
  if (this._drawHandler) {
    if (this._drawHandler.drawer) this._drawHandler.drawer.disable();
    if (this._drawHandler.handler) this.native.off(L.Draw.Event.CREATED, this._drawHandler.handler);
    if (this._drawHandler.clickHandler) this.native.off('click', this._drawHandler.clickHandler);
    if (this._drawHandler.dblclickHandler) this.native.off('dblclick', this._drawHandler.dblclickHandler);
    this._drawHandler = null;
  }
  // Clean up click-draw state
  if (this._clickDrawState) {
    var prevPoly = this._clickDrawState.getPreviewPolygon ? this._clickDrawState.getPreviewPolygon() : null;
    var prevLine = this._clickDrawState.getPreviewPolyline ? this._clickDrawState.getPreviewPolyline() : null;
    if (prevPoly) this.removePolygon(prevPoly);
    if (prevLine) this.removePolyline(prevLine);
    this._clickDrawState = null;
    this.native.doubleClickZoom.enable();
  }
  var container = this.native.getContainer();
  if (container) container.classList.remove('bemap-drawing-active');
  return this;
};

bemap.LeafletMap.prototype.editPolygon = function(polygon, callback) {
  if (!polygon || !polygon.native) return new bemap.Listener();
  var _this = this;
  var map = this.native;
  var MERGE_PX = 15;

  // Remove any previous vertex handles for this polygon
  if (polygon._vertexHandlesLayer) {
    map.removeLayer(polygon._vertexHandlesLayer);
  }

  var vertexHandlesLayer = L.layerGroup().addTo(map);
  polygon._vertexHandlesLayer = vertexHandlesLayer;

  var vertexIcon = L.divIcon({
    className: 'leaflet-div-icon leaflet-editing-icon',
    iconSize: [10, 10],
    iconAnchor: [5, 5]
  });

  var middleIcon = L.divIcon({
    className: 'leaflet-div-icon leaflet-editing-icon',
    iconSize: [10, 10],
    iconAnchor: [5, 5]
  });

  function syncCoords() {
    var latlngs = polygon.native.getLatLngs()[0];
    var coords = [];
    for (var i = 0; i < latlngs.length; i++) {
      coords.push(new bemap.Coordinate(latlngs[i].lng, latlngs[i].lat));
    }
    polygon.coords = coords;
  }

  function rebuildHandles() {
    vertexHandlesLayer.clearLayers();
    var latlngs = polygon.native.getLatLngs()[0];
    if (!latlngs || latlngs.length < 3) return;

    // Vertex handles
    for (var i = 0; i < latlngs.length; i++) {
      (function(idx) {
        var handle = L.marker(latlngs[idx], {
          icon: vertexIcon,
          draggable: true,
          zIndexOffset: 2000
        });

        handle.on('drag', function(ev) {
          var pos = ev.target.getLatLng();
          var pts = polygon.native.getLatLngs()[0];
          pts[idx] = pos;
          polygon.native.setLatLngs(pts);
          syncCoords();
        });

        handle.on('dragend', function(ev) {
          var pts = polygon.native.getLatLngs()[0];
          if (pts.length <= 3) { rebuildHandles(); return; }
          var dropPx = map.latLngToLayerPoint(ev.target.getLatLng());
          for (var j = 0; j < pts.length; j++) {
            if (j === idx) continue;
            var px = map.latLngToLayerPoint(pts[j]);
            if (dropPx.distanceTo(px) < MERGE_PX) {
              var next = pts.filter(function(_, k) { return k !== idx; });
              polygon.native.setLatLngs(next);
              syncCoords();
              rebuildHandles();
              if (callback) {
                callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
              }
              return;
            }
          }
          rebuildHandles();
          if (callback) {
            callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
          }
        });

        // Right-click to delete vertex
        handle.on('contextmenu', function(ev) {
          L.DomEvent.preventDefault(ev.originalEvent);
          L.DomEvent.stopPropagation(ev.originalEvent);
          var pts = polygon.native.getLatLngs()[0];
          if (pts.length <= 3) return;
          var next = pts.filter(function(_, k) { return k !== idx; });
          polygon.native.setLatLngs(next);
          syncCoords();
          rebuildHandles();
          if (callback) {
            callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
          }
        });

        vertexHandlesLayer.addLayer(handle);
      })(i);
    }

    // Middle handles (click or drag to insert new vertex)
    for (var m = 0; m < latlngs.length; m++) {
      (function(idx) {
        var a = latlngs[idx];
        var b = latlngs[(idx + 1) % latlngs.length];
        var mid = L.latLng((a.lat + b.lat) / 2, (a.lng + b.lng) / 2);
        var midHandle = L.marker(mid, {
          icon: middleIcon,
          draggable: true,
          opacity: 0.5,
          zIndexOffset: 1500
        });

        midHandle.on('dragstart', function() {
          // Insert the new vertex
          var pts = polygon.native.getLatLngs()[0].slice();
          pts.splice(idx + 1, 0, midHandle.getLatLng());
          polygon.native.setLatLngs(pts);
          syncCoords();
        });

        midHandle.on('drag', function(ev) {
          var pos = ev.target.getLatLng();
          var pts = polygon.native.getLatLngs()[0];
          pts[idx + 1] = pos;
          polygon.native.setLatLngs(pts);
          syncCoords();
        });

        midHandle.on('dragend', function() {
          rebuildHandles();
          if (callback) {
            callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
          }
        });

        vertexHandlesLayer.addLayer(midHandle);
      })(m);
    }
  }

  // Remove previous click handler if editPolygon was called before
  if (polygon._editClickHandler) {
    polygon.native.off('click', polygon._editClickHandler);
  }

  // Also allow clicking on polygon edge to insert vertex
  var editClickHandler = function(ev) {
    var pts = polygon.native.getLatLngs()[0];
    var clickPx = map.latLngToLayerPoint(ev.latlng);
    var bestIdx = 0, bestDist = Infinity;
    for (var i = 0; i < pts.length; i++) {
      var a = map.latLngToLayerPoint(pts[i]);
      var b = map.latLngToLayerPoint(pts[(i + 1) % pts.length]);
      var d = _this._distToSegmentPxLeaflet(clickPx, a, b);
      if (d < bestDist) { bestDist = d; bestIdx = i; }
    }
    if (bestDist < 20) {
      var next = pts.slice();
      next.splice(bestIdx + 1, 0, ev.latlng);
      polygon.native.setLatLngs(next);
      syncCoords();
      rebuildHandles();
      if (callback) {
        callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
      }
    }
  };
  polygon.native.on('click', editClickHandler);
  polygon._editClickHandler = editClickHandler;

  rebuildHandles();

  return new bemap.Listener({ native: polygon.native, bemapObject: polygon, key: 'editPolygon' });
};

bemap.LeafletMap.prototype._distToSegmentPxLeaflet = function(p, a, b) {
  var dx = b.x - a.x, dy = b.y - a.y;
  if (dx === 0 && dy === 0) return p.distanceTo(a);
  var t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy)));
  var proj = L.point(a.x + t * dx, a.y + t * dy);
  return p.distanceTo(proj);
};

/**
 * Click-based drawing fallback (when L.Draw is not loaded).
 */
bemap.LeafletMap.prototype._clickDraw = function(type, opts, callback) {
  var _this = this;
  var coords = [];
  var previewPolyline = null;
  var previewPolygon = null;
  var defaultPolyStyle = new bemap.PolygonStyle({
    fillColor: new bemap.Color(0, 102, 204, 0.2),
    borderColor: new bemap.Color(0, 102, 204, 1),
    borderWidth: 2
  });
  var defaultLineStyle = new bemap.LineStyle({
    color: new bemap.Color(230, 57, 70, 1),
    width: 3
  });
  var style = opts.style || (type === 'Polyline' ? defaultLineStyle : defaultPolyStyle);

  this.native.doubleClickZoom.disable();

  var clickHandler = function(e) {
    coords.push(new bemap.Coordinate(e.latlng.lng, e.latlng.lat));

    // Update preview
    if (previewPolygon) { _this.removePolygon(previewPolygon); previewPolygon = null; }
    if (previewPolyline) { _this.removePolyline(previewPolyline); previewPolyline = null; }

    if (coords.length === 2) {
      previewPolyline = new bemap.Polyline(coords, { style: style });
      _this.addPolyline(previewPolyline);
    } else if (coords.length >= 3) {
      if (type === 'Polygon') {
        previewPolygon = new bemap.Polygon(coords, { style: style });
        _this.addPolygon(previewPolygon);
      } else {
        previewPolyline = new bemap.Polyline(coords, { style: style });
        _this.addPolyline(previewPolyline);
      }
    }
  };

  var dblclickHandler = function() {
    _this.native.off('click', clickHandler);
    _this.native.off('dblclick', dblclickHandler);
    _this.native.doubleClickZoom.enable();

    if (previewPolygon) { _this.removePolygon(previewPolygon); previewPolygon = null; }
    if (previewPolyline) { _this.removePolyline(previewPolyline); previewPolyline = null; }

    var container = _this.native.getContainer();
    if (container) container.classList.remove('bemap-drawing-active');

    if (coords.length >= 3 && type === 'Polygon') {
      var polygon = new bemap.Polygon(coords, { style: style });
      if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polygon, coordinate: coords[0] }));
      if (opts.addToMap !== false) _this.addPolygon(polygon);
      if (opts.editable !== false) _this.editPolygon(polygon, callback);
    } else if (coords.length >= 2 && type === 'Polyline') {
      var polyline = new bemap.Polyline(coords, { style: style });
      if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polyline, coordinate: coords[0] }));
      if (opts.addToMap !== false) _this.addPolyline(polyline);
    }
    _this._clickDrawState = null;
  };

  this.native.on('click', clickHandler);
  this.native.on('dblclick', dblclickHandler);
  this._drawHandler = { clickHandler: clickHandler, dblclickHandler: dblclickHandler };
  this._clickDrawState = {
    coords: coords,
    getPreviewPolygon: function() { return previewPolygon; },
    getPreviewPolyline: function() { return previewPolyline; }
  };

  var container = this.native.getContainer();
  if (container) container.classList.add('bemap-drawing-active');

  return new bemap.Listener();
};

/**
 * Helper: convert bemap.Color to hex string.
 */
bemap.LeafletMap.prototype._colorToHex = function(color) {
  if (!color) return '#000000';
  var r = color.getRed ? color.getRed() : 0;
  var g = color.getGreen ? color.getGreen() : 0;
  var b = color.getBlue ? color.getBlue() : 0;
  return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};