/** * Cut Manager Module * Handles cut CRUD operations and display functionality */ import { showStatus } from './utils.js'; export class CutManager { constructor() { this.cuts = []; this.currentCut = null; this.currentCutLayer = null; this.map = null; this.isInitialized = false; // Add support for multiple cuts this.displayedCuts = new Map(); // Track multiple displayed cuts this.cutLayers = new Map(); // Track cut layers by ID } /** * Initialize the cut manager */ async initialize(map) { this.map = map; this.isInitialized = true; // Load public cuts for display await this.loadPublicCuts(); console.log('Cut manager initialized'); } /** * Load all cuts (admin) or public cuts (users) */ async loadCuts(adminMode = false) { try { const endpoint = adminMode ? '/api/cuts' : '/api/cuts/public'; const response = await fetch(endpoint, { credentials: 'include' }); if (!response.ok) { throw new Error(`Failed to load cuts: ${response.statusText}`); } const data = await response.json(); this.cuts = data.list || []; console.log(`Loaded ${this.cuts.length} cuts`); return this.cuts; } catch (error) { console.error('Error loading cuts:', error); showStatus('Failed to load cuts', 'error'); return []; } } /** * Load public cuts for map display */ async loadPublicCuts() { return await this.loadCuts(false); } /** * Get single cut by ID */ async getCut(id) { try { const response = await fetch(`/api/cuts/${id}`, { credentials: 'include' }); if (!response.ok) { throw new Error(`Failed to load cut: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error loading cut:', error); showStatus('Failed to load cut', 'error'); return null; } } /** * Create new cut */ 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(); showStatus('Cut created successfully', 'success'); // Reload cuts await this.loadCuts(true); return result; } catch (error) { console.error('Error creating cut:', error); showStatus(error.message, 'error'); return null; } } /** * Update existing cut */ 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(); showStatus('Cut updated successfully', 'success'); // Reload cuts await this.loadCuts(true); return result; } catch (error) { console.error('Error updating cut:', error); showStatus(error.message, 'error'); return null; } } /** * Delete cut */ async deleteCut(id) { try { const response = await fetch(`/api/cuts/${id}`, { method: 'DELETE', credentials: 'include' }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `Failed to delete cut: ${response.statusText}`); } showStatus('Cut deleted successfully', 'success'); // If this was the currently displayed cut, hide it if (this.currentCut && this.currentCut.id === id) { this.hideCut(); } // Reload cuts await this.loadCuts(true); return true; } catch (error) { console.error('Error deleting cut:', error); showStatus(error.message, 'error'); return false; } } // ...existing code... displayCut(cutData, autoDisplayed = false) { if (!this.map) { console.error('Map not initialized'); return false; } // Normalize field names for consistent access const normalizedCut = { ...cutData, id: cutData.id || cutData.Id || cutData.ID, name: cutData.name || cutData.Name, description: cutData.description || cutData.Description, color: cutData.color || cutData.Color, opacity: cutData.opacity || cutData.Opacity, category: cutData.category || cutData.Category, geojson: cutData.geojson || cutData.GeoJSON || cutData['GeoJSON Data'], is_public: cutData.is_public || cutData['Public Visibility'], is_official: cutData.is_official || cutData['Official Cut'], autoDisplayed: autoDisplayed // Track if this was auto-displayed }; // Check if already displayed if (this.cutLayers.has(normalizedCut.id)) { console.log(`Cut already displayed: ${normalizedCut.name}`); return true; } if (!normalizedCut.geojson) { console.error('Cut has no GeoJSON data'); return false; } try { const geojsonData = typeof normalizedCut.geojson === 'string' ? JSON.parse(normalizedCut.geojson) : normalizedCut.geojson; // Parse opacity value - ensure it's a number between 0 and 1 let opacityValue = parseFloat(normalizedCut.opacity); // Validate opacity is within range if (isNaN(opacityValue) || opacityValue < 0 || opacityValue > 1) { opacityValue = 0.3; // Default fallback console.log(`Invalid opacity value (${normalizedCut.opacity}), using default: ${opacityValue}`); } const cutLayer = L.geoJSON(geojsonData, { style: { color: normalizedCut.color || '#3388ff', fillColor: normalizedCut.color || '#3388ff', fillOpacity: opacityValue, weight: 2, opacity: 0.8, // Stroke opacity - keeping this slightly transparent for better visibility className: 'cut-polygon' }, // Add onEachFeature to apply styles to each individual feature onEachFeature: function (feature, layer) { // Apply styles directly to the layer to ensure they override CSS if (layer.setStyle) { layer.setStyle({ fillOpacity: opacityValue, color: normalizedCut.color || '#3388ff', fillColor: normalizedCut.color || '#3388ff', weight: 2, opacity: 0.8 }); } // Add cut-polygon class to the path element if (layer._path) { layer._path.classList.add('cut-polygon'); } } }); // Add popup with cut info cutLayer.bindPopup(`

${normalizedCut.name}

${normalizedCut.description ? `

${normalizedCut.description}

` : ''} ${normalizedCut.category ? `

Category: ${normalizedCut.category}

` : ''} ${normalizedCut.is_official ? 'Official Cut' : ''}
`); cutLayer.addTo(this.map); // Ensure cut-polygon class is applied to all path elements after adding to map cutLayer.eachLayer(function(layer) { if (layer._path) { layer._path.classList.add('cut-polygon'); // Force the fill-opacity style to ensure it overrides CSS layer._path.style.fillOpacity = opacityValue; } }); // Store in both tracking systems this.cutLayers.set(normalizedCut.id, cutLayer); this.displayedCuts.set(normalizedCut.id, normalizedCut); // Update current cut reference (for legacy compatibility) this.currentCut = normalizedCut; this.currentCutLayer = cutLayer; console.log(`Displayed cut: ${normalizedCut.name} (ID: ${normalizedCut.id}) with opacity: ${opacityValue} (raw: ${normalizedCut.opacity})`); return true; } catch (error) { console.error('Error displaying cut:', error); return false; } } // ...existing code... hideCut() { this.hideAllCuts(); } /** * Hide specific cut by ID */ hideCutById(cutId) { // Try different ID formats to handle type mismatches let layer = this.cutLayers.get(cutId); let actualKey = cutId; if (!layer) { // Try as string const stringId = String(cutId); layer = this.cutLayers.get(stringId); if (layer) actualKey = stringId; } if (!layer) { // Try as number const numberId = Number(cutId); if (!isNaN(numberId)) { layer = this.cutLayers.get(numberId); if (layer) actualKey = numberId; } } if (layer && this.map) { this.map.removeLayer(layer); this.cutLayers.delete(actualKey); this.displayedCuts.delete(actualKey); console.log(`Successfully hidden cut ID: ${actualKey} (original: ${cutId})`); return true; } console.warn(`Failed to hide cut ID: ${cutId} - not found in layers`); return false; } /** * Hide all displayed cuts */ hideAllCuts() { // Hide all cuts using the new system Array.from(this.cutLayers.keys()).forEach(cutId => { this.hideCutById(cutId); }); // Legacy cleanup if (this.currentCutLayer && this.map) { this.map.removeLayer(this.currentCutLayer); this.currentCutLayer = null; this.currentCut = null; } console.log('All cuts hidden'); } /** * Toggle cut visibility */ toggleCut(cutData) { if (this.currentCut && this.currentCut.id === cutData.id) { this.hideCut(); return false; // Hidden } else { this.displayCut(cutData); return true; // Shown } } /** * Get currently displayed cut */ getCurrentCut() { return this.currentCut; } /** * Check if a cut is currently displayed */ isCutDisplayed(cutId) { // Try different ID types to handle string/number mismatches const hasInMap = this.displayedCuts.has(cutId); const hasInMapAsString = this.displayedCuts.has(String(cutId)); const hasInMapAsNumber = this.displayedCuts.has(Number(cutId)); const currentCutMatch = this.currentCut && this.currentCut.id === cutId; return hasInMap || hasInMapAsString || hasInMapAsNumber || currentCutMatch; } /** * Get all displayed cuts */ getDisplayedCuts() { return Array.from(this.displayedCuts.values()); } /** * Get all available cuts */ getCuts() { return this.cuts; } /** * Get cuts by category */ getCutsByCategory(category) { return this.cuts.filter(cut => { const cutCategory = cut.category || cut.Category || 'Other'; return cutCategory === category; }); } /** * Search cuts by name */ searchCuts(query) { if (!query) return this.cuts; const searchTerm = query.toLowerCase(); return this.cuts.filter(cut => { // Handle different possible field names const name = cut.name || cut.Name || ''; const description = cut.description || cut.Description || ''; return name.toLowerCase().includes(searchTerm) || description.toLowerCase().includes(searchTerm); }); } /** * Export cuts as JSON */ exportCuts(cutsToExport = null) { const cuts = cutsToExport || this.cuts; const exportData = { version: '1.0', timestamp: new Date().toISOString(), cuts: cuts.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 })) }; return JSON.stringify(exportData, null, 2); } /** * Validate cut data for import */ validateCutData(cutData) { const errors = []; if (!cutData.name || typeof cutData.name !== 'string') { errors.push('Name is required and must be a string'); } if (!cutData.geojson) { errors.push('GeoJSON data is required'); } else { try { const geojson = JSON.parse(cutData.geojson); if (!geojson.type || !['Polygon', 'MultiPolygon'].includes(geojson.type)) { errors.push('GeoJSON must be a Polygon or MultiPolygon'); } } catch (e) { errors.push('Invalid GeoJSON format'); } } if (cutData.opacity !== undefined) { const opacity = parseFloat(cutData.opacity); if (isNaN(opacity) || opacity < 0 || opacity > 1) { errors.push('Opacity must be a number between 0 and 1'); } } return errors; } /** * Get cut statistics */ getStatistics() { const stats = { total: this.cuts.length, public: this.cuts.filter(cut => { const isPublic = cut.is_public || cut['Public Visibility']; return isPublic === true || isPublic === 1 || isPublic === '1'; }).length, private: this.cuts.filter(cut => { const isPublic = cut.is_public || cut['Public Visibility']; return !(isPublic === true || isPublic === 1 || isPublic === '1'); }).length, official: this.cuts.filter(cut => { const isOfficial = cut.is_official || cut['Official Cut']; return isOfficial === true || isOfficial === 1 || isOfficial === '1'; }).length, byCategory: {} }; // Count by category this.cuts.forEach(cut => { const category = cut.category || cut.Category || 'Uncategorized'; stats.byCategory[category] = (stats.byCategory[category] || 0) + 1; }); return stats; } /** * Hide all displayed cuts */ /** * Get displayed cut data by ID */ getDisplayedCut(cutId) { return this.displayedCuts.get(cutId); } } // Create global instance export const cutManager = new CutManager();