freealberta/map/app/public/js/cut-drawing.js

426 lines
13 KiB
JavaScript

/**
* Cut Drawing Module
* Handles polygon drawing functionality for cut creation
*/
class CutDrawing {
constructor(map) {
this.map = map;
this.vertices = [];
this.markers = [];
this.polyline = null;
this.polygon = null; // Add polygon preview
this.finalPolygon = null; // Final polygon after finishing
this.isDrawing = false;
this.onFinishCallback = null;
this.onCancelCallback = null;
this.currentColor = '#3388ff';
this.currentOpacity = 0.3;
}
startDrawing(onFinish, onCancel) {
if (this.isDrawing) {
this.cancelDrawing();
}
this.isDrawing = true;
this.onFinishCallback = onFinish;
this.onCancelCallback = onCancel;
this.vertices = [];
this.markers = [];
this.map.getContainer().style.cursor = 'crosshair';
this.map.on('click', this.onMapClick.bind(this));
this.map.doubleClickZoom.disable();
console.log('Cut drawing started - click to add points');
}
onMapClick(e) {
if (!this.isDrawing) return;
// Check if clicking on first vertex to close polygon
if (this.vertices.length >= 3) {
const firstVertex = this.vertices[0];
const clickPoint = this.map.latLngToContainerPoint(e.latlng);
const firstPoint = this.map.latLngToContainerPoint(firstVertex);
const distance = Math.sqrt(
Math.pow(clickPoint.x - firstPoint.x, 2) +
Math.pow(clickPoint.y - firstPoint.y, 2)
);
// If clicked within 15 pixels of first vertex, close the polygon
if (distance < 15) {
console.log('Closing polygon by clicking first vertex');
this.finishDrawing();
return;
}
}
// Add vertex marker with special styling for first vertex
const isFirstVertex = this.vertices.length === 0;
const marker = L.marker(e.latlng, {
icon: L.divIcon({
className: 'cut-vertex-marker' + (isFirstVertex ? ' first-vertex' : ''),
html: `<div class="vertex-point${isFirstVertex ? ' first' : ''}"></div>`,
iconSize: [12, 12],
iconAnchor: [6, 6]
}),
draggable: false
}).addTo(this.map);
// Add tooltip to first vertex after 3 points
if (isFirstVertex) {
marker.bindTooltip('Click to close polygon', {
permanent: false,
direction: 'top',
offset: [0, -10]
});
}
this.vertices.push(e.latlng);
this.markers.push(marker);
this.updatePolyline();
// Show tooltip on first vertex when we have enough points
if (this.vertices.length === 3 && this.markers[0]) {
this.markers[0].openTooltip();
}
// Call update callback if available
if (this.onUpdate) {
this.onUpdate();
}
console.log(`Added vertex ${this.vertices.length} at`, e.latlng);
}
updatePolyline() {
// Use stored color and opacity values
const color = this.currentColor;
const opacity = this.currentOpacity;
if (this.polyline) {
this.map.removeLayer(this.polyline);
}
if (this.polygon) {
this.map.removeLayer(this.polygon);
this.polygon = null;
}
if (this.vertices.length > 1) {
// Show polyline for incomplete polygon
this.polyline = L.polyline(this.vertices, {
color: color,
weight: 2,
dashArray: '5, 5',
opacity: 1.0 // Keep polyline stroke visible
}).addTo(this.map);
// Show preview polygon if we have 3+ vertices
if (this.vertices.length >= 3) {
this.polygon = L.polygon(this.vertices, {
color: color,
fillColor: color,
fillOpacity: opacity,
weight: 2,
opacity: 0.8,
dashArray: '5, 5'
}).addTo(this.map);
// Add cut-polygon class and force inline style
if (this.polygon._path) {
this.polygon._path.classList.add('cut-polygon');
// Use setProperty with important flag for stronger override
this.polygon._path.style.setProperty('fill-opacity', opacity, 'important');
this.polygon._path.style.setProperty('fill', color, 'important');
console.log(`Created/updated polygon with opacity: ${opacity}`);
}
console.log('Created/updated polygon with opacity:', opacity);
}
}
}
finishDrawing() {
console.log('finishDrawing() called');
console.log('isDrawing:', this.isDrawing);
console.log('vertices count:', this.vertices.length);
if (!this.isDrawing) {
console.log('Not in drawing mode, returning null');
return null;
}
if (this.vertices.length < 3) {
alert('A cut must have at least 3 points. Click more points or cancel drawing.');
return null;
}
// Store vertices before cleanup
const finalVertices = [...this.vertices];
// Use stored color and opacity values
const color = this.currentColor;
const opacity = this.currentOpacity;
// Create polygon from vertices
const polygon = L.polygon(finalVertices, {
color: color,
fillColor: color,
fillOpacity: opacity,
weight: 2,
opacity: 1.0 // Keep stroke visible
});
// Get GeoJSON and bounds
const geojson = polygon.toGeoJSON().geometry; // Get just the geometry part
const bounds = polygon.getBounds();
// Clean up drawing elements but keep the final polygon
this.cleanupDrawingElements();
// Add the final polygon to the map
this.finalPolygon = polygon.addTo(this.map);
// Call finish callback with the data
console.log('Calling finish callbacks...');
const callbackData = {
geojson: JSON.stringify(geojson),
bounds: JSON.stringify([
[bounds.getSouth(), bounds.getWest()],
[bounds.getNorth(), bounds.getEast()]
]),
vertexCount: finalVertices.length,
polygon: this.finalPolygon
};
if (this.onFinishCallback) {
console.log('Calling onFinishCallback');
this.onFinishCallback(callbackData);
} else {
console.log('No onFinishCallback set');
}
// Also call the onFinish callback if set
if (this.onFinish) {
console.log('Calling onFinish');
this.onFinish(callbackData);
} else {
console.log('No onFinish callback set');
}
console.log('Cut drawing finished with', finalVertices.length, 'vertices');
return geojson;
}
cancelDrawing() {
if (!this.isDrawing) return;
console.log('Cut drawing cancelled');
this.cleanup();
if (this.onCancelCallback) {
this.onCancelCallback();
}
// Also call the onCancel callback if set
if (this.onCancel) {
this.onCancel();
}
}
undoLastVertex() {
if (!this.isDrawing || this.vertices.length === 0) return;
this.vertices.pop();
const lastMarker = this.markers.pop();
if (lastMarker) {
this.map.removeLayer(lastMarker);
}
this.updatePolyline();
// Call update callback if available
if (this.onUpdate) {
this.onUpdate();
}
console.log('Removed last vertex, remaining:', this.vertices.length);
}
clearVertices() {
if (!this.isDrawing) return;
this.markers.forEach(marker => {
this.map.removeLayer(marker);
});
if (this.polyline) {
this.map.removeLayer(this.polyline);
this.polyline = null;
}
if (this.polygon) {
this.map.removeLayer(this.polygon);
this.polygon = null;
}
this.vertices = [];
this.markers = [];
// Call update callback if available
if (this.onUpdate) {
this.onUpdate();
}
console.log('Cleared all vertices');
}
cleanupDrawingElements() {
// Remove drawing elements but preserve final polygon
this.markers.forEach(marker => {
this.map.removeLayer(marker);
});
if (this.polyline) {
this.map.removeLayer(this.polyline);
}
if (this.polygon) {
this.map.removeLayer(this.polygon);
}
this.map.getContainer().style.cursor = '';
this.map.off('click', this.onMapClick);
this.map.doubleClickZoom.enable();
this.isDrawing = false;
this.vertices = [];
this.markers = [];
this.polyline = null;
this.polygon = null;
this.onFinishCallback = null;
this.onCancelCallback = null;
}
cleanup() {
this.markers.forEach(marker => {
this.map.removeLayer(marker);
});
if (this.polyline) {
this.map.removeLayer(this.polyline);
}
if (this.polygon) {
this.map.removeLayer(this.polygon);
}
if (this.finalPolygon) {
this.map.removeLayer(this.finalPolygon);
}
this.map.getContainer().style.cursor = '';
this.map.off('click', this.onMapClick);
this.map.doubleClickZoom.enable();
this.isDrawing = false;
this.vertices = [];
this.markers = [];
this.polyline = null;
this.polygon = null;
this.finalPolygon = null;
this.onFinishCallback = null;
this.onCancelCallback = null;
}
getState() {
return {
isDrawing: this.isDrawing,
vertexCount: this.vertices.length,
canFinish: this.vertices.length >= 3
};
}
// Add method to update current drawing style
updateDrawingStyle(color, opacity) {
this.currentColor = color;
this.currentOpacity = opacity;
console.log(`CutDrawing.updateDrawingStyle called with color: ${color}, opacity: ${opacity}`);
// Update polyline color
if (this.polyline) {
this.polyline.setStyle({ color: color });
}
// Update polygon if it exists
if (this.polygon) {
this.polygon.setStyle({
color: color,
fillColor: color,
fillOpacity: opacity,
opacity: 0.8 // Border opacity
});
// Force inline style update and browser reflow
if (this.polygon._path) {
// Use setProperty with important flag for stronger override
this.polygon._path.style.setProperty('fill-opacity', opacity, 'important');
this.polygon._path.style.setProperty('fill', color, 'important');
// Force multiple reflows to ensure update
void this.polygon._path.offsetHeight;
void this.polygon._path.offsetWidth;
// Force repaint by temporarily changing a property
const oldDisplay = this.polygon._path.style.display;
this.polygon._path.style.display = 'none';
void this.polygon._path.offsetHeight;
this.polygon._path.style.display = oldDisplay;
}
// Also try Leaflet's internal redraw
if (this.polygon._updatePath) {
this.polygon._updatePath();
}
console.log(`Updated active drawing polygon with opacity: ${opacity}`);
} else {
// If no polygon exists but we have vertices, force a complete redraw
console.log('No polygon exists, forcing complete redraw');
this.forceRedraw();
}
}
clearPreview() {
// Clear any preview polygons but not the final polygon
if (this.polygon) {
this.map.removeLayer(this.polygon);
this.polygon = null;
}
if (this.polyline && this.isDrawing) {
this.map.removeLayer(this.polyline);
this.polyline = null;
}
}
// Force a complete redraw with current style settings
forceRedraw() {
if (this.vertices.length > 1) {
console.log('Forcing complete redraw with vertices:', this.vertices.length);
this.updatePolyline();
}
}
}
// Export the class if using modules, otherwise it's global
if (typeof module !== 'undefined' && module.exports) {
module.exports = CutDrawing;
} else {
window.CutDrawing = CutDrawing;
}