Download static/index.html from john5050/ProjectX: direct link, hf CLI and curl.
- Browser
- Download file 24 kB
-
https://huggingface.co/spaces/john5050/ProjectX/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/john5050/ProjectX/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/john5050/ProjectX/resolve/main/static/index.html
24 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>ConstructScan β Illegal Construction Detector</title> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" | |
| rel="stylesheet" /> | |
| <style> | |
| :root { | |
| --bg: #0a0a0a; | |
| --surface: #111; | |
| --surface2: #1a1a1a; | |
| --border: #2a2a2a; | |
| --accent: #ff3c00; | |
| --text: #f0ede8; | |
| --muted: #666; | |
| --safe: #00e676; | |
| --danger: #ff3c00; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: 'DM Sans', sans-serif; | |
| min-height: 100vh; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 999; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); | |
| } | |
| header { | |
| border-bottom: 1px solid var(--border); | |
| padding: 1.2rem 2.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| position: sticky; | |
| top: 0; | |
| background: rgba(10, 10, 10, 0.96); | |
| backdrop-filter: blur(12px); | |
| z-index: 100; | |
| } | |
| .logo { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 1.7rem; | |
| letter-spacing: .1em; | |
| } | |
| .logo span { | |
| color: var(--accent); | |
| } | |
| .badge { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .65rem; | |
| letter-spacing: .15em; | |
| text-transform: uppercase; | |
| padding: .3rem .7rem; | |
| border: 1px solid var(--border); | |
| color: var(--muted); | |
| } | |
| main { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 3.5rem 2rem; | |
| } | |
| .hero { | |
| margin-bottom: 2.5rem; | |
| } | |
| .hero h1 { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: clamp(2.8rem, 7vw, 6rem); | |
| line-height: .92; | |
| letter-spacing: .02em; | |
| margin-bottom: 1rem; | |
| } | |
| .hero h1 em { | |
| font-style: normal; | |
| color: var(--accent); | |
| display: block; | |
| } | |
| .hero p { | |
| font-size: .95rem; | |
| color: var(--muted); | |
| max-width: 460px; | |
| line-height: 1.7; | |
| font-weight: 300; | |
| } | |
| /* Tabs */ | |
| .tabs { | |
| display: flex; | |
| gap: 1px; | |
| margin-bottom: 1px; | |
| background: var(--border); | |
| } | |
| .tab { | |
| flex: 1; | |
| padding: .9rem 1.5rem; | |
| background: var(--surface2); | |
| cursor: pointer; | |
| font-family: 'DM Mono', monospace; | |
| font-size: .7rem; | |
| letter-spacing: .15em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| border: none; | |
| transition: all .2s; | |
| text-align: center; | |
| } | |
| .tab.active { | |
| background: var(--surface); | |
| color: var(--text); | |
| border-bottom: 2px solid var(--accent); | |
| } | |
| .tab:hover:not(.active) { | |
| color: var(--text); | |
| background: #161616; | |
| } | |
| .tab-panel { | |
| display: none; | |
| } | |
| .tab-panel.active { | |
| display: block; | |
| } | |
| /* Upload tab */ | |
| .upload-zone { | |
| border: 1px dashed var(--border); | |
| padding: 2.5rem 2rem; | |
| text-align: center; | |
| cursor: pointer; | |
| background: var(--surface); | |
| position: relative; | |
| transition: all .2s; | |
| margin-bottom: 1rem; | |
| } | |
| .upload-zone:hover, | |
| .upload-zone.drag-over { | |
| border-color: var(--accent); | |
| background: #1a0906; | |
| } | |
| .upload-zone input { | |
| position: absolute; | |
| inset: 0; | |
| opacity: 0; | |
| cursor: pointer; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .upload-zone svg { | |
| width: 40px; | |
| height: 40px; | |
| margin: 0 auto .8rem; | |
| opacity: .35; | |
| display: block; | |
| } | |
| .upload-zone h3 { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .8rem; | |
| letter-spacing: .1em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: .4rem; | |
| } | |
| .upload-zone p { | |
| font-size: .75rem; | |
| color: #444; | |
| } | |
| #preview-wrap { | |
| display: none; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| } | |
| #preview-img { | |
| width: 100%; | |
| max-height: 280px; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .preview-tag { | |
| position: absolute; | |
| top: .8rem; | |
| left: .8rem; | |
| font-family: 'DM Mono', monospace; | |
| font-size: .6rem; | |
| letter-spacing: .15em; | |
| text-transform: uppercase; | |
| background: rgba(0, 0, 0, .85); | |
| padding: .25rem .55rem; | |
| color: var(--muted); | |
| } | |
| /* Map tab */ | |
| .map-instructions { | |
| padding: .8rem 1rem; | |
| background: var(--surface2); | |
| border-left: 3px solid var(--accent); | |
| font-family: 'DM Mono', monospace; | |
| font-size: .7rem; | |
| letter-spacing: .08em; | |
| color: var(--muted); | |
| margin-bottom: 1rem; | |
| } | |
| .map-instructions strong { | |
| color: var(--text); | |
| } | |
| #map-container { | |
| position: relative; | |
| margin-bottom: 1rem; | |
| } | |
| #map { | |
| width: 100%; | |
| height: 480px; | |
| background: #111; | |
| } | |
| .map-crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| z-index: 10; | |
| } | |
| .map-crosshair svg { | |
| width: 40px; | |
| height: 40px; | |
| filter: drop-shadow(0 0 4px rgba(0, 0, 0, .8)); | |
| } | |
| .map-info-bar { | |
| display: flex; | |
| gap: 1px; | |
| background: var(--border); | |
| margin-bottom: 1rem; | |
| } | |
| .map-info-cell { | |
| flex: 1; | |
| background: var(--surface2); | |
| padding: .8rem 1rem; | |
| } | |
| .map-info-cell .k { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .6rem; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: .3rem; | |
| } | |
| .map-info-cell .v { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .85rem; | |
| color: var(--text); | |
| } | |
| .zoom-note { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .65rem; | |
| color: #444; | |
| letter-spacing: .08em; | |
| margin-bottom: 1rem; | |
| } | |
| /* Shared */ | |
| .btn { | |
| width: 100%; | |
| padding: 1.1rem; | |
| background: var(--accent); | |
| color: #fff; | |
| border: none; | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 1.3rem; | |
| letter-spacing: .15em; | |
| cursor: pointer; | |
| transition: background .2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: .7rem; | |
| } | |
| .btn:hover { | |
| background: #e03500; | |
| } | |
| .btn:disabled { | |
| background: #333; | |
| color: #555; | |
| cursor: not-allowed; | |
| } | |
| .btn.secondary { | |
| background: var(--surface2); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| margin-bottom: 1rem; | |
| } | |
| .btn.secondary:hover { | |
| background: #222; | |
| } | |
| .spinner { | |
| width: 18px; | |
| height: 18px; | |
| border: 2px solid rgba(255, 255, 255, .25); | |
| border-top-color: #fff; | |
| border-radius: 50%; | |
| animation: spin .7s linear infinite; | |
| display: none; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| #error { | |
| display: none; | |
| margin-top: .8rem; | |
| padding: .9rem 1.2rem; | |
| background: #1a0500; | |
| border-left: 3px solid var(--danger); | |
| font-family: 'DM Mono', monospace; | |
| font-size: .75rem; | |
| color: var(--danger); | |
| } | |
| /* Map preview */ | |
| #map-preview-wrap { | |
| display: none; | |
| margin-bottom: 1rem; | |
| position: relative; | |
| } | |
| #map-preview-img { | |
| width: 100%; | |
| max-height: 280px; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| /* Results */ | |
| #results { | |
| display: none; | |
| margin-top: 2.5rem; | |
| animation: fadeUp .45s ease forwards; | |
| } | |
| @keyframes fadeUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(18px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .verdict-bar { | |
| padding: 1.8rem 2rem; | |
| margin-bottom: 1px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 1.5rem; | |
| flex-wrap: wrap; | |
| } | |
| .verdict-bar.danger { | |
| background: #1a0400; | |
| border-left: 4px solid var(--danger); | |
| } | |
| .verdict-bar.safe { | |
| background: #001508; | |
| border-left: 4px solid var(--safe); | |
| } | |
| .verdict-label { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: clamp(1.4rem, 3.5vw, 2.5rem); | |
| letter-spacing: .04em; | |
| } | |
| .verdict-bar.danger .verdict-label { | |
| color: var(--danger); | |
| } | |
| .verdict-bar.safe .verdict-label { | |
| color: var(--safe); | |
| } | |
| .verdict-meta { | |
| display: flex; | |
| gap: 2rem; | |
| } | |
| .vmeta-item { | |
| text-align: right; | |
| } | |
| .vmeta-item .num { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 2.2rem; | |
| line-height: 1; | |
| } | |
| .vmeta-item .key { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .6rem; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| .grid3 { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 1px; | |
| background: var(--border); | |
| margin-bottom: 1px; | |
| } | |
| .img-panel { | |
| background: var(--surface); | |
| overflow: hidden; | |
| } | |
| .panel-label { | |
| padding: .6rem 1rem; | |
| font-family: 'DM Mono', monospace; | |
| font-size: .6rem; | |
| letter-spacing: .14em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: .4rem; | |
| } | |
| .dot { | |
| width: 5px; | |
| height: 5px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| } | |
| .dot.g { | |
| background: var(--safe); | |
| } | |
| .img-panel img { | |
| width: 100%; | |
| aspect-ratio: 1; | |
| object-fit: cover; | |
| display: block; | |
| } | |
| .stats4 { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 1px; | |
| background: var(--border); | |
| } | |
| .stat { | |
| background: var(--surface2); | |
| padding: 1.3rem 1.5rem; | |
| } | |
| .stat .v { | |
| font-family: 'Bebas Neue', sans-serif; | |
| font-size: 1.8rem; | |
| color: var(--text); | |
| line-height: 1; | |
| margin-bottom: .25rem; | |
| } | |
| .stat .k { | |
| font-family: 'DM Mono', monospace; | |
| font-size: .6rem; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| @media(max-width:700px) { | |
| header { | |
| padding: 1rem; | |
| } | |
| main { | |
| padding: 2rem 1rem; | |
| } | |
| .grid3, | |
| .stats4 { | |
| grid-template-columns: 1fr; | |
| } | |
| .verdict-meta { | |
| gap: 1rem; | |
| } | |
| #map { | |
| height: 320px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo">Construct<span>Scan</span></div> | |
| <div class="badge">EfficientNet-B3 Β· U-Net Β· RTX 4050</div> | |
| </header> | |
| <main> | |
| <div class="hero"> | |
| <h1>Detect <em>Illegal</em> Construction</h1> | |
| <p>Upload a satellite image or pick any location on the map. The model segments buildings and flags unauthorized | |
| construction.</p> | |
| </div> | |
| <!-- Tabs --> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('upload')">β¬ Upload Image</button> | |
| <button class="tab" onclick="switchTab('map')">π Pick on Map</button> | |
| </div> | |
| <!-- Upload Panel --> | |
| <div class="tab-panel active" id="panel-upload"> | |
| <div class="upload-zone" id="upload-zone"> | |
| <input type="file" id="file-input" accept="image/*" /> | |
| <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5"> | |
| <rect x="4" y="4" width="40" height="40" rx="2" /> | |
| <path d="M24 32V16M16 24l8-8 8 8" /> | |
| </svg> | |
| <h3>Drop image here or click to upload</h3> | |
| <p>Satellite / aerial imagery β JPG, PNG, TIFF</p> | |
| </div> | |
| <div id="preview-wrap"> | |
| <img id="preview-img" src="" alt="Preview" /> | |
| <span class="preview-tag">Input</span> | |
| </div> | |
| </div> | |
| <!-- Map Panel --> | |
| <div class="tab-panel" id="panel-map"> | |
| <div class="map-instructions"> | |
| <strong>How to use:</strong> Navigate to any location β zoom in to building level β click <strong>Capture This | |
| View</strong> | |
| </div> | |
| <div id="map-container"> | |
| <div id="map"></div> | |
| <div class="map-crosshair"> | |
| <svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <circle cx="20" cy="20" r="8" stroke="#ff3c00" stroke-width="2" /> | |
| <line x1="20" y1="2" x2="20" y2="12" stroke="#ff3c00" stroke-width="2" /> | |
| <line x1="20" y1="28" x2="20" y2="38" stroke="#ff3c00" stroke-width="2" /> | |
| <line x1="2" y1="20" x2="12" y2="20" stroke="#ff3c00" stroke-width="2" /> | |
| <line x1="28" y1="20" x2="38" y2="20" stroke="#ff3c00" stroke-width="2" /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="map-info-bar"> | |
| <div class="map-info-cell"> | |
| <div class="k">Latitude</div> | |
| <div class="v" id="info-lat">β</div> | |
| </div> | |
| <div class="map-info-cell"> | |
| <div class="k">Longitude</div> | |
| <div class="v" id="info-lng">β</div> | |
| </div> | |
| <div class="map-info-cell"> | |
| <div class="k">Zoom Level</div> | |
| <div class="v" id="info-zoom">β</div> | |
| </div> | |
| </div> | |
| <p class="zoom-note">β Zoom in to at least level 17β19 for best building-level detection results</p> | |
| <button class="btn secondary" id="capture-btn" onclick="captureMap()">πΈ CAPTURE THIS VIEW</button> | |
| <div id="map-preview-wrap"> | |
| <img id="map-preview-img" src="" alt="Captured map" /> | |
| <span class="preview-tag">Captured Satellite View</span> | |
| </div> | |
| </div> | |
| <!-- Analyze button (shared) --> | |
| <button class="btn" id="analyze-btn" disabled> | |
| <div class="spinner" id="spinner"></div> | |
| <span id="btn-text">ANALYZE IMAGE</span> | |
| </button> | |
| <div id="error"></div> | |
| <!-- Results --> | |
| <div id="results"> | |
| <div class="verdict-bar" id="verdict-bar"> | |
| <div class="verdict-label" id="verdict-label"></div> | |
| <div class="verdict-meta"> | |
| <div class="vmeta-item"> | |
| <div class="num" id="vm-illegal">0</div> | |
| <div class="key">Illegal Buildings</div> | |
| </div> | |
| <div class="vmeta-item"> | |
| <div class="num" id="vm-total">0</div> | |
| <div class="key">Total Buildings</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid3"> | |
| <div class="img-panel"> | |
| <div class="panel-label"><span class="dot g"></span> Original</div> | |
| <img id="out-orig" src="" alt="Original" /> | |
| </div> | |
| <div class="img-panel"> | |
| <div class="panel-label"><span class="dot"></span> Segmentation Mask</div> | |
| <img id="out-mask" src="" alt="Mask" /> | |
| </div> | |
| <div class="img-panel"> | |
| <div class="panel-label"><span class="dot"></span> Illegal Overlay</div> | |
| <img id="out-overlay" src="" alt="Overlay" /> | |
| </div> | |
| </div> | |
| <div class="stats4"> | |
| <div class="stat"> | |
| <div class="v" id="s-illegal">β</div> | |
| <div class="k">Illegal Buildings</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="v" id="s-legal">β</div> | |
| <div class="k">Legal Buildings</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="v" id="s-pct">β</div> | |
| <div class="k">Area Flagged %</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="v" id="s-device">β</div> | |
| <div class="k">Inference Device</div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const API_KEY = 'AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk'; | |
| let map, currentLat = 28.6139, currentLng = 77.2090; // Default: New Delhi | |
| let currentZoom = 18; | |
| let selectedFile = null; | |
| let capturedBlob = null; | |
| let activeSource = 'upload'; // 'upload' or 'map' | |
| // ββ Tab switching ββββββββββββββββββββββββββββββββββββββββββββ | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); | |
| document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active'); | |
| document.getElementById(`panel-${tab}`).classList.add('active'); | |
| activeSource = tab; | |
| if (tab === 'map' && !map) initMap(); | |
| updateAnalyzeBtn(); | |
| } | |
| // ββ Google Maps init βββββββββββββββββββββββββββββββββββββββββ | |
| function initMap() { | |
| map = new google.maps.Map(document.getElementById('map'), { | |
| center: { lat: currentLat, lng: currentLng }, | |
| zoom: currentZoom, | |
| mapTypeId: 'satellite', | |
| tilt: 0, | |
| disableDefaultUI: false, | |
| mapTypeControl: false, | |
| streetViewControl: false, | |
| fullscreenControl: true, | |
| zoomControl: true, | |
| styles: [] | |
| }); | |
| map.addListener('center_changed', updateMapInfo); | |
| map.addListener('zoom_changed', updateMapInfo); | |
| updateMapInfo(); | |
| } | |
| function updateMapInfo() { | |
| if (!map) return; | |
| const c = map.getCenter(); | |
| currentLat = c.lat(); | |
| currentLng = c.lng(); | |
| currentZoom = map.getZoom(); | |
| document.getElementById('info-lat').textContent = currentLat.toFixed(6); | |
| document.getElementById('info-lng').textContent = currentLng.toFixed(6); | |
| document.getElementById('info-zoom').textContent = currentZoom; | |
| } | |
| // ββ Capture map view via Static Maps API ββββββββββββββββββββ | |
| function captureMap() { | |
| if (!map) return; | |
| const c = map.getCenter(); | |
| const lat = c.lat(); | |
| const lng = c.lng(); | |
| const zoom = map.getZoom(); | |
| const size = '640x640'; | |
| const url = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&maptype=satellite&key=${API_KEY}`; | |
| document.getElementById('map-preview-img').src = url; | |
| document.getElementById('map-preview-wrap').style.display = 'block'; | |
| // Fetch as blob so we can POST it | |
| fetch(url) | |
| .then(r => r.blob()) | |
| .then(blob => { | |
| capturedBlob = blob; | |
| activeSource = 'map'; | |
| updateAnalyzeBtn(); | |
| }) | |
| .catch(() => { | |
| showError('Failed to fetch satellite image. Check your API key or network.'); | |
| }); | |
| } | |
| // ββ Upload tab βββββββββββββββββββββββββββββββββββββββββββββββ | |
| const fileInput = document.getElementById('file-input'); | |
| const uploadZone = document.getElementById('upload-zone'); | |
| uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); }); | |
| uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over')); | |
| uploadZone.addEventListener('drop', e => { | |
| e.preventDefault(); uploadZone.classList.remove('drag-over'); | |
| if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); }); | |
| function handleFile(file) { | |
| selectedFile = file; | |
| activeSource = 'upload'; | |
| const r = new FileReader(); | |
| r.onload = e => { | |
| document.getElementById('preview-img').src = e.target.result; | |
| document.getElementById('preview-wrap').style.display = 'block'; | |
| }; | |
| r.readAsDataURL(file); | |
| updateAnalyzeBtn(); | |
| document.getElementById('results').style.display = 'none'; | |
| document.getElementById('error').style.display = 'none'; | |
| } | |
| function updateAnalyzeBtn() { | |
| const btn = document.getElementById('analyze-btn'); | |
| const hasUpload = activeSource === 'upload' && selectedFile; | |
| const hasMap = activeSource === 'map' && capturedBlob; | |
| btn.disabled = !(hasUpload || hasMap); | |
| } | |
| // ββ Analyze ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.getElementById('analyze-btn').addEventListener('click', async () => { | |
| const btn = document.getElementById('analyze-btn'); | |
| const spinner = document.getElementById('spinner'); | |
| const btnText = document.getElementById('btn-text'); | |
| btn.disabled = true; | |
| spinner.style.display = 'block'; | |
| btnText.textContent = 'ANALYZING...'; | |
| document.getElementById('error').style.display = 'none'; | |
| document.getElementById('results').style.display = 'none'; | |
| const fd = new FormData(); | |
| if (activeSource === 'upload' && selectedFile) { | |
| fd.append('image', selectedFile); | |
| } else if (activeSource === 'map' && capturedBlob) { | |
| fd.append('image', capturedBlob, 'satellite_capture.png'); | |
| } else { | |
| showError('No image selected.'); return; | |
| } | |
| try { | |
| const res = await fetch('/predict', { method: 'POST', body: fd }); | |
| const d = await res.json(); | |
| if (d.error) throw new Error(d.error); | |
| const isIllegal = d.illegal_count > 0; | |
| const bar = document.getElementById('verdict-bar'); | |
| bar.className = 'verdict-bar ' + (isIllegal ? 'danger' : 'safe'); | |
| document.getElementById('verdict-label').textContent = d.verdict; | |
| document.getElementById('vm-illegal').textContent = d.illegal_count; | |
| document.getElementById('vm-total').textContent = d.total_count; | |
| document.getElementById('out-orig').src = 'data:image/png;base64,' + d.original; | |
| document.getElementById('out-mask').src = 'data:image/png;base64,' + d.mask; | |
| document.getElementById('out-overlay').src = 'data:image/png;base64,' + d.overlay; | |
| document.getElementById('s-illegal').textContent = d.illegal_count; | |
| document.getElementById('s-legal').textContent = d.legal_count; | |
| document.getElementById('s-pct').textContent = d.illegal_percent + '%'; | |
| document.getElementById('s-device').textContent = d.device.toUpperCase(); | |
| document.getElementById('results').style.display = 'block'; | |
| document.getElementById('results').scrollIntoView({ behavior: 'smooth' }); | |
| } catch (err) { | |
| showError(err.message || 'Server error. Is Flask running?'); | |
| } finally { | |
| btn.disabled = false; | |
| spinner.style.display = 'none'; | |
| btnText.textContent = 'ANALYZE AGAIN'; | |
| updateAnalyzeBtn(); | |
| } | |
| }); | |
| function showError(msg) { | |
| const e = document.getElementById('error'); | |
| e.textContent = 'β ' + msg; | |
| e.style.display = 'block'; | |
| document.getElementById('spinner').style.display = 'none'; | |
| document.getElementById('btn-text').textContent = 'ANALYZE AGAIN'; | |
| document.getElementById('analyze-btn').disabled = false; | |
| updateAnalyzeBtn(); | |
| } | |
| </script> | |
| <!-- Load Google Maps --> | |
| <script | |
| src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk&callback=Function.prototype" | |
| async defer></script> | |
| </body> | |
| </html> |