const statusDiv = document.getElementById('status'); const statusPanel = document.getElementById('statusPanel'); const qcResultDiv = document.getElementById('qcResult'); const loadingOverlay = document.getElementById('loadingOverlay'); // Global endpoint markers layer group for toggling visibility let endpointMarkersLayer = null; // ============================================ // STYLE CONFIGURATION // ============================================ // Centralized styling configuration for easy customization const LAYER_STYLES = { // Zone polygon styles zonePolygons: { color: '#6366f1', // Border color (indigo) fillColor: '#818cf8', // Fill color (lighter indigo) fillOpacity: 0.15, // Transparency (15%) weight: 2, // Border width opacity: 0.7 // Border opacity }, // Segment styles segments: { aerial: { color: '#0066cc', // Blue for aerial weight: 4, opacity: 0.85 }, underground: { color: '#cc0000', // Red for underground weight: 4, opacity: 0.85 }, default: { color: '#808080', // Gray for other types weight: 4, opacity: 0.7 } }, // Point marker styles markers: { sites: { radius: 7, fillColor: '#3b82f6', color: '#ffffff', weight: 2, fillOpacity: 0.9 }, poles: { radius: 5, fillColor: '#1a1a1a', color: '#ffffff', weight: 2, fillOpacity: 0.9 }, handholes: { // Square marker created via divIcon size: [18, 18], backgroundColor: '#10b981', borderColor: '#ffffff', borderWidth: 2 } } }; function showStatus(msg, isError = false) { statusDiv.textContent = msg; statusDiv.style.color = isError ? '#ef4444' : '#6b7280'; statusPanel.classList.add('visible'); // Auto-hide after 5 seconds if not an error if (!isError && !msg.includes('QC')) { setTimeout(() => { if (!qcResultDiv.innerHTML) { statusPanel.classList.remove('visible'); } }, 5000); } } function hideStatus() { statusPanel.classList.remove('visible'); qcResultDiv.innerHTML = ''; } function showLoading(show = true) { loadingOverlay.style.display = show ? 'flex' : 'none'; } function setButtonLoading(button, loading = true) { if (loading) { button.classList.add('loading'); button.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; } } const map = L.map('map').setView([29.2, -99.7], 12); // Create a custom pane for endpoint markers with high z-index map.createPane('endpointPane'); map.getPane('endpointPane').style.zIndex = 650; // Higher than markerPane (600) // Initialize endpoint markers layer group for toggling visibility endpointMarkersLayer = L.layerGroup().addTo(map); // Define multiple basemap options const baseMaps = { 'Clean (Light)': L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 20 }), 'Clean (Dark)': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 20 }), 'OpenStreetMap': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }), 'Satellite': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community', maxZoom: 19 }), 'Satellite + Labels': L.layerGroup([ L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri', maxZoom: 19 }), L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', { attribution: '© CARTO', subdomains: 'abcd', maxZoom: 20 }) ]) }; // Add default basemap (Clean Light) baseMaps['Clean (Light)'].addTo(map); const layers = { segments: L.layerGroup().addTo(map), sites: L.layerGroup().addTo(map), info: L.layerGroup().addTo(map), handholes: L.layerGroup().addTo(map), poles: L.layerGroup().addTo(map), permits: L.layerGroup().addTo(map), }; // Add layer control with basemaps and overlay layers L.control.layers(baseMaps, { 'Segments': layers.segments, 'Sites': layers.sites, 'Info Objects': layers.info, 'Handholes': layers.handholes, 'Poles': layers.poles, 'Permits': layers.permits }).addTo(map); // Create custom legend control const legend = L.control({ position: 'bottomright' }); legend.onAdd = function(map) { const div = L.DomUtil.create('div', 'legend'); div.innerHTML = `
Layer Legend
Segments
Features
`; return div; }; legend.addTo(map); // Function to toggle layers function toggleLayer(layerName) { if (map.hasLayer(layers[layerName])) { map.removeLayer(layers[layerName]); } else { map.addLayer(layers[layerName]); } } function clearLayers() { Object.values(layers).forEach(layer => layer.clearLayers()); } function loadMapData(market, zone) { clearLayers(); hideStatus(); showLoading(true); showStatus("Loading map data..."); const endpoints = { segments: '/api/segments', sites: '/api/sites', info: '/api/info', handholes: '/api/access_points', poles: '/api/poles', permits: '/api/permits' }; let loadedCount = 0; const totalLayers = Object.keys(endpoints).length; for (const [key, url] of Object.entries(endpoints)) { fetch(`${url}?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { console.log(`Loaded ${key} data:`, data); const layerOptions = { pointToLayer: (f, latlng) => { switch (key) { case 'sites': return L.circleMarker(latlng, { radius: 7, fillColor: '#3b82f6', color: '#ffffff', weight: 2, opacity: 1, fillOpacity: 0.9 }); case 'handholes': // Create a square marker for handholes (18x18 including 2px border on each side) const squareIcon = L.divIcon({ className: 'handhole-marker', html: '
', iconSize: [18, 18], iconAnchor: [9, 9] }); return L.marker(latlng, { icon: squareIcon }); case 'poles': return L.circleMarker(latlng, { radius: 5, fillColor: '#1a1a1a', color: '#ffffff', weight: 2, opacity: 1, fillOpacity: 0.9 }); default: return L.marker(latlng); } }, style: (f) => { switch (key) { case 'segments': // Color based on segment type const segmentType = f.properties.segment_type?.toLowerCase(); if (segmentType === 'aerial') { return { color: '#0066cc', weight: 4, opacity: 0.85, lineCap: 'round', lineJoin: 'round' }; // Blue for Aerial } else if (segmentType === 'underground') { return { color: '#cc0000', weight: 4, opacity: 0.85, lineCap: 'round', lineJoin: 'round' }; // Red for Underground } else { return { color: '#808080', weight: 4, opacity: 0.7, lineCap: 'round', lineJoin: 'round' }; // Gray for other types } case 'info': return LAYER_STYLES.zonePolygons; case 'permits': return { color: '#f59e0b', fillColor: '#fbbf24', fillOpacity: 0.2, weight: 2, opacity: 0.7 }; default: return {}; } }, filter: (f) => { if (key === 'segments') { const selectedType = document.getElementById('segmentTypeFilter').value; if (!selectedType) return true; const segmentType = f.properties.segment_type?.toLowerCase(); return segmentType === selectedType.toLowerCase(); } return true; } }; const geoLayer = L.geoJSON(data, layerOptions).addTo(layers[key]); console.log(`${key} layer added`, geoLayer); // Add popups to all features if (key === 'sites') { geoLayer.eachLayer(layer => { if (layer.feature && layer.feature.properties) { const props = layer.feature.properties; layer.bindPopup(` 🏠 Site Details
ID: ${props.id || 'N/A'}
Name: ${props.name || 'N/A'}
Zone Assignment: ${props.group1 || props['Group 1'] || 'NULL/Unassigned'}
Address: ${props.address || props.address1 || 'N/A'}
City: ${props.city || 'N/A'}
State: ${props.state || 'N/A'}
Zip: ${props.zip || 'N/A'} `); } }); } if (key === 'poles') { geoLayer.eachLayer(layer => { if (layer.feature && layer.feature.properties) { const props = layer.feature.properties; layer.bindPopup(` ⚡ Pole Details
ID: ${props.id || 'N/A'}
Name: ${props.name || 'N/A'}
Zone Assignment: ${props.group1 || 'NULL/Unassigned'}
Owner: ${props.owner || 'N/A'}
Height: ${props.poleheight || 'N/A'} ft
Attachment Height: ${props.attachmentheight || 'N/A'} ft `); } }); } if (key === 'handholes') { geoLayer.eachLayer(layer => { if (layer.feature && layer.feature.properties) { const props = layer.feature.properties; layer.bindPopup(` 🔧 Handhole/Access Point
ID: ${props.id || 'N/A'}
Name: ${props.name || 'N/A'}
Zone Assignment: ${props.group1 || 'NULL/Unassigned'}
Manufacturer: ${props.manufacturer || 'N/A'}
Size: ${props.size || 'N/A'}
Description: ${props.description || 'N/A'} `); } }); } // Add labels and popups to zones if (key === 'info') { geoLayer.eachLayer(layer => { if (layer.feature && layer.feature.properties) { const props = layer.feature.properties; const zoneName = props.name || props.group_1 || 'Unknown Zone'; // Add popup layer.bindPopup(` Zone: ${zoneName}
${props.description ? `${props.description}
` : ''} ${props.group_1 ? `Group: ${props.group_1}` : ''} `); // Add permanent label at center of zone const center = layer.getBounds().getCenter(); L.marker(center, { icon: L.divIcon({ className: 'zone-label', html: `
${zoneName}
`, iconSize: [100, 20], iconAnchor: [50, 10] }) }).addTo(layers[key]); } }); // Zoom to zones if (geoLayer.getBounds().isValid()) { console.log("Zooming to zones bounds:", geoLayer.getBounds()); map.fitBounds(geoLayer.getBounds(), { padding: [50, 50] }); } } // Add interactivity for segments if (key === 'segments') { geoLayer.eachLayer(layer => { // Add hover effect layer.on('mouseover', function(e) { this.setStyle({ weight: 6, opacity: 1 }); }); layer.on('mouseout', function(e) { geoLayer.resetStyle(this); }); // Add popup with segment info if (layer.feature && layer.feature.properties) { const props = layer.feature.properties; const zoneDisplay = props.group_1 || props['Group 1'] || 'NULL/Unassigned'; layer.bindPopup(` 📏 Segment Details
ID: ${props.id || 'N/A'}
Type: ${props.segment_type || 'N/A'}
Zone Assignment: ${zoneDisplay}
Status: ${props.segment_status || 'N/A'}
Protection: ${props.protection_status || 'N/A'}
QC Flag: ${props.qc_flag || 'None'} `); } }); // Zoom to segments immediately after they load if (geoLayer.getBounds().isValid()) { console.log("Zooming to segments bounds:", geoLayer.getBounds()); map.fitBounds(geoLayer.getBounds(), { padding: [50, 50] }); } } loadedCount++; if (loadedCount === totalLayers) { showLoading(false); showStatus("All layers loaded successfully."); } }) .catch(err => { console.error(`Failed to load ${key}:`, err); showLoading(false); showStatus(`Error loading ${key}.`, true); }); } } // Populate dropdowns const marketSelect = document.getElementById('marketSelect'); const zoneSelect = document.getElementById('zoneSelect'); function loadMarkets() { fetch('/api/markets') .then(res => res.json()) .then(data => { console.log('Markets data:', data); marketSelect.innerHTML = ''; data.forEach(m => { const opt = document.createElement('option'); opt.value = m.mapid; opt.textContent = m.project; marketSelect.appendChild(opt); }); }) .catch(err => { console.error('Failed to load markets:', err); marketSelect.innerHTML = ''; }); } function loadZones() { const market = marketSelect.value; if (!market) { zoneSelect.innerHTML = ''; zoneSelect.disabled = true; return; } console.log(`/api/zones?map_id=${market}`); fetch(`/api/zones?map_id=${market}`) .then(res => res.json()) .then(data => { zoneSelect.innerHTML = ''; data.forEach(z => { const opt = document.createElement('option'); opt.value = z; opt.textContent = z; zoneSelect.appendChild(opt); }); zoneSelect.disabled = false; }) .catch(err => { console.error('Failed to load zones:', err); zoneSelect.innerHTML = ''; zoneSelect.disabled = true; }); } // Show the clear QC button function showClearQCButton() { document.getElementById('clearQCButton').style.display = 'inline-block'; } function showEndpointToggle() { document.getElementById('endpointToggleContainer').style.display = 'flex'; } // Clear all QC result layers function clearQCResults() { // Clear connectivity layers if (connectivityLayers && connectivityLayers.length > 0) { connectivityLayers.forEach(layer => map.removeLayer(layer)); connectivityLayers = []; } // Clear invalid span layer if (invalidSpanLayer) { map.removeLayer(invalidSpanLayer); invalidSpanLayer = null; } // Clear disconnected handhole layer if (disconnectedHandholeLayer) { map.removeLayer(disconnectedHandholeLayer); disconnectedHandholeLayer = null; } // Clear disconnected sites layer if (disconnectedSitesLayer) { map.removeLayer(disconnectedSitesLayer); disconnectedSitesLayer = null; } // Clear invalid aerial endpoint layer if (invalidAerialEndpointLayer) { map.removeLayer(invalidAerialEndpointLayer); invalidAerialEndpointLayer = null; } // Clear invalid underground layer if (invalidUndergroundLayer) { map.removeLayer(invalidUndergroundLayer); invalidUndergroundLayer = null; } // Clear invalid zone containment layer if (invalidZoneContainmentLayer) { map.removeLayer(invalidZoneContainmentLayer); invalidZoneContainmentLayer = null; } // Clear endpoint markers if (endpointMarkersLayer) { endpointMarkersLayer.clearLayers(); } // Restore base segments layer if it was hidden if (layers.segments && !map.hasLayer(layers.segments)) { map.addLayer(layers.segments); } // Hide clear button document.getElementById('clearQCButton').style.display = 'none'; // Hide endpoint toggle document.getElementById('endpointToggleContainer').style.display = 'none'; // Hide status panel hideStatus(); console.log('All QC results cleared'); } // Clear QC button handler document.getElementById('clearQCButton').addEventListener('click', clearQCResults); // Endpoint toggle handler document.getElementById('endpointToggle').addEventListener('change', function() { if (this.checked) { // Show endpoints if (endpointMarkersLayer && !map.hasLayer(endpointMarkersLayer)) { map.addLayer(endpointMarkersLayer); } } else { // Hide endpoints if (endpointMarkersLayer && map.hasLayer(endpointMarkersLayer)) { map.removeLayer(endpointMarkersLayer); } } }); marketSelect.addEventListener('change', () => { const market = marketSelect.value; console.log("Selected Market ID:", market); loadZones(); // Clear QC results when changing market clearQCResults(); const zone = zoneSelect.value; if (market && zone) { loadMapData(market, zone); } }); zoneSelect.addEventListener('change', () => { const market = marketSelect.value; const zone = zoneSelect.value; console.log("Selected Market ID & Zone:", market, zone); // Clear QC results when changing zone clearQCResults(); if (market && zone) { loadMapData(market, zone); } }); loadMarkets(); loadZones(); const segmentTypeFilter = document.getElementById('segmentTypeFilter'); segmentTypeFilter.addEventListener('change', () => { const market = marketSelect.value; const zone = zoneSelect.value; if (market && zone) { loadMapData(market, zone); } }); // Enable/disable Run QC button based on dropdown selection const qcOperationSelect = document.getElementById('qcOperationSelect'); const runQCButton = document.getElementById('runQCButton'); qcOperationSelect.addEventListener('change', () => { runQCButton.disabled = !qcOperationSelect.value; }); // Consolidated QC button handler runQCButton.addEventListener('click', () => { const operation = qcOperationSelect.value; if (!operation) return; switch(operation) { case 'connectivity': runConnectivityQC(); break; case 'single-span': runSingleSpanQC(); break; case 'aerial-endpoints': runAerialEndpointQC(); break; case 'underground-endpoints': runUndergroundEndpointQC(); break; case 'zone-containment': runZoneContainmentQC(); break; case 'handhole-connectivity': runAccessPointQC(); break; case 'site-connectivity': runSiteConnectivityQC(); break; } }); /// Connectivity QC let connectivityLayers = []; function runConnectivityQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous connectivity layers connectivityLayers.forEach(layer => map.removeLayer(layer)); connectivityLayers = []; // Hide the base segments layer temporarily so QC colors are visible if (map.hasLayer(layers.segments)) { map.removeLayer(layers.segments); } fetch(`/api/qc/connectivity?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { console.log("Connectivity QC data received:", data); console.log("Number of features:", data.features ? data.features.length : 0); if (data.features && data.features.length > 0) { console.log("First feature:", data.features[0]); } const { components, totalSegments } = getConnectedComponents(data); setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (components.length === 1) { qcResultDiv.textContent = "✅ Segment network is fully connected."; qcResultDiv.style.color = "#10b981"; showClearQCButton(); // Show the single connected network in bright blue with white outline // Draw white outline first const outlineLayer = L.geoJSON(components[0].segments, { style: { color: 'white', weight: 10, opacity: 1.0 } }).addTo(map); connectivityLayers.push(outlineLayer); // Draw bright blue line on top const layer = L.geoJSON(components[0].segments, { style: { color: '#00bfff', weight: 6, opacity: 1.0 }, onEachFeature: (feature, layer) => { const props = feature.properties; // Add hover effect layer.on('mouseover', function() { this.setStyle({ weight: 8, opacity: 1 }); }); layer.on('mouseout', function() { this.setStyle({ weight: 6, opacity: 1.0 }); }); layer.bindPopup(` ✅ Main Connected Network
Segment ID: ${props.id || 'N/A'}
ID_0: ${props.id_0 || 'N/A'}
Type: ${props.segment_type || 'N/A'}
Status: ${props.segment_status || 'N/A'}
Protection: ${props.protection_status || 'N/A'}
Map ID: ${props.mapid || 'N/A'} `); // Add endpoint markers if (feature.geometry && feature.geometry.coordinates) { let coords = feature.geometry.coordinates; // Handle MultiLineString - extract first linestring if (feature.geometry.type === 'MultiLineString' && coords.length > 0) { coords = coords[0]; } console.log('Adding endpoint markers for segment:', props.id, 'coords length:', coords.length); if (coords && coords.length >= 2) { const startCoord = [coords[0][1], coords[0][0]]; const endCoord = [coords[coords.length - 1][1], coords[coords.length - 1][0]]; console.log('Start coord:', startCoord, 'End coord:', endCoord); // Start point (yellow) L.circleMarker(startCoord, { radius: 6, fillColor: '#fbbf24', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane', zIndexOffset: 1000 }).bindPopup(`🟡 Segment Start
ID: ${props.id || 'N/A'}`).addTo(endpointMarkersLayer); // End point (red) L.circleMarker(endCoord, { radius: 6, fillColor: '#ef4444', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane', zIndexOffset: 1000 }).bindPopup(`🔴 Segment End
ID: ${props.id || 'N/A'}`).addTo(endpointMarkersLayer); } } } }).addTo(map); connectivityLayers.push(layer); // Show endpoint toggle since we've added endpoint markers showEndpointToggle(); } else { // Multiple components - color code them const largestComponent = components[0]; const disconnectedComponents = components.slice(1); const totalDisconnected = disconnectedComponents.reduce((sum, c) => sum + c.segments.length, 0); qcResultDiv.innerHTML = ` 📊 Network Connectivity Analysis:
• Main Network: ${largestComponent.segments.length} segments (bright blue)
• Disconnected Groups: ${disconnectedComponents.length} (${totalDisconnected} segments)
• Total Segments: ${totalSegments} `; qcResultDiv.style.color = "#f59e0b"; showClearQCButton(); // Color palette for different components // First color is for main network (bright blue with special styling) // Remaining colors are for disconnected groups (avoiding blues/cyans) const colors = [ '#00bfff', // Bright blue for main network (solid, thick) '#ff0000', // Red '#ff8c00', // Orange '#00ff00', // Lime/Green '#ff00ff', // Magenta '#ffff00', // Yellow '#ff1493', // Deep pink '#9400d3', // Violet/Purple '#ff6347', // Tomato/Coral '#ffa500', // Gold/Amber '#32cd32', // Bright lime '#ff69b4', // Hot pink '#8b4513', // Saddle brown '#ff4500', // Orange red '#9370db' // Medium purple ]; // Visualize each component with its color components.forEach((component, index) => { const color = colors[index % colors.length]; const isMainNetwork = index === 0; // For main network, add a white outline for extra distinction if (isMainNetwork) { // Draw white outline first (underneath) const outlineLayer = L.geoJSON(component.segments, { style: { color: 'white', weight: 10, opacity: 1.0 } }).addTo(map); connectivityLayers.push(outlineLayer); } // Draw the main colored line const layer = L.geoJSON(component.segments, { style: { color: color, weight: isMainNetwork ? 6 : 7, opacity: 1.0, dashArray: isMainNetwork ? '' : '10,6' }, onEachFeature: (feature, layer) => { const props = feature.properties; const componentLabel = isMainNetwork ? `✅ Main Connected Network` : `🚨 Disconnected Group ${index}`; const componentInfo = isMainNetwork ? `` : `Group Size: ${component.segments.length} segment(s)
`; // Add hover effect layer.on('mouseover', function() { this.setStyle({ weight: isMainNetwork ? 8 : 9, opacity: 1 }); }); layer.on('mouseout', function() { this.setStyle({ weight: isMainNetwork ? 6 : 7, opacity: 1.0 }); }); layer.bindPopup(` ${componentLabel}
${componentInfo} Segment ID: ${props.id || 'N/A'}
ID_0: ${props.id_0 || 'N/A'}
Type: ${props.segment_type || 'N/A'}
Status: ${props.segment_status || 'N/A'}
Protection: ${props.protection_status || 'N/A'}
Map ID: ${props.mapid || 'N/A'} `); // Add endpoint markers if (feature.geometry && feature.geometry.coordinates) { let coords = feature.geometry.coordinates; // Handle MultiLineString - extract first linestring if (feature.geometry.type === 'MultiLineString' && coords.length > 0) { coords = coords[0]; } console.log('Multi-component: Adding endpoint markers for segment:', props.id, 'coords length:', coords.length, 'Group:', index); if (coords && coords.length >= 2) { const startCoord = [coords[0][1], coords[0][0]]; const endCoord = [coords[coords.length - 1][1], coords[coords.length - 1][0]]; console.log('Multi-component Start coord:', startCoord, 'End coord:', endCoord); // Start point (yellow) L.circleMarker(startCoord, { radius: 6, fillColor: '#fbbf24', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane', zIndexOffset: 1000 }).bindPopup(`🟡 Segment Start
ID: ${props.id || 'N/A'}
Group: ${isMainNetwork ? 'Main Network' : 'Disconnected ' + index}`).addTo(endpointMarkersLayer); // End point (red) L.circleMarker(endCoord, { radius: 6, fillColor: '#ef4444', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane', zIndexOffset: 1000 }).bindPopup(`🔴 Segment End
ID: ${props.id || 'N/A'}
Group: ${isMainNetwork ? 'Main Network' : 'Disconnected ' + index}`).addTo(endpointMarkersLayer); } } } }).addTo(map); connectivityLayers.push(layer); }); // Show endpoint toggle since we've added endpoint markers showEndpointToggle(); // Zoom to show all segments const allBounds = L.latLngBounds(); connectivityLayers.forEach(layer => { // Only GeoJSON layers have getBounds, skip markers if (layer.getBounds && typeof layer.getBounds === 'function') { const bounds = layer.getBounds(); if (bounds.isValid()) { allBounds.extend(bounds); } } else if (layer.getLatLng && typeof layer.getLatLng === 'function') { // For individual markers, extend by their position allBounds.extend(layer.getLatLng()); } }); if (allBounds.isValid()) { map.fitBounds(allBounds, { padding: [30, 30] }); } // Tag disconnected segments in DB const disconnectedIds = disconnectedComponents.flatMap(c => c.segments.map(s => s.properties.id) ); fetch('/api/qc/tag-disconnected', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: disconnectedIds }) }) .then(res => res.json()) .then(data => { console.log("Tagging complete:", data.message); }) .catch(err => console.error("Error tagging disconnected segments:", err)); } }) .catch(err => { console.error("Connectivity QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); }); } /// Single Span QC - NEW FUNCTIONALITY let invalidSpanLayer; function runSingleSpanQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous invalid span layer if (invalidSpanLayer) { map.removeLayer(invalidSpanLayer); invalidSpanLayer = null; } showStatus("Running Single Span QC..."); fetch(`/api/qc/single-span?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (data.invalid_segments === 0) { qcResultDiv.innerHTML = `✅ All ${data.total_aerial_segments} aerial segments have valid single spans.`; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Single Span QC completed - All segments valid."); } else { qcResultDiv.innerHTML = ` ❌ Single Span QC Results:
• Total Aerial Segments: ${data.total_aerial_segments}
• Valid: ${data.valid_segments}
• Invalid: ${data.invalid_segments}
• Pass Rate: ${data.pass_rate.toFixed(1)}% `; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); showStatus(`Single Span QC completed - ${data.invalid_segments} segments need attention.`, true); // Get invalid segments and highlight them on map const invalidSegments = data.results.filter(r => !r.is_valid); console.log("Invalid segments:", invalidSegments); // Create GeoJSON features for invalid segments const invalidFeatures = []; invalidSegments.forEach(segment => { if (segment.geometry) { invalidFeatures.push({ type: "Feature", geometry: segment.geometry, properties: { segment_id: segment.segment_id, error_message: segment.error_message, vertex_count: segment.vertex_count } }); } console.log(`Invalid segment ${segment.segment_id}: ${segment.error_message}`); }); // Add invalid segments to map if we have geometry data if (invalidFeatures.length > 0) { invalidSpanLayer = L.geoJSON(invalidFeatures, { style: { color: 'orange', weight: 5, dashArray: '10,5' }, onEachFeature: (feature, layer) => { // Add hover effect layer.on('mouseover', function() { this.setStyle({ weight: 7 }); }); layer.on('mouseout', function() { this.setStyle({ weight: 5 }); }); layer.bindPopup(` 🚨 Invalid Single Span
Segment ID: ${feature.properties.segment_id}
Vertices: ${feature.properties.vertex_count}
Error: ${feature.properties.error_message} `); // Add endpoint markers if (feature.geometry && feature.geometry.coordinates) { let coords = feature.geometry.coordinates; // Handle MultiLineString - extract first linestring if (feature.geometry.type === 'MultiLineString' && coords.length > 0) { coords = coords[0]; } if (coords && coords.length >= 2) { // Start point (yellow) L.circleMarker([coords[0][1], coords[0][0]], { radius: 6, fillColor: '#fbbf24', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment Start
ID: ${feature.properties.segment_id || 'N/A'}`).addTo(map); // End point (red) L.circleMarker([coords[coords.length - 1][1], coords[coords.length - 1][0]], { radius: 6, fillColor: '#ef4444', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment End
ID: ${feature.properties.segment_id || 'N/A'}`).addTo(map); } } } }).addTo(map); } } }) .catch(err => { console.error("Single Span QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running Single Span QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); showStatus("Error running Single Span QC", true); }); } function getConnectedComponents(geojson) { const nodes = new Map(); const edges = []; console.log("Processing geojson with", geojson.features.length, "features"); // Build graph from segments for (const feat of geojson.features) { if (!feat.geometry || !feat.geometry.coordinates) { console.warn("Feature missing geometry:", feat); continue; } let coords = feat.geometry.coordinates; // Handle MultiLineString - extract first LineString if (feat.geometry.type === 'MultiLineString') { console.log("MultiLineString detected, coords:", coords); if (!coords || coords.length === 0 || !coords[0] || coords[0].length < 2) { console.warn("Invalid MultiLineString:", coords); continue; } coords = coords[0]; // Use first linestring } if (!coords || coords.length < 2) { console.warn("Invalid coords:", coords); continue; } // Verify coordinate structure if (!coords[0] || !Array.isArray(coords[0]) || coords[0].length < 2) { console.warn("Invalid start coordinate:", coords[0]); continue; } if (!coords[coords.length - 1] || !Array.isArray(coords[coords.length - 1])) { console.warn("Invalid end coordinate:", coords[coords.length - 1]); continue; } const start = coords[0].join(','); const end = coords[coords.length - 1].join(','); edges.push([start, end, feat]); if (!nodes.has(start)) nodes.set(start, []); if (!nodes.has(end)) nodes.set(end, []); nodes.get(start).push(end); nodes.get(end).push(start); } // Check if we have any edges to process if (edges.length === 0) { return { components: [], totalSegments: 0 }; } // Find all connected components using DFS const visitedNodes = new Set(); const components = []; // Helper function to perform DFS and collect all nodes in a component function dfs(startNode) { const componentNodes = new Set(); const stack = [startNode]; while (stack.length) { const node = stack.pop(); if (componentNodes.has(node)) continue; componentNodes.add(node); visitedNodes.add(node); for (const neighbor of nodes.get(node) || []) { if (!componentNodes.has(neighbor)) { stack.push(neighbor); } } } return componentNodes; } // Find all connected components for (const node of nodes.keys()) { if (!visitedNodes.has(node)) { const componentNodes = dfs(node); // Collect all edges (segments) that belong to this component const componentSegments = []; for (const [start, end, feature] of edges) { if (componentNodes.has(start) && componentNodes.has(end)) { componentSegments.push(feature); } } if (componentSegments.length > 0) { components.push({ nodes: Array.from(componentNodes), segments: componentSegments, size: componentSegments.length }); } } } // Sort components by size (largest first) components.sort((a, b) => b.size - a.size); console.log(`Found ${components.length} connected component(s):`, components.map(c => `${c.size} segments`)); return { components: components, totalSegments: edges.length }; } let disconnectedHandholeLayer; function runAccessPointQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous layer if (disconnectedHandholeLayer) { map.removeLayer(disconnectedHandholeLayer); disconnectedHandholeLayer = null; } showStatus("Running Handhole Connectivity QC..."); const segmentFeatures = []; layers.segments.eachLayer(layer => { if (layer.feature) segmentFeatures.push(layer.feature); }); const handholeFeatures = []; layers.handholes.eachLayer(layer => { if (layer.feature) handholeFeatures.push(layer.feature); }); if (!segmentFeatures.length || !handholeFeatures.length) { setButtonLoading(runQCButton, false); showStatus("Segments or handholes not loaded.", true); return; } const endpoints = []; segmentFeatures.forEach(feature => { const coords = feature.geometry.coordinates; endpoints.push(turf.point(coords[0])); endpoints.push(turf.point(coords[coords.length - 1])); }); const endpointCollection = turf.featureCollection(endpoints); const disconnectedHHs = []; handholeFeatures.forEach(hh => { const hhPoint = turf.point(hh.geometry.coordinates); const nearest = turf.nearestPoint(hhPoint, endpointCollection); const distance = turf.distance(hhPoint, nearest, { units: 'meters' }); if (distance > 1.0) { disconnectedHHs.push(hh); } }); setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (disconnectedHHs.length === 0) { qcResultDiv.textContent = "✅ All handholes are connected."; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Handhole Connectivity QC passed."); } else { qcResultDiv.textContent = `❌ Found ${disconnectedHHs.length} disconnected handhole(s).`; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); showStatus(`Found ${disconnectedHHs.length} disconnected handhole(s).`, true); disconnectedHandholeLayer = L.geoJSON(disconnectedHHs, { pointToLayer: (f, latlng) => L.circleMarker(latlng, { radius: 8, color: 'red', fillOpacity: 0.8 }), onEachFeature: (feature, layer) => { const id = feature.properties.id || 'Unknown'; const name = feature.properties.name || 'Unnamed'; layer.bindPopup(`🚨 Disconnected Handhole
ID: ${id}
Name: ${name}`); } }).addTo(map); } } /// Site Connectivity QC - NEW FUNCTIONALITY let disconnectedSitesLayer; function runSiteConnectivityQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market) { alert("Select a market first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous disconnected sites layer if (disconnectedSitesLayer) { map.removeLayer(disconnectedSitesLayer); disconnectedSitesLayer = null; } showStatus("Checking site connectivity..."); // Build query parameters let queryParams = `map_id=${market}`; if (zone) { queryParams += `&zone=${zone}`; } // Add max distance parameter (default 50m, user could modify this) queryParams += `&max_distance=50`; fetch(`/api/qc/site-connectivity?${queryParams}`) .then(res => res.json()) .then(data => { setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (data.disconnected_sites === 0) { qcResultDiv.innerHTML = `✅ All ${data.total_sites} sites are connected to the network (within 50m).`; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Site Connectivity QC completed - All sites connected."); } else { qcResultDiv.innerHTML = ` 📡 Site Connectivity Results:
• Total Sites: ${data.total_sites}
• Connected: ${data.connected_sites}
• Disconnected: ${data.disconnected_sites}
• Connectivity Rate: ${data.connectivity_rate.toFixed(1)}%
• Max Distance: ${data.max_distance_meters}m `; qcResultDiv.style.color = data.disconnected_sites > 0 ? "#ef4444" : "#10b981"; showClearQCButton(); showStatus(`Site Connectivity QC completed - ${data.disconnected_sites} sites need attention.`, data.disconnected_sites > 0); // Get disconnected sites and highlight them on map const disconnectedSites = data.results.filter(r => !r.is_connected); console.log("Disconnected sites:", disconnectedSites); // Create GeoJSON features for disconnected sites const disconnectedFeatures = []; disconnectedSites.forEach(site => { if (site.geometry) { disconnectedFeatures.push({ type: "Feature", geometry: JSON.parse(site.geometry), properties: { site_id: site.site_id, site_name: site.site_name, address: site.address, city: site.city, state: site.state, nearest_distance: site.nearest_distance, connectivity_status: site.connectivity_status } }); } }); // Add disconnected sites to map if we have geometry data if (disconnectedFeatures.length > 0) { disconnectedSitesLayer = L.geoJSON(disconnectedFeatures, { pointToLayer: (f, latlng) => L.circleMarker(latlng, { radius: 8, color: 'red', fillColor: 'red', fillOpacity: 0.8, weight: 3 }), onEachFeature: (feature, layer) => { const props = feature.properties; layer.bindPopup(` 🚨 Disconnected Site
ID: ${props.site_id}
Name: ${props.site_name || 'Unnamed'}
Address: ${props.address || 'N/A'}
Distance to Network: ${props.nearest_distance.toFixed(1)}m
Status: ${props.connectivity_status} `); } }).addTo(map); // Fit map to disconnected sites if any exist if (disconnectedSitesLayer.getBounds().isValid()) { map.fitBounds(disconnectedSitesLayer.getBounds(), { padding: [20, 20] }); } } // Log database update confirmation console.log("Site connectivity status updated in database for QGIS analysis"); } }) .catch(err => { console.error("Site Connectivity QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running Site Connectivity QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); showStatus("Error running Site Connectivity QC", true); }); } /// Aerial Endpoint QC - NEW FUNCTIONALITY let invalidAerialEndpointLayer; function runAerialEndpointQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous invalid aerial endpoint layer if (invalidAerialEndpointLayer) { map.removeLayer(invalidAerialEndpointLayer); invalidAerialEndpointLayer = null; } showStatus("Running Aerial Endpoint QC..."); fetch(`/api/qc/aerial-endpoints?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (data.invalid_segments === 0) { qcResultDiv.innerHTML = `✅ All ${data.total_aerial_segments} aerial segments have exactly one pole at each endpoint.`; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Aerial Endpoint QC completed - All segments valid."); } else { qcResultDiv.innerHTML = ` 🔌 Aerial Endpoint QC Results:
• Total Aerial Segments: ${data.total_aerial_segments}
• Valid: ${data.valid_segments}
• Invalid: ${data.invalid_segments}
• Pass Rate: ${data.pass_rate.toFixed(1)}% `; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); showStatus(`Aerial Endpoint QC completed - ${data.invalid_segments} segments need attention.`, true); // Get invalid segments and highlight them on map const invalidSegments = data.results.filter(r => !r.is_valid); console.log("Invalid aerial endpoint segments:", invalidSegments); // Create GeoJSON features for invalid segments const invalidFeatures = []; invalidSegments.forEach(segment => { if (segment.geometry) { invalidFeatures.push({ type: "Feature", geometry: segment.geometry, properties: { segment_id: segment.segment_id, error_message: segment.error_message, start_pole_count: segment.start_pole_count, end_pole_count: segment.end_pole_count, start_pole_ids: segment.start_pole_ids, end_pole_ids: segment.end_pole_ids } }); } console.log(`Invalid aerial segment ${segment.segment_id}: ${segment.error_message}`); }); // Add invalid segments to map if we have geometry data if (invalidFeatures.length > 0) { invalidAerialEndpointLayer = L.geoJSON(invalidFeatures, { style: { color: 'yellow', weight: 6, dashArray: '10,5' }, onEachFeature: (feature, layer) => { const props = feature.properties; const startPoleIds = props.start_pole_ids && props.start_pole_ids.length > 0 ? props.start_pole_ids.join(', ') : 'None'; const endPoleIds = props.end_pole_ids && props.end_pole_ids.length > 0 ? props.end_pole_ids.join(', ') : 'None'; // Add hover effect layer.on('mouseover', function() { this.setStyle({ weight: 8 }); }); layer.on('mouseout', function() { this.setStyle({ weight: 6 }); }); layer.bindPopup(` 🔌 Invalid Aerial Endpoint
Segment ID: ${props.segment_id}
Start Poles: ${props.start_pole_count} (IDs: ${startPoleIds})
End Poles: ${props.end_pole_count} (IDs: ${endPoleIds})
Issue: ${props.error_message} `); // Add endpoint markers if (feature.geometry && feature.geometry.coordinates) { let coords = feature.geometry.coordinates; // Handle MultiLineString - extract first linestring if (feature.geometry.type === 'MultiLineString' && coords.length > 0) { coords = coords[0]; } if (coords && coords.length >= 2) { // Start point (yellow) L.circleMarker([coords[0][1], coords[0][0]], { radius: 6, fillColor: '#fbbf24', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment Start
ID: ${props.segment_id}
Poles: ${props.start_pole_count}`).addTo(endpointMarkersLayer); // End point (red) L.circleMarker([coords[coords.length - 1][1], coords[coords.length - 1][0]], { radius: 6, fillColor: '#ef4444', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment End
ID: ${props.segment_id}
Poles: ${props.end_pole_count}`).addTo(endpointMarkersLayer); } } } }).addTo(map); // Show endpoint toggle since we've added endpoint markers showEndpointToggle(); // Update QC flags in database for invalid segments const invalidSegmentIds = invalidSegments.map(s => s.segment_id); fetch('/api/qc/aerial-endpoints/update-flags', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ segment_ids: invalidSegmentIds, map_id: market, zone: zone }) }) .then(res => res.json()) .then(data => { console.log("QC flags updated:", data.message); }) .catch(err => console.error("Error updating QC flags:", err)); } } }) .catch(err => { console.error("Aerial Endpoint QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running Aerial Endpoint QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); showStatus("Error running Aerial Endpoint QC", true); }); } /// Underground Endpoint QC - NEW FUNCTIONALITY let invalidUndergroundLayer; function runUndergroundEndpointQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous invalid underground layer if (invalidUndergroundLayer) { map.removeLayer(invalidUndergroundLayer); invalidUndergroundLayer = null; } showStatus("Running Underground Endpoint QC..."); fetch(`/api/qc/underground-endpoints?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (data.invalid_segments === 0) { qcResultDiv.innerHTML = `✅ All ${data.total_underground_segments} underground segments have valid endpoints.`; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Underground Endpoint QC completed - All segments valid."); } else { qcResultDiv.innerHTML = ` 🔧 Underground Endpoint QC Results:
• Total Underground Segments: ${data.total_underground_segments}
• Valid: ${data.valid_segments}
• Invalid: ${data.invalid_segments}
• Pass Rate: ${data.pass_rate.toFixed(1)}% `; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); showStatus(`Underground Endpoint QC completed - ${data.invalid_segments} segments need endpoints.`, true); // Get invalid segments and highlight them on map const invalidSegments = data.results.filter(r => !r.is_valid); console.log("Invalid underground segments:", invalidSegments); // Create GeoJSON features for invalid segments const invalidFeatures = []; invalidSegments.forEach(segment => { if (segment.geometry) { invalidFeatures.push({ type: "Feature", geometry: segment.geometry, properties: { segment_id: segment.segment_id, error_message: segment.error_message, start_endpoint: segment.start_endpoint, end_endpoint: segment.end_endpoint } }); } console.log(`Invalid underground segment ${segment.segment_id}: ${segment.error_message}`); }); // Add invalid segments to map if we have geometry data if (invalidFeatures.length > 0) { invalidUndergroundLayer = L.geoJSON(invalidFeatures, { style: { color: 'purple', weight: 6, dashArray: '8,4' }, onEachFeature: (feature, layer) => { // Add hover effect layer.on('mouseover', function() { this.setStyle({ weight: 8 }); }); layer.on('mouseout', function() { this.setStyle({ weight: 6 }); }); layer.bindPopup(` 🔧 Invalid Underground Endpoint
Segment ID: ${feature.properties.segment_id}
Start Endpoint: ${feature.properties.start_endpoint}
End Endpoint: ${feature.properties.end_endpoint}
Issue: ${feature.properties.error_message} `); // Add endpoint markers if (feature.geometry && feature.geometry.coordinates) { let coords = feature.geometry.coordinates; // Handle MultiLineString - extract first linestring if (feature.geometry.type === 'MultiLineString' && coords.length > 0) { coords = coords[0]; } if (coords && coords.length >= 2) { // Start point (yellow) L.circleMarker([coords[0][1], coords[0][0]], { radius: 6, fillColor: '#fbbf24', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment Start
ID: ${feature.properties.segment_id}
Status: ${feature.properties.start_endpoint}`).addTo(endpointMarkersLayer); // End point (red) L.circleMarker([coords[coords.length - 1][1], coords[coords.length - 1][0]], { radius: 6, fillColor: '#ef4444', color: '#000000', weight: 2, opacity: 1, fillOpacity: 1, pane: 'endpointPane' }).bindPopup(`Segment End
ID: ${feature.properties.segment_id}
Status: ${feature.properties.end_endpoint}`).addTo(endpointMarkersLayer); } } } }).addTo(map); // Show endpoint toggle since we've added endpoint markers showEndpointToggle(); // Update QC flags in database for invalid segments const invalidSegmentIds = invalidSegments.map(s => s.segment_id); fetch('/api/qc/underground-endpoints/update-flags', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ segment_ids: invalidSegmentIds, map_id: market, zone: zone }) }) .then(res => res.json()) .then(data => { console.log("QC flags updated:", data.message); }) .catch(err => console.error("Error updating QC flags:", err)); } } }) .catch(err => { console.error("Underground Endpoint QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running Underground Endpoint QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); showStatus("Error running Underground Endpoint QC", true); }); } /// Zone Containment QC let invalidZoneContainmentLayer; function runZoneContainmentQC() { const market = marketSelect.value; const zone = zoneSelect.value; if (!market || !zone) { alert("Select a market and zone first."); return; } setButtonLoading(runQCButton, true); hideStatus(); // Clear previous invalid zone containment layer if (invalidZoneContainmentLayer) { map.removeLayer(invalidZoneContainmentLayer); invalidZoneContainmentLayer = null; } showStatus("Running Zone Containment QC..."); fetch(`/api/qc/zone-containment?map_id=${market}&zone=${zone}`) .then(res => res.json()) .then(data => { setButtonLoading(runQCButton, false); statusPanel.classList.add('visible'); if (data.invalid_elements === 0) { qcResultDiv.innerHTML = `✅ All ${data.total_elements} network elements are within their assigned zones.`; qcResultDiv.style.color = "#10b981"; showClearQCButton(); showStatus("Zone Containment QC completed - All elements valid."); } else { // Build detailed breakdown by element type let typeBreakdown = ''; for (const [elementType, summary] of Object.entries(data.by_type)) { if (summary.total > 0) { const icon = summary.invalid > 0 ? '❌' : '✅'; typeBreakdown += ` ${icon} ${elementType}: ${summary.valid}/${summary.total} valid
`; } } qcResultDiv.innerHTML = ` 📍 Zone Containment QC Results:
• Total Elements: ${data.total_elements}
• Valid: ${data.valid_elements}
• Invalid: ${data.invalid_elements}
• Pass Rate: ${data.pass_rate.toFixed(1)}%

By Type:
${typeBreakdown} `; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); showStatus(`Zone Containment QC completed - ${data.invalid_elements} elements need attention.`, true); // Get invalid elements and highlight them on map const invalidElements = data.results.filter(r => !r.is_valid); console.log("Invalid zone containment elements:", invalidElements); // Create GeoJSON features for invalid elements const invalidFeatures = []; invalidElements.forEach(element => { if (element.geometry) { invalidFeatures.push({ type: "Feature", geometry: element.geometry, properties: { element_id: element.element_id, element_type: element.element_type, element_name: element.element_name, assigned_zone: element.assigned_zone, actual_zones: element.actual_zones, error_message: element.error_message } }); } console.log(`Invalid ${element.element_type} ${element.element_id}: ${element.error_message}`); }); // Add invalid elements to map if we have geometry data if (invalidFeatures.length > 0) { invalidZoneContainmentLayer = L.geoJSON(invalidFeatures, { pointToLayer: (feature, latlng) => { // Different marker colors by element type const colors = { 'site': '#ef4444', // red 'pole': '#f59e0b', // amber 'access_point': '#8b5cf6' // purple }; const color = colors[feature.properties.element_type] || '#ef4444'; return L.circleMarker(latlng, { radius: 8, fillColor: color, color: '#ffffff', weight: 2, opacity: 1, fillOpacity: 0.9 }); }, style: (feature) => { // For segments (LineString) if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiLineString') { return { color: '#fbbf24', // yellow weight: 6, dashArray: '10,5', opacity: 1 }; } }, onEachFeature: (feature, layer) => { const props = feature.properties; const assignedZone = props.assigned_zone || 'NULL/Blank'; const actualZones = props.actual_zones && props.actual_zones.length > 0 ? props.actual_zones.join(', ') : 'None'; // Add hover effect layer.on('mouseover', function() { if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiLineString') { this.setStyle({ weight: 8 }); } else { this.setStyle({ radius: 10 }); } }); layer.on('mouseout', function() { if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiLineString') { this.setStyle({ weight: 6 }); } else { this.setStyle({ radius: 8 }); } }); const elementTypeName = props.element_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()); layer.bindPopup(` 📍 Invalid Zone Containment
Type: ${elementTypeName}
ID: ${props.element_id}
${props.element_name ? `Name: ${props.element_name}
` : ''} Assigned Zone: ${assignedZone}
Actual Zones: ${actualZones}
Issue: ${props.error_message} `); } }).addTo(map); // Update QC flags in database for invalid segments const invalidSegmentIds = invalidElements .filter(e => e.element_type === 'segment') .map(e => e.element_id); if (invalidSegmentIds.length > 0) { fetch('/api/qc/zone-containment/update-flags', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ segment_ids: invalidSegmentIds, map_id: market, zone: zone }) }) .then(res => res.json()) .then(data => { console.log("QC flags updated:", data.message); }) .catch(err => console.error("Error updating QC flags:", err)); } // Zoom to invalid elements const bounds = invalidZoneContainmentLayer.getBounds(); if (bounds.isValid()) { map.fitBounds(bounds, { padding: [50, 50] }); } } } }) .catch(err => { console.error("Zone Containment QC error:", err); setButtonLoading(runQCButton, false); qcResultDiv.textContent = "Error running Zone Containment QC"; qcResultDiv.style.color = "#ef4444"; showClearQCButton(); statusPanel.classList.add('visible'); showStatus("Error running Zone Containment QC", true); }); }