Download static/app.js from ALAMDIENG/waste-prediction-api: direct link, hf CLI and curl.
- Browser
- Download file 21.5 kB
-
https://huggingface.co/spaces/ALAMDIENG/waste-prediction-api/resolve/refs%2Fpr%2F1/static/app.js
- Command line
-
hf download hf://spaces/ALAMDIENG/waste-prediction-api@refs/pr/1/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/ALAMDIENG/waste-prediction-api/resolve/refs%2Fpr%2F1/static/app.js
21.5 kB
| // Coordinates and Map Data | |
| const LOCATION_COORDINATES = { | |
| "GBK": {latitude: -6.2183, longitude: 106.8022}, | |
| "JIS": {latitude: -6.1244, longitude: 106.8622}, | |
| "Pasar Senen": {latitude: -6.1744, longitude: 106.8444}, | |
| "Gang Sempit Tambora": {latitude: -6.1500, longitude: 106.8000} | |
| }; | |
| const LOCATION_MAP_DATA = { | |
| "JIS": {coords: [-6.1244, 106.8622], label: "JIS"}, | |
| "GBK": {coords: [-6.2183, 106.8022], label: "GBK"}, | |
| "Pasar Senen": {coords: [-6.1744, 106.8444], label: "Senen"}, | |
| "Gang Sempit Tambora": {coords: [-6.1500, 106.8000], label: "Tambora"} | |
| }; | |
| const LOCATION_RADIUS = { | |
| "JIS": "1.5 km", | |
| "GBK": "2.0 km", | |
| "Pasar Senen": "1.2 km", | |
| "Gang Sempit Tambora": "0.8 km" | |
| }; | |
| const BANTARGEBANG_COORDS = [-6.3477, 106.9939]; | |
| // UI Elements | |
| const locationSelect = document.getElementById("location-select"); | |
| const modelSelect = document.getElementById("model-select"); | |
| const forecastSlider = document.getElementById("forecast-slider"); | |
| const forecastVal = document.getElementById("forecast-val"); | |
| const rainOverride = document.getElementById("rain-override"); | |
| const rainOverrideVal = document.getElementById("rain-override-val"); | |
| const eventOverride = document.getElementById("event-override"); | |
| const predictBtn = document.getElementById("predict-btn"); | |
| const exportBtn = document.getElementById("export-btn"); | |
| // Weather elements | |
| const weatherForecastText = document.getElementById("weather-forecast-text"); | |
| const weatherLocationText = document.getElementById("weather-location-text"); | |
| const weatherPrecip = document.getElementById("weather-precip"); | |
| const weatherAlert = document.getElementById("weather-alert"); | |
| const eventDescText = document.getElementById("event-desc-text"); | |
| // Stats elements | |
| const statTotalVolume = document.getElementById("stat-total-volume"); | |
| const statRiskStatus = document.getElementById("stat-risk-status"); | |
| const statTrucks = document.getElementById("stat-trucks"); | |
| // Metadata elements | |
| const statPeriodMeta = document.getElementById("stat-period-meta"); | |
| const statLocationMeta = document.getElementById("stat-location-meta"); | |
| // Composition elements | |
| const valOrganic = document.getElementById("val-organic"); | |
| const valPlastic = document.getElementById("val-plastic"); | |
| const barOrganic = document.getElementById("bar-organic"); | |
| const barPlastic = document.getElementById("bar-plastic"); | |
| // Logistics elements | |
| const logManpower = document.getElementById("log-manpower"); | |
| const logDuration = document.getElementById("log-duration"); | |
| const logEfficiency = document.getElementById("log-efficiency"); | |
| const logConfidence = document.getElementById("log-confidence"); | |
| // Timeline & Hourly | |
| const timelineList = document.getElementById("timeline-list"); | |
| const hourlySection = document.getElementById("hourly-section"); | |
| const hourlyGrid = document.getElementById("hourly-grid"); | |
| // State | |
| let selectedLocation = "JIS"; | |
| let rainValue = 0; // 0 means Auto (Open-Meteo) | |
| let map; | |
| let mapMarkers = {}; | |
| let routeLine = null; | |
| // Event Listeners for controls | |
| forecastSlider.addEventListener("input", (e) => { | |
| forecastVal.textContent = e.target.value; | |
| }); | |
| rainOverride.addEventListener("input", (e) => { | |
| const val = parseInt(e.target.value); | |
| rainValue = val; | |
| if (val === 0) { | |
| rainOverrideVal.textContent = "Auto (Open-Meteo)"; | |
| } else { | |
| rainOverrideVal.textContent = `${val} mm`; | |
| } | |
| updateRainAnimationIntensity(val); | |
| }); | |
| locationSelect.addEventListener("change", (e) => { | |
| selectedLocation = e.target.value; | |
| updateActiveMapMarker(selectedLocation); | |
| panToLocation(selectedLocation); | |
| fetchLiveWeather(selectedLocation); | |
| }); | |
| // Initialize Leaflet Map | |
| function initMap() { | |
| // Centered around Central Jakarta | |
| map = L.map('map', { | |
| zoomControl: true, | |
| attributionControl: false, | |
| maxZoom: 15, | |
| minZoom: 9 | |
| }).setView([-6.175, 106.825], 11.5); | |
| // CartoDB Dark Matter tile layer for premium dark look | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { | |
| maxZoom: 20 | |
| }).addTo(map); | |
| // Add Special Final Disposal Site (TPST Bantargebang) Marker | |
| const bantarIcon = L.divIcon({ | |
| className: 'leaflet-custom-marker bantar-marker', | |
| html: `<div class="marker-pulse" style="background:#FF9900;opacity:0.25;"></div><div class="marker-core" style="background:#FF9900;border:2px solid #FFF;"></div><div class="marker-label" style="color:#FF9900;border-color:#FF9900;">Bantargebang</div>`, | |
| iconSize: [24, 24], | |
| iconAnchor: [12, 12] | |
| }); | |
| L.marker(BANTARGEBANG_COORDS, { icon: bantarIcon }).addTo(map).bindPopup(` | |
| <div class="route-popup" style="border-left: 3px solid #FF9900;"> | |
| <h3 style="color:#FF9900;">TPST BANTARGEBANG</h3> | |
| <div>Disposal Facility (Bekasi)</div> | |
| <div>Active Capacity: <b>Calibrated</b></div> | |
| </div> | |
| `); | |
| // Add Custom Location Markers | |
| Object.keys(LOCATION_MAP_DATA).forEach(loc => { | |
| const data = LOCATION_MAP_DATA[loc]; | |
| const customIcon = L.divIcon({ | |
| className: 'leaflet-custom-marker', | |
| html: `<div class="marker-pulse"></div><div class="marker-core"></div><div class="marker-label">${data.label}</div>`, | |
| iconSize: [24, 24], | |
| iconAnchor: [12, 12] | |
| }); | |
| const marker = L.marker(data.coords, { icon: customIcon }).addTo(map); | |
| // Marker Click logic | |
| marker.on('click', () => { | |
| selectedLocation = loc; | |
| locationSelect.value = loc; | |
| updateActiveMapMarker(loc); | |
| panToLocation(loc); | |
| fetchLiveWeather(loc); | |
| runPrediction(); | |
| }); | |
| // Save reference | |
| mapMarkers[loc] = marker; | |
| }); | |
| // Initial Active Node highlight | |
| setTimeout(() => { | |
| updateActiveMapMarker(selectedLocation); | |
| }, 1000); | |
| } | |
| function updateActiveMapMarker(locName) { | |
| Object.keys(mapMarkers).forEach(loc => { | |
| const marker = mapMarkers[loc]; | |
| const el = marker.getElement(); | |
| if (el) { | |
| if (loc === locName) { | |
| el.classList.add("active"); | |
| } else { | |
| el.classList.remove("active"); | |
| } | |
| } | |
| }); | |
| } | |
| function panToLocation(locName) { | |
| const coords = LOCATION_MAP_DATA[locName]?.coords; | |
| if (coords && map) { | |
| map.panTo(coords); | |
| } | |
| } | |
| function updateMarkerRisk(locName, riskStatus) { | |
| const marker = mapMarkers[locName]; | |
| if (marker) { | |
| const el = marker.getElement(); | |
| if (el) { | |
| el.classList.remove("safe", "warning", "critical"); | |
| el.classList.add(riskStatus.toLowerCase()); | |
| } | |
| } | |
| } | |
| // Draw polyline route to TPST Bantargebang | |
| function drawTransitRoute(locName) { | |
| const startCoords = LOCATION_MAP_DATA[locName]?.coords; | |
| if (!startCoords || !map) return; | |
| if (routeLine) { | |
| map.removeLayer(routeLine); | |
| } | |
| // Cyan glowing dashed line representing logistical transit path | |
| routeLine = L.polyline([startCoords, BANTARGEBANG_COORDS], { | |
| color: '#00F0FF', | |
| weight: 3.5, | |
| opacity: 0.75, | |
| dashArray: '8, 8', | |
| className: 'glowing-route' | |
| }).addTo(map); | |
| // Hardcoded logistics profile distance mappings | |
| const distanceMap = { | |
| "JIS": "41.2 km", | |
| "GBK": "38.5 km", | |
| "Pasar Senen": "34.8 km", | |
| "Gang Sempit Tambora": "43.5 km" | |
| }; | |
| const timeMap = { | |
| "JIS": "1.5 Hours", | |
| "GBK": "1.8 Hours", | |
| "Pasar Senen": "1.4 Hours", | |
| "Gang Sempit Tambora": "2.1 Hours" | |
| }; | |
| routeLine.bindPopup(` | |
| <div class="route-popup"> | |
| <h3>LOGISTICS DISPATCH ROUTE</h3> | |
| <div>Start: <b>${locName}</b></div> | |
| <div>Destination: <b>TPST Bantargebang</b></div> | |
| <div>Transit Distance: <b class="highlight">${distanceMap[locName]}</b></div> | |
| <div>Est. Travel Time: <b class="highlight">${timeMap[locName]}</b></div> | |
| </div> | |
| `).openPopup(); | |
| // Automatically zoom/fit bounds to show both the collection point and Bantargebang nicely | |
| map.fitBounds([startCoords, BANTARGEBANG_COORDS], { | |
| padding: [60, 60] | |
| }); | |
| } | |
| // Fetch Live Weather from Open-Meteo | |
| async function fetchLiveWeather(loc) { | |
| const coord = LOCATION_COORDINATES[loc]; | |
| if (!coord) return; | |
| weatherForecastText.textContent = "Fetching..."; | |
| weatherPrecip.textContent = "0.0 mm"; | |
| weatherAlert.textContent = "Checking..."; | |
| const url = `https://api.open-meteo.com/v1/forecast?latitude=${coord.latitude}&longitude=${coord.longitude}¤t_weather=true&daily=precipitation_sum&timezone=Asia/Jakarta&past_days=2`; | |
| try { | |
| const response = await fetch(url); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| const temp = data.current_weather.temperature; | |
| const wind = data.current_weather.windspeed; | |
| const code = data.current_weather.weathercode; | |
| // Set precip sum for today | |
| const dailyData = data.daily || {}; | |
| const precipList = dailyData.precipitation_sum || []; | |
| // past_days=2 means indices: 0 (H-2), 1 (H-1), 2 (H0) | |
| const precipToday = precipList[2] || 0; | |
| let cond = "Cloudy"; | |
| if (code === 0) cond = "Clear Sky"; | |
| else if (code > 0 && code < 4) cond = "Partly Cloudy"; | |
| else if (code >= 51 && code <= 67) cond = "Rainy"; | |
| else if (code >= 80 && code <= 82) cond = "Showers"; | |
| weatherForecastText.textContent = `${temp}°C - ${cond}`; | |
| weatherLocationText.textContent = `${loc} coordinates`; | |
| weatherPrecip.textContent = `${precipToday.toFixed(1)} mm`; | |
| if (precipToday > 30) { | |
| weatherAlert.textContent = "HEAVY RAIN 🟡"; | |
| weatherAlert.className = "highlight text-warning"; | |
| } else if (precipToday > 50) { | |
| weatherAlert.textContent = "FLOOD DANGER 🔴"; | |
| weatherAlert.className = "highlight text-red"; | |
| } else { | |
| weatherAlert.textContent = "Normal conditions"; | |
| weatherAlert.className = "highlight"; | |
| } | |
| } else { | |
| throw new Error("HTTP Error"); | |
| } | |
| } catch (err) { | |
| weatherForecastText.textContent = "Weather Unavailable"; | |
| weatherPrecip.textContent = "N/A"; | |
| weatherAlert.textContent = "Error fetching"; | |
| } | |
| } | |
| // Run prediction calling FastAPI backend | |
| async function runPrediction() { | |
| predictBtn.disabled = true; | |
| predictBtn.querySelector(".btn-text").textContent = "PROCESSING FORECAST..."; | |
| const payload = { | |
| forecast_days: parseInt(forecastSlider.value), | |
| rainfall_mm: parseFloat(rainValue), | |
| event_scale: parseInt(eventOverride.value), | |
| location: selectedLocation, | |
| model_type: modelSelect.value, | |
| granularity: forecastSlider.value <= 7 ? "hourly" : "daily" // auto hourly for short horizons | |
| }; | |
| try { | |
| const response = await fetch("/api/v1/predict", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (response.ok) { | |
| const resData = await response.json(); | |
| updateDashboardData(resData.data, resData.confidence_score, resData.message); | |
| } else { | |
| alert("Prediction failed. Make sure the API server is running."); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| alert("Network error connecting to Waste Intelligence API."); | |
| } finally { | |
| predictBtn.disabled = false; | |
| predictBtn.querySelector(".btn-text").textContent = "RUN PREDICTION"; | |
| } | |
| } | |
| function updateDashboardData(data, confScore, message) { | |
| const results = data.prediction_results; | |
| if (results.length === 0) return; | |
| // Calculate sum of tonnage | |
| const totalVolume = results.reduce((acc, curr) => acc + curr.total_volume_ton, 0); | |
| const avgVolume = totalVolume / results.length; | |
| // Update main cards | |
| statTotalVolume.innerHTML = `${totalVolume.toFixed(2)} <span class="unit">Tons</span>`; | |
| // Determine overall risk | |
| let maxRisk = "SAFE"; | |
| results.forEach(r => { | |
| if (r.risk_status === "CRITICAL") maxRisk = "CRITICAL"; | |
| else if (r.risk_status === "WARNING" && maxRisk !== "CRITICAL") maxRisk = "WARNING"; | |
| }); | |
| statRiskStatus.textContent = maxRisk; | |
| statRiskStatus.className = `card-value status-badge ${maxRisk.toLowerCase()}`; | |
| // Update map marker risk status dynamically | |
| updateMarkerRisk(selectedLocation, maxRisk); | |
| // Draw active logistics route to TPST Bantargebang | |
| drawTransitRoute(selectedLocation); | |
| statTrucks.innerHTML = `${data.logistics_plan.trucks_needed} <span class="unit">Trucks (5T)</span>`; | |
| // Update metadata labels (Prediction Period & Target Location with Radius) | |
| const startDateStr = results[0].date; | |
| const endDateStr = results[results.length - 1].date; | |
| statPeriodMeta.textContent = `Period: ${startDateStr} to ${endDateStr}`; | |
| statLocationMeta.textContent = `${selectedLocation} (Radius ${LOCATION_RADIUS[selectedLocation]})`; | |
| // Composition Breakdown | |
| const totalOrganic = results.reduce((acc, curr) => acc + curr.organic_waste_ton, 0); | |
| const totalPlastic = results.reduce((acc, curr) => acc + curr.plastic_waste_ton, 0); | |
| valOrganic.textContent = `${totalOrganic.toFixed(2)} Ton`; | |
| valPlastic.textContent = `${totalPlastic.toFixed(2)} Ton`; | |
| const organicPercentage = totalVolume > 0 ? (totalOrganic / totalVolume) * 100 : 0; | |
| const plasticPercentage = totalVolume > 0 ? (totalPlastic / totalVolume) * 100 : 0; | |
| barOrganic.style.width = `${organicPercentage}%`; | |
| barPlastic.style.width = `${plasticPercentage}%`; | |
| // Logistics plan | |
| logManpower.textContent = `${data.logistics_plan.manpower} Crew`; | |
| logDuration.textContent = `${data.logistics_plan.estimated_duration_hours.toFixed(1)} Hours`; | |
| logEfficiency.textContent = data.logistics_plan.efficiency_rate; | |
| logConfidence.textContent = `${(confScore * 100).toFixed(1)}%`; | |
| // Event Info banner | |
| const eventDay = results.find(r => r.event_info !== null); | |
| if (eventDay) { | |
| eventDescText.innerHTML = `⚠️ <strong>${eventDay.event_info}</strong> on ${eventDay.date}. Heavy crowd expected near site.`; | |
| document.getElementById("event-box").style.borderColor = "var(--red)"; | |
| } else { | |
| eventDescText.textContent = "No major public events scheduled for this location in the forecast window."; | |
| document.getElementById("event-box").style.borderColor = "var(--yellow)"; | |
| } | |
| // Daily timeline breakdown cards | |
| timelineList.innerHTML = ""; | |
| results.forEach(day => { | |
| const card = document.createElement("div"); | |
| card.className = "timeline-card"; | |
| const dateObj = new Date(day.date); | |
| const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' }); | |
| const displayDate = `${dayName}, ${dateObj.getDate()} ${dateObj.toLocaleString('en-US', { month: 'short' })}`; | |
| card.innerHTML = ` | |
| <span class="timeline-date">${displayDate}</span> | |
| <span class="timeline-vol">${day.total_volume_ton.toFixed(1)} T</span> | |
| <span class="timeline-status ${day.risk_status.toLowerCase()}">${day.risk_status}</span> | |
| `; | |
| timelineList.appendChild(card); | |
| }); | |
| // Hourly Risk Heatmap | |
| const hourlyDay = results[0]; // show hourly for first day if requested | |
| if (hourlyDay && hourlyDay.hourly_breakdown) { | |
| hourlySection.style.display = "block"; | |
| hourlyGrid.innerHTML = ""; | |
| hourlyDay.hourly_breakdown.forEach(hour => { | |
| const cell = document.createElement("div"); | |
| cell.className = "hourly-cell"; | |
| let intensityClass = "low"; | |
| if (hour.risk_indicator === "MEDIUM") intensityClass = "medium"; | |
| else if (hour.risk_indicator === "HIGH") intensityClass = "high"; | |
| cell.innerHTML = ` | |
| <div class="cell-block ${intensityClass}" title="Vol: ${hour.estimated_volume_ton} Ton - Risk: ${hour.risk_indicator}"></div> | |
| <span class="cell-time">${hour.hour}</span> | |
| `; | |
| hourlyGrid.appendChild(cell); | |
| }); | |
| } else { | |
| hourlySection.style.display = "none"; | |
| } | |
| } | |
| // Request CSV from Backend API and download it | |
| async function runExport() { | |
| exportBtn.disabled = true; | |
| exportBtn.querySelector(".btn-text").textContent = "EXPORTING..."; | |
| const payload = { | |
| forecast_days: parseInt(forecastSlider.value), | |
| rainfall_mm: parseFloat(rainValue), | |
| event_scale: parseInt(eventOverride.value), | |
| location: selectedLocation, | |
| model_type: modelSelect.value, | |
| granularity: forecastSlider.value <= 7 ? "hourly" : "daily" | |
| }; | |
| try { | |
| const response = await fetch("/api/v1/predict/csv", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `waste_forecast_${selectedLocation.replace(/\s+/g, "_")}_${forecastSlider.value}d.csv`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| window.URL.revokeObjectURL(url); | |
| } else { | |
| alert("CSV export failed. Ensure the server is online."); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| alert("Network error connecting to Export API."); | |
| } finally { | |
| exportBtn.disabled = false; | |
| exportBtn.querySelector(".btn-text").textContent = "EXPORT CSV"; | |
| } | |
| } | |
| predictBtn.addEventListener("click", runPrediction); | |
| exportBtn.addEventListener("click", runExport); | |
| // Initial loading setup | |
| window.addEventListener("DOMContentLoaded", () => { | |
| initMap(); | |
| fetchLiveWeather("JIS"); | |
| // Run initial prediction after models load | |
| setTimeout(runPrediction, 1000); | |
| }); | |
| // ========================================== | |
| // BACKGROUND CANVAS: INTERACTIVE RAIN EFFECT | |
| // ========================================== | |
| const canvas = document.getElementById("rain-canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| let width = canvas.width = window.innerWidth; | |
| let height = canvas.height = window.innerHeight; | |
| window.addEventListener("resize", () => { | |
| width = canvas.width = window.innerWidth; | |
| height = canvas.height = window.innerHeight; | |
| }); | |
| let drops = []; | |
| let particles = []; | |
| let maxPrecip = 0; // Current precipitation override | |
| function updateRainAnimationIntensity(precipVal) { | |
| maxPrecip = precipVal; | |
| } | |
| // Particle class for normal state (no rain) | |
| class DataParticle { | |
| constructor() { | |
| this.reset(); | |
| } | |
| reset() { | |
| this.x = Math.random() * width; | |
| this.y = Math.random() * height; | |
| this.size = Math.random() * 2 + 1; | |
| this.speedX = Math.random() * 0.4 - 0.2; | |
| this.speedY = Math.random() * -0.5 - 0.2; | |
| this.alpha = Math.random() * 0.5 + 0.1; | |
| } | |
| update() { | |
| this.x += this.speedX; | |
| this.y += this.speedY; | |
| if (this.y < 0 || this.x < 0 || this.x > width) { | |
| this.reset(); | |
| this.y = height; | |
| } | |
| } | |
| draw() { | |
| ctx.fillStyle = `rgba(0, 240, 255, ${this.alpha})`; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| // Rain Drop class for rainy state | |
| class RainDrop { | |
| constructor() { | |
| this.reset(); | |
| } | |
| reset() { | |
| this.x = Math.random() * width; | |
| this.y = Math.random() * -100 - 10; | |
| this.length = Math.random() * 15 + 10; | |
| this.speed = Math.random() * 12 + 15; | |
| this.weight = Math.random() * 1 + 0.5; | |
| this.alpha = Math.random() * 0.3 + 0.1; | |
| } | |
| update() { | |
| this.y += this.speed; | |
| if (this.y > height) { | |
| this.reset(); | |
| } | |
| } | |
| draw() { | |
| ctx.strokeStyle = `rgba(0, 240, 255, ${this.alpha})`; | |
| ctx.lineWidth = this.weight; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.x, this.y); | |
| ctx.lineTo(this.x + (maxPrecip * 0.05), this.y + this.length); | |
| ctx.stroke(); | |
| } | |
| } | |
| // Initialize particles and rain drops | |
| for (let i = 0; i < 60; i++) { | |
| particles.push(new DataParticle()); | |
| } | |
| for (let i = 0; i < 150; i++) { | |
| drops.push(new RainDrop()); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, width, height); | |
| if (maxPrecip === 0) { | |
| // Normal floating data particles | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(); | |
| }); | |
| } else { | |
| // Cyber rain drops falling | |
| // Number of raindrops drawn depends on the rain scale override | |
| const activeCount = Math.min(Math.floor(maxPrecip * 1.5), 150); | |
| for (let i = 0; i < activeCount; i++) { | |
| drops[i].update(); | |
| drops[i].draw(); | |
| } | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| animate(); | |