/**
* 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);
};