john5050 commited on
Commit
e68f6e0
Β·
1 Parent(s): 76479e1

added feature to directly browse google maps in app to directly scan you location and get analysis of the area.

Browse files
Files changed (2) hide show
  1. app.py +3 -1
  2. static/index.html +860 -255
app.py CHANGED
@@ -25,7 +25,9 @@ model = smp.Unet(
25
 
26
  MODEL_PATH = "best_model.pth"
27
  if os.path.exists(MODEL_PATH):
28
- model.load_state_dict(torch.load(MODEL_PATH, map_location=device, weight_only=True))
 
 
29
  print(f"βœ… Model loaded β€” device: {device}")
30
  else:
31
  print("⚠️ best_model.pth not found β€” running in demo mode")
 
25
 
26
  MODEL_PATH = "best_model.pth"
27
  if os.path.exists(MODEL_PATH):
28
+ model.load_state_dict(
29
+ torch.load(MODEL_PATH, map_location=device, weights_only=True)
30
+ )
31
  print(f"βœ… Model loaded β€” device: {device}")
32
  else:
33
  print("⚠️ best_model.pth not found β€” running in demo mode")
static/index.html CHANGED
@@ -1,272 +1,877 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>ConstructScan β€” Illegal Construction Detector</title>
7
- <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"/>
8
- <style>
9
- :root {
10
- --bg: #0a0a0a;
11
- --surface: #111111;
12
- --surface2: #1a1a1a;
13
- --border: #2a2a2a;
14
- --accent: #ff3c00;
15
- --text: #f0ede8;
16
- --muted: #666;
17
- --safe: #00e676;
18
- --danger: #ff3c00;
19
- }
20
- * { margin:0; padding:0; box-sizing:border-box; }
21
- body { background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; min-height:100vh; }
22
- body::before {
23
- content:''; position:fixed; inset:0; pointer-events:none; z-index:999;
24
- 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");
25
- }
26
- header {
27
- border-bottom:1px solid var(--border); padding:1.2rem 2.5rem;
28
- display:flex; align-items:center; justify-content:space-between;
29
- position:sticky; top:0; background:rgba(10,10,10,0.96); backdrop-filter:blur(12px); z-index:100;
30
- }
31
- .logo { font-family:'Bebas Neue',sans-serif; font-size:1.7rem; letter-spacing:.1em; }
32
- .logo span { color:var(--accent); }
33
- .badge {
34
- font-family:'DM Mono',monospace; font-size:.65rem; letter-spacing:.15em;
35
- text-transform:uppercase; padding:.3rem .7rem; border:1px solid var(--border); color:var(--muted);
36
- }
37
- main { max-width:1100px; margin:0 auto; padding:3.5rem 2rem; }
38
- .hero { margin-bottom:3rem; }
39
- .hero h1 {
40
- font-family:'Bebas Neue',sans-serif; font-size:clamp(2.8rem,7vw,6rem);
41
- line-height:.92; letter-spacing:.02em; margin-bottom:1.2rem;
42
- }
43
- .hero h1 em { font-style:normal; color:var(--accent); display:block; }
44
- .hero p { font-size:.95rem; color:var(--muted); max-width:460px; line-height:1.7; font-weight:300; }
45
-
46
- .upload-zone {
47
- border:1px dashed var(--border); padding:2.5rem 2rem; text-align:center;
48
- cursor:pointer; background:var(--surface); position:relative; transition:all .2s;
49
- margin-bottom:1rem;
50
- }
51
- .upload-zone:hover, .upload-zone.drag-over { border-color:var(--accent); background:#1a0906; }
52
- .upload-zone input { position:absolute; inset:0; opacity:0; cursor:pointer; width:100%; height:100%; }
53
- .upload-zone svg { width:40px; height:40px; margin:0 auto .8rem; opacity:.35; display:block; }
54
- .upload-zone h3 {
55
- font-family:'DM Mono',monospace; font-size:.8rem; letter-spacing:.1em;
56
- text-transform:uppercase; color:var(--muted); margin-bottom:.4rem;
57
- }
58
- .upload-zone p { font-size:.75rem; color:#444; }
59
-
60
- #preview-wrap { display:none; margin-bottom:1rem; position:relative; }
61
- #preview-img { width:100%; max-height:280px; object-fit:cover; display:block; }
62
- .preview-tag {
63
- position:absolute; top:.8rem; left:.8rem; font-family:'DM Mono',monospace;
64
- font-size:.6rem; letter-spacing:.15em; text-transform:uppercase;
65
- background:rgba(0,0,0,.85); padding:.25rem .55rem; color:var(--muted);
66
- }
67
-
68
- .btn {
69
- width:100%; padding:1.1rem; background:var(--accent); color:#fff; border:none;
70
- font-family:'Bebas Neue',sans-serif; font-size:1.3rem; letter-spacing:.15em;
71
- cursor:pointer; transition:background .2s; display:flex; align-items:center; justify-content:center; gap:.7rem;
72
- }
73
- .btn:hover { background:#e03500; }
74
- .btn:disabled { background:#333; color:#555; cursor:not-allowed; }
75
- .spinner {
76
- width:18px; height:18px; border:2px solid rgba(255,255,255,.25); border-top-color:#fff;
77
- border-radius:50%; animation:spin .7s linear infinite; display:none;
78
- }
79
- @keyframes spin { to { transform:rotate(360deg); } }
80
-
81
- #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); }
82
-
83
- #results { display:none; margin-top:2.5rem; animation:fadeUp .45s ease forwards; }
84
- @keyframes fadeUp { from { opacity:0; transform:translateY(18px); } to { opacity:1; transform:translateY(0); } }
85
-
86
- .verdict-bar {
87
- padding:1.8rem 2rem; margin-bottom:1px; display:flex;
88
- align-items:center; justify-content:space-between; gap:1.5rem; flex-wrap:wrap;
89
- }
90
- .verdict-bar.danger { background:#1a0400; border-left:4px solid var(--danger); }
91
- .verdict-bar.safe { background:#001508; border-left:4px solid var(--safe); }
92
- .verdict-label { font-family:'Bebas Neue',sans-serif; font-size:clamp(1.4rem,3.5vw,2.5rem); letter-spacing:.04em; }
93
- .verdict-bar.danger .verdict-label { color:var(--danger); }
94
- .verdict-bar.safe .verdict-label { color:var(--safe); }
95
- .verdict-meta { display:flex; gap:2rem; }
96
- .vmeta-item { text-align:right; }
97
- .vmeta-item .num { font-family:'Bebas Neue',sans-serif; font-size:2.2rem; line-height:1; }
98
- .vmeta-item .key { font-family:'DM Mono',monospace; font-size:.6rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
99
-
100
- .grid3 { display:grid; grid-template-columns:repeat(3,1fr); gap:1px; background:var(--border); margin-bottom:1px; }
101
- .img-panel { background:var(--surface); overflow:hidden; }
102
- .panel-label {
103
- padding:.6rem 1rem; font-family:'DM Mono',monospace; font-size:.6rem;
104
- letter-spacing:.14em; text-transform:uppercase; color:var(--muted);
105
- border-bottom:1px solid var(--border); display:flex; align-items:center; gap:.4rem;
106
- }
107
- .dot { width:5px; height:5px; border-radius:50%; background:var(--accent); }
108
- .dot.g { background:var(--safe); }
109
- .img-panel img { width:100%; aspect-ratio:1; object-fit:cover; display:block; }
110
-
111
- .stats4 { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:var(--border); }
112
- .stat { background:var(--surface2); padding:1.3rem 1.5rem; }
113
- .stat .v { font-family:'Bebas Neue',sans-serif; font-size:1.8rem; color:var(--text); line-height:1; margin-bottom:.25rem; }
114
- .stat .k { font-family:'DM Mono',monospace; font-size:.6rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
115
-
116
- @media(max-width:700px) {
117
- header { padding:1rem; }
118
- main { padding:2rem 1rem; }
119
- .grid3, .stats4 { grid-template-columns:1fr; }
120
- .verdict-meta { gap:1rem; }
121
- }
122
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  </head>
 
124
  <body>
125
 
126
- <header>
127
- <div class="logo">Construct<span>Scan</span></div>
128
- <div class="badge">EfficientNet-B3 Β· U-Net Β· SMP</div>
129
- </header>
130
-
131
- <main>
132
- <div class="hero">
133
- <h1>Detect <em>Illegal</em> Construction</h1>
134
- <p>Upload a satellite or aerial image. The model segments buildings, then flags those in restricted zones as illegal.</p>
135
- </div>
136
-
137
- <div class="upload-zone" id="upload-zone">
138
- <input type="file" id="file-input" accept="image/*"/>
139
- <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
140
- <rect x="4" y="4" width="40" height="40" rx="2"/>
141
- <path d="M24 32V16M16 24l8-8 8 8"/>
142
- </svg>
143
- <h3>Drop image here or click to upload</h3>
144
- <p>Satellite / aerial imagery β€” JPG, PNG, TIFF</p>
145
- </div>
146
-
147
- <div id="preview-wrap">
148
- <img id="preview-img" src="" alt="Preview"/>
149
- <span class="preview-tag">Input</span>
150
- </div>
151
-
152
- <button class="btn" id="analyze-btn" disabled>
153
- <div class="spinner" id="spinner"></div>
154
- <span id="btn-text">ANALYZE IMAGE</span>
155
- </button>
156
-
157
- <div id="error"></div>
158
-
159
- <div id="results">
160
- <div class="verdict-bar" id="verdict-bar">
161
- <div class="verdict-label" id="verdict-label"></div>
162
- <div class="verdict-meta">
163
- <div class="vmeta-item">
164
- <div class="num" id="vm-illegal">0</div>
165
- <div class="key">Illegal Buildings</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
- <div class="vmeta-item">
168
- <div class="num" id="vm-total">0</div>
169
- <div class="key">Total Buildings</div>
170
  </div>
171
  </div>
 
 
 
 
 
 
172
  </div>
173
 
174
- <div class="grid3">
175
- <div class="img-panel">
176
- <div class="panel-label"><span class="dot g"></span> Original</div>
177
- <img id="out-orig" src="" alt="Original"/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  </div>
179
- <div class="img-panel">
180
- <div class="panel-label"><span class="dot"></span> Segmentation Mask</div>
181
- <img id="out-mask" src="" alt="Mask"/>
 
 
 
 
 
 
 
 
 
 
182
  </div>
183
- <div class="img-panel">
184
- <div class="panel-label"><span class="dot"></span> Illegal Overlay</div>
185
- <img id="out-overlay" src="" alt="Overlay"/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  </div>
187
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
- <div class="stats4">
190
- <div class="stat"><div class="v" id="s-illegal">β€”</div><div class="k">Illegal Buildings</div></div>
191
- <div class="stat"><div class="v" id="s-legal">β€”</div><div class="k">Legal Buildings</div></div>
192
- <div class="stat"><div class="v" id="s-pct">β€”</div><div class="k">Area Flagged %</div></div>
193
- <div class="stat"><div class="v" id="s-device">β€”</div><div class="k">Inference Device</div></div>
194
- </div>
195
- </div>
196
- </main>
197
-
198
- <script>
199
- const fileInput = document.getElementById('file-input');
200
- const uploadZone = document.getElementById('upload-zone');
201
- const previewWrap = document.getElementById('preview-wrap');
202
- const previewImg = document.getElementById('preview-img');
203
- const analyzeBtn = document.getElementById('analyze-btn');
204
- const spinner = document.getElementById('spinner');
205
- const btnText = document.getElementById('btn-text');
206
- const errorDiv = document.getElementById('error');
207
- const results = document.getElementById('results');
208
- let selectedFile = null;
209
-
210
- uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
211
- uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
212
- uploadZone.addEventListener('drop', e => {
213
- e.preventDefault(); uploadZone.classList.remove('drag-over');
214
- if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
215
- });
216
- fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); });
217
-
218
- function handleFile(file) {
219
- selectedFile = file;
220
- const r = new FileReader();
221
- r.onload = e => { previewImg.src = e.target.result; previewWrap.style.display = 'block'; };
222
- r.readAsDataURL(file);
223
- analyzeBtn.disabled = false;
224
- results.style.display = 'none';
225
- errorDiv.style.display = 'none';
226
- }
227
-
228
- analyzeBtn.addEventListener('click', async () => {
229
- if (!selectedFile) return;
230
- analyzeBtn.disabled = true;
231
- spinner.style.display = 'block';
232
- btnText.textContent = 'ANALYZING...';
233
- errorDiv.style.display = 'none';
234
- results.style.display = 'none';
235
-
236
- const fd = new FormData();
237
- fd.append('image', selectedFile);
238
-
239
- try {
240
- const res = await fetch('/predict', { method:'POST', body:fd });
241
- const d = await res.json();
242
- if (d.error) throw new Error(d.error);
243
-
244
- const isIllegal = d.illegal_count > 0;
245
- const bar = document.getElementById('verdict-bar');
246
- bar.className = 'verdict-bar ' + (isIllegal ? 'danger' : 'safe');
247
- document.getElementById('verdict-label').textContent = d.verdict;
248
- document.getElementById('vm-illegal').textContent = d.illegal_count;
249
- document.getElementById('vm-total').textContent = d.total_count;
250
-
251
- document.getElementById('out-orig').src = 'data:image/png;base64,' + d.original;
252
- document.getElementById('out-mask').src = 'data:image/png;base64,' + d.mask;
253
- document.getElementById('out-overlay').src = 'data:image/png;base64,' + d.overlay;
254
-
255
- document.getElementById('s-illegal').textContent = d.illegal_count;
256
- document.getElementById('s-legal').textContent = d.legal_count;
257
- document.getElementById('s-pct').textContent = d.illegal_percent + '%';
258
- document.getElementById('s-device').textContent = d.device.toUpperCase();
259
-
260
- results.style.display = 'block';
261
- } catch(err) {
262
- errorDiv.textContent = '⚠ ' + (err.message || 'Server error. Is Flask running?');
263
- errorDiv.style.display = 'block';
264
- } finally {
265
- analyzeBtn.disabled = false;
266
- spinner.style.display = 'none';
267
- btnText.textContent = 'ANALYZE AGAIN';
268
- }
269
- });
270
- </script>
271
  </body>
272
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ConstructScan β€” Illegal Construction Detector</title>
8
+ <link
9
+ href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap"
10
+ rel="stylesheet" />
11
+ <style>
12
+ :root {
13
+ --bg: #0a0a0a;
14
+ --surface: #111;
15
+ --surface2: #1a1a1a;
16
+ --border: #2a2a2a;
17
+ --accent: #ff3c00;
18
+ --text: #f0ede8;
19
+ --muted: #666;
20
+ --safe: #00e676;
21
+ --danger: #ff3c00;
22
+ }
23
+
24
+ * {
25
+ margin: 0;
26
+ padding: 0;
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ body {
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ font-family: 'DM Sans', sans-serif;
34
+ min-height: 100vh;
35
+ }
36
+
37
+ body::before {
38
+ content: '';
39
+ position: fixed;
40
+ inset: 0;
41
+ pointer-events: none;
42
+ z-index: 999;
43
+ 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");
44
+ }
45
+
46
+ header {
47
+ border-bottom: 1px solid var(--border);
48
+ padding: 1.2rem 2.5rem;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ position: sticky;
53
+ top: 0;
54
+ background: rgba(10, 10, 10, 0.96);
55
+ backdrop-filter: blur(12px);
56
+ z-index: 100;
57
+ }
58
+
59
+ .logo {
60
+ font-family: 'Bebas Neue', sans-serif;
61
+ font-size: 1.7rem;
62
+ letter-spacing: .1em;
63
+ }
64
+
65
+ .logo span {
66
+ color: var(--accent);
67
+ }
68
+
69
+ .badge {
70
+ font-family: 'DM Mono', monospace;
71
+ font-size: .65rem;
72
+ letter-spacing: .15em;
73
+ text-transform: uppercase;
74
+ padding: .3rem .7rem;
75
+ border: 1px solid var(--border);
76
+ color: var(--muted);
77
+ }
78
+
79
+ main {
80
+ max-width: 1100px;
81
+ margin: 0 auto;
82
+ padding: 3.5rem 2rem;
83
+ }
84
+
85
+ .hero {
86
+ margin-bottom: 2.5rem;
87
+ }
88
+
89
+ .hero h1 {
90
+ font-family: 'Bebas Neue', sans-serif;
91
+ font-size: clamp(2.8rem, 7vw, 6rem);
92
+ line-height: .92;
93
+ letter-spacing: .02em;
94
+ margin-bottom: 1rem;
95
+ }
96
+
97
+ .hero h1 em {
98
+ font-style: normal;
99
+ color: var(--accent);
100
+ display: block;
101
+ }
102
+
103
+ .hero p {
104
+ font-size: .95rem;
105
+ color: var(--muted);
106
+ max-width: 460px;
107
+ line-height: 1.7;
108
+ font-weight: 300;
109
+ }
110
+
111
+ /* Tabs */
112
+ .tabs {
113
+ display: flex;
114
+ gap: 1px;
115
+ margin-bottom: 1px;
116
+ background: var(--border);
117
+ }
118
+
119
+ .tab {
120
+ flex: 1;
121
+ padding: .9rem 1.5rem;
122
+ background: var(--surface2);
123
+ cursor: pointer;
124
+ font-family: 'DM Mono', monospace;
125
+ font-size: .7rem;
126
+ letter-spacing: .15em;
127
+ text-transform: uppercase;
128
+ color: var(--muted);
129
+ border: none;
130
+ transition: all .2s;
131
+ text-align: center;
132
+ }
133
+
134
+ .tab.active {
135
+ background: var(--surface);
136
+ color: var(--text);
137
+ border-bottom: 2px solid var(--accent);
138
+ }
139
+
140
+ .tab:hover:not(.active) {
141
+ color: var(--text);
142
+ background: #161616;
143
+ }
144
+
145
+ .tab-panel {
146
+ display: none;
147
+ }
148
+
149
+ .tab-panel.active {
150
+ display: block;
151
+ }
152
+
153
+ /* Upload tab */
154
+ .upload-zone {
155
+ border: 1px dashed var(--border);
156
+ padding: 2.5rem 2rem;
157
+ text-align: center;
158
+ cursor: pointer;
159
+ background: var(--surface);
160
+ position: relative;
161
+ transition: all .2s;
162
+ margin-bottom: 1rem;
163
+ }
164
+
165
+ .upload-zone:hover,
166
+ .upload-zone.drag-over {
167
+ border-color: var(--accent);
168
+ background: #1a0906;
169
+ }
170
+
171
+ .upload-zone input {
172
+ position: absolute;
173
+ inset: 0;
174
+ opacity: 0;
175
+ cursor: pointer;
176
+ width: 100%;
177
+ height: 100%;
178
+ }
179
+
180
+ .upload-zone svg {
181
+ width: 40px;
182
+ height: 40px;
183
+ margin: 0 auto .8rem;
184
+ opacity: .35;
185
+ display: block;
186
+ }
187
+
188
+ .upload-zone h3 {
189
+ font-family: 'DM Mono', monospace;
190
+ font-size: .8rem;
191
+ letter-spacing: .1em;
192
+ text-transform: uppercase;
193
+ color: var(--muted);
194
+ margin-bottom: .4rem;
195
+ }
196
+
197
+ .upload-zone p {
198
+ font-size: .75rem;
199
+ color: #444;
200
+ }
201
+
202
+ #preview-wrap {
203
+ display: none;
204
+ margin-bottom: 1rem;
205
+ position: relative;
206
+ }
207
+
208
+ #preview-img {
209
+ width: 100%;
210
+ max-height: 280px;
211
+ object-fit: cover;
212
+ display: block;
213
+ }
214
+
215
+ .preview-tag {
216
+ position: absolute;
217
+ top: .8rem;
218
+ left: .8rem;
219
+ font-family: 'DM Mono', monospace;
220
+ font-size: .6rem;
221
+ letter-spacing: .15em;
222
+ text-transform: uppercase;
223
+ background: rgba(0, 0, 0, .85);
224
+ padding: .25rem .55rem;
225
+ color: var(--muted);
226
+ }
227
+
228
+ /* Map tab */
229
+ .map-instructions {
230
+ padding: .8rem 1rem;
231
+ background: var(--surface2);
232
+ border-left: 3px solid var(--accent);
233
+ font-family: 'DM Mono', monospace;
234
+ font-size: .7rem;
235
+ letter-spacing: .08em;
236
+ color: var(--muted);
237
+ margin-bottom: 1rem;
238
+ }
239
+
240
+ .map-instructions strong {
241
+ color: var(--text);
242
+ }
243
+
244
+ #map-container {
245
+ position: relative;
246
+ margin-bottom: 1rem;
247
+ }
248
+
249
+ #map {
250
+ width: 100%;
251
+ height: 480px;
252
+ background: #111;
253
+ }
254
+
255
+ .map-crosshair {
256
+ position: absolute;
257
+ top: 50%;
258
+ left: 50%;
259
+ transform: translate(-50%, -50%);
260
+ pointer-events: none;
261
+ z-index: 10;
262
+ }
263
+
264
+ .map-crosshair svg {
265
+ width: 40px;
266
+ height: 40px;
267
+ filter: drop-shadow(0 0 4px rgba(0, 0, 0, .8));
268
+ }
269
+
270
+ .map-info-bar {
271
+ display: flex;
272
+ gap: 1px;
273
+ background: var(--border);
274
+ margin-bottom: 1rem;
275
+ }
276
+
277
+ .map-info-cell {
278
+ flex: 1;
279
+ background: var(--surface2);
280
+ padding: .8rem 1rem;
281
+ }
282
+
283
+ .map-info-cell .k {
284
+ font-family: 'DM Mono', monospace;
285
+ font-size: .6rem;
286
+ letter-spacing: .12em;
287
+ text-transform: uppercase;
288
+ color: var(--muted);
289
+ margin-bottom: .3rem;
290
+ }
291
+
292
+ .map-info-cell .v {
293
+ font-family: 'DM Mono', monospace;
294
+ font-size: .85rem;
295
+ color: var(--text);
296
+ }
297
+
298
+ .zoom-note {
299
+ font-family: 'DM Mono', monospace;
300
+ font-size: .65rem;
301
+ color: #444;
302
+ letter-spacing: .08em;
303
+ margin-bottom: 1rem;
304
+ }
305
+
306
+ /* Shared */
307
+ .btn {
308
+ width: 100%;
309
+ padding: 1.1rem;
310
+ background: var(--accent);
311
+ color: #fff;
312
+ border: none;
313
+ font-family: 'Bebas Neue', sans-serif;
314
+ font-size: 1.3rem;
315
+ letter-spacing: .15em;
316
+ cursor: pointer;
317
+ transition: background .2s;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ gap: .7rem;
322
+ }
323
+
324
+ .btn:hover {
325
+ background: #e03500;
326
+ }
327
+
328
+ .btn:disabled {
329
+ background: #333;
330
+ color: #555;
331
+ cursor: not-allowed;
332
+ }
333
+
334
+ .btn.secondary {
335
+ background: var(--surface2);
336
+ color: var(--text);
337
+ border: 1px solid var(--border);
338
+ margin-bottom: 1rem;
339
+ }
340
+
341
+ .btn.secondary:hover {
342
+ background: #222;
343
+ }
344
+
345
+ .spinner {
346
+ width: 18px;
347
+ height: 18px;
348
+ border: 2px solid rgba(255, 255, 255, .25);
349
+ border-top-color: #fff;
350
+ border-radius: 50%;
351
+ animation: spin .7s linear infinite;
352
+ display: none;
353
+ }
354
+
355
+ @keyframes spin {
356
+ to {
357
+ transform: rotate(360deg);
358
+ }
359
+ }
360
+
361
+ #error {
362
+ display: none;
363
+ margin-top: .8rem;
364
+ padding: .9rem 1.2rem;
365
+ background: #1a0500;
366
+ border-left: 3px solid var(--danger);
367
+ font-family: 'DM Mono', monospace;
368
+ font-size: .75rem;
369
+ color: var(--danger);
370
+ }
371
+
372
+ /* Map preview */
373
+ #map-preview-wrap {
374
+ display: none;
375
+ margin-bottom: 1rem;
376
+ position: relative;
377
+ }
378
+
379
+ #map-preview-img {
380
+ width: 100%;
381
+ max-height: 280px;
382
+ object-fit: cover;
383
+ display: block;
384
+ }
385
+
386
+ /* Results */
387
+ #results {
388
+ display: none;
389
+ margin-top: 2.5rem;
390
+ animation: fadeUp .45s ease forwards;
391
+ }
392
+
393
+ @keyframes fadeUp {
394
+ from {
395
+ opacity: 0;
396
+ transform: translateY(18px);
397
+ }
398
+
399
+ to {
400
+ opacity: 1;
401
+ transform: translateY(0);
402
+ }
403
+ }
404
+
405
+ .verdict-bar {
406
+ padding: 1.8rem 2rem;
407
+ margin-bottom: 1px;
408
+ display: flex;
409
+ align-items: center;
410
+ justify-content: space-between;
411
+ gap: 1.5rem;
412
+ flex-wrap: wrap;
413
+ }
414
+
415
+ .verdict-bar.danger {
416
+ background: #1a0400;
417
+ border-left: 4px solid var(--danger);
418
+ }
419
+
420
+ .verdict-bar.safe {
421
+ background: #001508;
422
+ border-left: 4px solid var(--safe);
423
+ }
424
+
425
+ .verdict-label {
426
+ font-family: 'Bebas Neue', sans-serif;
427
+ font-size: clamp(1.4rem, 3.5vw, 2.5rem);
428
+ letter-spacing: .04em;
429
+ }
430
+
431
+ .verdict-bar.danger .verdict-label {
432
+ color: var(--danger);
433
+ }
434
+
435
+ .verdict-bar.safe .verdict-label {
436
+ color: var(--safe);
437
+ }
438
+
439
+ .verdict-meta {
440
+ display: flex;
441
+ gap: 2rem;
442
+ }
443
+
444
+ .vmeta-item {
445
+ text-align: right;
446
+ }
447
+
448
+ .vmeta-item .num {
449
+ font-family: 'Bebas Neue', sans-serif;
450
+ font-size: 2.2rem;
451
+ line-height: 1;
452
+ }
453
+
454
+ .vmeta-item .key {
455
+ font-family: 'DM Mono', monospace;
456
+ font-size: .6rem;
457
+ letter-spacing: .12em;
458
+ text-transform: uppercase;
459
+ color: var(--muted);
460
+ }
461
+
462
+ .grid3 {
463
+ display: grid;
464
+ grid-template-columns: repeat(3, 1fr);
465
+ gap: 1px;
466
+ background: var(--border);
467
+ margin-bottom: 1px;
468
+ }
469
+
470
+ .img-panel {
471
+ background: var(--surface);
472
+ overflow: hidden;
473
+ }
474
+
475
+ .panel-label {
476
+ padding: .6rem 1rem;
477
+ font-family: 'DM Mono', monospace;
478
+ font-size: .6rem;
479
+ letter-spacing: .14em;
480
+ text-transform: uppercase;
481
+ color: var(--muted);
482
+ border-bottom: 1px solid var(--border);
483
+ display: flex;
484
+ align-items: center;
485
+ gap: .4rem;
486
+ }
487
+
488
+ .dot {
489
+ width: 5px;
490
+ height: 5px;
491
+ border-radius: 50%;
492
+ background: var(--accent);
493
+ }
494
+
495
+ .dot.g {
496
+ background: var(--safe);
497
+ }
498
+
499
+ .img-panel img {
500
+ width: 100%;
501
+ aspect-ratio: 1;
502
+ object-fit: cover;
503
+ display: block;
504
+ }
505
+
506
+ .stats4 {
507
+ display: grid;
508
+ grid-template-columns: repeat(4, 1fr);
509
+ gap: 1px;
510
+ background: var(--border);
511
+ }
512
+
513
+ .stat {
514
+ background: var(--surface2);
515
+ padding: 1.3rem 1.5rem;
516
+ }
517
+
518
+ .stat .v {
519
+ font-family: 'Bebas Neue', sans-serif;
520
+ font-size: 1.8rem;
521
+ color: var(--text);
522
+ line-height: 1;
523
+ margin-bottom: .25rem;
524
+ }
525
+
526
+ .stat .k {
527
+ font-family: 'DM Mono', monospace;
528
+ font-size: .6rem;
529
+ letter-spacing: .12em;
530
+ text-transform: uppercase;
531
+ color: var(--muted);
532
+ }
533
+
534
+ @media(max-width:700px) {
535
+ header {
536
+ padding: 1rem;
537
+ }
538
+
539
+ main {
540
+ padding: 2rem 1rem;
541
+ }
542
+
543
+ .grid3,
544
+ .stats4 {
545
+ grid-template-columns: 1fr;
546
+ }
547
+
548
+ .verdict-meta {
549
+ gap: 1rem;
550
+ }
551
+
552
+ #map {
553
+ height: 320px;
554
+ }
555
+ }
556
+ </style>
557
  </head>
