Spaces:
Sleeping
Sleeping
| document.addEventListener('DOMContentLoaded', function() { | |
| // --- Global Variables --- | |
| let map; | |
| let currentMarker; | |
| let crimeTypeChartInstance = null; | |
| let crimeTrendChartInstance = null; | |
| // --- DOM Elements --- | |
| const liveMapTab = document.getElementById('liveMapTab'); | |
| const modelAnalyticsTab = document.getElementById('modelAnalyticsTab'); | |
| const liveMapContent = document.getElementById('liveMapContent'); | |
| const modelAnalyticsContent = document.getElementById('modelAnalyticsContent'); | |
| const analysisCardContainer = document.getElementById('analysis-card-container'); | |
| const mapElement = document.getElementById('map'); | |
| // --- Tab Switching Logic --- | |
| function showContent(element) { | |
| element.style.display = 'flex'; | |
| void element.offsetWidth; // Trigger reflow for transition | |
| element.classList.remove('hidden'); | |
| element.classList.add('active'); | |
| } | |
| function hideContent(element) { | |
| element.classList.add('hidden'); | |
| element.classList.remove('active'); | |
| setTimeout(() => { | |
| if (!element.classList.contains('active')) { | |
| element.style.display = 'none'; | |
| } | |
| }, 500); // Match CSS transition duration | |
| } | |
| liveMapTab.addEventListener('click', () => { | |
| if (!liveMapTab.classList.contains('active')) { | |
| liveMapTab.classList.add('active'); | |
| modelAnalyticsTab.classList.remove('active'); | |
| hideContent(modelAnalyticsContent); | |
| showContent(liveMapContent); | |
| if (map) { | |
| map.resize(); | |
| } | |
| } | |
| }); | |
| modelAnalyticsTab.addEventListener('click', () => { | |
| if (!modelAnalyticsTab.classList.contains('active')) { | |
| modelAnalyticsTab.classList.add('active'); | |
| liveMapTab.classList.remove('active'); | |
| hideContent(liveMapContent); | |
| showContent(modelAnalyticsContent); | |
| // Notify charts.js that its tab is active | |
| document.dispatchEvent(new Event('analyticsTabActivated')); | |
| } | |
| }); | |
| // --- Map Initialization --- | |
| function initializeMap() { | |
| if (!mapElement) return; | |
| mapboxgl.accessToken = 'pk.eyJ1IjoibmF1bWFua2hhbmtoYW4iLCJhIjoiY21qZ3RuejdyMDRuMzNmc2xvemZsY2ZueiJ9.om5voCTqgCN2xFZ6_CVDsg'; // Replace with your token if needed | |
| map = new mapboxgl.Map({ | |
| container: 'map', | |
| style: 'mapbox://styles/mapbox/dark-v11', | |
| center: [-87.6298, 41.8781], // Chicago | |
| zoom: 12, | |
| pitch: 55, | |
| bearing: -20, | |
| }); | |
| map.addControl(new mapboxgl.NavigationControl(), 'top-right'); | |
| map.on('load', () => { | |
| const layers = map.getStyle().layers; | |
| const labelLayerId = layers.find(layer => layer.type === 'symbol' && layer.layout['text-field'])?.id; | |
| map.addLayer({ | |
| 'id': '3d-buildings', | |
| 'source': 'composite', | |
| 'source-layer': 'building', | |
| 'filter': ['==', 'extrude', 'true'], | |
| 'type': 'fill-extrusion', | |
| 'minzoom': 15, | |
| 'paint': { | |
| 'fill-extrusion-color': '#aaa', | |
| 'fill-extrusion-height': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'height']], | |
| 'fill-extrusion-base': ['interpolate', ['linear'], ['zoom'], 15, 0, 15.05, ['get', 'min_height']], | |
| 'fill-extrusion-opacity': 0.6 | |
| } | |
| }, labelLayerId); | |
| }); | |
| map.on('click', handleMapClick); | |
| } | |
| // --- Map and Prediction Logic --- | |
| function handleMapClick(e) { | |
| if (currentMarker) { | |
| currentMarker.remove(); | |
| } | |
| currentMarker = new mapboxgl.Marker({ color: '#FF0000' }) | |
| .setLngLat(e.lngLat) | |
| .addTo(map); | |
| map.flyTo({ center: e.lngLat, zoom: 15 }); | |
| fetchAndDisplayAnalysis(e.lngLat.lat, e.lngLat.lng); | |
| } | |
| async function fetchAndDisplayAnalysis(latitude, longitude) { | |
| if (!analysisCardContainer) return; | |
| analysisCardContainer.innerHTML = ` | |
| <div class="analysis-card loading"> | |
| <h3>Analyzing Location...</h3> | |
| <p>Latitude: ${latitude.toFixed(4)}, Longitude: ${longitude.toFixed(4)}</p> | |
| <div class="loader"></div> | |
| </div>`; | |
| try { | |
| const response = await fetch('/predict_crime', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ latitude, longitude }) | |
| }); | |
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
| const data = await response.json(); | |
| renderAnalysisCard(data, latitude, longitude); | |
| } catch (error) { | |
| console.error('Error fetching crime prediction:', error); | |
| analysisCardContainer.innerHTML = ` | |
| <div class="analysis-card error"> | |
| <h3>Analysis Failed</h3> | |
| <p>Could not fetch crime prediction data.</p> | |
| <p>${error.message}</p> | |
| </div>`; | |
| } | |
| } | |
| function renderAnalysisCard(data, latitude, longitude) { | |
| const { prediction, risk_level, historical_insights } = data; | |
| const confidence = Math.max(0, 100 - (prediction * 10)).toFixed(0) + '% Confidence (in safety)'; | |
| let explanation = 'Low predicted crime intensity. This area appears relatively safe.'; | |
| if (risk_level === "High") { | |
| explanation = 'High predicted crime intensity. Consider exercising caution.'; | |
| } else if (risk_level === "Moderate") { | |
| explanation = 'Moderate predicted crime intensity. Be aware of your surroundings.'; | |
| } | |
| analysisCardContainer.innerHTML = ` | |
| <div class="analysis-card"> | |
| <h3>Crime Risk Analysis</h3> | |
| <p><b>Location:</b> ${latitude.toFixed(4)}, ${longitude.toFixed(4)}</p> | |
| <p><b>Predicted Crime Intensity:</b> ${prediction.toFixed(2)}</p> | |
| <p><b>Risk Level:</b> <span class="risk-level-${risk_level.toLowerCase()}">${risk_level}</span></p> | |
| <p><b>Confidence:</b> ${confidence}</p> | |
| <p><b>Explanation:</b> ${explanation}</p> | |
| </div> | |
| <div class="analysis-card"> | |
| <h4>Historical Data (12 Mo.)</h4> | |
| <p>Total Crimes: ${historical_insights.total_crimes}</p> | |
| <div class="chart-container"> | |
| <h5>Top Crime Types</h5> | |
| <canvas id="crimeTypeChart"></canvas> | |
| </div> | |
| <div class="chart-container"> | |
| <h5>Crimes per Month</h5> | |
| <canvas id="crimeTrendChart"></canvas> | |
| </div> | |
| </div>`; | |
| renderAnalysisCharts(historical_insights); | |
| } | |
| function renderAnalysisCharts({ crime_counts, crime_trends }) { | |
| // Destroy previous charts to prevent conflicts | |
| if (crimeTypeChartInstance) crimeTypeChartInstance.destroy(); | |
| if (crimeTrendChartInstance) crimeTrendChartInstance.destroy(); | |
| const crimeTypeCtx = document.getElementById('crimeTypeChart'); | |
| if (crimeTypeCtx && crime_counts && Object.keys(crime_counts).length > 0) { | |
| crimeTypeChartInstance = new Chart(crimeTypeCtx, createChartConfig('pie', crime_counts)); | |
| } | |
| const crimeTrendCtx = document.getElementById('crimeTrendChart'); | |
| if (crimeTrendCtx && crime_trends && Object.keys(crime_trends).length > 0) { | |
| crimeTrendChartInstance = new Chart(crimeTrendCtx, createChartConfig('line', crime_trends)); | |
| } | |
| } | |
| function createChartConfig(type, data) { | |
| const labels = Object.keys(data); | |
| const values = Object.values(data); | |
| const baseOptions = { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { legend: { labels: { color: 'var(--text-light)' } } }, | |
| scales: { | |
| x: { ticks: { color: 'var(--text-light)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, | |
| y: { ticks: { color: 'var(--text-light)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } } | |
| } | |
| }; | |
| if (type === 'pie') { | |
| return { | |
| type: 'pie', | |
| data: { | |
| labels, | |
| datasets: [{ | |
| data: values, | |
| backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF', '#7A7A7A', '#2ECC71', '#E74C3C'], | |
| borderColor: 'var(--bg-dark)', | |
| }] | |
| }, | |
| options: { ...baseOptions, plugins: { legend: { position: 'right', ...baseOptions.plugins.legend } } } | |
| }; | |
| } | |
| if (type === 'line') { | |
| return { | |
| type: 'line', | |
| data: { | |
| labels, | |
| datasets: [{ | |
| label: 'Number of Crimes', | |
| data: values, | |
| borderColor: '#4BC0C0', | |
| backgroundColor: 'rgba(75, 192, 192, 0.2)', | |
| fill: true, | |
| tension: 0.3 | |
| }] | |
| }, | |
| options: { ...baseOptions, plugins: { legend: { display: false } } } | |
| }; | |
| } | |
| } | |
| // --- Initial Setup --- | |
| initializeMap(); | |
| liveMapContent.style.display = 'flex'; | |
| modelAnalyticsContent.style.display = 'none'; | |
| }); |