loko/streetup/common/static/common/custom-draw.js
2026-07-22 14:48:40 +02:00

2039 lines
76 KiB
JavaScript

/**
* Custom Drawing Module for Maplibre GL
*
* A replacement for MapboxDraw that provides:
* - Point, Line, and Polygon drawing
* - Vertex snapping functionality
* - Single 'edit' mode for vertex editing and feature dragging
* - Compatible with Maplibre GL
* - Uses only turf.js for geometry operations
*
* @version 2.0.0
* @requires maplibre-gl
* @requires turf.js
*/
(function(window) {
'use strict';
// Constants
const SNAP_DISTANCE_PX = 15;
const SNAP_VERTEX_PRIORITY_DISTANCE = 0.0005; // ~50m in degrees
const VERTEX_CLICK_TOLERANCE_PX = 20; // Increased for touch support
const DRAW_LAYER_PREFIX = 'custom-draw-';
/**
* Main CustomDraw class
*/
class CustomDraw {
constructor(options = {}) {
this.options = {
displayControlsDefault: options.displayControlsDefault !== false,
controls: options.controls || {},
defaultMode: options.defaultMode || 'edit',
clickTolerance: options.clickTolerance || 2,
touchBuffer: options.touchBuffer || 0,
snap: options.snap !== false,
snapOptions: options.snapOptions || {},
styles: options.styles || this._getDefaultStyles(),
userProperties: options.userProperties !== false
};
this.map = null;
this.mode = this.options.defaultMode;
this.features = new Map(); // Store all features by ID
this.selectedFeatureIds = new Set();
this.currentFeature = null; // Feature being drawn
this.handlers = {}; // Event handlers
this.eventListeners = {}; // User event listeners
// Snapping configuration
this.snapOptions = {
snapPx: this.options.snapOptions.snapPx || SNAP_DISTANCE_PX,
snapToMidPoints: this.options.snapOptions.snapToMidPoints !== false,
snapToSegments: this.options.snapOptions.snapToSegments !== false,
snapVertexPriorityDistance: this.options.snapOptions.snapVertexPriorityDistance || SNAP_VERTEX_PRIORITY_DISTANCE,
snapIncludeMapFeatures: this.options.snapOptions.snapIncludeMapFeatures !== false,
snapGetFeatures: this.options.snapOptions.snapGetFeatures || null
};
// State for edit mode
this.draggedVertex = null;
this.draggedFeature = null; // For dragging entire features
this.dragStartPoint = null; // Starting point for feature drag
this.hoveredVertex = null;
this.isDragging = false;
this.isDraggingFeature = false; // True when dragging whole feature
this._lastDragEndTime = 0; // Timestamp when drag ended (to prevent click after drag)
// Snapping cache
this._snapFeaturesCache = null;
this._snapFeaturesCacheTime = 0;
this._snapFeaturesCacheTTL = 100; // Cache for 100ms
// Mode-specific snap setting (can override global snap)
this._modeSnap = this.options.snap;
// Double-click detection (fallback since dblclick event is unreliable)
this._lastClickTime = 0;
this._lastClickPoint = null;
this._doubleClickThreshold = 300; // ms
this._doubleClickDistanceThreshold = 10; // pixels
this._setupEventHandlers();
}
/**
* Add the draw control to a map
*/
onAdd(map) {
this.map = map;
this._addLayers();
this._attachMapHandlers();
this._updateLayers();
return document.createElement('div'); // Dummy container for IControl
}
/**
* Remove the draw control from the map
*/
onRemove() {
this._detachMapHandlers();
this._removeLayers();
this.map = null;
}
/**
* Get all features as a GeoJSON FeatureCollection
*/
getAll() {
return {
type: 'FeatureCollection',
features: Array.from(this.features.values()).map(f => ({
type: 'Feature',
id: f.id,
geometry: f.geometry,
properties: f.properties || {}
}))
};
}
/**
* Add a feature or features
*/
add(featureOrCollection) {
const features = featureOrCollection.type === 'FeatureCollection'
? featureOrCollection.features
: [featureOrCollection];
features.forEach(feature => {
const id = feature.id || this._generateId();
this.features.set(id, {
id: id,
type: 'Feature',
geometry: feature.geometry,
properties: feature.properties || {}
});
});
this._updateLayers();
return features.map(f => f.id);
}
/**
* Get a feature by ID
*/
get(id) {
return this.features.get(id) || null;
}
/**
* Get selected features
*/
getSelected() {
return {
type: 'FeatureCollection',
features: Array.from(this.selectedFeatureIds)
.map(id => this.features.get(id))
.filter(f => f)
};
}
/**
* Get selected feature IDs
*/
getSelectedIds() {
return Array.from(this.selectedFeatureIds);
}
/**
* Delete a feature or features by ID(s)
*/
delete(ids) {
const idArray = Array.isArray(ids) ? ids : [ids];
idArray.forEach(id => {
this.features.delete(id);
this.selectedFeatureIds.delete(id);
});
this._updateLayers();
return this;
}
/**
* Delete all features
*/
deleteAll() {
this.features.clear();
this.selectedFeatureIds.clear();
this._updateLayers();
return this;
}
/**
* Set features (replace all)
*/
set(featureCollection) {
this.features.clear();
this.selectedFeatureIds.clear();
this.add(featureCollection);
return this;
}
/**
* Change the drawing mode
*/
changeMode(mode, options = {}) {
const oldMode = this.mode;
this.mode = mode;
// Clear current drawing state
this.currentFeature = null;
this.draggedVertex = null;
this.isDragging = false;
// Store mode-specific snap option
if (options.snap !== undefined) {
this._modeSnap = options.snap;
} else {
// Use global snap setting as default
this._modeSnap = this.options.snap;
}
// Update snapOptions if provided in mode options
if (options.snapOptions) {
this.snapOptions = {
...this.snapOptions,
...options.snapOptions
};
}
// Handle mode-specific setup
// Map old mode names to new 'edit' mode for compatibility
if (mode === 'simple_select' || mode === 'direct_select') {
this.mode = 'edit';
}
if ((mode === 'edit' || mode === 'simple_select' || mode === 'direct_select') && options.featureIds) {
this.selectedFeatureIds.clear();
options.featureIds.forEach(id => this.selectedFeatureIds.add(id));
} else if (options.featureId) {
// Support direct_select style single featureId option
this.selectedFeatureIds.clear();
this.selectedFeatureIds.add(options.featureId);
} else {
// console.log('[CustomDraw.changeMode] No featureIds in options, selectedFeatureIds:', Array.from(this.selectedFeatureIds));
}
this._updateLayers();
this._updateMapCursor();
this._fireEvent('mode.change', { mode: mode });
this._fireEvent('draw.modechange', { mode: mode }); // Alias for compatibility
return this;
}
/**
* Get the current mode
*/
getMode() {
return this.mode;
}
/**
* Trash/delete selected features
*/
trash() {
const ids = Array.from(this.selectedFeatureIds);
if (ids.length > 0) {
this.delete(ids);
this._fireEvent('draw.delete', {
features: ids.map(id => this.features.get(id)).filter(f => f)
});
}
return this;
}
/**
* Add an event listener
*/
on(event, handler) {
if (!this.eventListeners[event]) {
this.eventListeners[event] = [];
}
this.eventListeners[event].push(handler);
return this;
}
/**
* Remove an event listener
*/
off(event, handler) {
if (!this.eventListeners[event]) return this;
if (handler) {
this.eventListeners[event] = this.eventListeners[event]
.filter(h => h !== handler);
} else {
this.eventListeners[event] = [];
}
return this;
}
// ========== Private Methods ==========
/**
* Fire an event to all listeners
*/
_fireEvent(eventName, data = {}) {
const listeners = this.eventListeners[eventName] || [];
listeners.forEach(handler => {
try {
handler(data);
} catch (err) {
console.error(`Error in ${eventName} handler:`, err);
}
});
// Also fire on the map for compatibility with existing code
if (this.map) {
this.map.fire(eventName, data);
}
}
/**
* Generate a unique ID for a feature
*/
_generateId() {
// More robust UUID-like generation to avoid collisions
const timestamp = Date.now().toString(36);
const randomPart = Math.random().toString(36).substr(2, 9);
const counterPart = (this._idCounter = (this._idCounter || 0) + 1).toString(36);
return 'feature_' + timestamp + '_' + randomPart + '_' + counterPart;
}
/**
* Setup event handler methods
*/
_setupEventHandlers() {
this.handlers.click = this._handleClick.bind(this);
this.handlers.mousemove = this._handleMouseMove.bind(this);
this.handlers.mousedown = this._handleMouseDown.bind(this);
this.handlers.mouseup = this._handleMouseUp.bind(this);
this.handlers.dblclick = this._handleDoubleClick.bind(this);
this.handlers.contextmenu = this._handleContextMenu.bind(this);
// Touch events are handled separately via canvas event listeners with passive: false
}
/**
* Attach map event handlers
*/
_attachMapHandlers() {
if (!this.map) return;
this.map.on('click', this.handlers.click);
this.map.on('mousemove', this.handlers.mousemove);
this.map.on('mousedown', this.handlers.mousedown);
this.map.on('mouseup', this.handlers.mouseup);
this.map.on('dblclick', this.handlers.dblclick);
this.map.on('contextmenu', this.handlers.contextmenu);
// Touch events - attach directly to canvas with passive: false to allow preventDefault
const canvas = this.map.getCanvas();
if (canvas) {
this._canvasTouchHandlers = {
touchstart: this._handleCanvasTouchStart.bind(this),
touchmove: this._handleCanvasTouchMove.bind(this),
touchend: this._handleCanvasTouchEnd.bind(this)
};
canvas.addEventListener('touchstart', this._canvasTouchHandlers.touchstart, { passive: false });
canvas.addEventListener('touchmove', this._canvasTouchHandlers.touchmove, { passive: false });
canvas.addEventListener('touchend', this._canvasTouchHandlers.touchend, { passive: false });
}
}
/**
* Detach map event handlers
*/
_detachMapHandlers() {
if (!this.map) return;
this.map.off('click', this.handlers.click);
this.map.off('mousemove', this.handlers.mousemove);
this.map.off('mousedown', this.handlers.mousedown);
this.map.off('mouseup', this.handlers.mouseup);
this.map.off('dblclick', this.handlers.dblclick);
this.map.off('contextmenu', this.handlers.contextmenu);
// Remove canvas touch handlers
const canvas = this.map.getCanvas();
if (canvas && this._canvasTouchHandlers) {
canvas.removeEventListener('touchstart', this._canvasTouchHandlers.touchstart);
canvas.removeEventListener('touchmove', this._canvasTouchHandlers.touchmove);
canvas.removeEventListener('touchend', this._canvasTouchHandlers.touchend);
}
}
/**
* Convert touch event to MapLibre-like event object
*/
_touchEventToMapEvent(e) {
if (!e.touches || e.touches.length === 0) {
// For touchend, use changedTouches
if (!e.changedTouches || e.changedTouches.length === 0) return null;
const touch = e.changedTouches[0];
const canvas = this.map.getCanvas();
const rect = canvas.getBoundingClientRect();
const point = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
const lngLat = this.map.unproject(point);
return { point, lngLat, originalEvent: e };
}
const touch = e.touches[0];
const canvas = this.map.getCanvas();
const rect = canvas.getBoundingClientRect();
const point = { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
const lngLat = this.map.unproject(point);
return { point, lngLat, originalEvent: e };
}
/**
* Handle touch start on canvas
*/
_handleCanvasTouchStart(e) {
const mapEvent = this._touchEventToMapEvent(e);
if (!mapEvent) return;
// Check if we're touching a vertex or feature in edit mode
if (this.mode === 'edit') {
// Log the vertices of selected features for debugging
for (const featureId of this.selectedFeatureIds) {
const feature = this.features.get(featureId);
if (feature && feature.geometry) {
if (feature.geometry.type === 'Polygon') {
const ring = feature.geometry.coordinates[0];
for (let i = 0; i < Math.min(3, ring.length); i++) {
const projected = this.map.project(ring[i]);
}
} else if (feature.geometry.type === 'MultiPolygon') {
const ring = feature.geometry.coordinates[0][0];
for (let i = 0; i < Math.min(3, ring.length); i++) {
const projected = this.map.project(ring[i]);
}
}
}
}
const vertex = this._getVertexAtPoint(mapEvent.point);
if (vertex) {
this.isDragging = true;
this.draggedVertex = vertex;
// Disable map drag pan to prevent interference
if (this.map.dragPan) this.map.dragPan.disable();
e.preventDefault(); // Prevent map panning
return;
}
const midpoint = this._getMidpointAtPoint(mapEvent.point);
if (midpoint) {
const newVertex = this._insertVertexAtMidpoint(midpoint);
if (newVertex) {
this.isDragging = true;
this.draggedVertex = newVertex;
// Disable map drag pan to prevent interference
if (this.map.dragPan) this.map.dragPan.disable();
e.preventDefault();
return;
}
}
const clickedFeatures = this._getFeatureAtPoint(mapEvent.point);
if (clickedFeatures.length > 0) {
// Don't start drag immediately - wait for movement
// Store potential drag info for touchmove to decide
this._potentialFeatureDrag = {
feature: clickedFeatures[0],
startPoint: mapEvent.lngLat,
startScreenPoint: mapEvent.point
};
// Don't call preventDefault here - allow click event for deselection
// e.preventDefault();
return;
}
}
}
/**
* Handle touch move on canvas
*/
_handleCanvasTouchMove(e) {
const mapEvent = this._touchEventToMapEvent(e);
if (!mapEvent) return;
// Check if we have a potential feature drag that should now become a real drag
if (this._potentialFeatureDrag && !this.isDraggingFeature) {
const dx = mapEvent.point.x - this._potentialFeatureDrag.startScreenPoint.x;
const dy = mapEvent.point.y - this._potentialFeatureDrag.startScreenPoint.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Only start drag if moved more than 5 pixels
if (distance > 5) {
this.isDraggingFeature = true;
this.draggedFeature = this._potentialFeatureDrag.feature;
this.dragStartPoint = this._potentialFeatureDrag.startPoint;
if (this.map.dragPan) this.map.dragPan.disable();
}
}
if (!this.isDragging && !this.isDraggingFeature) return;
e.preventDefault(); // Prevent map panning during drag
if (this.isDragging && this.draggedVertex) {
this._handleVertexDrag(mapEvent);
} else if (this.isDraggingFeature && this.draggedFeature) {
this._handleFeatureDrag(mapEvent);
}
}
/**
* Handle touch end on canvas
*/
_handleCanvasTouchEnd(e) {
// Re-enable map drag pan
if (this.map && this.map.dragPan) {
this.map.dragPan.enable();
}
// Check if this was a tap on a feature (potentialDrag that never became a real drag)
// If so, fire a custom event for deselection
if (this._potentialFeatureDrag && !this.isDraggingFeature) {
const tappedFeature = this._potentialFeatureDrag.feature;
this._potentialFeatureDrag = null;
// Fire a custom event that the toolbar can listen to
this._fireEvent('draw.featureTap', {
feature: tappedFeature,
featureId: tappedFeature.id
});
return;
}
// Clear potential drag
this._potentialFeatureDrag = null;
if (this.isDragging || this.isDraggingFeature) {
const mapEvent = this._touchEventToMapEvent(e);
this._handleEditMouseUp(mapEvent || { point: { x: 0, y: 0 }, lngLat: { lng: 0, lat: 0 } });
}
}
/**
* Handle click events
*/
_handleClick(e) {
const mode = this.mode;
const now = Date.now();
// Double-click detection for drawing modes
if (mode === 'draw_line_string' || mode === 'draw_polygon') {
const timeDiff = now - this._lastClickTime;
const isSamePoint = this._lastClickPoint &&
Math.abs(e.point.x - this._lastClickPoint.x) < this._doubleClickDistanceThreshold &&
Math.abs(e.point.y - this._lastClickPoint.y) < this._doubleClickDistanceThreshold;
if (timeDiff < this._doubleClickThreshold && isSamePoint) {
this._lastClickTime = 0;
this._lastClickPoint = null;
this._finishDrawing();
return;
}
this._lastClickTime = now;
this._lastClickPoint = { x: e.point.x, y: e.point.y };
}
// In edit mode, check if we clicked on a vertex or draw feature
// If not, let the event propagate to allow other layers to handle it
if (mode === 'edit') {
const vertex = this._getVertexAtPoint(e.point);
const clickedFeatures = this._getFeatureAtPoint(e.point);
if (!vertex && clickedFeatures.length === 0) {
// No vertex or draw feature clicked - let the event propagate
return;
}
// Don't handle click in edit mode - dragging is handled by mousedown/mouseup
return;
}
// For drawing modes, prevent default (if cancelable)
if (mode === 'draw_point' || mode === 'draw_line_string' || mode === 'draw_polygon') {
if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) {
e.preventDefault();
}
}
if (mode === 'draw_point') {
this._handleDrawPointClick(e);
} else if (mode === 'draw_line_string') {
this._handleDrawLineClick(e);
} else if (mode === 'draw_polygon') {
this._handleDrawPolygonClick(e);
}
}
/**
* Handle mouse move events
*/
_handleMouseMove(e) {
const mode = this.mode;
if (mode === 'draw_line_string' || mode === 'draw_polygon') {
this._updateCurrentFeatureLastVertex(e);
} else if (mode === 'edit') {
// Handle vertex dragging
if (this.isDragging && this.draggedVertex) {
this._handleVertexDrag(e);
}
// Handle feature dragging
else if (this.isDraggingFeature && this.draggedFeature) {
this._handleFeatureDrag(e);
}
}
this._updateMapCursor(e);
}
/**
* Handle mouse down events
*/
_handleMouseDown(e) {
if (this.mode === 'edit') {
this._handleEditMouseDown(e);
}
}
/**
* Handle mouse up events
*/
_handleMouseUp(e) {
if (this.mode === 'edit') {
this._handleEditMouseUp(e);
}
}
/**
* Handle edit mode mouse down - start vertex or feature dragging
*/
_handleEditMouseDown(e) {
// First check if we clicked on a vertex
const vertex = this._getVertexAtPoint(e.point);
if (vertex) {
this.isDragging = true;
this.draggedVertex = vertex;
this.map.getCanvas().style.cursor = 'grabbing';
// Prevent default map behavior (panning)
// Check if event is cancelable to avoid passive listener warnings
if (e.originalEvent && e.originalEvent.cancelable !== false) {
e.originalEvent.stopPropagation();
e.originalEvent.preventDefault();
}
if (e.preventDefault) e.preventDefault();
return;
}
// Check if we clicked on a midpoint (to add a new vertex)
const midpoint = this._getMidpointAtPoint(e.point);
if (midpoint) {
// Insert the new vertex into the feature
const newVertex = this._insertVertexAtMidpoint(midpoint);
if (newVertex) {
// Start dragging the newly created vertex
this.isDragging = true;
this.draggedVertex = newVertex;
this.map.getCanvas().style.cursor = 'grabbing';
// Prevent default map behavior (panning)
// Check if event is cancelable to avoid passive listener warnings
if (e.originalEvent && e.originalEvent.cancelable !== false) {
e.originalEvent.stopPropagation();
e.originalEvent.preventDefault();
}
if (e.preventDefault) e.preventDefault();
}
return;
}
// Check if we clicked on a feature (for dragging the whole feature)
const clickedFeatures = this._getFeatureAtPoint(e.point);
if (clickedFeatures.length > 0) {
const feature = clickedFeatures[0];
this.isDraggingFeature = true;
this.draggedFeature = feature;
this.dragStartPoint = e.lngLat;
this.map.getCanvas().style.cursor = 'grabbing';
// Prevent default map behavior (panning)
// Check if event is cancelable to avoid passive listener warnings
if (e.originalEvent && e.originalEvent.cancelable !== false) {
e.originalEvent.stopPropagation();
e.originalEvent.preventDefault();
}
if (e.preventDefault) e.preventDefault();
return;
}
}
/**
* Handle edit mode mouse up - end vertex or feature dragging
*/
_handleEditMouseUp(e) {
if (this.isDragging && this.draggedVertex) {
this.isDragging = false;
this._lastDragEndTime = Date.now(); // Track when drag ended
this._ignoringClickAfterDrag = true; // Flag to ignore next click event
const feature = this.features.get(this.draggedVertex.featureId);
if (feature) {
// Include synced features in the update event
const allModifiedFeatures = [feature];
if (this._lastSyncedFeatures && this._lastSyncedFeatures.length > 0) {
allModifiedFeatures.push(...this._lastSyncedFeatures);
}
this._fireEvent('draw.update', {
features: allModifiedFeatures,
action: 'move'
});
// Clear synced features after firing event
this._lastSyncedFeatures = null;
}
this.draggedVertex = null;
this.map.getCanvas().style.cursor = '';
}
if (this.isDraggingFeature && this.draggedFeature) {
this.isDraggingFeature = false;
this._lastDragEndTime = Date.now(); // Track when drag ended
this._ignoringClickAfterDrag = true; // Flag to ignore next click event
const feature = this.features.get(this.draggedFeature.id);
if (feature) {
this._fireEvent('draw.update', {
features: [feature],
action: 'move'
});
}
this.draggedFeature = null;
this.dragStartPoint = null;
this.map.getCanvas().style.cursor = '';
}
}
/**
* Handle feature dragging (move entire geometry)
*/
_handleFeatureDrag(e) {
if (!this.draggedFeature || !this.dragStartPoint) return;
const feature = this.features.get(this.draggedFeature.id);
if (!feature) return;
// Calculate the delta
const dx = e.lngLat.lng - this.dragStartPoint.lng;
const dy = e.lngLat.lat - this.dragStartPoint.lat;
// Update all coordinates by the delta
this._translateGeometry(feature.geometry, dx, dy);
// Update the start point for next move
this.dragStartPoint = e.lngLat;
this._updateLayers();
}
/**
* Translate all coordinates in a geometry by dx, dy
*/
_translateGeometry(geometry, dx, dy) {
if (geometry.type === 'Point') {
geometry.coordinates[0] += dx;
geometry.coordinates[1] += dy;
} else if (geometry.type === 'LineString') {
geometry.coordinates.forEach(coord => {
coord[0] += dx;
coord[1] += dy;
});
} else if (geometry.type === 'Polygon') {
geometry.coordinates.forEach(ring => {
ring.forEach(coord => {
coord[0] += dx;
coord[1] += dy;
});
});
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates.forEach(polygon => {
polygon.forEach(ring => {
ring.forEach(coord => {
coord[0] += dx;
coord[1] += dy;
});
});
});
}
}
/**
* Handle double click events
*/
_handleDoubleClick(e) {
if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) {
e.preventDefault();
}
if (this.mode === 'draw_line_string' || this.mode === 'draw_polygon') {
this._finishDrawing();
}
}
/**
* Handle right click / context menu
*/
_handleContextMenu(e) {
if (e.preventDefault && (!e.originalEvent || e.originalEvent.cancelable !== false)) {
e.preventDefault();
}
if (this.mode === 'draw_line_string' || this.mode === 'draw_polygon') {
this._finishDrawing();
}
}
/**
* Handle point drawing click
*/
_handleDrawPointClick(e) {
const coords = this._getSnappedCoordinates(e);
const feature = {
id: this._generateId(),
type: 'Feature',
geometry: {
type: 'Point',
coordinates: coords
},
properties: {}
};
this.features.set(feature.id, feature);
this._updateLayers();
this._fireEvent('draw.create', {
features: [feature]
});
// Stay in draw_point mode for multiple points
}
/**
* Handle line drawing click
*/
_handleDrawLineClick(e) {
const coords = this._getSnappedCoordinates(e);
if (!this.currentFeature) {
// Start new line
this.currentFeature = {
id: this._generateId(),
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [coords, coords] // Start with duplicate for preview
},
properties: {}
};
} else {
// If clicking near the last fixed point, finish the line
const coordinates = this.currentFeature.geometry.coordinates;
if (coordinates.length >= 3) {
const lastFixed = coordinates[coordinates.length - 2];
const lastPoint = this.map.project(lastFixed);
const distance = Math.sqrt(
Math.pow(e.point.x - lastPoint.x, 2) +
Math.pow(e.point.y - lastPoint.y, 2)
);
if (distance < VERTEX_CLICK_TOLERANCE_PX) {
this._finishDrawing();
return;
}
}
// Add point to line
coordinates[coordinates.length - 1] = coords;
coordinates.push(coords); // Add new preview point
}
this._updateLayers();
}
/**
* Handle polygon drawing click
*/
_handleDrawPolygonClick(e) {
const coords = this._getSnappedCoordinates(e);
if (!this.currentFeature) {
// Start new polygon
this.currentFeature = {
id: this._generateId(),
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [[coords, coords, coords, coords]] // Start with 4 duplicates for preview
},
properties: {}
};
} else {
// Add point to polygon
const ring = this.currentFeature.geometry.coordinates[0];
// Check if clicking near the first point to close the polygon
// We need at least 3 points (excluding preview) to close
if (ring.length >= 4) { // At least 3 real points + 1 preview
const firstPoint = ring[0];
const point = this.map.project([firstPoint[0], firstPoint[1]]);
const distance = Math.sqrt(
Math.pow(e.point.x - point.x, 2) +
Math.pow(e.point.y - point.y, 2)
);
// If close to first point (within 15px), finish the polygon
if (distance < 15) {
this._finishDrawing();
return;
}
}
ring[ring.length - 2] = coords; // Update the second-to-last point
ring.push(coords); // Add new preview point
}
this._updateLayers();
}
/**
* Handle vertex dragging
*/
_handleVertexDrag(e) {
if (!this.draggedVertex) return;
const coords = this._getSnappedCoordinates(e);
const feature = this.features.get(this.draggedVertex.featureId);
if (!feature) return;
// Get the OLD coordinates before updating (for shared vertex detection)
const { coordPath } = this.draggedVertex;
const oldCoords = this._getCoordinateAtPath(feature.geometry, coordPath);
// Update the vertex coordinates
this._setCoordinateAtPath(feature.geometry, coordPath, coords);
// For polygons, if we moved the first or last vertex, update both (they must be identical to close the polygon)
if (feature.geometry.type === 'Polygon' && coordPath.length === 3) {
const ringIdx = coordPath[1];
const vertexIdx = coordPath[2];
const ring = feature.geometry.coordinates[ringIdx];
// If it's the first vertex (index 0), also update the last vertex (index length-1)
if (vertexIdx === 0) {
ring[ring.length - 1] = [...coords];
}
// If it's the last vertex, also update the first vertex
else if (vertexIdx === ring.length - 1) {
ring[0] = [...coords];
}
}
// For MultiPolygon, coordPath is [coordinates, polyIdx, ringIdx, vertexIdx]
else if (feature.geometry.type === 'MultiPolygon' && coordPath.length === 4) {
const polyIdx = coordPath[1];
const ringIdx = coordPath[2];
const vertexIdx = coordPath[3];
const ring = feature.geometry.coordinates[polyIdx][ringIdx];
// If it's the first vertex (index 0), also update the last vertex (index length-1)
if (vertexIdx === 0) {
ring[ring.length - 1] = [...coords];
}
// If it's the last vertex, also update the first vertex
else if (vertexIdx === ring.length - 1) {
ring[0] = [...coords];
}
}
// Sync shared vertices across all selected features
if (oldCoords && this.selectedFeatureIds.size > 1) {
const syncedFeatures = this._syncSharedVertices(this.draggedVertex.featureId, oldCoords, coords);
// Store synced features for inclusion in draw.update event
this._lastSyncedFeatures = syncedFeatures;
}
this._updateLayers();
}
/**
* Sync shared vertices: when a vertex is moved, update matching vertices in other selected features
* Returns array of modified features (excluding source)
*/
_syncSharedVertices(sourceFeatureId, oldCoords, newCoords) {
const TOLERANCE = 1e-9; // Very tight tolerance for exact matches
const modifiedFeatures = [];
this.selectedFeatureIds.forEach(featureId => {
if (featureId === sourceFeatureId) return;
const feature = this.features.get(featureId);
if (!feature || feature.geometry.type !== 'Polygon') return;
let modified = false;
feature.geometry.coordinates.forEach((ring, ringIdx) => {
ring.forEach((vertex, vertexIdx) => {
// Check if this vertex matches the OLD position of the dragged vertex
if (Math.abs(vertex[0] - oldCoords[0]) <= TOLERANCE &&
Math.abs(vertex[1] - oldCoords[1]) <= TOLERANCE) {
// Update to new position
feature.geometry.coordinates[ringIdx][vertexIdx] = [...newCoords];
modified = true;
// If it's first or last vertex of a ring, update the corresponding one too
if (vertexIdx === 0) {
ring[ring.length - 1] = [...newCoords];
} else if (vertexIdx === ring.length - 1) {
ring[0] = [...newCoords];
}
}
});
});
if (modified) {
modifiedFeatures.push(feature);
}
});
return modifiedFeatures;
}
/**
* Get coordinate at a nested path
*/
_getCoordinateAtPath(geometry, path) {
let coords = geometry.coordinates;
for (let i = 1; i < path.length; i++) {
coords = coords[path[i]];
}
return coords ? [...coords] : null;
}
/**
* Update the last vertex of the current feature being drawn
*/
_updateCurrentFeatureLastVertex(e) {
if (!this.currentFeature) return;
const coords = this._getSnappedCoordinates(e);
const geometry = this.currentFeature.geometry;
if (geometry.type === 'LineString') {
const coordinates = geometry.coordinates;
coordinates[coordinates.length - 1] = coords;
} else if (geometry.type === 'Polygon') {
const ring = geometry.coordinates[0];
ring[ring.length - 1] = coords;
}
this._updateLayers();
}
/**
* Finish drawing the current feature
*/
_finishDrawing() {
if (!this.currentFeature) return;
const geometry = this.currentFeature.geometry;
let isValid = false;
if (geometry.type === 'LineString') {
// Remove the preview point
const coords = geometry.coordinates;
coords.pop();
// Line needs at least 2 points
isValid = coords.length >= 2;
} else if (geometry.type === 'Polygon') {
// Remove the preview point
const ring = geometry.coordinates[0];
ring.pop();
// Close the ring
if (ring.length >= 3) {
ring.push(ring[0]);
isValid = true;
}
}
if (isValid) {
this.features.set(this.currentFeature.id, this.currentFeature);
this._fireEvent('draw.create', {
features: [this.currentFeature]
});
} else {
}
this.currentFeature = null;
this._updateLayers();
}
/**
* Get coordinates with snapping applied
*/
_getSnappedCoordinates(e) {
const lngLat = e.lngLat;
let coords = [lngLat.lng, lngLat.lat];
// Check if snapping is enabled (either globally or for this mode)
const isSnapEnabled = this.options.snap || this._modeSnap;
if (!isSnapEnabled) {
return coords;
}
// Get features to snap to
const snapFeatures = this._getSnapFeatures();
if (snapFeatures.length === 0) {
return coords;
}
// Find nearest snap point
const snapPoint = this._findNearestSnapPoint(e.point, e.lngLat, snapFeatures);
if (snapPoint) {
return snapPoint.coordinates;
}
return coords;
}
/**
* Get features to snap to (with caching)
*/
_getSnapFeatures() {
// Use cache if still valid
const now = Date.now();
if (this._snapFeaturesCache && (now - this._snapFeaturesCacheTime) < this._snapFeaturesCacheTTL) {
return this._snapFeaturesCache;
}
let snapFeatures = [];
// Use custom snap function if provided
if (this.snapOptions.snapGetFeatures) {
try {
const customFeatures = this.snapOptions.snapGetFeatures(this.map, this, []);
snapFeatures = customFeatures;
} catch (err) {
console.error('Error getting snap features:', err);
}
}
// Include rendered map features (excluding draw layers)
if (this.map && this.snapOptions.snapIncludeMapFeatures) {
try {
const rendered = this.map.queryRenderedFeatures();
const drawSourceId = DRAW_LAYER_PREFIX + 'source';
rendered.forEach(mapFeature => {
const layerId = (mapFeature.layer && mapFeature.layer.id) ? mapFeature.layer.id : '';
if (layerId && layerId.startsWith(DRAW_LAYER_PREFIX)) return;
if (mapFeature.source && mapFeature.source === drawSourceId) return;
if (!mapFeature.geometry) return;
snapFeatures.push({
id: mapFeature.id || mapFeature.properties?.id || null,
type: 'Feature',
geometry: mapFeature.geometry,
properties: mapFeature.properties || {}
});
});
} catch (err) {
console.error('Error getting rendered map features for snapping:', err);
}
}
// Also include current drawing features from the draw control
const drawFeatures = Array.from(this.features.values());
snapFeatures = snapFeatures.concat(drawFeatures);
// Cache the result
this._snapFeaturesCache = snapFeatures;
this._snapFeaturesCacheTime = now;
return snapFeatures;
}
/**
* Find the nearest snap point
*/
_findNearestSnapPoint(screenPoint, lngLat, features) {
const snapPx = this.snapOptions.snapPx;
const vertexPriorityDistance = this.snapOptions.snapVertexPriorityDistance;
let nearestPoint = null;
let nearestDistance = Infinity;
let nearestVertex = null;
let nearestVertexDistance = Infinity;
let nearestVertexGeoDistance = Infinity;
let nearestSegment = null;
let nearestSegmentDistance = Infinity;
features.forEach(feature => {
if (!feature.geometry) return;
// Extract all coordinates from the feature
const coordinates = this._extractCoordinates(feature.geometry);
coordinates.forEach(coord => {
const point = this.map.project(coord);
const distance = Math.sqrt(
Math.pow(point.x - screenPoint.x, 2) +
Math.pow(point.y - screenPoint.y, 2)
);
if (distance < snapPx && distance < nearestVertexDistance) {
nearestVertexDistance = distance;
const geoDistance = lngLat
? Math.sqrt(
Math.pow(coord[0] - lngLat.lng, 2) +
Math.pow(coord[1] - lngLat.lat, 2)
)
: Infinity;
nearestVertexGeoDistance = geoDistance;
nearestVertex = {
coordinates: coord,
distance: distance,
geoDistance: geoDistance
};
}
});
// Also check midpoints if enabled
if (this.snapOptions.snapToMidPoints) {
const midpoints = this._getMidpoints(feature.geometry);
midpoints.forEach(coord => {
const point = this.map.project(coord);
const distance = Math.sqrt(
Math.pow(point.x - screenPoint.x, 2) +
Math.pow(point.y - screenPoint.y, 2)
);
if (distance < snapPx && distance < nearestDistance) {
nearestDistance = distance;
nearestPoint = {
coordinates: coord,
distance: distance
};
}
});
}
// Also check nearest point on segments if enabled
if (this.snapOptions.snapToSegments) {
const segmentPoint = this._findNearestPointOnSegments(screenPoint, feature.geometry);
if (segmentPoint && segmentPoint.distance < snapPx && segmentPoint.distance < nearestSegmentDistance) {
nearestSegmentDistance = segmentPoint.distance;
nearestSegment = {
coordinates: segmentPoint.coordinates,
distance: segmentPoint.distance
};
}
}
});
// Vertex priority within a tolerance (in degrees)
if (nearestVertex && vertexPriorityDistance !== null && vertexPriorityDistance !== undefined) {
if (nearestVertexGeoDistance <= vertexPriorityDistance) {
return nearestVertex;
}
}
// Otherwise, pick the closest between vertex/segment/midpoint candidates
nearestPoint = nearestPoint || nearestSegment || nearestVertex;
nearestDistance = nearestPoint ? nearestPoint.distance : Infinity;
if (nearestSegment && nearestSegment.distance < nearestDistance) {
nearestPoint = nearestSegment;
nearestDistance = nearestSegment.distance;
}
if (nearestVertex && nearestVertex.distance < nearestDistance) {
nearestPoint = nearestVertex;
}
return nearestPoint;
}
/**
* Find nearest point on any segment of a geometry in screen space
*/
_findNearestPointOnSegments(screenPoint, geometry) {
if (!this.map || !geometry) return null;
let nearest = null;
let nearestDistance = Infinity;
const checkSegment = (coordA, coordB) => {
const a = this.map.project(coordA);
const b = this.map.project(coordB);
const dx = b.x - a.x;
const dy = b.y - a.y;
const lengthSq = dx * dx + dy * dy;
if (lengthSq === 0) return;
let t = ((screenPoint.x - a.x) * dx + (screenPoint.y - a.y) * dy) / lengthSq;
t = Math.max(0, Math.min(1, t));
const closestScreen = {
x: a.x + t * dx,
y: a.y + t * dy
};
const distance = Math.sqrt(
Math.pow(closestScreen.x - screenPoint.x, 2) +
Math.pow(closestScreen.y - screenPoint.y, 2)
);
if (distance < nearestDistance) {
nearestDistance = distance;
const lngLat = this.map.unproject(closestScreen);
nearest = {
coordinates: [lngLat.lng, lngLat.lat],
distance: distance
};
}
};
const processLine = (coords) => {
for (let i = 0; i < coords.length - 1; i++) {
checkSegment(coords[i], coords[i + 1]);
}
};
if (geometry.type === 'LineString') {
processLine(geometry.coordinates);
} else if (geometry.type === 'Polygon') {
geometry.coordinates.forEach(ring => processLine(ring));
} else if (geometry.type === 'MultiLineString') {
geometry.coordinates.forEach(line => processLine(line));
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates.forEach(polygon => {
polygon.forEach(ring => processLine(ring));
});
}
return nearest;
}
/**
* Extract all coordinates from a geometry
*/
_extractCoordinates(geometry) {
const coords = [];
if (geometry.type === 'Point') {
coords.push(geometry.coordinates);
} else if (geometry.type === 'LineString') {
coords.push(...geometry.coordinates);
} else if (geometry.type === 'Polygon') {
geometry.coordinates.forEach(ring => {
coords.push(...ring);
});
} else if (geometry.type === 'MultiPoint') {
coords.push(...geometry.coordinates);
} else if (geometry.type === 'MultiLineString') {
geometry.coordinates.forEach(line => {
coords.push(...line);
});
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates.forEach(polygon => {
polygon.forEach(ring => {
coords.push(...ring);
});
});
}
return coords;
}
/**
* Get midpoints of line segments in a geometry
*/
_getMidpoints(geometry) {
const midpoints = [];
const processRing = (coords) => {
for (let i = 0; i < coords.length - 1; i++) {
const midpoint = [
(coords[i][0] + coords[i + 1][0]) / 2,
(coords[i][1] + coords[i + 1][1]) / 2
];
midpoints.push(midpoint);
}
};
if (geometry.type === 'LineString') {
processRing(geometry.coordinates);
} else if (geometry.type === 'Polygon') {
geometry.coordinates.forEach(ring => processRing(ring));
} else if (geometry.type === 'MultiLineString') {
geometry.coordinates.forEach(line => processRing(line));
} else if (geometry.type === 'MultiPolygon') {
geometry.coordinates.forEach(polygon => {
polygon.forEach(ring => processRing(ring));
});
}
return midpoints;
}
/**
* Get feature at a screen point
*/
_getFeatureAtPoint(point) {
if (!this.map) return [];
const tolerance = this.options.clickTolerance;
const bbox = [
[point.x - tolerance, point.y - tolerance],
[point.x + tolerance, point.y + tolerance]
];
const layers = this._getDrawLayerIds();
const mapFeatures = this.map.queryRenderedFeatures(bbox, { layers: layers });
// Map back to our feature objects
const features = [];
mapFeatures.forEach(mapFeature => {
const featureId = mapFeature.properties.id || mapFeature.id;
const feature = this.features.get(featureId);
if (feature) {
features.push(feature);
}
});
return features;
}
/**
* Get vertex at a screen point
*/
_getVertexAtPoint(point) {
if (!this.map) return null;
const tolerance = VERTEX_CLICK_TOLERANCE_PX;
// Check selected features for vertices
for (const featureId of this.selectedFeatureIds) {
const feature = this.features.get(featureId);
if (!feature) continue;
const vertex = this._findVertexNearPoint(feature, point, tolerance);
if (vertex) {
return vertex;
}
}
return null;
}
/**
* Get midpoint at a screen point
*/
_getMidpointAtPoint(point) {
if (!this.map) return null;
const tolerance = VERTEX_CLICK_TOLERANCE_PX;
// Check selected features for midpoints
for (const featureId of this.selectedFeatureIds) {
const feature = this.features.get(featureId);
if (!feature) continue;
if (feature.geometry.type !== 'Polygon' && feature.geometry.type !== 'MultiPolygon') continue;
const midpoint = this._findMidpointNearPoint(feature, point, tolerance);
if (midpoint) {
return midpoint;
}
}
return null;
}
/**
* Find a midpoint near a point
*/
_findMidpointNearPoint(feature, screenPoint, tolerance) {
const geometry = feature.geometry;
const checkRing = (ring, ringIdx, polyIdx = null) => {
for (let i = 0; i < ring.length - 1; i++) {
const coord1 = ring[i];
const coord2 = ring[i + 1];
// Calculate midpoint
const midCoord = [
(coord1[0] + coord2[0]) / 2,
(coord1[1] + coord2[1]) / 2
];
const point = this.map.project(midCoord);
const distance = Math.sqrt(
Math.pow(point.x - screenPoint.x, 2) +
Math.pow(point.y - screenPoint.y, 2)
);
if (distance < tolerance) {
return {
featureId: feature.id,
coordinates: midCoord,
polyIdx: polyIdx,
ringIdx: ringIdx,
insertAfterIdx: i,
distance: distance
};
}
}
return null;
};
if (geometry.type === 'Polygon') {
for (let ringIdx = 0; ringIdx < geometry.coordinates.length; ringIdx++) {
const result = checkRing(geometry.coordinates[ringIdx], ringIdx);
if (result) return result;
}
} else if (geometry.type === 'MultiPolygon') {
for (let polyIdx = 0; polyIdx < geometry.coordinates.length; polyIdx++) {
const polygon = geometry.coordinates[polyIdx];
for (let ringIdx = 0; ringIdx < polygon.length; ringIdx++) {
const result = checkRing(polygon[ringIdx], ringIdx, polyIdx);
if (result) return result;
}
}
}
return null;
}
/**
* Insert a vertex at a midpoint position
*/
_insertVertexAtMidpoint(midpoint) {
const feature = this.features.get(midpoint.featureId);
if (!feature) return null;
let ring;
let coordPath;
if (feature.geometry.type === 'Polygon') {
ring = feature.geometry.coordinates[midpoint.ringIdx];
coordPath = ['coordinates', midpoint.ringIdx, midpoint.insertAfterIdx + 1];
} else if (feature.geometry.type === 'MultiPolygon' && midpoint.polyIdx !== null) {
ring = feature.geometry.coordinates[midpoint.polyIdx][midpoint.ringIdx];
coordPath = ['coordinates', midpoint.polyIdx, midpoint.ringIdx, midpoint.insertAfterIdx + 1];
} else {
return null;
}
// Insert the new vertex after insertAfterIdx
ring.splice(midpoint.insertAfterIdx + 1, 0, midpoint.coordinates);
// Update layers
this._updateLayers();
// Return the new vertex info for dragging
return {
featureId: midpoint.featureId,
coordinates: midpoint.coordinates,
coordPath: coordPath
};
}
/**
* Find a vertex near a point
*/
_findVertexNearPoint(feature, screenPoint, tolerance) {
const geometry = feature.geometry;
const checkCoord = (coord, path) => {
const point = this.map.project(coord);
const distance = Math.sqrt(
Math.pow(point.x - screenPoint.x, 2) +
Math.pow(point.y - screenPoint.y, 2)
);
if (distance < tolerance) {
return {
featureId: feature.id,
coordinates: coord,
coordPath: path,
distance: distance
};
}
return null;
};
if (geometry.type === 'Point') {
return checkCoord(geometry.coordinates, ['coordinates']);
} else if (geometry.type === 'LineString') {
for (let i = 0; i < geometry.coordinates.length; i++) {
const vertex = checkCoord(geometry.coordinates[i], ['coordinates', i]);
if (vertex) return vertex;
}
} else if (geometry.type === 'Polygon') {
for (let ringIdx = 0; ringIdx < geometry.coordinates.length; ringIdx++) {
const ring = geometry.coordinates[ringIdx];
for (let i = 0; i < ring.length; i++) {
const vertex = checkCoord(ring[i], ['coordinates', ringIdx, i]);
if (vertex) return vertex;
}
}
} else if (geometry.type === 'MultiPolygon') {
for (let polyIdx = 0; polyIdx < geometry.coordinates.length; polyIdx++) {
const polygon = geometry.coordinates[polyIdx];
for (let ringIdx = 0; ringIdx < polygon.length; ringIdx++) {
const ring = polygon[ringIdx];
for (let i = 0; i < ring.length; i++) {
const vertex = checkCoord(ring[i], ['coordinates', polyIdx, ringIdx, i]);
if (vertex) return vertex;
}
}
}
}
return null;
}
/**
* Set coordinate at a nested path
*/
_setCoordinateAtPath(geometry, path, newCoord) {
let obj = geometry;
for (let i = 0; i < path.length - 1; i++) {
obj = obj[path[i]];
}
obj[path[path.length - 1]] = newCoord;
}
/**
* Update map cursor based on mode and hover state
*/
_updateMapCursor(e) {
if (!this.map) return;
const mode = this.mode;
if (mode === 'draw_point' || mode === 'draw_line_string' || mode === 'draw_polygon') {
this.map.getCanvas().style.cursor = 'crosshair';
} else if (mode === 'direct_select') {
// Check if hovering over a vertex
if (e) {
const vertex = this._getVertexAtPoint(e.point);
this.map.getCanvas().style.cursor = vertex ? 'move' : 'default';
} else {
this.map.getCanvas().style.cursor = 'default';
}
} else {
this.map.getCanvas().style.cursor = '';
}
}
/**
* Add map layers for rendering
*/
_addLayers() {
if (!this.map) return;
// Add source
this.map.addSource(DRAW_LAYER_PREFIX + 'source', {
type: 'geojson',
data: this._getSourceData()
});
// Add layers based on styles
const styles = this.options.styles;
styles.forEach(style => {
const layer = {
...style,
id: DRAW_LAYER_PREFIX + style.id,
source: DRAW_LAYER_PREFIX + 'source'
};
try {
this.map.addLayer(layer);
} catch (err) {
console.error('Error adding layer:', err);
}
});
}
/**
* Remove map layers
*/
_removeLayers() {
if (!this.map) return;
const styles = this.options.styles;
styles.forEach(style => {
const layerId = DRAW_LAYER_PREFIX + style.id;
if (this.map.getLayer(layerId)) {
this.map.removeLayer(layerId);
}
});
if (this.map.getSource(DRAW_LAYER_PREFIX + 'source')) {
this.map.removeSource(DRAW_LAYER_PREFIX + 'source');
}
}
/**
* Update map layers with current data
*/
_updateLayers() {
if (!this.map) return;
const source = this.map.getSource(DRAW_LAYER_PREFIX + 'source');
if (source) {
source.setData(this._getSourceData());
}
// Invalidate snap features cache when features change
this._snapFeaturesCache = null;
}
/**
* Get source data as GeoJSON
*/
_getSourceData() {
const features = [];
// Add all features
this.features.forEach(feature => {
const isActive = this.selectedFeatureIds.has(feature.id);
const isVertex = false;
features.push({
type: 'Feature',
id: feature.id,
geometry: feature.geometry,
properties: {
...feature.properties,
id: feature.id,
active: isActive ? 'true' : 'false',
meta: 'feature'
}
});
// Add vertices for selected features in edit mode (includes direct_select)
if (isActive && (this.mode === 'edit' || this.mode === 'direct_select')) {
this._addVertexFeatures(feature, features);
}
});
// Add current drawing feature if any
if (this.currentFeature) {
features.push({
type: 'Feature',
id: this.currentFeature.id,
geometry: this.currentFeature.geometry,
properties: {
id: this.currentFeature.id,
active: 'true',
meta: 'feature'
}
});
// Add vertices for the current drawing feature so user can see the points
if (this.mode === 'draw_polygon' || this.mode === 'draw_line_string') {
this._addVertexFeatures(this.currentFeature, features);
}
}
return {
type: 'FeatureCollection',
features: features
};
}
/**
* Add vertex features for a feature
*/
_addVertexFeatures(feature, features) {
const coordinates = this._extractCoordinates(feature.geometry);
// Add actual vertices
coordinates.forEach((coord, idx) => {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: coord
},
properties: {
meta: 'vertex',
parent: feature.id,
coord_path: idx
}
});
});
// Add midpoints for adding new vertices
const midpoints = this._getMidpoints(feature.geometry);
midpoints.forEach((coord, idx) => {
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: coord
},
properties: {
meta: 'midpoint',
parent: feature.id,
coord_path: idx
}
});
});
}
/**
* Get draw layer IDs
*/
_getDrawLayerIds() {
return this.options.styles
.filter(s => s.type === 'fill' || s.type === 'line' || s.type === 'circle')
.map(s => DRAW_LAYER_PREFIX + s.id);
}
/**
* Get default styles
*/
_getDefaultStyles() {
return [
// Polygon fill inactive
{
id: 'polygon-fill-inactive',
type: 'fill',
filter: ['all',
['==', ['get', 'active'], 'false'],
['==', ['geometry-type'], 'Polygon'],
['==', ['get', 'meta'], 'feature']
],
paint: {
'fill-color': '#3bb2d0',
'fill-opacity': 0.1
}
},
// Polygon fill active
{
id: 'polygon-fill-active',
type: 'fill',
filter: ['all',
['==', ['get', 'active'], 'true'],
['==', ['geometry-type'], 'Polygon'],
['==', ['get', 'meta'], 'feature']
],
paint: {
'fill-color': '#fbb03b',
'fill-opacity': 0.1
}
},
// Polygon outline inactive
{
id: 'polygon-stroke-inactive',
type: 'line',
filter: ['all',
['==', ['get', 'active'], 'false'],
['==', ['geometry-type'], 'Polygon'],
['==', ['get', 'meta'], 'feature']
],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2
}
},
// Polygon outline active
{
id: 'polygon-stroke-active',
type: 'line',
filter: ['all',
['==', ['get', 'active'], 'true'],
['==', ['geometry-type'], 'Polygon'],
['==', ['get', 'meta'], 'feature']
],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': '#fbb03b',
'line-width': 2
}
},
// Line inactive
{
id: 'line-inactive',
type: 'line',
filter: ['all',
['==', ['get', 'active'], 'false'],
['==', ['geometry-type'], 'LineString'],
['==', ['get', 'meta'], 'feature']
],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': '#3bb2d0',
'line-width': 2
}
},
// Line active
{
id: 'line-active',
type: 'line',
filter: ['all',
['==', ['get', 'active'], 'true'],
['==', ['geometry-type'], 'LineString'],
['==', ['get', 'meta'], 'feature']
],
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': '#fbb03b',
'line-width': 2
}
},
// Point inactive
{
id: 'point-inactive',
type: 'circle',
filter: ['all',
['==', ['get', 'active'], 'false'],
['==', ['geometry-type'], 'Point'],
['==', ['get', 'meta'], 'feature']
],
paint: {
'circle-radius': 5,
'circle-color': '#3bb2d0'
}
},
// Point active
{
id: 'point-active',
type: 'circle',
filter: ['all',
['==', ['get', 'active'], 'true'],
['==', ['geometry-type'], 'Point'],
['==', ['get', 'meta'], 'feature']
],
paint: {
'circle-radius': 7,
'circle-color': '#fbb03b'
}
},
// Vertex points
{
id: 'vertex-halo',
type: 'circle',
filter: ['all',
['==', ['get', 'meta'], 'vertex']
],
paint: {
'circle-radius': 5,
'circle-color': '#FFF'
}
},
{
id: 'vertex',
type: 'circle',
filter: ['all',
['==', ['get', 'meta'], 'vertex']
],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b'
}
},
// Midpoint points (for adding new vertices)
{
id: 'midpoint',
type: 'circle',
filter: ['all',
['==', ['get', 'meta'], 'midpoint']
],
paint: {
'circle-radius': 3,
'circle-color': '#fbb03b',
'circle-opacity': 0.6
}
}
];
}
}
// Export to global scope
window.CustomDraw = CustomDraw;
})(window);