558
+
559
  <body>
560
 
561
+ <header>
562
+ <div class="logo">Construct<span>Scan</span></div>
563
+ <div class="badge">EfficientNet-B3 Β· U-Net Β· RTX 4050</div>
564
+ </header>
565
+
566
+ <main>
567
+ <div class="hero">
568
+ <h1>Detect <em>Illegal</em> Construction</h1>
569
+ <p>Upload a satellite image or pick any location on the map. The model segments buildings and flags unauthorized
570
+ construction.</p>
571
+ </div>
572
+
573
+ <!-- Tabs -->
574
+ <div class="tabs">
575
+ <button class="tab active" onclick="switchTab('upload')">⬆ Upload Image</button>
576
+ <button class="tab" onclick="switchTab('map')">🌍 Pick on Map</button>
577
+ </div>
578
+
579
+ <!-- Upload Panel -->
580
+ <div class="tab-panel active" id="panel-upload">
581
+ <div class="upload-zone" id="upload-zone">
582
+ <input type="file" id="file-input" accept="image/*" />
583
+ <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5">
584
+ <rect x="4" y="4" width="40" height="40" rx="2" />
585
+ <path d="M24 32V16M16 24l8-8 8 8" />
586
+ </svg>
587
+ <h3>Drop image here or click to upload</h3>
588
+ <p>Satellite / aerial imagery β€” JPG, PNG, TIFF</p>
589
+ </div>
590
+ <div id="preview-wrap">
591
+ <img id="preview-img" src="" alt="Preview" />
592
+ <span class="preview-tag">Input</span>
593
+ </div>
594
+ </div>
595
+
596
+ <!-- Map Panel -->
597
+ <div class="tab-panel" id="panel-map">
598
+ <div class="map-instructions">
599
+ <strong>How to use:</strong> Navigate to any location β†’ zoom in to building level β†’ click <strong>Capture This
600
+ View</strong>
601
+ </div>
602
+ <div id="map-container">
603
+ <div id="map"></div>
604
+ <div class="map-crosshair">
605
+ <svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
606
+ <circle cx="20" cy="20" r="8" stroke="#ff3c00" stroke-width="2" />
607
+ <line x1="20" y1="2" x2="20" y2="12" stroke="#ff3c00" stroke-width="2" />
608
+ <line x1="20" y1="28" x2="20" y2="38" stroke="#ff3c00" stroke-width="2" />
609
+ <line x1="2" y1="20" x2="12" y2="20" stroke="#ff3c00" stroke-width="2" />
610
+ <line x1="28" y1="20" x2="38" y2="20" stroke="#ff3c00" stroke-width="2" />
611
+ </svg>
612
+ </div>
613
+ </div>
614
+ <div class="map-info-bar">
615
+ <div class="map-info-cell">
616
+ <div class="k">Latitude</div>
617
+ <div class="v" id="info-lat">β€”</div>
618
+ </div>
619
+ <div class="map-info-cell">
620
+ <div class="k">Longitude</div>
621
+ <div class="v" id="info-lng">β€”</div>
622
  </div>
