1053 lines
52 KiB
JavaScript
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;
|
|
}
|