freealberta/map/app/public/js/admin-cuts.js

1991 lines
75 KiB
JavaScript

/**
* 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: `<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();
}
}
}
// 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 = '<p class="no-data">No cuts found</p>';
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('<span class="cut-item-badge public">Public</span>');
else badges.push('<span class="cut-item-badge private">Private</span>');
if (isOfficial) badges.push('<span class="cut-item-badge official">Official</span>');
// 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 `
<div class="cut-item" data-cut-id="${cutId}">
<div class="cut-item-header">
<span class="cut-item-name">${cutName}</span>
<span class="cut-item-category ${cutCategory.toLowerCase()}">${cutCategory}</span>
</div>
${cutDescription ? `<div class="cut-item-description">${cutDescription}</div>` : ''}
<div class="cut-item-meta">
<div class="cut-item-badges">${badges.join('')}</div>
<span class="cut-item-date">${new Date(cutCreatedAt).toLocaleDateString()}</span>
</div>
<div class="cut-item-actions">
<button data-action="view" data-cut-id="${cutId}" class="primary">View</button>
<button data-action="edit" data-cut-id="${cutId}">Edit</button>
<button data-action="duplicate" data-cut-id="${cutId}">Duplicate</button>
<button data-action="delete" data-cut-id="${cutId}" class="danger">Delete</button>
</div>
</div>
`;
}
handleCutActionClick(event) {
console.log('handleCutActionClick called', event);
const button = event.target;
console.log('Button:', button, 'Has data-action:', button.hasAttribute('data-action'));
if (!button.hasAttribute('data-action')) return;
event.preventDefault();
event.stopPropagation();
const action = button.getAttribute('data-action');
const cutId = button.getAttribute('data-cut-id');
console.log('Action:', action, 'Cut ID:', cutId);
if (!cutId) return;
switch (action) {
case 'view':
console.log('Calling viewCut');
this.viewCut(cutId);
break;
case 'edit':
console.log('Calling editCut');
this.editCut(cutId);
break;
case 'duplicate':
console.log('Calling duplicateCut');
this.duplicateCut(cutId);
break;
case 'delete':
console.log('Calling deleteCut');
this.deleteCut(cutId);
break;
default:
console.warn('Unknown cut action:', action);
}
}
async viewCut(cutId) {
console.log('viewCut called with ID:', cutId);
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
console.log('Found cut:', cut);
if (!cut) return;
this.displayCut(cut);
const cutName = cut.name || cut.Name || 'Unknown';
this.showStatus(`Viewing cut: ${cutName}`, 'info');
}
displayCut(cutData) {
if (this.currentCutLayer) {
this.cutsMap.removeLayer(this.currentCutLayer);
this.currentCutLayer = null;
}
if (!cutData) return false;
// Get geojson from different possible field names
const geojson = cutData.geojson || cutData.Geojson || cutData.GeoJSON || cutData['GeoJSON Data'];
if (!geojson) {
console.error('No geojson data found in cut:', cutData);
return false;
}
try {
const parsedGeojson = JSON.parse(geojson);
// Get color and opacity from different possible field names
const color = cutData.color || cutData.Color || '#3388ff';
const opacity = cutData.opacity || cutData.Opacity || 0.3;
console.log('displayCut: Using color:', color, 'opacity:', opacity);
// Create GeoJSON layer with static style object (not function) for proper setStyle() support
this.currentCutLayer = L.geoJSON(parsedGeojson, {
style: {
color: color,
fillColor: color,
fillOpacity: opacity,
weight: 2,
opacity: 1.0 // Keep stroke solid
}
});
this.currentCutLayer.addTo(this.cutsMap);
// Force apply the opacity using our enhanced styling approach
this.updateLayerStyle(this.currentCutLayer, color, opacity);
console.log('displayCut: Created currentCutLayer with opacity:', opacity);
// Get bounds from different possible field names
const bounds = cutData.bounds || cutData.Bounds;
if (bounds) {
try {
const parsedBounds = JSON.parse(bounds);
this.cutsMap.fitBounds(parsedBounds, { padding: [20, 20] });
} catch (boundsError) {
this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] });
}
} else {
this.cutsMap.fitBounds(this.currentCutLayer.getBounds(), { padding: [20, 20] });
}
return true;
} catch (error) {
console.error('Error displaying cut:', error);
this.showStatus('Failed to display cut', 'error');
return false;
}
}
async editCut(cutId) {
console.log('editCut called with ID:', cutId);
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
console.log('Found cut for editing:', cut);
if (!cut) return;
this.editingCutId = cutId;
// Use both lowercase and uppercase field names
document.getElementById('cut-name').value = cut.name || cut.Name || '';
document.getElementById('cut-description').value = cut.description || cut.Description || '';
// Set toolbar values (these are the ones we actually use now)
const cutColor = cut.color || cut.Color || '#3388ff';
const cutOpacity = cut.opacity || cut.Opacity || 0.3;
const toolbarColor = document.getElementById('toolbar-color');
const toolbarOpacity = document.getElementById('toolbar-opacity');
const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display');
if (toolbarColor) toolbarColor.value = cutColor;
if (toolbarOpacity) toolbarOpacity.value = cutOpacity;
if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = Math.round(cutOpacity * 100) + '%';
document.getElementById('cut-category').value = cut.category || cut.Category || 'Custom';
document.getElementById('cut-public').checked = cut.is_public || cut.Is_public || cut['Public Visibility'] || false;
document.getElementById('cut-official').checked = cut.is_official || cut.Is_official || cut['Official Cut'] || false;
document.getElementById('cut-geojson').value = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || '';
document.getElementById('cut-bounds').value = cut.bounds || cut.Bounds || '';
document.getElementById('cut-id').value = cut.id || cut.Id || cut.ID || cut._id;
// Store the existing geometry in form dataset
const form = document.getElementById('cut-form');
const geojsonData = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'];
const boundsData = cut.bounds || cut.Bounds;
if (form && geojsonData) {
form.dataset.geojson = geojsonData;
form.dataset.bounds = boundsData || '';
}
const cutName = cut.name || cut.Name || 'Unknown';
document.getElementById('cut-form-title').textContent = `Edit Cut: ${cutName}`;
document.getElementById('save-cut-btn').textContent = 'Update Cut';
document.getElementById('save-cut-btn').disabled = false;
document.getElementById('cancel-edit-btn').style.display = 'inline-block';
document.getElementById('start-drawing-btn').style.display = 'none';
this.updateColorValue();
this.updateOpacityValue();
this.displayCut(cut);
this.showStatus(`Editing cut: ${cutName}`, 'info');
}
async duplicateCut(cutId) {
console.log('duplicateCut called with ID:', cutId);
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
console.log('Found cut for duplication:', cut);
if (!cut) return;
// Use both lowercase and uppercase field names
const cutName = cut.name || cut.Name || 'Unknown';
const cutDescription = cut.description || cut.Description || '';
const cutColor = cut.color || cut.Color || '#3388ff';
const cutOpacity = cut.opacity || cut.Opacity || 0.3;
const cutCategory = cut.category || cut.Category || 'Custom';
const cutGeojson = cut.geojson || cut.Geojson || cut.GeoJSON || cut['GeoJSON Data'] || '';
const cutBounds = cut.bounds || cut.Bounds || '';
const duplicateData = {
name: `${cutName} (Copy)`,
description: cutDescription,
color: cutColor,
opacity: cutOpacity,
category: cutCategory,
is_public: false,
is_official: false,
geojson: cutGeojson,
bounds: cutBounds
};
console.log('Duplicate data:', duplicateData);
const result = await this.createCut(duplicateData);
if (result) {
await this.loadCuts();
this.showStatus(`Duplicated cut: ${cutName}`, 'success');
}
}
async deleteCut(cutId) {
console.log('deleteCut called with ID:', cutId);
const cut = this.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == cutId);
console.log('Found cut for deletion:', cut);
if (!cut) return;
const cutName = cut.name || cut.Name || 'Unknown';
if (!confirm(`Are you sure you want to delete the cut "${cutName}"? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/api/cuts/${cutId}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `Failed to delete cut: ${response.statusText}`);
}
this.showStatus('Cut deleted successfully', 'success');
if (this.currentCutLayer) {
this.cutsMap.removeLayer(this.currentCutLayer);
this.currentCutLayer = null;
}
await this.loadCuts();
} catch (error) {
console.error('Error deleting cut:', error);
this.showStatus(error.message, 'error');
}
}
cancelEdit() {
this.resetForm();
// Hide the cancel button
const cancelBtn = document.getElementById('cancel-edit-btn');
if (cancelBtn) {
cancelBtn.style.display = 'none';
}
}
resetForm() {
this.form.reset();
document.getElementById('cut-id').value = '';
document.getElementById('cut-geojson').value = '';
document.getElementById('cut-bounds').value = '';
// Reset toolbar values to defaults
const toolbarColor = document.getElementById('toolbar-color');
const toolbarOpacity = document.getElementById('toolbar-opacity');
const toolbarOpacityDisplay = document.getElementById('toolbar-opacity-display');
if (toolbarColor) toolbarColor.value = '#3388ff';
if (toolbarOpacity) toolbarOpacity.value = '0.3';
if (toolbarOpacityDisplay) toolbarOpacityDisplay.textContent = '30%';
// Update UI
document.getElementById('cut-form-title').textContent = 'Cut Properties';
document.getElementById('cancel-edit-btn').style.display = 'none';
// Only disable save button (form inputs stay enabled)
const saveCutBtn = document.getElementById('save-cut-btn');
if (saveCutBtn) {
saveCutBtn.disabled = true;
}
// Clear current cut
this.currentCutId = null;
// Clear any preview
if (this.cutDrawing) {
this.cutDrawing.clearPreview();
}
}
updateColorValue() {
const colorInput = document.getElementById('cut-color');
const colorValue = document.getElementById('cut-color-text');
if (colorInput && colorValue) {
colorValue.value = colorInput.value;
}
}
updateOpacityValue() {
const opacityInput = document.getElementById('cut-opacity');
const opacityValue = document.getElementById('opacity-value');
if (opacityInput && opacityValue) {
const percentage = Math.round(opacityInput.value * 100);
opacityValue.textContent = `${percentage}%`;
}
}
filterCuts() {
const searchTerm = document.getElementById('cuts-search').value.toLowerCase();
const categoryFilter = document.getElementById('cuts-category-filter').value;
let filteredCuts = this.allCuts;
if (searchTerm) {
filteredCuts = filteredCuts.filter(cut =>
cut.name.toLowerCase().includes(searchTerm) ||
(cut.description && cut.description.toLowerCase().includes(searchTerm))
);
}
if (categoryFilter) {
filteredCuts = filteredCuts.filter(cut => cut.category === categoryFilter);
}
this.filteredCuts = filteredCuts;
this.renderCutsList();
}
exportCuts() {
const exportData = {
version: '1.0',
timestamp: new Date().toISOString(),
cuts: this.allCuts.map(cut => ({
name: cut.name,
description: cut.description,
color: cut.color,
opacity: cut.opacity,
category: cut.category,
is_official: cut.is_official,
geojson: cut.geojson,
bounds: cut.bounds
}))
};
const data = JSON.stringify(exportData, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cuts-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showStatus('Cuts exported successfully', 'success');
}
async handleImportFile(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
if (!data.cuts || !Array.isArray(data.cuts)) {
throw new Error('Invalid import file format');
}
let successCount = 0;
let errorCount = 0;
for (const cutData of data.cuts) {
const result = await this.createCut(cutData);
if (result) {
successCount++;
} else {
errorCount++;
}
}
await this.loadCuts();
if (successCount > 0) {
this.showStatus(`Successfully imported ${successCount} cuts${errorCount > 0 ? `, ${errorCount} failed` : ''}`, 'success');
} else {
this.showStatus('No cuts were imported', 'error');
}
} catch (error) {
console.error('Import error:', error);
this.showStatus('Failed to import cuts: ' + error.message, 'error');
}
event.target.value = '';
}
// Debug method to check form state
debugFormState() {
console.log('=== Form State Debug ===');
const inputs = [
'cut-name', 'cut-description', 'cut-color',
'cut-opacity', 'cut-category', 'cut-public', 'cut-official', 'save-cut-btn'
];
inputs.forEach(id => {
const element = document.getElementById(id);
if (element) {
console.log(`${id}: disabled=${element.disabled}, value="${element.value || element.checked}"`);
} else {
console.log(`${id}: NOT FOUND`);
}
});
console.log(`currentDrawingData exists: ${!!this.currentDrawingData}`);
console.log(`previewLayer exists: ${!!this.previewLayer}`);
console.log('========================');
}
// Add a debug method to check layer opacity state specifically
debugOpacityState() {
const colorElement = document.getElementById('cut-color');
const opacityElement = document.getElementById('cut-opacity');
console.log('=== DEBUG: Opacity State ===');
console.log('Color value:', colorElement?.value);
console.log('Opacity value:', opacityElement?.value);
console.log('Opacity parsed:', parseFloat(opacityElement?.value));
if (this.previewLayer) {
console.log('Preview layer exists');
this.previewLayer.eachLayer((layer) => {
console.log('Layer options fillOpacity:', layer.options.fillOpacity);
if (layer._path) {
const svgOpacity = layer._path.getAttribute('fill-opacity');
const computedStyle = window.getComputedStyle(layer._path);
console.log('SVG fill-opacity attribute:', svgOpacity);
console.log('Computed fill-opacity style:', computedStyle.fillOpacity);
console.log('SVG fill color:', layer._path.getAttribute('fill'));
}
});
} else {
console.log('No preview layer found');
}
if (this.currentCutLayer) {
console.log('Current cut layer exists');
if (this.currentCutLayer.eachLayer) {
this.currentCutLayer.eachLayer((layer) => {
console.log('Current layer options fillOpacity:', layer.options.fillOpacity);
if (layer._path) {
console.log('Current SVG fill-opacity:', layer._path.getAttribute('fill-opacity'));
}
});
}
} else {
console.log('No current cut layer found');
}
console.log('========================');
}
showStatus(message, type) {
// Use existing admin notification system if available
if (typeof showNotification === 'function') {
showNotification(message, type);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
}
// Global instance
const adminCutsManager = new AdminCutsManager();
window.adminCutsManager = adminCutsManager;
// Expose debug function globally for easy access
window.debugCutsForm = () => adminCutsManager.debugFormState();
window.debugOpacity = () => adminCutsManager.debugOpacityState();
window.forceUpdateCutStyle = () => adminCutsManager.forceUpdateDrawingStyle();
window.syncToolbarValues = () => adminCutsManager.syncToolbarDisplayValues();
// Initialize when cuts section becomes visible
document.addEventListener('DOMContentLoaded', function() {
// Initialize immediately if cuts section is already visible
const cutsSection = document.getElementById('cuts');
if (cutsSection && cutsSection.style.display !== 'none') {
adminCutsManager.initialize().catch(error => {
console.error('Failed to initialize cuts manager:', error);
});
return;
}
// Otherwise, watch for it to become visible
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' &&
mutation.attributeName === 'style' &&
mutation.target.id === 'cuts' &&
mutation.target.style.display !== 'none' &&
!adminCutsManager.isInitialized) {
// Small delay to ensure DOM is fully rendered
setTimeout(() => {
adminCutsManager.initialize().catch(error => {
console.error('Failed to initialize cuts manager:', error);
});
}, 100);
}
});
});
if (cutsSection) {
observer.observe(cutsSection, { attributes: true });
}
});