623
+ <div class="map-info-cell">
624
+ <div class="k">Zoom Level</div>
625
+ <div class="v" id="info-zoom">β€”</div>
626
  </div>
627
  </div>
628
+ <p class="zoom-note">⚠ Zoom in to at least level 17–19 for best building-level detection results</p>
629
+ <button class="btn secondary" id="capture-btn" onclick="captureMap()">πŸ“Έ CAPTURE THIS VIEW</button>
630
+ <div id="map-preview-wrap">
631
+ <img id="map-preview-img" src="" alt="Captured map" />
632
+ <span class="preview-tag">Captured Satellite View</span>
633
+ </div>
634
  </div>
635
 
636
+ <!-- Analyze button (shared) -->
637
+ <button class="btn" id="analyze-btn" disabled>
638
+ <div class="spinner" id="spinner"></div>
639
+ <span id="btn-text">ANALYZE IMAGE</span>
640
+ </button>
641
+
642
+ <div id="error"></div>
643
+
644
+ <!-- Results -->
645
+ <div id="results">
646
+ <div class="verdict-bar" id="verdict-bar">
647
+ <div class="verdict-label" id="verdict-label"></div>
648
+ <div class="verdict-meta">
649
+ <div class="vmeta-item">
650
+ <div class="num" id="vm-illegal">0</div>
651
+ <div class="key">Illegal Buildings</div>
652
+ </div>
653
+ <div class="vmeta-item">
654
+ <div class="num" id="vm-total">0</div>
655
+ <div class="key">Total Buildings</div>
656
+ </div>
657
+ </div>
658
  </div>
