/**
* 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 = `
Cut Map View
Map showing cut boundaries and ${locationCount} location markers
`;
} 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 = `
Cut Boundaries & Location Summary
Map Center:
${center.lat.toFixed(6)}, ${center.lng.toFixed(6)}
Zoom Level: ${zoom}
Geographic Bounds:
North: ${mapBounds.getNorth().toFixed(6)}
South: ${mapBounds.getSouth().toFixed(6)}
East: ${mapBounds.getEast().toFixed(6)}
West: ${mapBounds.getWest().toFixed(6)}
Cut Statistics:
Total Locations: ${this.locationManager.currentCutLocations.length}
Active Filters: ${this.locationManager.getActiveFiltersCount()}
Showing: ${this.locationManager.currentCutLocations.length} locations
Individual location coordinates and details are listed in the table below.
`;
}
// 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 = `
`;
}
} 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 = `
${cutPath}
${!cutPath ? '
' : ''}
${locationMarkers.map((location, index) => `
`).join('')}
Zoom: ${zoom}
Center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}
Support Levels
Strong Support
Lean Support
Oppose
Strong Oppose
Cut Boundary
`;
return `
Cut Map View (${locationMarkers.length} locations)
${mapVisualization}
Visual representation of cut boundaries and location positions with realistic map styling
`;
} 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 = `
Cut: ${cutName}
Cut: ${cutName}
${this.locationManager.getCurrentFiltersDisplay()}
Cut Statistics
${document.getElementById('total-locations')?.textContent || '0'}
Total Locations
${document.getElementById('support-1')?.textContent || '0'}
Strong Support
${document.getElementById('support-2')?.textContent || '0'}
Lean Support
${document.getElementById('has-signs')?.textContent || '0'}
Lawn Signs
${document.getElementById('has-email')?.textContent || '0'}
Email Contacts
${document.getElementById('has-phone')?.textContent || '0'}
Phone Contacts
Map Information
${mapContent}
Location Details
| Name |
Address |
Coordinates |
Support Level |
Contact Info |
Lawn Sign |
Notes |
${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 `
| ${name} |
${address} |
${coordinates} |
${supportLevel} |
${contact} |
${hasSign} |
${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''} |
`;
}).join('')}
Generated by Campaign Map System - ${new Date().toLocaleDateString()}
`;
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 = `
Cut: ${cutName}
Cut: ${cutName}
${this.locationManager.getCurrentFiltersDisplay()}
Cut Statistics
${document.getElementById('total-locations')?.textContent || '0'}
Total Locations
${document.getElementById('support-1')?.textContent || '0'}
Strong Support
${document.getElementById('support-2')?.textContent || '0'}
Lean Support
${document.getElementById('has-signs')?.textContent || '0'}
Lawn Signs
${document.getElementById('has-email')?.textContent || '0'}
Email Contacts
${document.getElementById('has-phone')?.textContent || '0'}
Phone Contacts
Location Details
| Name |
Address |
Coordinates |
Support Level |
Contact Info |
Lawn Sign |
Notes |
${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 `
| ${name} |
${address} |
${coordinates} |
${supportLevel} |
${contact} |
${hasSign} |
${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''} |
`;
}).join('')}
Generated by Campaign Map System - ${new Date().toLocaleDateString()}
`;
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;
}