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

534 lines
16 KiB
JavaScript

/**
* 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(`
<div class="cut-popup">
<h3>${normalizedCut.name}</h3>
${normalizedCut.description ? `<p>${normalizedCut.description}</p>` : ''}
${normalizedCut.category ? `<p><strong>Category:</strong> ${normalizedCut.category}</p>` : ''}
${normalizedCut.is_official ? '<span class="badge official">Official Cut</span>' : ''}
</div>
`);
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();