659
+ <div class="grid3">
660
+ <div class="img-panel">
661
+ <div class="panel-label"><span class="dot g"></span> Original</div>
662
+ <img id="out-orig" src="" alt="Original" />
663
+ </div>
664
+ <div class="img-panel">
665
+ <div class="panel-label"><span class="dot"></span> Segmentation Mask</div>
666
+ <img id="out-mask" src="" alt="Mask" />
667
+ </div>
668
+ <div class="img-panel">
669
+ <div class="panel-label"><span class="dot"></span> Illegal Overlay</div>
670
+ <img id="out-overlay" src="" alt="Overlay" />
671
+ </div>
672
  </div>
673
+ <div class="stats4">
674
+ <div class="stat">
675
+ <div class="v" id="s-illegal">β€”</div>
676
+ <div class="k">Illegal Buildings</div>
677
+ </div>
678
+ <div class="stat">
679
+ <div class="v" id="s-legal">β€”</div>
680
+ <div class="k">Legal Buildings</div>
681
+ </div>
682
+ <div class="stat">
683
+ <div class="v" id="s-pct">β€”</div>
684
+ <div class="k">Area Flagged %</div>
685
+ </div>
686
+ <div class="stat">
687
+ <div class="v" id="s-device">β€”</div>
688
+ <div class="k">Inference Device</div>
689
+ </div>
690
  </div>
