/** * 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

Cut Map

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('')}
N
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}

Generated: ${new Date().toLocaleString()}
Cut Category: ${cut?.category || cut?.Category || 'Unknown'}
Assigned To: ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
${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

${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 ` `; }).join('')}
Name Address Coordinates Support Level Contact Info Lawn Sign Notes
${name} ${address} ${coordinates} ${supportLevel} ${contact} ${hasSign} ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}

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}

Generated: ${new Date().toLocaleString()}
Cut Category: ${cut?.category || cut?.Category || 'Unknown'}
Assigned To: ${cut?.assigned_to || cut?.Assigned_to || cut['Assigned To'] || 'Unassigned'}
${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

${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 ` `; }).join('')}
Name Address Coordinates Support Level Contact Info Lawn Sign Notes
${name} ${address} ${coordinates} ${supportLevel} ${contact} ${hasSign} ${notes.substring(0, 100)}${notes.length > 100 ? '...' : ''}

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; }