/** * Admin Cuts Management Module * Handles cut creation, editing, and management in the admin panel */ // Cut Drawing Class (converted to regular JS) 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: `
`, 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(); } } } // Admin Cuts Manager Class class AdminCutsManager { constructor() { this.cutsMap = null; this.cutDrawing = null; this.currentCutId = null; this.allCuts = []; this.filteredCuts = []; this.currentCutLayer = null; this.isInitialized = false; // Add initialization flag // Bind event handler once to avoid issues with removing listeners this.boundHandleCutActionClick = this.handleCutActionClick.bind(this); } async initialize() { // Prevent double initialization if (this.isInitialized) { console.log('AdminCutsManager already initialized'); return; } console.log('Initializing admin cuts manager...'); // Initialize map first this.initializeMap(); // Initialize form first this.initializeFormState(); // Initialize cuts list element this.cutsList = document.getElementById('cuts-list'); // Initialize drawing this.initializeDrawing(); // Load existing cuts await this.loadCuts(); // Set initialized flag BEFORE logging to prevent re-entry this.isInitialized = true; console.log('Admin cuts manager initialized'); } initializeFormState() { console.log('Initializing form state...'); // Set up form elements this.form = document.getElementById('cut-form'); if (!this.form) { console.error('Cut form not found'); return; } // Keep form enabled at all times - users should be able to fill properties anytime // Only disable the save button until we have geometry const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = true; } // Set up form submission this.form.addEventListener('submit', (e) => this.handleFormSubmit(e)); // NOTE: Opacity event listener is set up in setupFormControls() to avoid conflicts // Set up other form controls this.setupFormControls(); // Set up toolbar controls for drawing this.setupToolbarControls(); console.log('Form state initialized - form inputs enabled, save button disabled until geometry complete'); } // Add method to update preview style when color/opacity changes updatePreviewStyle() { // Simplified update method - uses toolbar values console.log('Note: updatePreviewStyle() called but now using toolbar controls'); // This method is no longer needed since toolbar controls update drawing style directly } // Method that accepts direct values to avoid DOM reading issues (kept for compatibility) updatePreviewStyleWithValues(colorOverride = null, opacityOverride = null) { const color = colorOverride || this.getCurrentColor(); const opacity = opacityOverride !== null ? opacityOverride : this.getCurrentOpacity(); console.log('updatePreviewStyleWithValues called with:', color, opacity); // Update drawing style if drawing is active if (this.cutDrawing) { this.cutDrawing.updateDrawingStyle(color, opacity); } } // Method to apply styles to all relevant layers applyStyleToLayers(color, opacity) { console.log('applyStyleToLayers called with color:', color, 'opacity:', opacity); // First, update the drawing tool's current style if drawing is active if (this.cutDrawing) { this.cutDrawing.updateDrawingStyle(color, opacity); } // Update any preview layer (GeoJSON) - this is the critical fix if (this.previewLayer) { this.updateLayerStyle(this.previewLayer, color, opacity); console.log('Preview GeoJSON layer style updated with opacity:', opacity); } // Update current cut layer if it exists - Enhanced handling if (this.currentCutLayer) { this.updateLayerStyle(this.currentCutLayer, color, opacity); console.log('Updated currentCutLayer with opacity:', opacity); } // If preview layer doesn't exist but we have drawing data, refresh the preview if (!this.previewLayer && this.currentDrawingData) { console.log('No preview layer found, refreshing preview with drawing data'); this.updateDrawingPreview(this.currentDrawingData); } } // New unified method to update any layer style updateLayerStyle(layer, color, opacity) { if (!layer) return; // Update layer options layer.options.fillOpacity = opacity; layer.options.fillColor = color; layer.options.color = color; // Apply new style layer.setStyle({ fillColor: color, color: color, fillOpacity: opacity, opacity: 0.8 // Border opacity }); // Force update on the path element(s) if (layer._path) { layer._path.style.setProperty('fill-opacity', opacity, 'important'); layer._path.style.setProperty('fill', color, 'important'); layer._path.style.setProperty('stroke', color, 'important'); // Force browser to recognize the change layer._path.style.opacity = ''; // Clear any overall opacity void layer._path.offsetHeight; // Force reflow } // Handle renderer sub-layers if (layer._renderer && layer._renderer._container) { const paths = layer._renderer._container.querySelectorAll('path'); paths.forEach(path => { path.style.setProperty('fill-opacity', opacity, 'important'); path.style.setProperty('fill', color, 'important'); path.style.setProperty('stroke', color, 'important'); // Force browser to recognize the change path.style.opacity = ''; // Clear any overall opacity void path.offsetHeight; // Force reflow }); } // Handle GeoJSON layers and layer groups if (layer.eachLayer) { // It's a layer group or GeoJSON - iterate through each feature layer.eachLayer((subLayer) => { if (subLayer.setStyle) { subLayer.setStyle({ color: color, fillColor: color, fillOpacity: opacity, opacity: 0.8 }); // Force inline styles on sub-layer paths if (subLayer._path) { subLayer._path.style.setProperty('fill-opacity', opacity, 'important'); subLayer._path.style.setProperty('fill', color, 'important'); subLayer._path.style.setProperty('stroke', color, 'important'); subLayer._path.style.opacity = ''; void subLayer._path.offsetHeight; } // Add CSS class to identify cut polygons const pathElement = subLayer.getElement(); if (pathElement) { pathElement.classList.add('cut-polygon'); pathElement.style.setProperty('fill-opacity', opacity, 'important'); console.log('Added cut-polygon class to sub-layer path element'); } // Force DOM update immediately this.forceLayerRedraw(subLayer); } }); // Also force a redraw of the entire layer group if (layer.redraw) { layer.redraw(); } } else if (layer.setStyle) { // It's a single layer (Leaflet Polygon) layer.setStyle({ fillColor: color, color: color, fillOpacity: opacity, opacity: 0.8 }); // Add CSS class to identify cut polygons const pathElement = layer.getElement(); if (pathElement) { pathElement.classList.add('cut-polygon'); console.log('Added cut-polygon class to single layer path element'); } // Force DOM update immediately this.forceLayerRedraw(layer); } } // New method to force layer redraw - addresses browser rendering issues forceLayerRedraw(layer) { if (layer._path) { // Direct SVG path manipulation for immediate visual update const path = layer._path; const targetOpacity = layer.options.fillOpacity; const targetColor = layer.options.fillColor || layer.options.color; console.log('forceLayerRedraw called:'); console.log(' - layer.options.fillOpacity:', targetOpacity); console.log(' - layer.options.fillColor:', targetColor); console.log(' - layer._path exists:', !!path); // Set the attribute directly on the SVG element path.setAttribute('fill-opacity', targetOpacity); path.setAttribute('fill', targetColor); // Also try setting as CSS style with important flag for better browser compatibility path.style.setProperty('fill-opacity', targetOpacity, 'important'); path.style.setProperty('fill', targetColor, 'important'); // Force browser reflow by temporarily changing a property const originalDisplay = path.style.display; path.style.display = 'none'; // Use requestAnimationFrame for better timing requestAnimationFrame(() => { path.style.display = originalDisplay; // Double-check the attribute was set const finalOpacity = path.getAttribute('fill-opacity'); const finalColor = path.getAttribute('fill'); const styleOpacity = path.style.fillOpacity; console.log('forceLayerRedraw completed:'); console.log(' - target opacity:', targetOpacity); console.log(' - target color:', targetColor); console.log(' - SVG attr opacity result:', finalOpacity); console.log(' - SVG attr color result:', finalColor); console.log(' - CSS style opacity result:', styleOpacity); // If attributes don't match, try one more time if (finalOpacity !== targetOpacity.toString()) { path.setAttribute('fill-opacity', targetOpacity); path.style.setProperty('fill-opacity', targetOpacity, 'important'); console.log(' - Re-applied fill-opacity attribute and style'); } }); } else { console.log('forceLayerRedraw: no _path found on layer'); } } setupFormControls() { // Set up start drawing button const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { // Remove any existing listeners first startDrawingBtn.removeEventListener('click', this.boundHandleStartDrawing); // Create bound method if it doesn't exist if (!this.boundHandleStartDrawing) { this.boundHandleStartDrawing = this.handleStartDrawing.bind(this); } startDrawingBtn.addEventListener('click', this.boundHandleStartDrawing); } // Set up reset form button const resetFormBtn = document.getElementById('reset-form-btn'); if (resetFormBtn) { resetFormBtn.addEventListener('click', () => this.resetForm()); } // Set up cancel edit button const cancelEditBtn = document.getElementById('cancel-edit-btn'); if (cancelEditBtn) { cancelEditBtn.addEventListener('click', () => this.cancelEdit()); } // Set up refresh cuts button const refreshCutsBtn = document.getElementById('refresh-cuts-btn'); if (refreshCutsBtn) { refreshCutsBtn.addEventListener('click', () => this.loadCuts()); } // Set up export button const exportCutsBtn = document.getElementById('export-cuts-btn'); if (exportCutsBtn) { exportCutsBtn.addEventListener('click', () => this.exportCuts()); } // Set up import file input const importCutsFile = document.getElementById('import-cuts-file'); if (importCutsFile) { importCutsFile.addEventListener('change', (e) => this.handleImport(e)); } // Set up search and filter const searchInput = document.getElementById('cuts-search'); if (searchInput) { searchInput.addEventListener('input', () => this.filterCuts()); } const categoryFilter = document.getElementById('cuts-category-filter'); if (categoryFilter) { categoryFilter.addEventListener('change', () => this.filterCuts()); } // NOTE: Color and opacity controls moved to toolbar for real-time feedback // Form-based color/opacity controls are no longer used // Add drawing toolbar button handlers const finishDrawingBtn = document.getElementById('finish-cut-btn'); // Fixed ID if (finishDrawingBtn) { finishDrawingBtn.addEventListener('click', () => { console.log('Finish button clicked'); if (this.cutDrawing) { console.log('Cut drawing exists, calling finishDrawing()'); console.log('Drawing state:', this.cutDrawing.getState()); this.cutDrawing.finishDrawing(); } else { console.error('Cut drawing not initialized'); } }); } else { console.error('Finish drawing button not found'); } const undoVertexBtn = document.getElementById('undo-vertex-btn'); if (undoVertexBtn) { undoVertexBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.undoLastVertex(); this.updateDrawingUI(); } }); } const clearVerticesBtn = document.getElementById('clear-vertices-btn'); if (clearVerticesBtn) { clearVerticesBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.clearVertices(); this.updateDrawingUI(); } }); } const cancelDrawingBtn = document.getElementById('cancel-cut-btn'); // Fixed ID if (cancelDrawingBtn) { cancelDrawingBtn.addEventListener('click', () => { if (this.cutDrawing) { this.cutDrawing.cancelDrawing(); } }); } } // Set up toolbar controls for real-time drawing feedback setupToolbarControls() { const colorPicker = document.getElementById('toolbar-color'); const opacitySlider = document.getElementById('toolbar-opacity'); const opacityDisplay = document.getElementById('toolbar-opacity-display'); console.log('Setting up toolbar controls...', { colorPicker: !!colorPicker, opacitySlider: !!opacitySlider, opacityDisplay: !!opacityDisplay }); if (colorPicker) { colorPicker.addEventListener('input', (e) => { const color = e.target.value; console.log('Toolbar color changed to:', color); // Update drawing style immediately if (this.cutDrawing) { const opacity = this.getCurrentOpacity(); console.log('Updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } }); } if (opacitySlider && opacityDisplay) { opacitySlider.addEventListener('input', (e) => { const opacity = parseFloat(e.target.value); const percentage = Math.round(opacity * 100); opacityDisplay.textContent = percentage + '%'; console.log('Toolbar opacity changed to:', opacity, 'percentage:', percentage); // Update drawing style immediately if (this.cutDrawing) { const color = this.getCurrentColor(); console.log('Updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } else { console.warn('cutDrawing instance not available'); } }); } console.log('Toolbar controls setup complete'); } // Helper methods to get current toolbar values getCurrentColor() { const colorPicker = document.getElementById('toolbar-color'); return colorPicker ? colorPicker.value : '#3388ff'; } getCurrentOpacity() { const opacitySlider = document.getElementById('toolbar-opacity'); return opacitySlider ? parseFloat(opacitySlider.value) : 0.3; } // Force update the drawing style - useful for debugging or manual refresh forceUpdateDrawingStyle() { if (this.cutDrawing) { const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('Force updating drawing style with color:', color, 'opacity:', opacity); this.cutDrawing.updateDrawingStyle(color, opacity); } else { console.warn('cutDrawing instance not available for force update'); } } // Sync toolbar display values with actual slider values syncToolbarDisplayValues() { const opacitySlider = document.getElementById('toolbar-opacity'); const opacityDisplay = document.getElementById('toolbar-opacity-display'); if (opacitySlider && opacityDisplay) { const opacity = parseFloat(opacitySlider.value); const percentage = Math.round(opacity * 100); opacityDisplay.textContent = percentage + '%'; console.log('Synced opacity display to:', percentage + '%'); } // Also sync any existing preview layers with current toolbar values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); if (this.previewLayer) { console.log('Syncing preview layer with toolbar values:', color, opacity); this.updateLayerStyle(this.previewLayer, color, opacity); } if (this.currentCutLayer) { console.log('Syncing current cut layer with toolbar values:', color, opacity); this.updateLayerStyle(this.currentCutLayer, color, opacity); } } // Remove the form disable/enable methods since we keep form enabled at all times updateDrawingUI() { if (!this.cutDrawing) return; const state = this.cutDrawing.getState(); const vertexCount = document.getElementById('vertex-count'); const finishBtn = document.getElementById('finish-cut-btn'); // Fixed ID const undoBtn = document.getElementById('undo-vertex-btn'); if (vertexCount) { vertexCount.textContent = `${state.vertexCount} points`; } if (finishBtn) { finishBtn.disabled = !state.canFinish; } if (undoBtn) { undoBtn.disabled = state.vertexCount === 0; } } async initializeMap() { const mapContainer = document.getElementById('cuts-map'); if (!mapContainer) { console.error('Cuts map container not found'); return; } // Check if map is already initialized if (this.cutsMap) { console.log('Cuts map already initialized'); return; } // Check if container already has a map instance if (mapContainer._leaflet_id) { console.log('Map container already has a Leaflet instance, cleaning up...'); // Try to find and remove existing map const existingMap = L.map.hasOwnProperty(mapContainer._leaflet_id) ? L.map[mapContainer._leaflet_id] : null; if (existingMap) { existingMap.remove(); } delete mapContainer._leaflet_id; } // Initialize map this.cutsMap = L.map('cuts-map').setView([53.5461, -113.4938], 11); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.cutsMap); console.log('Cuts map initialized'); } initializeDrawing() { this.cutDrawing = new CutDrawing(this.cutsMap); // Ensure clean state this.cutDrawing.isDrawing = false; // Bind drawing events this.cutDrawing.onFinish = (data) => this.handleDrawingFinished(data); this.cutDrawing.onCancel = () => this.handleDrawingCancelled(); this.cutDrawing.onUpdate = () => this.updateDrawingUI(); console.log('Drawing initialized, isDrawing:', this.cutDrawing.isDrawing); } handleStartDrawing() { console.log('handleStartDrawing called, current drawing state:', this.cutDrawing.isDrawing); // Prevent double-click issues by adding a small delay check if (this.handleStartDrawing._processing) { console.log('Already processing start drawing, ignoring...'); return; } this.handleStartDrawing._processing = true; try { if (this.cutDrawing.isDrawing) { console.log('Already drawing, canceling...'); this.cutDrawing.cancelDrawing(); return; } console.log('Starting new drawing...'); // Get current toolbar values instead of form values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('Starting new drawing with color:', color, 'opacity:', opacity); // Update the drawing tool with current style if (this.cutDrawing) { this.cutDrawing.currentColor = color; this.cutDrawing.currentOpacity = opacity; // Force update the drawing style immediately this.cutDrawing.updateDrawingStyle(color, opacity); } // Clear any existing preview layers if (this.previewLayer) { this.cutsMap.removeLayer(this.previewLayer); this.previewLayer = null; } // Clear any existing current cut layer if (this.currentCutLayer) { if (this.currentCutLayer.remove) { this.currentCutLayer.remove(); } else { this.cutsMap.removeLayer(this.currentCutLayer); } this.currentCutLayer = null; } // Clear form data this.currentDrawingData = null; document.getElementById('cut-geojson').value = ''; document.getElementById('cut-bounds').value = ''; // Clear any existing preview from the drawing tool if (this.cutDrawing && this.cutDrawing.clearPreview) { this.cutDrawing.clearPreview(); } // Update button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Cancel Drawing'; } this.cutDrawing.startDrawing( (data) => this.handleDrawingFinished(data), () => this.handleDrawingCancelled() ); // Show drawing toolbar const toolbar = document.getElementById('cut-drawing-toolbar'); if (toolbar) { toolbar.classList.add('active'); // Sync toolbar display values and apply to any existing layers this.syncToolbarDisplayValues(); } // Update UI this.updateDrawingUI(); } finally { // Clear the processing flag after a short delay setTimeout(() => { this.handleStartDrawing._processing = false; }, 100); } } handleDrawingFinished(drawingData) { console.log('handleDrawingFinished() called with data:', drawingData); // Store the drawing data this.currentDrawingData = drawingData; // Hide drawing toolbar document.getElementById('cut-drawing-toolbar').classList.remove('active'); // Store geojson and bounds in hidden form fields document.getElementById('cut-geojson').value = drawingData.geojson; document.getElementById('cut-bounds').value = drawingData.bounds; // Store the geojson in form dataset for form submission const form = document.getElementById('cut-form'); if (form) { form.dataset.geojson = drawingData.geojson; form.dataset.bounds = drawingData.bounds; } // Store the polygon reference for later use if (drawingData.polygon) { this.currentCutLayer = drawingData.polygon; } // Update form title const titleElement = document.getElementById('cut-form-title'); if (titleElement) { titleElement.textContent = 'Cut Properties - Ready to Save'; } // Enable the save button now that we have geometry const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = false; } // Update start drawing button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Redraw Polygon'; } // Update preview with the drawn polygon using current toolbar values this.updateDrawingPreview(drawingData); // Force immediate style update with current toolbar values const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('handleDrawingFinished: Applying toolbar values - color:', color, 'opacity:', opacity); // Apply to the preview layer immediately if (this.previewLayer) { this.updateLayerStyle(this.previewLayer, color, opacity); } // Also update the polygon from drawing data if it exists if (drawingData.polygon) { this.updateLayerStyle(drawingData.polygon, color, opacity); } this.showStatus('Cut drawing completed. Fill in the properties and save.', 'success'); } handleDrawingCancelled() { console.log('handleDrawingCancelled called'); const toolbar = document.getElementById('cut-drawing-toolbar'); if (toolbar) { toolbar.classList.remove('active'); } // Clear stored drawing data this.currentDrawingData = null; const geojsonField = document.getElementById('cut-geojson'); const boundsField = document.getElementById('cut-bounds'); if (geojsonField) geojsonField.value = ''; if (boundsField) boundsField.value = ''; // Clear form dataset const form = document.getElementById('cut-form'); if (form) { delete form.dataset.geojson; delete form.dataset.bounds; } const saveCutBtn = document.getElementById('save-cut-btn'); if (saveCutBtn) { saveCutBtn.disabled = true; } // Reset start drawing button text const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Start Drawing'; } console.log('Drawing cancelled, state reset'); this.showStatus('Cut drawing cancelled', 'info'); } reset() { console.log('Resetting cut form and drawing...'); // Reset the drawing if active if (this.cutDrawing) { this.cutDrawing.reset(); } // Reset form if (this.cutForm) { this.cutForm.reset(); // Set default values const colorInput = document.getElementById('cut-color'); const opacityInput = document.getElementById('cut-opacity'); if (colorInput) colorInput.value = '#3388ff'; if (opacityInput) opacityInput.value = '0.3'; this.updateColorValue(); this.updateOpacityValue(); } // Hide drawing toolbar const drawingToolbar = document.getElementById('cut-drawing-toolbar'); if (drawingToolbar) { drawingToolbar.classList.remove('active'); } // Reset buttons const startDrawingBtn = document.getElementById('start-drawing-btn'); if (startDrawingBtn) { startDrawingBtn.textContent = 'Start Drawing'; startDrawingBtn.classList.remove('danger'); } console.log('Cut form and drawing reset complete'); } async handleFormSubmit(event) { event.preventDefault(); console.log('Form submitted!'); const formData = new FormData(this.form); // Use this.form instead of this.cutForm console.log('Form data entries:'); for (let [key, value] of formData.entries()) { console.log(`${key}: ${value}`); } const cutData = { name: formData.get('name'), // Use the actual HTML name attributes description: formData.get('description'), color: this.getCurrentColor(), // Get from toolbar instead of form opacity: this.getCurrentOpacity(), // Get from toolbar instead of form category: formData.get('category'), is_public: formData.has('is_public'), is_official: formData.has('is_official') }; // Add the geojson and bounds from stored data if (this.currentDrawingData || event.target.dataset.geojson) { cutData.geojson = this.currentDrawingData?.geojson || event.target.dataset.geojson; cutData.bounds = this.currentDrawingData?.bounds || event.target.dataset.bounds; } else if (this.editingCutId) { // If editing and no new drawing, keep existing geometry const existingCut = this.allCuts.find(c => c.id === this.editingCutId); if (existingCut) { cutData.geojson = existingCut.geojson; cutData.bounds = existingCut.bounds; } } else { // Also try to get from hidden form fields as fallback cutData.geojson = formData.get('geojson'); cutData.bounds = formData.get('bounds'); } console.log('Cut data:', cutData); if (!cutData.name || !cutData.geojson) { this.showStatus('Name and geometry are required', 'error'); console.log('Validation failed - missing name or geojson'); return; } try { let result; if (this.editingCutId) { result = await this.updateCut(this.editingCutId, cutData); } else { result = await this.createCut(cutData); } if (result) { this.resetForm(); this.currentDrawingData = null; await this.loadCuts(); } } catch (error) { console.error('Error saving cut:', error); this.showStatus('Failed to save cut', 'error'); } } // Add a new method to update the drawing preview updateDrawingPreview(drawingData) { if (!drawingData || !drawingData.geojson) return; try { const geojson = JSON.parse(drawingData.geojson); // Remove any existing preview layer if (this.previewLayer) { this.cutsMap.removeLayer(this.previewLayer); this.previewLayer = null; } // Get current toolbar colors - this is the key fix const color = this.getCurrentColor(); const opacity = this.getCurrentOpacity(); console.log('updateDrawingPreview: Using toolbar values - color:', color, 'opacity:', opacity); // Create the GeoJSON layer with a static style object (not a function) // This allows setStyle() to work properly later this.previewLayer = L.geoJSON(geojson, { style: { color: color, fillColor: color, fillOpacity: opacity, weight: 2, opacity: 0.8, // Change border opacity to 0.8 for consistency className: 'cut-polygon', dashArray: '3, 3' } }).addTo(this.cutsMap); // Add the cut-polygon CSS class to the path element and force inline styles if (this.previewLayer._layers) { Object.values(this.previewLayer._layers).forEach(layer => { if (layer._path) { layer._path.classList.add('cut-polygon'); // Force the fill-opacity inline style with important flag layer._path.style.setProperty('fill-opacity', opacity, 'important'); layer._path.style.setProperty('fill', color, 'important'); } }); } // Also check if we need to access sub-layers if (this.previewLayer._renderer && this.previewLayer._renderer._container) { const paths = this.previewLayer._renderer._container.querySelectorAll('path'); paths.forEach(path => { path.classList.add('cut-polygon'); // Force the fill-opacity inline style on all paths with important flag path.style.setProperty('fill-opacity', opacity, 'important'); path.style.setProperty('fill', color, 'important'); }); } // Force initial style application using our unified method this.updateLayerStyle(this.previewLayer, color, opacity); console.log('Drawing preview updated with opacity:', opacity); // Fit map to bounds if available if (drawingData.bounds) { const bounds = JSON.parse(drawingData.bounds); this.cutsMap.fitBounds(bounds, { padding: [20, 20] }); } } catch (error) { console.error('Error updating drawing preview:', error); } } // Method to refresh preview with current drawing data and form values refreshPreview() { if (this.currentDrawingData) { console.log('Refreshing preview with current form values'); this.updateDrawingPreview(this.currentDrawingData); } } async createCut(cutData) { try { const response = await fetch('/api/cuts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(cutData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to create cut: ${response.statusText}`); } const result = await response.json(); this.showStatus('Cut created successfully', 'success'); return result; } catch (error) { console.error('Error creating cut:', error); this.showStatus(error.message, 'error'); return null; } } async updateCut(id, cutData) { try { const response = await fetch(`/api/cuts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(cutData) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to update cut: ${response.statusText}`); } const result = await response.json(); this.showStatus('Cut updated successfully', 'success'); return result; } catch (error) { console.error('Error updating cut:', error); this.showStatus(error.message, 'error'); return null; } } async loadCuts() { try { const response = await fetch('/api/cuts', { credentials: 'include' }); if (!response.ok) { const errorText = await response.text(); console.error('Failed to load cuts:', response.status, errorText); throw new Error(`Failed to load cuts: ${response.statusText}`); } const data = await response.json(); // Handle case where cuts table might not exist or be empty if (!data.list) { console.log('No cuts data returned, initializing empty list'); data.list = []; } this.allCuts = data.list || []; this.filteredCuts = [...this.allCuts]; this.renderCutsList(); } catch (error) { console.error('Error loading cuts:', error); this.showNotification('Failed to load cuts. Please check if the cuts table exists.', 'error'); // Initialize with empty array so the UI still works this.allCuts = []; this.filteredCuts = []; this.renderCutsList(); } } renderCutsList() { if (!this.cutsList) return; // Remove existing event listener to prevent duplicates this.cutsList.removeEventListener('click', this.boundHandleCutActionClick); if (this.filteredCuts.length === 0) { this.cutsList.innerHTML = 'No cuts found
'; return; } const html = this.filteredCuts.map(cut => this.renderCutItem(cut)).join(''); this.cutsList.innerHTML = html; // Add event delegation for cut action buttons this.cutsList.addEventListener('click', this.boundHandleCutActionClick); } renderCutItem(cut) { console.log('Rendering cut item:', cut); // Debug log to see the cut structure const badges = []; const isPublic = cut.is_public || cut.Is_public || cut['Public Visibility']; const isOfficial = cut.is_official || cut.Is_official || cut['Official Cut']; if (isPublic) badges.push('Public'); else badges.push('Private'); if (isOfficial) badges.push('Official'); // Check different possible ID field names const cutId = cut.id || cut.Id || cut.ID || cut._id; const cutName = cut.name || cut.Name || 'Unknown'; const cutDescription = cut.description || cut.Description || ''; const cutCategory = cut.category || cut.Category || 'Custom'; const cutCreatedAt = cut.CreatedAt || cut.created_at || new Date().toISOString(); console.log('Cut ID found:', cutId, 'from cut object keys:', Object.keys(cut)); return `