691
  </div>
692
+ </main>
693
+
694
+ <script>
695
+ const API_KEY = 'AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk';
696
+ let map, currentLat = 28.6139, currentLng = 77.2090; // Default: New Delhi
697
+ let currentZoom = 18;
698
+ let selectedFile = null;
699
+ let capturedBlob = null;
700
+ let activeSource = 'upload'; // 'upload' or 'map'
701
+
702
+ // ── Tab switching ────────────────────────────────────────────
703
+ function switchTab(tab) {
704
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
705
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
706
+ document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active');
707
+ document.getElementById(`panel-${tab}`).classList.add('active');
708
+ activeSource = tab;
709
+
710
+ if (tab === 'map' && !map) initMap();
711
+ updateAnalyzeBtn();
712
+ }
713
+
714
+ // ── Google Maps init ─────────────────────────────────────────
715
+ function initMap() {
716
+ map = new google.maps.Map(document.getElementById('map'), {
717
+ center: { lat: currentLat, lng: currentLng },
718
+ zoom: currentZoom,
719
+ mapTypeId: 'satellite',
720
+ tilt: 0,
721
+ disableDefaultUI: false,
722
+ mapTypeControl: false,
723
+ streetViewControl: false,
724
+ fullscreenControl: true,
725
+ zoomControl: true,
726
+ styles: []
727
+ });
728
+
729
+ map.addListener('center_changed', updateMapInfo);
730
+ map.addListener('zoom_changed', updateMapInfo);
731
+ updateMapInfo();
732
+ }
733
+
734
+ function updateMapInfo() {
735
+ if (!map) return;
736
+ const c = map.getCenter();
737
+ currentLat = c.lat();
738
+ currentLng = c.lng();
739
+ currentZoom = map.getZoom();
740
+ document.getElementById('info-lat').textContent = currentLat.toFixed(6);
741
+ document.getElementById('info-lng').textContent = currentLng.toFixed(6);
742
+ document.getElementById('info-zoom').textContent = currentZoom;
743
+ }
744
+
745
+ // ── Capture map view via Static Maps API ────────────────────
746
+ function captureMap() {
747
+ if (!map) return;
748
+ const c = map.getCenter();
749
+ const lat = c.lat();
750
+ const lng = c.lng();
751
+ const zoom = map.getZoom();
752
+ const size = '640x640';
753
+
754
+ const url = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&maptype=satellite&key=${API_KEY}`;
755
+
756
+ document.getElementById('map-preview-img').src = url;
757
+ document.getElementById('map-preview-wrap').style.display = 'block';
758
+
759
+ // Fetch as blob so we can POST it
760
+ fetch(url)
761
+ .then(r => r.blob())
762
+ .then(blob => {
763
+ capturedBlob = blob;
764
+ activeSource = 'map';
765
+ updateAnalyzeBtn();
766
+ })
767
+ .catch(() => {
768
+ showError('Failed to fetch satellite image. Check your API key or network.');
769
+ });
770
+ }
771
+
772
+ // ── Upload tab ───────────────────────────────────────────────
773
+ const fileInput = document.getElementById('file-input');
774
+ const uploadZone = document.getElementById('upload-zone');
775
+
776
+ uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
777
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
778
+ uploadZone.addEventListener('drop', e => {
779
+ e.preventDefault(); uploadZone.classList.remove('drag-over');
780
+ if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
781
+ });
782
+ fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); });
783
+
784
+ function handleFile(file) {
785
+ selectedFile = file;
786
+ activeSource = 'upload';
787
+ const r = new FileReader();
788
+ r.onload = e => {
789
+ document.getElementById('preview-img').src = e.target.result;
790
+ document.getElementById('preview-wrap').style.display = 'block';
791
+ };
792
+ r.readAsDataURL(file);
793
+ updateAnalyzeBtn();
794
+ document.getElementById('results').style.display = 'none';
795
+ document.getElementById('error').style.display = 'none';
796
+ }
797
+
798
+ function updateAnalyzeBtn() {
799
+ const btn = document.getElementById('analyze-btn');
800
+ const hasUpload = activeSource === 'upload' && selectedFile;
801
+ const hasMap = activeSource === 'map' && capturedBlob;
802
+ btn.disabled = !(hasUpload || hasMap);
803
+ }
804
+
805
+ // ── Analyze ──────────────────────────────────────────────────
806
+ document.getElementById('analyze-btn').addEventListener('click', async () => {
807
+ const btn = document.getElementById('analyze-btn');
808
+ const spinner = document.getElementById('spinner');
809
+ const btnText = document.getElementById('btn-text');
810
+
811
+ btn.disabled = true;
812
+ spinner.style.display = 'block';
813
+ btnText.textContent = 'ANALYZING...';
814
+ document.getElementById('error').style.display = 'none';
815
+ document.getElementById('results').style.display = 'none';
816
+
817
+ const fd = new FormData();
818
+ if (activeSource === 'upload' && selectedFile) {
819
+ fd.append('image', selectedFile);
820
+ } else if (activeSource === 'map' && capturedBlob) {
821
+ fd.append('image', capturedBlob, 'satellite_capture.png');
822
+ } else {
823
+ showError('No image selected.'); return;
824
+ }
825
+
826
+ try {
827
+ const res = await fetch('/predict', { method: 'POST', body: fd });
828
+ const d = await res.json();
829
+ if (d.error) throw new Error(d.error);
830
+
831
+ const isIllegal = d.illegal_count > 0;
832
+ const bar = document.getElementById('verdict-bar');
833
+ bar.className = 'verdict-bar ' + (isIllegal ? 'danger' : 'safe');
834
+ document.getElementById('verdict-label').textContent = d.verdict;
835
+ document.getElementById('vm-illegal').textContent = d.illegal_count;
836
+ document.getElementById('vm-total').textContent = d.total_count;
837
+
838
+ document.getElementById('out-orig').src = 'data:image/png;base64,' + d.original;
839
+ document.getElementById('out-mask').src = 'data:image/png;base64,' + d.mask;
840
+ document.getElementById('out-overlay').src = 'data:image/png;base64,' + d.overlay;
841
+
842
+ document.getElementById('s-illegal').textContent = d.illegal_count;
843
+ document.getElementById('s-legal').textContent = d.legal_count;
844
+ document.getElementById('s-pct').textContent = d.illegal_percent + '%';
845
+ document.getElementById('s-device').textContent = d.device.toUpperCase();
846
+
847
+ document.getElementById('results').style.display = 'block';
848
+ document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
849
+ } catch (err) {
850
+ showError(err.message || 'Server error. Is Flask running?');
851
+ } finally {
852
+ btn.disabled = false;
853
+ spinner.style.display = 'none';
854
+ btnText.textContent = 'ANALYZE AGAIN';
855
+ updateAnalyzeBtn();
856
+ }
857
+ });
858
+
859
+ function showError(msg) {
860
+ const e = document.getElementById('error');
861
+ e.textContent = '⚠ ' + msg;
862
+ e.style.display = 'block';
863
+ document.getElementById('spinner').style.display = 'none';
864
+ document.getElementById('btn-text').textContent = 'ANALYZE AGAIN';
865
+ document.getElementById('analyze-btn').disabled = false;
866
+ updateAnalyzeBtn();
867
+ }
868
+ </script>
869
+
870
+ <!-- Load Google Maps -->
871
+ <script
872
+ src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk&callback=Function.prototype"
873
+ async defer></script>
874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
  </body>
876
+
877
+ </html>