freealberta/map/app/public/js/cut-print-utils.js

1053 lines
52 KiB
JavaScript

/**
* Cut Print Utilities Module
* Handles map capture, print generation, and export functionality for cuts
*/
class CutPrintUtils {
constructor(map, cutsManager, locationManager) {
this.map = map;
this.cutsManager = cutsManager;
this.locationManager = locationManager;
}
async printCutView() {
if (!this.locationManager.currentCutId) {
this.showStatus('No cut selected', 'warning');
return;
}
try {
// Get cut information
const cut = this.cutsManager.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.locationManager.currentCutId);
const cutName = cut?.name || cut?.Name || 'Unknown Cut';
// Ensure locations are loaded AND visible on the map before printing
this.showStatus('Preparing map for print (loading locations)...', 'info');
// Force load locations if not already loaded
if (!this.locationManager.currentCutLocations || this.locationManager.currentCutLocations.length === 0) {
try {
await this.locationManager.loadCutLocations();
} catch (loadError) {
console.log('Failed to load locations for print:', loadError);
this.locationManager.currentCutLocations = [];
}
}
// Ensure locations are actually displayed on the map
if (this.locationManager.currentCutLocations.length > 0 && !this.locationManager.showingLocations) {
console.log('Print: Displaying locations on map for capture...');
this.locationManager.displayLocationsOnMap(this.locationManager.currentCutLocations);
this.locationManager.showingLocations = true;
// Update the UI toggle button to reflect the new state
const toggleBtn = document.getElementById('toggle-location-visibility');
if (toggleBtn) {
toggleBtn.textContent = 'Hide Locations';
toggleBtn.classList.add('active');
toggleBtn.classList.remove('inactive');
}
// Give the map a moment to render the new markers
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('Print: Using', this.locationManager.currentCutLocations.length, 'locations');
console.log('Print: Locations showing on map:', this.locationManager.showingLocations);
// Debug: Check what's actually visible on the map
this.debugMapState();
// Try multiple methods to capture the map
let mapImageDataUrl = null;
let mapInfo = '';
try {
// Method 1: Try dom-to-image first (better for SVG coordinate preservation)
if (typeof domtoimage !== 'undefined') {
console.log('Attempting map capture with dom-to-image...');
mapImageDataUrl = await this.captureMapWithDomToImage();
if (mapImageDataUrl) {
console.log('✓ Successfully captured with dom-to-image');
}
}
if (!mapImageDataUrl) {
// Method 2: Try html2canvas if available (fallback for complex DOM)
if (typeof html2canvas !== 'undefined') {
console.log('Attempting map capture with html2canvas...');
mapImageDataUrl = await this.captureMapWithHtml2Canvas();
if (mapImageDataUrl) {
console.log('✓ Successfully captured with html2canvas');
}
}
}
if (!mapImageDataUrl) {
// Method 3: Try Leaflet's built-in screenshot capability using canvas
console.log('Attempting map capture with Leaflet canvas...');
mapImageDataUrl = await this.captureMapWithLeaflet();
if (mapImageDataUrl) {
console.log('✓ Successfully captured with Leaflet canvas');
}
}
if (!mapImageDataUrl) {
// Method 4: Try browser's built-in canvas capture
console.log('Attempting map capture with browser canvas...');
mapImageDataUrl = await this.captureMapWithCanvas();
if (mapImageDataUrl) {
console.log('✓ Successfully captured with browser canvas');
}
}
if (mapImageDataUrl) {
// We successfully captured the map
const locationCount = this.locationManager.currentCutLocations.length;
const showingLocations = this.locationManager.showingLocations;
console.log(`Successfully captured map with ${locationCount} locations (showing: ${showingLocations})`);
mapInfo = `
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Map View</h3>
<img src="${mapImageDataUrl}" style="max-width: 100%; border: 1px solid #ccc; border-radius: 3px;" alt="Cut Map">
<p style="font-style: italic; color: #6c757d; margin-top: 10px; font-size: 0.9em;">
Map showing cut boundaries and ${locationCount} location markers
</p>
</div>
`;
} else {
// Method 5: Generate a static map visualization as fallback
console.log('All capture methods failed, generating static map...');
const staticMap = await this.generateStaticMapImage();
if (staticMap) {
console.log('✓ Generated static map fallback');
mapInfo = staticMap;
} else {
// Final fallback to map information
console.log('Static map generation also failed');
throw new Error('All map capture methods failed');
}
}
} catch (mapError) {
console.log('Map capture failed, using fallback info:', mapError);
// Get map bounds and center for basic map info
const mapBounds = this.map.getBounds();
const center = this.map.getCenter();
const zoom = this.map.getZoom();
mapInfo = `
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Boundaries & Location Summary</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; text-align: left;">
<div>
<p><strong>Map Center:</strong><br>${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}</p>
<p><strong>Zoom Level:</strong> ${zoom}</p>
</div>
<div>
<p><strong>Geographic Bounds:</strong><br>
North: ${mapBounds.getNorth().toFixed(6)}<br>
South: ${mapBounds.getSouth().toFixed(6)}<br>
East: ${mapBounds.getEast().toFixed(6)}<br>
West: ${mapBounds.getWest().toFixed(6)}
</p>
</div>
<div>
<p><strong>Cut Statistics:</strong><br>
Total Locations: ${this.locationManager.currentCutLocations.length}<br>
Active Filters: ${this.locationManager.getActiveFiltersCount()}<br>
Showing: ${this.locationManager.currentCutLocations.length} locations
</p>
</div>
</div>
<p style="font-style: italic; color: #6c757d; margin-top: 15px; font-size: 0.9em;">
Individual location coordinates and details are listed in the table below.
</p>
</div>
`;
}
// Create print content with map info and location data
this.generatePrintContent(cutName, cut, mapInfo);
} catch (error) {
console.error('Error creating print view:', error);
this.showStatus('Failed to create print view', 'error');
// Fallback to simple print without map
this.printCutViewSimple();
}
}
async captureMapWithLeaflet() {
try {
// Simple approach: try to get the map's SVG or canvas content
const mapContainer = this.map.getContainer();
// Look for the leaflet pane that contains the actual map content
const mapPane = mapContainer.querySelector('.leaflet-map-pane');
if (!mapPane) {
console.log('Leaflet map pane not found');
return null;
}
// Try to find any canvas elements in the map
const canvases = mapPane.querySelectorAll('canvas');
if (canvases.length > 0) {
console.log('Found canvas elements, attempting to capture...');
const canvas = canvases[0];
try {
const dataUrl = canvas.toDataURL('image/png');
console.log('Successfully captured canvas');
return dataUrl;
} catch (canvasError) {
console.log('Canvas capture failed:', canvasError);
}
}
// Try SVG capture
const svgs = mapPane.querySelectorAll('svg');
if (svgs.length > 0) {
console.log('Found SVG elements, attempting to capture...');
try {
const svg = svgs[0];
const svgData = new XMLSerializer().serializeToString(svg);
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const svgUrl = URL.createObjectURL(svgBlob);
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#f8f9fa';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(svgUrl);
console.log('Successfully captured SVG');
resolve(canvas.toDataURL('image/png'));
};
img.onerror = () => {
URL.revokeObjectURL(svgUrl);
console.log('SVG image load failed');
resolve(null);
};
img.src = svgUrl;
});
} catch (svgError) {
console.log('SVG capture failed:', svgError);
}
}
console.log('No capturable elements found in Leaflet map');
return null;
} catch (error) {
console.log('Leaflet capture failed:', error);
return null;
}
}
async captureMapWithHtml2Canvas() {
try {
// Check if html2canvas is available
if (typeof html2canvas === 'undefined') {
console.log('html2canvas library not available');
return null;
}
const mapElement = this.map.getContainer();
console.log('Attempting to capture map with html2canvas...');
// Wait for any pending map animations/renders to complete
await new Promise(resolve => setTimeout(resolve, 200));
// Configure html2canvas options for better map capture
const options = {
allowTaint: false,
useCORS: true,
scale: 1, // Use scale 1 to avoid coordinate system shifts
width: mapElement.offsetWidth,
height: mapElement.offsetHeight,
backgroundColor: '#f8f9fa',
logging: false,
foreignObjectRendering: false, // Disable to avoid coordinate issues
removeContainer: true,
ignoreElements: (element) => {
// Skip zoom controls and attribution
if (element.classList) {
return element.classList.contains('leaflet-control-zoom') ||
element.classList.contains('leaflet-control-attribution');
}
return false;
}
};
const canvas = await html2canvas(mapElement, options);
const dataUrl = canvas.toDataURL('image/png', 0.95); // Slightly reduce quality for better performance
console.log('Map capture successful with html2canvas');
return dataUrl;
} catch (error) {
console.log('html2canvas capture failed:', error);
return null;
}
}
async captureMapWithDomToImage() {
try {
// Check if dom-to-image is available
if (typeof domtoimage === 'undefined') {
console.log('dom-to-image library not available');
return null;
}
const mapElement = this.map.getContainer();
// Ensure the map container has proper dimensions
const containerRect = mapElement.getBoundingClientRect();
const width = Math.max(800, containerRect.width);
const height = Math.max(600, containerRect.height);
console.log(`Capturing map with dimensions: ${width}x${height}`);
// Add some options to improve capture quality
const options = {
width: width,
height: height,
quality: 0.95,
bgcolor: '#f8f9fa',
style: {
transform: 'scale(1)',
transformOrigin: 'top left',
width: `${width}px`,
height: `${height}px`
},
filter: (node) => {
// Skip attribution and some problematic elements but keep markers and overlays
if (node.classList) {
// Skip zoom controls and attribution
if (node.classList.contains('leaflet-control-zoom') ||
node.classList.contains('leaflet-control-attribution')) {
return false;
}
// Keep marker layers, overlays, and the main map content
if (node.classList.contains('leaflet-marker-icon') ||
node.classList.contains('leaflet-marker-shadow') ||
node.classList.contains('leaflet-overlay-pane') ||
node.classList.contains('leaflet-map-pane') ||
node.classList.contains('leaflet-tile-pane') ||
node.classList.contains('location-marker')) {
return true;
}
}
return true;
},
// Use pixelRatio for better quality on high-DPI displays
pixelRatio: window.devicePixelRatio || 1
};
console.log('Attempting to capture map with dom-to-image...');
// Try to capture the map element
const dataUrl = await domtoimage.toPng(mapElement, options);
console.log('Map capture successful with dom-to-image');
return dataUrl;
} catch (error) {
console.log('Dom-to-image capture failed:', error);
return null;
}
}
async captureMapWithCanvas() {
try {
// Try the simple canvas approach for SVG/Canvas elements
const mapElement = this.map.getContainer();
// Check if the map container has a canvas child
const canvasElements = mapElement.querySelectorAll('canvas');
if (canvasElements.length > 0) {
// Use the first canvas
const sourceCanvas = canvasElements[0];
return sourceCanvas.toDataURL('image/png');
}
// Try to capture SVG elements
const svgElements = mapElement.querySelectorAll('svg');
if (svgElements.length > 0) {
const svg = svgElements[0];
const serializer = new XMLSerializer();
const svgData = serializer.serializeToString(svg);
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = svg.clientWidth || 800;
canvas.height = svg.clientHeight || 600;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
resolve(canvas.toDataURL('image/png'));
};
img.onerror = () => {
URL.revokeObjectURL(url);
resolve(null);
};
img.src = url;
});
}
return null;
} catch (error) {
console.log('Canvas capture failed:', error);
return null;
}
}
async generateStaticMapImage() {
try {
// Create a static map representation using CSS and HTML
const mapBounds = this.map.getBounds();
const center = this.map.getCenter();
const zoom = this.map.getZoom();
// Get current locations for this cut
const cutLocations = this.locationManager.currentCutLocations || [];
console.log('Static map: Using locations:', cutLocations.length);
// Calculate location positions within map bounds
const locationMarkers = cutLocations.map(location => {
const lat = parseFloat(location.latitude || location.Latitude || 0);
const lng = parseFloat(location.longitude || location.Longitude || 0);
console.log('Processing location:', location.first_name || location['First Name'], 'Coords:', lat, lng);
if (!lat || !lng) {
console.log('Invalid coordinates for location:', location);
return null;
}
// Convert to percentage positions within the map bounds
const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100;
const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100;
return {
...location,
latPercent: Math.max(2, Math.min(98, 100 - latPercent)), // Invert Y for screen coordinates
lngPercent: Math.max(2, Math.min(98, lngPercent))
};
}).filter(Boolean);
console.log('Static map: Calculated positions for', locationMarkers.length, 'locations');
// Get the cut geometry for visualization
let cutPath = '';
let cutPolygonPoints = '';
if (this.cutsManager.currentCutLayer && this.cutsManager.currentCutLayer.getLatLngs) {
try {
const latLngs = this.cutsManager.currentCutLayer.getLatLngs();
const coordinates = Array.isArray(latLngs[0]) ? latLngs[0] : latLngs;
// Convert coordinates to percentages for the map visualization
const pathPoints = coordinates.map(coord => {
const lat = coord.lat;
const lng = coord.lng;
const latPercent = ((lat - mapBounds.getSouth()) / (mapBounds.getNorth() - mapBounds.getSouth())) * 100;
const lngPercent = ((lng - mapBounds.getWest()) / (mapBounds.getEast() - mapBounds.getWest())) * 100;
// Apply same clamping as location markers for consistency
const clampedLatPercent = Math.max(0, Math.min(100, 100 - latPercent)); // Invert Y for screen coordinates
const clampedLngPercent = Math.max(0, Math.min(100, lngPercent));
return `${clampedLngPercent}%,${clampedLatPercent}%`;
});
cutPolygonPoints = pathPoints.join(' ');
if (pathPoints.length > 0) {
cutPath = `
<svg style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50;">
<polygon points="${cutPolygonPoints}"
fill="rgba(0, 124, 186, 0.2)"
stroke="#007cba"
stroke-width="3"
stroke-dasharray="10,5"/>
</svg>
`;
}
} catch (cutError) {
console.log('Could not extract cut geometry:', cutError);
}
}
// Create a realistic map background pattern
const mapBackground = `
background-image:
/* Street grid pattern */
linear-gradient(0deg, rgba(200,200,200,0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(200,200,200,0.3) 1px, transparent 1px),
/* Neighborhood blocks */
linear-gradient(45deg, rgba(220,220,220,0.2) 25%, transparent 25%),
linear-gradient(-45deg, rgba(220,220,220,0.2) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(220,220,220,0.2) 75%),
linear-gradient(-45deg, transparent 75%, rgba(220,220,220,0.2) 75%);
background-size:
40px 40px,
40px 40px,
80px 80px,
80px 80px,
80px 80px,
80px 80px;
background-position:
0 0,
0 0,
0 0,
0 20px,
20px -20px,
-20px 0px;
background-color: #f0f8ff;
`;
// Create a simple visual map representation
const mapVisualization = `
<div style="position: relative; width: 600px; height: 400px; border: 2px solid #ccc; margin: 0 auto; ${mapBackground}">
<!-- Cut boundary visualization -->
${cutPath}
<!-- Fallback cut boundary if SVG failed -->
${!cutPath ? '<div style="position: absolute; top: 15%; left: 15%; right: 15%; bottom: 15%; border: 3px dashed #007cba; background: rgba(0, 124, 186, 0.15); border-radius: 8px;"></div>' : ''}
<!-- Location markers -->
${locationMarkers.map((location, index) => `
<div style="position: absolute;
top: ${Math.max(2, Math.min(98, location.latPercent))}%;
left: ${Math.max(2, Math.min(98, location.lngPercent))}%;
width: 14px; height: 14px;
background: ${this.locationManager.getSupportColor(location.support_level || location['Support Level'])};
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
transform: translate(-50%, -50%);
z-index: 100;
cursor: pointer;"
title="${location.first_name || location['First Name'] || ''} ${location.last_name || location['Last Name'] || ''} - ${location.address || location.Address || ''}">
<div style="position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border-radius: 50%; background: ${this.locationManager.getSupportColor(location.support_level || location['Support Level'])}; opacity: 0.3; animation: pulse 2s infinite;"></div>
</div>
`).join('')}
<!-- Map features for realism -->
<div style="position: absolute; top: 20%; left: 10%; width: 80px; height: 4px; background: rgba(100,100,100,0.6); border-radius: 2px;" title="Main Road"></div>
<div style="position: absolute; top: 60%; left: 20%; width: 60px; height: 3px; background: rgba(100,100,100,0.4); border-radius: 1px;" title="Secondary Road"></div>
<div style="position: absolute; top: 40%; left: 70%; width: 3px; height: 40px; background: rgba(100,100,100,0.4); border-radius: 1px;" title="Cross Street"></div>
<!-- Compass -->
<div style="position: absolute; top: 10px; right: 10px; width: 45px; height: 45px; background: white; border: 2px solid #333; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">
<div style="color: #007cba;">N</div>
</div>
<!-- Scale indicator -->
<div style="position: absolute; bottom: 10px; left: 10px; background: rgba(255,255,255,0.9); padding: 6px 8px; border: 1px solid #333; font-size: 11px; border-radius: 3px;">
Zoom: ${zoom}<br>
Center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}
</div>
<!-- Legend -->
<div style="position: absolute; bottom: 10px; right: 10px; background: rgba(255,255,255,0.95); padding: 10px; border: 1px solid #333; font-size: 11px; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
<div style="font-weight: bold; margin-bottom: 6px; color: #333;">Support Levels</div>
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #28a745; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Strong Support</div>
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #ffc107; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Lean Support</div>
<div style="margin-bottom: 3px;"><span style="display: inline-block; width: 12px; height: 12px; background: #fd7e14; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Oppose</div>
<div><span style="display: inline-block; width: 12px; height: 12px; background: #dc3545; border: 1px solid white; border-radius: 50%; margin-right: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.3);"></span>Strong Oppose</div>
</div>
<!-- Cut boundary info -->
<div style="position: absolute; top: 10px; left: 10px; background: rgba(0, 124, 186, 0.9); color: white; padding: 6px 10px; border-radius: 3px; font-size: 11px; font-weight: bold;">
Cut Boundary
</div>
</div>
<style>
@keyframes pulse {
0% { transform: scale(1); opacity: 0.3; }
50% { transform: scale(1.4); opacity: 0.1; }
100% { transform: scale(1); opacity: 0.3; }
}
</style>
`;
return `
<div style="border: 2px solid #dee2e6; border-radius: 5px; padding: 20px; text-align: center; background: #f8f9fa; margin: 20px 0;">
<h3 style="margin: 0 0 15px 0; color: #495057;">Cut Map View (${locationMarkers.length} locations)</h3>
${mapVisualization}
<p style="font-style: italic; color: #6c757d; margin-top: 15px; font-size: 0.9em;">
Visual representation of cut boundaries and location positions with realistic map styling
</p>
</div>
`;
} catch (error) {
console.log('Static map generation failed:', error);
return null;
}
}
generatePrintContent(cutName, cut, mapContent) {
const printWindow = window.open('', '_blank');
if (!printWindow) {
this.showStatus('Popup blocked - please allow popups for print view', 'warning');
return;
}
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Cut: ${cutName}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
color: #333;
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
h2 {
color: #34495e;
margin-top: 30px;
}
.header-info {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-item {
padding: 15px;
border: 1px solid #dee2e6;
border-radius: 5px;
background: #f8f9fa;
text-align: center;
}
.stat-value {
font-size: 1.5em;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
font-size: 0.9em;
color: #6c757d;
margin-top: 5px;
}
.locations-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
font-size: 0.9em;
}
.locations-table th, .locations-table td {
border: 1px solid #dee2e6;
padding: 8px;
text-align: left;
}
.locations-table th {
background-color: #e9ecef;
font-weight: bold;
color: #495057;
}
.locations-table tr:nth-child(even) {
background-color: #f8f9fa;
}
.support-level-1 { color: #28a745; font-weight: bold; }
.support-level-2 { color: #ffc107; font-weight: bold; }
.support-level-3 { color: #fd7e14; font-weight: bold; }
.support-level-4 { color: #dc3545; font-weight: bold; }
.filters-info {
background: #e7f3ff;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 0.9em;
}
@media print {
body { margin: 0; }
.locations-table { font-size: 0.8em; }
}
</style>
</head>
<body>
<h1>Cut: ${cutName}</h1>
<div class="header-info">
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
<strong>Cut Category:</strong> ${cut?.category || cut?.Category || 'Unknown'}<br>
<strong>Assigned To:</strong> ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
</div>
${this.locationManager.getCurrentFiltersDisplay()}
<h2>Cut Statistics</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${document.getElementById('total-locations')?.textContent || '0'}</div>
<div class="stat-label">Total Locations</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('support-1')?.textContent || '0'}</div>
<div class="stat-label">Strong Support</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('support-2')?.textContent || '0'}</div>
<div class="stat-label">Lean Support</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-signs')?.textContent || '0'}</div>
<div class="stat-label">Lawn Signs</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-email')?.textContent || '0'}</div>
<div class="stat-label">Email Contacts</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-phone')?.textContent || '0'}</div>
<div class="stat-label">Phone Contacts</div>
</div>
</div>
<h2>Map Information</h2>
${mapContent}
<h2>Location Details</h2>
<table class="locations-table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Coordinates</th>
<th>Support Level</th>
<th>Contact Info</th>
<th>Lawn Sign</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
${this.locationManager.currentCutLocations.map(location => {
const firstName = location.first_name || location['First Name'] || '';
const lastName = location.last_name || location['Last Name'] || '';
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
const address = location.address || location.Address || '';
const lat = location.latitude || location.Latitude || location.lat;
const lng = location.longitude || location.Longitude || location.lng;
const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A';
const supportLevel = location.support_level || location['Support Level'] || '';
const email = location.email || location.Email || '';
const phone = location.phone || location.Phone || '';
const contact = [email, phone].filter(Boolean).join(', ');
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
const notes = location.notes || location.Notes || '';
return `
<tr>
<td>${name}</td>
<td>${address}</td>
<td style="font-family: monospace; font-size: 0.8em;">${coordinates}</td>
<td class="support-level-${supportLevel}">${supportLevel}</td>
<td>${contact}</td>
<td>${hasSign}</td>
<td>${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 0.9em;">
<p>Generated by Campaign Map System - ${new Date().toLocaleDateString()}</p>
</div>
</body>
</html>
`;
try {
printWindow.document.write(printContent);
printWindow.document.close();
// Give the content time to render, then print
setTimeout(() => {
printWindow.print();
}, 500);
this.showStatus('Print view generated successfully', 'success');
} catch (printError) {
console.error('Error writing to print window:', printError);
printWindow.close();
this.showStatus('Print window error - using fallback', 'warning');
this.printCutViewSimple();
}
}
printCutViewSimple() {
// Simple print view without map image
if (!this.locationManager.currentCutId) {
this.showStatus('No cut selected', 'warning');
return;
}
const cut = this.cutsManager.allCuts.find(c => (c.id || c.Id || c.ID || c._id) == this.locationManager.currentCutId);
const cutName = cut?.name || cut?.Name || 'Unknown Cut';
const printWindow = window.open('', '_blank');
if (!printWindow) {
this.showStatus('Popup blocked - please allow popups for print view', 'warning');
return;
}
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Cut: ${cutName}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; color: #333; }
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
h2 { color: #34495e; margin-top: 30px; }
.header-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 20px 0; }
.stat-item { padding: 15px; border: 1px solid #dee2e6; border-radius: 5px; background: #f8f9fa; text-align: center; }
.stat-value { font-size: 1.5em; font-weight: bold; color: #2c3e50; }
.stat-label { font-size: 0.9em; color: #6c757d; margin-top: 5px; }
.locations-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9em; }
.locations-table th, .locations-table td { border: 1px solid #dee2e6; padding: 8px; text-align: left; }
.locations-table th { background-color: #e9ecef; font-weight: bold; color: #495057; }
.locations-table tr:nth-child(even) { background-color: #f8f9fa; }
.support-level-1 { color: #28a745; font-weight: bold; }
.support-level-2 { color: #ffc107; font-weight: bold; }
.support-level-3 { color: #fd7e14; font-weight: bold; }
.support-level-4 { color: #dc3545; font-weight: bold; }
.filters-info { background: #e7f3ff; padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 0.9em; }
</style>
</head>
<body>
<h1>Cut: ${cutName}</h1>
<div class="header-info">
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
<strong>Cut Category:</strong> ${cut?.category || cut?.Category || 'Unknown'}<br>
<strong>Assigned To:</strong> ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
</div>
${this.locationManager.getCurrentFiltersDisplay()}
<h2>Cut Statistics</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${document.getElementById('total-locations')?.textContent || '0'}</div>
<div class="stat-label">Total Locations</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('support-1')?.textContent || '0'}</div>
<div class="stat-label">Strong Support</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('support-2')?.textContent || '0'}</div>
<div class="stat-label">Lean Support</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-signs')?.textContent || '0'}</div>
<div class="stat-label">Lawn Signs</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-email')?.textContent || '0'}</div>
<div class="stat-label">Email Contacts</div>
</div>
<div class="stat-item">
<div class="stat-value">${document.getElementById('has-phone')?.textContent || '0'}</div>
<div class="stat-label">Phone Contacts</div>
</div>
</div>
<h2>Location Details</h2>
<table class="locations-table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Coordinates</th>
<th>Support Level</th>
<th>Contact Info</th>
<th>Lawn Sign</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
${this.locationManager.currentCutLocations.map(location => {
const firstName = location.first_name || location['First Name'] || '';
const lastName = location.last_name || location['Last Name'] || '';
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Unknown';
const address = location.address || location.Address || '';
const lat = location.latitude || location.Latitude || location.lat;
const lng = location.longitude || location.Longitude || location.lng;
const coordinates = (lat && lng) ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'N/A';
const supportLevel = location.support_level || location['Support Level'] || '';
const email = location.email || location.Email || '';
const phone = location.phone || location.Phone || '';
const contact = [email, phone].filter(Boolean).join(', ');
const hasSign = location.sign || location.Sign ? 'Yes' : 'No';
const notes = location.notes || location.Notes || '';
return `
<tr>
<td>${name}</td>
<td>${address}</td>
<td style="font-family: monospace; font-size: 0.8em;">${coordinates}</td>
<td class="support-level-${supportLevel}">${supportLevel}</td>
<td>${contact}</td>
<td>${hasSign}</td>
<td>${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
<div style="margin-top: 30px; text-align: center; color: #6c757d; font-size: 0.9em;">
<p>Generated by Campaign Map System - ${new Date().toLocaleDateString()}</p>
</div>
</body>
</html>
`;
try {
if (!printWindow || printWindow.closed) {
this.showStatus('Print window unavailable', 'error');
return;
}
printWindow.document.write(printContent);
printWindow.document.close();
// Give content time to load then print
setTimeout(() => {
if (!printWindow.closed) {
printWindow.print();
}
}, 300);
this.showStatus('Simple print view generated', 'success');
} catch (printError) {
console.error('Error in simple print:', printError);
if (printWindow && !printWindow.closed) {
printWindow.close();
}
this.showStatus('Print failed - please try again', 'error');
}
}
showStatus(message, type) {
// Use existing admin notification system if available
if (typeof showNotification === 'function') {
showNotification(message, type);
} else if (this.cutsManager && this.cutsManager.showStatus) {
this.cutsManager.showStatus(message, type);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
debugMapState() {
console.log('=== MAP STATE DEBUG ===');
console.log('Map container:', this.map.getContainer());
console.log('Map layers:', this.map._layers);
console.log('Current cut layer:', this.cutsManager.currentCutLayer);
console.log('Location markers layer:', this.locationManager.locationMarkersLayer);
console.log('Showing locations:', this.locationManager.showingLocations);
// Check for visible markers
const mapContainer = this.map.getContainer();
const markers = mapContainer.querySelectorAll('.leaflet-marker-icon, .location-marker');
console.log('Visible marker elements:', markers.length);
// Check for overlays
const overlays = mapContainer.querySelectorAll('.leaflet-overlay-pane *');
console.log('Overlay elements:', overlays.length);
// Debug cut layer positioning
if (this.cutsManager.currentCutLayer) {
try {
const bounds = this.cutsManager.currentCutLayer.getBounds();
const mapBounds = this.map.getBounds();
console.log('Cut layer bounds:', {
north: bounds.getNorth(),
south: bounds.getSouth(),
east: bounds.getEast(),
west: bounds.getWest()
});
console.log('Map bounds:', {
north: mapBounds.getNorth(),
south: mapBounds.getSouth(),
east: mapBounds.getEast(),
west: mapBounds.getWest()
});
// Check the actual SVG path element
const pathElement = mapContainer.querySelector('path.cut-polygon');
if (pathElement) {
console.log('Cut polygon SVG element found');
console.log('Cut polygon d attribute length:', pathElement.getAttribute('d')?.length || 0);
console.log('Cut polygon transform:', pathElement.getAttribute('transform'));
console.log('Cut polygon fill:', pathElement.getAttribute('fill'));
console.log('Cut polygon fill-opacity:', pathElement.getAttribute('fill-opacity'));
// Get the bounding box of the SVG element
const bbox = pathElement.getBBox();
console.log('SVG path bounding box:', bbox);
} else {
console.log('No cut polygon SVG element found with class "cut-polygon"');
// Look for any path elements
const allPaths = mapContainer.querySelectorAll('path');
console.log('Total path elements found:', allPaths.length);
allPaths.forEach((path, index) => {
console.log(`Path ${index} classes:`, path.className.baseVal || path.className);
});
}
} catch (error) {
console.log('Error getting cut layer debug info:', error);
}
} else {
console.log('No current cut layer found');
}
console.log('=== END MAP STATE DEBUG ===');
}
}
// Export the class if using modules, otherwise it's global
if (typeof module !== 'undefined' && module.exports) {
module.exports = CutPrintUtils;
} else {
window.CutPrintUtils = CutPrintUtils;
}