/**
* BeNomad BeMap JavaScript API - MapLibre v5 - Draw Interaction
* Manual implementation using GeoJSON sources + mouse events.
*/
bemap.MapLibreMap.prototype.drawPolygon = function(options, callback) {
return this._startDraw('Polygon', options, callback);
};
bemap.MapLibreMap.prototype.drawPolyline = function(options, callback) {
return this._startDraw('Polyline', options, callback);
};
bemap.MapLibreMap.prototype.drawRectangle = function(options, callback) {
return this._startDraw('Rectangle', options, callback);
};
bemap.MapLibreMap.prototype.drawCircle = function(options, callback) {
return this._startDraw('Circle', options, callback);
};
bemap.MapLibreMap.prototype.drawMarker = function(options, callback) {
var _this = this;
this.cancelDraw();
var clickHandler = function(e) {
_this.native.off('click', clickHandler);
var container = _this.native.getContainer();
if (container) container.classList.remove('bemap-drawing-active');
var coord = new bemap.Coordinate(e.lngLat.lng, e.lngLat.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._drawState = { clickHandler: clickHandler };
var container = this.native.getContainer();
if (container) container.classList.add('bemap-drawing-active');
return new bemap.Listener();
};
bemap.MapLibreMap.prototype._startDraw = function(type, options, callback) {
var opts = options || {};
var _this = this;
this.cancelDraw();
var coords = [];
var sourceId = '_bemap-draw-src';
var lineLayerId = '_bemap-draw-line';
var fillLayerId = '_bemap-draw-fill';
var vertexLayerId = '_bemap-draw-vertices';
var rubberLineId = '_bemap-draw-rubber';
var lineColor = 'rgba(0,102,204,1)';
var fillColor = 'rgba(0,102,204,0.2)';
var lineWidth = 2;
if (opts.style) {
if (opts.style.borderColor) lineColor = this._colorToRgba(opts.style.borderColor);
if (opts.style.fillColor) fillColor = this._colorToRgba(opts.style.fillColor);
if (opts.style.borderWidth) lineWidth = opts.style.borderWidth;
}
// Disable double-click zoom during drawing
this.native.doubleClickZoom.disable();
// Create GeoJSON source for preview
var emptyGeoJson = { type: 'FeatureCollection', features: [] };
this.native.addSource(sourceId, { type: 'geojson', data: emptyGeoJson });
// Fill layer (for polygon)
if (type === 'Polygon') {
this.native.addLayer({
id: fillLayerId, type: 'fill', source: sourceId,
filter: ['==', '$type', 'Polygon'],
paint: { 'fill-color': fillColor, 'fill-opacity': 1 }
});
}
// Line layer
this.native.addLayer({
id: lineLayerId, type: 'line', source: sourceId,
filter: ['==', '$type', type === 'Polygon' ? 'Polygon' : 'LineString'],
paint: { 'line-color': lineColor, 'line-width': lineWidth },
layout: { 'line-cap': 'round', 'line-join': 'round' }
});
// Vertex dots layer
this.native.addSource(vertexLayerId + '-src', { type: 'geojson', data: emptyGeoJson });
this.native.addLayer({
id: vertexLayerId, type: 'circle', source: vertexLayerId + '-src',
paint: { 'circle-radius': 5, 'circle-color': lineColor, 'circle-stroke-color': '#fff', 'circle-stroke-width': 2 }
});
// Rubber-band line (cursor to last point)
this.native.addSource(rubberLineId + '-src', { type: 'geojson', data: emptyGeoJson });
this.native.addLayer({
id: rubberLineId, type: 'line', source: rubberLineId + '-src',
paint: { 'line-color': lineColor, 'line-width': 1, 'line-dasharray': [4, 4] }
});
var updatePreview = function() {
var lngLats = coords.map(function(c) { return [c.getLon(), c.getLat()]; });
// Vertex dots
var vertexFeatures = lngLats.map(function(ll) {
return { type: 'Feature', geometry: { type: 'Point', coordinates: ll } };
});
// The preview sources can be gone when this runs — a style reload (the server
// charte loads async after login) drops all sources, and the click handler is
// setTimeout-deferred so it may fire after cleanup. Guard before setData().
var _vSrc = _this.native.getSource(vertexLayerId + '-src');
if (!_vSrc) return;
_vSrc.setData({ type: 'FeatureCollection', features: vertexFeatures });
// Main shape
var data;
if (type === 'Rectangle' && lngLats.length >= 2) {
// Show rectangle preview from 2 diagonal corners
var c0 = lngLats[0], c1 = lngLats[lngLats.length - 1];
var rectRing = [c0, [c1[0], c0[1]], c1, [c0[0], c1[1]], c0];
data = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [rectRing] } };
} else if (type === 'Polygon' && lngLats.length >= 3) {
var ring = lngLats.concat([lngLats[0]]); // close the ring
data = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [ring] } };
} else if (lngLats.length >= 2) {
data = { type: 'Feature', geometry: { type: 'LineString', coordinates: lngLats } };
} else {
data = emptyGeoJson;
}
var _mSrc = _this.native.getSource(sourceId);
if (_mSrc) _mSrc.setData(data.type === 'FeatureCollection' ? data : { type: 'FeatureCollection', features: [data] });
};
// Mouse move → rubber-band line from last vertex to cursor
var mouseMoveHandler = function(e) {
if (coords.length === 0) return;
var last = [coords[coords.length - 1].getLon(), coords[coords.length - 1].getLat()];
var cursor = [e.lngLat.lng, e.lngLat.lat];
var _rSrc = _this.native.getSource(rubberLineId + '-src');
if (_rSrc) _rSrc.setData({
type: 'Feature', geometry: { type: 'LineString', coordinates: [last, cursor] }
});
};
// Default styles
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 _defaultLineStyle = opts.style || new bemap.LineStyle({
color: new bemap.Color(230, 57, 70, 1),
width: 3
});
// Click → add vertex (debounced to avoid dblclick phantom vertex)
var CLOSE_PX = 15;
var clickTimer = null;
var clickHandler = function(e) {
var _e = e;
clickTimer = setTimeout(function() {
clickTimer = null;
// Polygon: close if clicking near first point
if (type === 'Polygon' && coords.length >= 3) {
var firstPx = _this.native.project([coords[0].getLon(), coords[0].getLat()]);
var clickPx = _this.native.project([_e.lngLat.lng, _e.lngLat.lat]);
var dist = Math.sqrt(Math.pow(firstPx.x - clickPx.x, 2) + Math.pow(firstPx.y - clickPx.y, 2));
if (dist < CLOSE_PX) {
finishDraw(_e);
return;
}
}
coords.push(new bemap.Coordinate(_e.lngLat.lng, _e.lngLat.lat));
updatePreview();
// Rectangle: auto-finish on 2nd click
if (type === 'Rectangle' && coords.length >= 2) {
finishDraw(_e);
}
// Circle: auto-finish on 2nd click (center + edge)
if (type === 'Circle' && coords.length >= 2) {
finishDraw(_e);
}
}, 220);
};
// Double-click → finish
var dblclickHandler = function(e) {
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
if (e && e.preventDefault) e.preventDefault();
finishDraw(e);
};
// Finish drawing and create the shape
var finishDraw = function(e) {
cleanup();
var bemapObj = null;
if (type === 'Rectangle' && coords.length >= 2) {
var c0 = coords[0], c1 = coords[coords.length - 1];
var rectCoords = [
c0,
new bemap.Coordinate(c1.getLon(), c0.getLat()),
c1,
new bemap.Coordinate(c0.getLon(), c1.getLat())
];
bemapObj = new bemap.Polygon(rectCoords, { style: _defaultPolyStyle });
} else if (type === 'Circle' && coords.length >= 2) {
var center = coords[0];
var edge = coords[1];
var R = 6371000;
var dLat = (edge.getLat() - center.getLat()) * Math.PI / 180;
var dLon = (edge.getLon() - center.getLon()) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(center.getLat() * Math.PI / 180) * Math.cos(edge.getLat() * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2);
var radius = 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(center, radius, { style: _circleStyle });
} else if (type === 'Polygon' && coords.length >= 3) {
bemapObj = new bemap.Polygon(coords, { style: _defaultPolyStyle });
} else if (type === 'Polyline' && coords.length >= 2) {
bemapObj = new bemap.Polyline(coords, { style: _defaultLineStyle });
}
if (bemapObj) {
if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: bemapObj, coordinate: coords[0] }));
// 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 for polygons
if (opts.editable !== false && bemap.inheritsof(bemapObj, bemap.Polygon)) {
_this.editPolygon(bemapObj, callback);
}
}
};
var cleanup = function() {
if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; }
_this.native.off('click', clickHandler);
_this.native.off('dblclick', dblclickHandler);
_this.native.off('mousemove', mouseMoveHandler);
try { if (_this.native.getLayer(fillLayerId)) _this.native.removeLayer(fillLayerId); } catch(e) {}
try { if (_this.native.getLayer(lineLayerId)) _this.native.removeLayer(lineLayerId); } catch(e) {}
try { if (_this.native.getLayer(vertexLayerId)) _this.native.removeLayer(vertexLayerId); } catch(e) {}
try { if (_this.native.getLayer(rubberLineId)) _this.native.removeLayer(rubberLineId); } catch(e) {}
try { if (_this.native.getSource(sourceId)) _this.native.removeSource(sourceId); } catch(e) {}
try { if (_this.native.getSource(vertexLayerId + '-src')) _this.native.removeSource(vertexLayerId + '-src'); } catch(e) {}
try { if (_this.native.getSource(rubberLineId + '-src')) _this.native.removeSource(rubberLineId + '-src'); } catch(e) {}
_this.native.doubleClickZoom.enable();
var container = _this.native.getContainer();
if (container) container.classList.remove('bemap-drawing-active');
_this._drawState = null;
};
this.native.on('click', clickHandler);
this.native.on('dblclick', dblclickHandler);
this.native.on('mousemove', mouseMoveHandler);
this._drawState = { cleanup: cleanup, coords: coords };
var container = this.native.getContainer();
if (container) container.classList.add('bemap-drawing-active');
return new bemap.Listener();
};
bemap.MapLibreMap.prototype.cancelDraw = function() {
if (this._drawState) {
if (this._drawState.cleanup) this._drawState.cleanup();
else {
if (this._drawState.clickHandler) this.native.off('click', this._drawState.clickHandler);
var container = this.native.getContainer();
if (container) container.classList.remove('bemap-drawing-active');
}
this._drawState = null;
}
return this;
};
bemap.MapLibreMap.prototype.editPolygon = function(polygon, callback) {
if (!polygon || !polygon.native) return new bemap.Listener();
var _this = this;
var MERGE_PX = 15;
// Store edit state on polygon for rebuild
polygon._editCallback = callback;
polygon._editMap = this;
this._rebuildVertexHandles(polygon);
// Click on polygon layer to insert vertex on nearest edge
var polyLayerId = polygon._maplibreFillLayerId || polygon._maplibreLayerId;
if (polyLayerId && this.native.getLayer(polyLayerId)) {
var insertHandler = function(e) {
_this._insertVertexOnEdge(polygon, e.lngLat);
};
this.native.on('click', polyLayerId, insertHandler);
polygon._insertHandler = insertHandler;
polygon._insertLayerId = polyLayerId;
}
return new bemap.Listener({ native: null, bemapObject: polygon, key: 'editPolygon' });
};
/**
* Rebuild all vertex handle markers for a polygon being edited.
*/
bemap.MapLibreMap.prototype._rebuildVertexHandles = function(polygon) {
var _this = this;
var coords = polygon.getCoordinates();
var MERGE_PX = 15;
var callback = polygon._editCallback;
// Remove old handles
if (polygon._vertexMarkers) {
for (var r = 0; r < polygon._vertexMarkers.length; r++) {
polygon._vertexMarkers[r].remove();
}
}
polygon._vertexMarkers = [];
for (var i = 0; i < coords.length; i++) {
(function(index) {
var el = document.createElement('div');
el.className = 'bemap-vertex-handle';
var vm = new maplibregl.Marker({ element: el, draggable: true })
.setLngLat([coords[index].getLon(), coords[index].getLat()])
.addTo(_this.native);
// Real-time drag — update polygon during drag
vm.on('drag', function() {
var ll = vm.getLngLat();
coords[index] = new bemap.Coordinate(ll.lng, ll.lat);
polygon.coords = coords;
_this._updatePolygonSource(polygon, coords);
});
// Dragend — merge check + callback
vm.on('dragend', function() {
var ll = vm.getLngLat();
var dropPoint = _this.native.project([ll.lng, ll.lat]);
// Merge: check distance to all other vertices
if (coords.length > 3) {
for (var j = 0; j < coords.length; j++) {
if (j === index) continue;
var otherPoint = _this.native.project([coords[j].getLon(), coords[j].getLat()]);
var dist = Math.sqrt(Math.pow(dropPoint.x - otherPoint.x, 2) + Math.pow(dropPoint.y - otherPoint.y, 2));
if (dist < MERGE_PX) {
// Merge: remove dragged vertex
coords.splice(index, 1);
polygon.coords = coords;
_this._updatePolygonSource(polygon, coords);
_this._rebuildVertexHandles(polygon);
if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
return;
}
}
}
polygon.coords = coords;
if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
});
// Right-click — delete vertex
el.addEventListener('contextmenu', function(domEvt) {
domEvt.preventDefault();
domEvt.stopPropagation();
if (coords.length <= 3) return; // min 3 vertices
coords.splice(index, 1);
polygon.coords = coords;
_this._updatePolygonSource(polygon, coords);
_this._rebuildVertexHandles(polygon);
if (callback) callback(new bemap.MapEvent({ map: _this, bemapObject: polygon }));
});
polygon._vertexMarkers.push(vm);
})(i);
}
};
/**
* Update polygon GeoJSON source from coordinates array.
*/
bemap.MapLibreMap.prototype._updatePolygonSource = function(polygon, coords) {
var lngLats = coords.map(function(c) { return [c.getLon(), c.getLat()]; });
var ring = lngLats.concat([lngLats[0]]);
var sourceId = polygon._maplibreSourceId;
if (sourceId && this.native.getSource(sourceId)) {
this.native.getSource(sourceId).setData({
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [ring] },
properties: polygon.properties || {}
});
}
};
/**
* Insert a vertex on the nearest polygon edge to the click point.
*/
bemap.MapLibreMap.prototype._insertVertexOnEdge = function(polygon, lngLat) {
var coords = polygon.getCoordinates();
var clickPx = this.native.project([lngLat.lng, lngLat.lat]);
var bestIdx = 0, bestDist = Infinity;
for (var i = 0; i < coords.length; i++) {
var a = this.native.project([coords[i].getLon(), coords[i].getLat()]);
var b = this.native.project([coords[(i + 1) % coords.length].getLon(), coords[(i + 1) % coords.length].getLat()]);
var d = this._distToSegmentPx(clickPx, a, b);
if (d < bestDist) { bestDist = d; bestIdx = i; }
}
// Only insert if click is reasonably close to an edge (within 20px)
if (bestDist > 20) return;
var newCoord = new bemap.Coordinate(lngLat.lng, lngLat.lat);
coords.splice(bestIdx + 1, 0, newCoord);
polygon.coords = coords;
this._updatePolygonSource(polygon, coords);
this._rebuildVertexHandles(polygon);
if (polygon._editCallback) polygon._editCallback(new bemap.MapEvent({ map: this, bemapObject: polygon }));
};
/**
* Distance from point P to segment AB (in pixels).
*/
bemap.MapLibreMap.prototype._distToSegmentPx = function(p, a, b) {
var dx = b.x - a.x, dy = b.y - a.y;
if (dx === 0 && dy === 0) return Math.sqrt(Math.pow(p.x - a.x, 2) + Math.pow(p.y - a.y, 2));
var t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy)));
var projX = a.x + t * dx, projY = a.y + t * dy;
return Math.sqrt(Math.pow(p.x - projX, 2) + Math.pow(p.y - projY, 2));
};