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
Raw
History Blame Contribute Delete
24 kB
<!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>