ProjectX / static /index.html
john5050's picture
added feature to directly browse google maps in app to directly scan you location and get analysis of the area.
e68f6e0
<!DOCTYPE html>
<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>