/* ═══════════════════════════════════════════════════════ ZONE IQ — Complete Application Logic Free APIs: Nominatim, Overpass, OpenAQ, Leaflet/OSM ═══════════════════════════════════════════════════════ */ (function () { 'use strict'; // ─── STATE ─────────────────────────────────────────── const state = { location: null, // { lat, lon, name, type } radius: 3, // km amenities: {}, // counts per category scores: {}, // component scores overallScore: 0, charts: {}, map: null, mapMarkers: [], radiusCircle: null, aqi: null, debounceTimer: null, }; // ─── CONFIG ────────────────────────────────────────── const NOMINATIM_URL = 'https://nominatim.openstreetmap.org'; const OVERPASS_URL = 'https://overpass-api.de/api/interpreter'; const OPENAQ_URL = 'https://api.openaq.io/v2/measurements'; // Category definitions const CATEGORIES = [ { key: 'school', label: 'Schools', icon: '🏫', color: '#3b82f6', osm: 'amenity=school' }, { key: 'college', label: 'Colleges', icon: '🎓', color: '#6366f1', osm: 'amenity=college' }, { key: 'university', label: 'Universities', icon: '🏛️', color: '#8b5cf6', osm: 'amenity=university' }, { key: 'hospital', label: 'Hospitals', icon: '🏥', color: '#ef4444', osm: 'amenity=hospital' }, { key: 'pharmacy', label: 'Pharmacies', icon: '💊', color: '#f43f5e', osm: 'amenity=pharmacy' }, { key: 'restaurant', label: 'Restaurants', icon: '🍽️', color: '#f59e0b', osm: 'amenity=restaurant' }, { key: 'bank', label: 'Banks', icon: '🏦', color: '#10b981', osm: 'amenity=bank' }, { key: 'bus_stop', label: 'Bus Stops', icon: '🚌', color: '#8b5cf6', osm: 'highway=bus_stop' }, { key: 'subway_station', label: 'Metro Stations', icon: '🚇', color: '#6366f1', osm: 'station=subway' }, { key: 'railway_station', label: 'Rail Stations', icon: '🚆', color: '#a855f7', osm: 'railway=station' }, { key: 'park', label: 'Parks', icon: '🌳', color: '#06b6d4', osm: 'leisure=park' }, { key: 'supermarket', label: 'Supermarkets', icon: '🛒', color: '#84cc16', osm: 'shop=supermarket' }, ]; const LIVABILITY_METRICS = [ { key: 'education', label: 'Education Access', keys: ['school','college','university'], max: 10, color: '#3b82f6' }, { key: 'healthcare', label: 'Healthcare Access', keys: ['hospital','pharmacy'], max: 8, color: '#ef4444' }, { key: 'convenience', label: 'Convenience Access', keys: ['supermarket','restaurant','bank'], max: 12, color: '#f59e0b' }, { key: 'parks', label: 'Parks & Nature', keys: ['park'], max: 6, color: '#10b981' }, { key: 'transit', label: 'Transit Access', keys: ['bus_stop','subway_station','railway_station'], max: 10, color: '#8b5cf6' }, ]; // ─── UTILITIES ─────────────────────────────────────── const $ = id => document.getElementById(id); const sleep = ms => new Promise(r => setTimeout(r, ms)); function showToast(msg, type = 'info') { const toast = $('toast'); const icon = $('toast-icon'); const msgEl = $('toast-msg'); const configs = { success: { bg: 'rgba(16,185,129,0.15)', border: 'rgba(16,185,129,0.3)', color: '#10b981', svg: ''}, error: { bg: 'rgba(244,63,94,0.15)', border: 'rgba(244,63,94,0.3)', color: '#f43f5e', svg: ''}, info: { bg: 'rgba(59,130,246,0.15)', border: 'rgba(59,130,246,0.3)', color: '#3b82f6', svg: ''}, }; const c = configs[type] || configs.info; toast.style.background = c.bg; toast.style.borderColor = c.border; icon.style.background = c.bg; icon.style.color = c.color; icon.innerHTML = `${c.svg}`; msgEl.textContent = msg; toast.classList.remove('hidden'); toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.classList.add('hidden'), 300); }, 3500); } function setLoadingMsg(title, msg) { $('loading-title').textContent = title; $('loading-msg').textContent = msg; } function showLoading(show) { $('loading-overlay').classList.toggle('hidden', !show); } function setStatus(text, color = '#10b981') { $('status-text').textContent = text; $('status-dot').style.background = color; } function animateValue(el, from, to, duration = 1000) { if (!el) return; const start = performance.now(); const update = now => { const t = Math.min((now - start) / duration, 1); const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; el.textContent = Math.round(from + (to - from) * ease); if (t < 1) requestAnimationFrame(update); }; requestAnimationFrame(update); } // Cache helpers function cacheGet(key) { try { const item = localStorage.getItem('ziq_' + key); if (!item) return null; const { data, ts } = JSON.parse(item); if (Date.now() - ts > 15 * 60 * 1000) { localStorage.removeItem('ziq_' + key); return null; } return data; } catch { return null; } } function cacheSet(key, data) { try { localStorage.setItem('ziq_' + key, JSON.stringify({ data, ts: Date.now() })); } catch {} } // ─── NOMINATIM GEOCODING ───────────────────────────── async function geocode(query) { const ckey = 'geo_' + query.toLowerCase().replace(/\s+/g, '_'); const cached = cacheGet(ckey); if (cached) return cached; const url = `${NOMINATIM_URL}/search?q=${encodeURIComponent(query)}&countrycodes=in&format=json&addressdetails=1&limit=5`; const res = await fetch(url, { headers: { 'Accept-Language': 'en' } }); if (!res.ok) throw new Error('Geocoding failed'); const data = await res.json(); cacheSet(ckey, data); return data; } async function getSuggestions(query) { if (query.length < 3) return []; try { const url = `${NOMINATIM_URL}/search?q=${encodeURIComponent(query + ' India')}&format=json&limit=5&addressdetails=1`; const res = await fetch(url, { headers: { 'Accept-Language': 'en' } }); return await res.json(); } catch { return []; } } // ─── OVERPASS API ───────────────────────────────────── function buildOverpassQuery(lat, lon, radiusMeters) { const queries = CATEGORIES.map(cat => { const [k, v] = cat.osm.split('='); return ` node["${k}"="${v}"](around:${radiusMeters},${lat},${lon}); way["${k}"="${v}"](around:${radiusMeters},${lat},${lon});`; }).join(''); return ` [out:json][timeout:30]; ( ${queries} ); out center;`; } async function fetchAmenities(lat, lon, radiusKm) { const radiusM = radiusKm * 1000; const ckey = `amenities_${lat.toFixed(3)}_${lon.toFixed(3)}_${radiusKm}`; const cached = cacheGet(ckey); if (cached) return cached; const query = buildOverpassQuery(lat, lon, radiusM); const res = await fetch(OVERPASS_URL, { method: 'POST', body: 'data=' + encodeURIComponent(query), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); if (!res.ok) throw new Error('Overpass API error'); const data = await res.json(); cacheSet(ckey, data); return data; } function parseAmenities(overpassData) { const counts = {}; CATEGORIES.forEach(c => { counts[c.key] = []; }); (overpassData.elements || []).forEach(el => { const tags = el.tags || {}; CATEGORIES.forEach(cat => { const [k, v] = cat.osm.split('='); if (tags[k] === v) { const center = el.center || el; counts[cat.key].push({ lat: center.lat, lon: center.lon, name: tags.name || cat.label.replace(/s$/, ''), tags, }); } }); }); return counts; } // ─── OPENAQ AIR QUALITY ─────────────────────────────── async function fetchAirQuality(lat, lon) { const ckey = `aqi_${lat.toFixed(2)}_${lon.toFixed(2)}`; const cached = cacheGet(ckey); if (cached) return cached; try { const url = `${OPENAQ_URL}?coordinates=${lat},${lon}&radius=50000&limit=100¶meters[]=pm25¶meters[]=pm10¶meters[]=no2¶meters[]=so2&order_by=datetime&sort=desc`; const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; const data = await res.json(); if (!data.results || data.results.length === 0) return null; cacheSet(ckey, data.results); return data.results; } catch { return null; } } function parseAQI(results) { if (!results || results.length === 0) return null; const latest = {}; results.forEach(r => { if (!latest[r.parameter] || new Date(r.date.utc) > new Date(latest[r.parameter].date.utc)) { latest[r.parameter] = r; } }); const pm25 = latest.pm25?.value ?? null; const pm10 = latest.pm10?.value ?? null; const no2 = latest.no2?.value ?? null; const so2 = latest.so2?.value ?? null; // Calculate AQI from PM2.5 (standard US EPA formula simplified) let aqi = null; if (pm25 !== null) { if (pm25 <= 12.0) aqi = Math.round((50/12) * pm25); else if (pm25 <= 35.4) aqi = Math.round(50 + ((100-50)/(35.4-12)) * (pm25 - 12)); else if (pm25 <= 55.4) aqi = Math.round(100 + ((150-100)/(55.4-35.4)) * (pm25 - 35.4)); else if (pm25 <= 150.4) aqi = Math.round(150 + ((200-150)/(150.4-55.4)) * (pm25 - 55.4)); else if (pm25 <= 250.4) aqi = Math.round(200 + ((300-200)/(250.4-150.4)) * (pm25 - 150.4)); else aqi = Math.round(300 + ((400-300)/(350.4-250.4)) * (pm25 - 250.4)); } return { aqi, pm25, pm10, no2, so2, source: 'OpenAQ' }; } function aqiCategory(aqi) { if (aqi === null) return { label: 'Unknown', color: '#6b7280', bg: 'rgba(107,114,128,0.1)', score: 60 }; if (aqi <= 50) return { label: 'Excellent', color: '#10b981', bg: 'rgba(16,185,129,0.1)', score: 95 }; if (aqi <= 100) return { label: 'Good', color: '#84cc16', bg: 'rgba(132,204,22,0.1)', score: 80 }; if (aqi <= 150) return { label: 'Moderate', color: '#f59e0b', bg: 'rgba(245,158,11,0.1)', score: 60 }; if (aqi <= 200) return { label: 'Poor', color: '#f97316', bg: 'rgba(249,115,22,0.1)', score: 40 }; if (aqi <= 300) return { label: 'Very Poor', color: '#ef4444', bg: 'rgba(239,68,68,0.1)', score: 20 }; return { label: 'Hazardous', color: '#991b1b', bg: 'rgba(153,27,27,0.1)', score: 5 }; } // ─── SCORE CALCULATORS ──────────────────────────────── function calcConnectivityScore(counts) { const weights = { school: 3, college: 4, university: 5, hospital: 5, pharmacy: 2, restaurant: 1, bank: 2, bus_stop: 2, subway_station: 8, railway_station: 6, park: 3, supermarket: 2, }; let raw = 0; let maxRaw = 0; CATEGORIES.forEach(cat => { const w = weights[cat.key] || 1; const n = (counts[cat.key] || []).length; const cap = cat.key === 'bus_stop' ? 20 : 10; // cap to prevent outlier inflation raw += Math.min(n, cap) * w; maxRaw += cap * w; }); return Math.min(100, Math.round((raw / maxRaw) * 100)); } function calcLivabilityScore(counts) { const scores = {}; LIVABILITY_METRICS.forEach(m => { const total = m.keys.reduce((s, k) => s + (counts[k] || []).length, 0); scores[m.key] = Math.min(100, Math.round((Math.min(total, m.max) / m.max) * 100)); }); const avg = Object.values(scores).reduce((a, b) => a + b, 0) / LIVABILITY_METRICS.length; return { overall: Math.round(avg), breakdown: scores }; } function calcInfrastructureScore(counts, locName) { // Derived from OSM data + name-based heuristics const name = (locName || '').toLowerCase(); const hasMetro = (counts.subway_station || []).length > 0; const hasRail = (counts.railway_station || []).length > 0; const hasBus = (counts.bus_stop || []).length >= 3; // Heuristic: IT hubs const itKeywords = ['gachibowli','madhapur','hitec','whitefield','koramangala','hinjewadi','cybercity','electronic','salcette','nanakramguda','kokapet','financial district']; const isITHub = itKeywords.some(k => name.includes(k)); // Airport proximity (city-level heuristic) const airportCities = ['hyderabad','bangalore','bengaluru','mumbai','delhi','chennai','pune','kolkata','ahmedabad']; const nearAirport = airportCities.some(c => name.includes(c)); // Highway access (approximation from bus density) const busCount = (counts.bus_stop || []).length; const highwayScore = Math.min(100, busCount * 5); const metroScore = hasMetro ? 90 : (hasRail ? 60 : 20); const railScore = hasRail ? 85 : 30; const airportScore = nearAirport ? 70 : 35; const itScore = isITHub ? 90 : 40; const overall = Math.round((highwayScore * 0.2 + metroScore * 0.3 + railScore * 0.15 + airportScore * 0.1 + itScore * 0.25)); return { overall, breakdown: { 'Highway Access': { score: highwayScore, icon: '🛣️' }, 'Metro Access': { score: metroScore, icon: '🚇' }, 'Rail Access': { score: railScore, icon: '🚆' }, 'Airport Proximity': { score: airportScore, icon: '✈️' }, 'IT Hub Proximity': { score: itScore, icon: '💻' }, } }; } function calcRiskScore(counts, aqiData, locName) { const name = (locName || '').toLowerCase(); const busCount = (counts.bus_stop || []).length; const restCount = (counts.restaurant || []).length; // Traffic Risk: high bus count + many restaurants = traffic const trafficRaw = Math.min(100, (busCount * 4 + restCount * 2)); const trafficRisk = trafficRaw > 70 ? 'High' : trafficRaw > 40 ? 'Medium' : 'Low'; // Pollution Risk: from AQI const aqi = aqiData?.aqi ?? null; const pollutionRisk = aqi === null ? 'Medium' : aqi > 150 ? 'High' : aqi > 100 ? 'Medium' : 'Low'; // Flood Risk: coastal/low-lying heuristic const floodKeywords = ['visakhapatnam','vizag','mumbai','kolkata','chennai','kochi','surat','coastal','delta','low']; const floodRisk = floodKeywords.some(k => name.includes(k)) ? 'Medium' : 'Low'; // Overcrowding Risk: high density of amenities const totalAmenities = CATEGORIES.reduce((s, c) => s + (counts[c.key] || []).length, 0); const overcrowdingRisk = totalAmenities > 150 ? 'High' : totalAmenities > 80 ? 'Medium' : 'Low'; const riskToScore = { Low: 90, Medium: 60, High: 25 }; const avg = (riskToScore[trafficRisk] + riskToScore[pollutionRisk] + riskToScore[floodRisk] + riskToScore[overcrowdingRisk]) / 4; return { overall: Math.round(avg), items: [ { label: 'Traffic Risk', risk: trafficRisk, icon: '🚦' }, { label: 'Pollution Risk', risk: pollutionRisk, icon: '🏭' }, { label: 'Flood Risk', risk: floodRisk, icon: '🌊' }, { label: 'Overcrowding Risk', risk: overcrowdingRisk, icon: '👥' }, ] }; } function calcOverallScore(scores) { const { connectivity, livability, environment, infrastructure, risk } = scores; return Math.round( connectivity * 0.30 + livability * 0.25 + environment * 0.15 + infrastructure* 0.20 + risk * 0.10 ); } function scoreGrade(score) { if (score >= 85) return { grade: 'A+', label: 'Exceptional', color: '#10b981' }; if (score >= 75) return { grade: 'A', label: 'Excellent', color: '#84cc16' }; if (score >= 65) return { grade: 'B+', label: 'Very Good', color: '#3b82f6' }; if (score >= 55) return { grade: 'B', label: 'Good', color: '#6366f1' }; if (score >= 45) return { grade: 'C+', label: 'Average', color: '#f59e0b' }; if (score >= 35) return { grade: 'C', label: 'Below Avg', color: '#f97316' }; return { grade: 'D', label: 'Poor', color: '#ef4444' }; } // ─── AI INSIGHTS GENERATOR ──────────────────────────── function generateInsights(locName, counts, scores, aqiData, riskData) { const name = locName.split(',')[0].trim(); const total = CATEGORIES.reduce((s, c) => s + (counts[c.key] || []).length, 0); const g = scoreGrade(scores.overall); const schools = (counts.school || []).length + (counts.college || []).length + (counts.university || []).length; const hospitals = (counts.hospital || []).length + (counts.pharmacy || []).length; const transit = (counts.bus_stop || []).length + (counts.subway_station || []).length + (counts.railway_station || []).length; const parks = (counts.park || []).length; const aqiInfo = aqiData ? aqiCategory(aqiData.aqi) : null; // Summary const summary = `${name} scores ${scores.overall}/100 on the Zone IQ Index, earning a grade of ${g.grade} — ${g.label}. The area has ${total} mapped amenities within the selected radius, with particularly ${scores.connectivity >= 65 ? 'strong' : 'moderate'} connectivity (${scores.connectivity}/100) and ${scores.livability >= 65 ? 'excellent' : 'developing'} livability (${scores.livability}/100). ${aqiInfo ? `Air quality is rated ${aqiInfo.label}.` : 'Air quality data was unavailable for this location.'}`; // Strengths const strengths = []; if (scores.connectivity >= 65) strengths.push(`Strong connectivity with ${total} nearby amenities`); if (schools >= 5) strengths.push(`Good educational infrastructure (${schools} institutions)`); if (hospitals >= 3) strengths.push(`Adequate healthcare access (${hospitals} facilities)`); if (transit >= 5) strengths.push(`Well-connected transit network (${transit} stops)`); if (parks >= 3) strengths.push(`Green spaces available (${parks} parks)`); if (scores.infrastructure >= 70) strengths.push('Strong infrastructure & IT hub proximity'); if (scores.environment >= 70) strengths.push('Favorable environmental conditions'); if (strengths.length === 0) strengths.push('Developing area with growth potential'); // Weaknesses const weaknesses = []; if (scores.connectivity < 50) weaknesses.push('Limited nearby amenities'); if (schools < 3) weaknesses.push('Limited educational institutions nearby'); if (hospitals < 2) weaknesses.push('Healthcare access needs improvement'); if (transit < 3) weaknesses.push('Public transit connectivity is limited'); if (parks < 2) weaknesses.push('Limited green spaces and parks'); if (scores.environment < 50) weaknesses.push('Air quality concerns in the area'); const highRisks = (riskData?.items || []).filter(r => r.risk === 'High'); if (highRisks.length > 0) weaknesses.push(`High ${highRisks.map(r => r.label.toLowerCase()).join(' & ')}`); if (weaknesses.length === 0) weaknesses.push('No major weaknesses identified'); // Best For const bestFor = []; if (scores.livability >= 65 && schools >= 3) bestFor.push('Families'); if (scores.infrastructure >= 65) bestFor.push('Working Professionals'); if (scores.connectivity >= 60) bestFor.push('Investors'); if (schools >= 4 && scores.connectivity >= 55) bestFor.push('Students'); if (scores.overall >= 70) bestFor.push('Retirees'); if (bestFor.length === 0) bestFor.push('General Residents'); // Investment Potential let investPotential, investText; if (scores.overall >= 75) { investPotential = 'High'; investText = `${name} shows strong investment potential. High connectivity, good infrastructure, and solid livability scores make this a prime location for residential and commercial real estate investment. Expect above-average appreciation.`; } else if (scores.overall >= 55) { investPotential = 'Moderate'; investText = `${name} presents moderate investment opportunity. The area has solid fundamentals with room for growth. Infrastructure development and improving amenities make this a reasonable medium-term investment.`; } else { investPotential = 'Developing'; investText = `${name} is in a developing phase. Current scores indicate limited infrastructure but potential for growth. Early-stage investors may benefit from lower entry points, though risks are higher.`; } return { summary, strengths, weaknesses, bestFor, investPotential, investText, grade: g }; } // ─── MAP MANAGEMENT ────────────────────────────────── function initMap(lat, lon) { if (!state.map) { state.map = L.map('map', { zoomControl: true, scrollWheelZoom: true }).setView([lat, lon], 14); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19, }).addTo(state.map); } else { state.map.setView([lat, lon], 14); } } function clearMapMarkers() { state.mapMarkers.forEach(m => state.map.removeLayer(m)); state.mapMarkers = []; if (state.radiusCircle) { state.map.removeLayer(state.radiusCircle); state.radiusCircle = null; } } function createColorIcon(color, emoji) { return L.divIcon({ className: '', html: `
${emoji}
`, iconSize: [28, 28], iconAnchor: [14, 14], }); } function plotMapMarkers(lat, lon, counts, radiusKm) { clearMapMarkers(); // Radius circle state.radiusCircle = L.circle([lat, lon], { radius: radiusKm * 1000, color: 'rgba(59,130,246,0.6)', fillColor: 'rgba(59,130,246,0.05)', fillOpacity: 1, weight: 1.5, dashArray: '6 4', }).addTo(state.map); // Center marker const centerMarker = L.marker([lat, lon], { icon: L.divIcon({ className: '', html: `
📍
`, iconSize: [36, 36], iconAnchor: [18, 18], }) }).addTo(state.map); centerMarker.bindPopup(`${state.location?.name || 'Selected Location'}
${lat.toFixed(4)}, ${lon.toFixed(4)}`); state.mapMarkers.push(centerMarker); // Amenity markers (cap per category to avoid overload) const CAP = 12; CATEGORIES.forEach(cat => { const items = (counts[cat.key] || []).slice(0, CAP); items.forEach(item => { if (!item.lat || !item.lon) return; const marker = L.marker([item.lat, item.lon], { icon: createColorIcon(cat.color, cat.icon) }) .addTo(state.map); marker.bindPopup(`
${cat.icon} ${item.name || cat.label}
${cat.label}
${item.tags?.['addr:street'] ? `
${item.tags['addr:street']}
` : ''}
`); state.mapMarkers.push(marker); }); }); } // ─── CHART MANAGEMENT ──────────────────────────────── function destroyChart(key) { if (state.charts[key]) { state.charts[key].destroy(); delete state.charts[key]; } } function buildGaugeChart(score) { destroyChart('gauge'); const g = scoreGrade(score); const ctx = document.getElementById('gauge-chart')?.getContext('2d'); if (!ctx) return; state.charts.gauge = new Chart(ctx, { type: 'doughnut', data: { datasets: [{ data: [score, 100 - score], backgroundColor: [g.color, '#1a2340'], borderWidth: 0, circumference: 270, rotation: 225, }] }, options: { responsive: false, cutout: '78%', plugins: { legend: { display: false }, tooltip: { enabled: false } }, animation: { duration: 1500, easing: 'easeInOutQuart' }, } }); $('gauge-score').textContent = score; $('gauge-grade').textContent = g.grade + ' · ' + g.label; $('gauge-grade').style.color = g.color; } function buildRadarChart(scores) { destroyChart('radar'); const ctx = document.getElementById('radar-chart')?.getContext('2d'); if (!ctx) return; state.charts.radar = new Chart(ctx, { type: 'radar', data: { labels: ['Connectivity', 'Livability', 'Environment', 'Infrastructure', 'Risk'], datasets: [{ label: state.location?.name || 'Zone IQ', data: [ scores.connectivity, scores.livability, scores.environment, scores.infrastructure, scores.risk, ], borderColor: 'rgba(59,130,246,0.9)', backgroundColor: 'rgba(59,130,246,0.12)', pointBackgroundColor: '#3b82f6', pointBorderColor: '#fff', pointRadius: 4, borderWidth: 2, }] }, options: { responsive: true, maintainAspectRatio: false, scales: { r: { min: 0, max: 100, ticks: { display: false, stepSize: 25 }, grid: { color: 'rgba(30,45,74,0.6)' }, angleLines: { color: 'rgba(30,45,74,0.6)' }, pointLabels: { color: '#6b7280', font: { size: 11, family: 'DM Sans' } }, } }, plugins: { legend: { display: false } }, animation: { duration: 1200 }, } }); } function buildBarChart(scores) { destroyChart('bar'); const ctx = document.getElementById('bar-chart')?.getContext('2d'); if (!ctx) return; state.charts.bar = new Chart(ctx, { type: 'bar', data: { labels: ['Connectivity', 'Livability', 'Environment', 'Infrastructure', 'Risk Score'], datasets: [{ data: [scores.connectivity, scores.livability, scores.environment, scores.infrastructure, scores.risk], backgroundColor: [ 'rgba(59,130,246,0.7)', 'rgba(16,185,129,0.7)', 'rgba(245,158,11,0.7)', 'rgba(139,92,246,0.7)', 'rgba(244,63,94,0.7)', ], borderColor: [ 'rgba(59,130,246,1)', 'rgba(16,185,129,1)', 'rgba(245,158,11,1)', 'rgba(139,92,246,1)', 'rgba(244,63,94,1)', ], borderWidth: 1.5, borderRadius: 6, }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { color: 'rgba(30,45,74,0.4)' }, ticks: { color: '#6b7280', font: { size: 11, family: 'DM Sans' } }, }, y: { min: 0, max: 100, grid: { color: 'rgba(30,45,74,0.4)' }, ticks: { color: '#6b7280', font: { size: 11, family: 'DM Sans' }, stepSize: 25 }, }, }, plugins: { legend: { display: false } }, animation: { duration: 1000 }, } }); } // ─── UI RENDERERS ───────────────────────────────────── function renderLocationHeader(loc, score) { $('loc-name').textContent = loc.name; $('loc-lat').textContent = loc.lat.toFixed(4); $('loc-lon').textContent = loc.lon.toFixed(4); $('loc-type').textContent = loc.type || 'Location'; const g = scoreGrade(score); animateValue($('header-score'), 0, score, 1200); $('header-grade').textContent = g.grade; $('header-grade').style.background = `linear-gradient(135deg, ${g.color}, ${g.color}99)`; } function renderScorePanel(scores) { const animate = (id, val) => { const el = $(id); if (el) animateValue(el, 0, val, 1000); }; animate('score-connectivity', scores.connectivity); animate('score-livability', scores.livability); animate('score-environment', scores.environment); animate('score-infrastructure', scores.infrastructure); animate('score-risk', scores.risk); setTimeout(() => { const set = (barId, val) => { const el = $(barId); if (el) el.style.width = val + '%'; }; set('bar-connectivity', scores.connectivity); set('bar-livability', scores.livability); set('bar-environment', scores.environment); set('bar-infrastructure', scores.infrastructure); set('bar-risk', scores.risk); }, 200); } function renderConnectivity(counts, score) { $('connectivity-score-display').textContent = score; const grid = $('connectivity-grid'); grid.innerHTML = ''; CATEGORIES.forEach(cat => { const n = (counts[cat.key] || []).length; const pct = Math.min(100, Math.round((n / 15) * 100)); grid.innerHTML += `
${cat.icon} ${n}
${cat.label}
`; }); } function renderLivability(counts, livData) { $('livability-score-display').textContent = livData.overall; const container = $('livability-bars'); container.innerHTML = ''; LIVABILITY_METRICS.forEach(m => { const val = livData.breakdown[m.key] ?? 0; container.innerHTML += `
${m.label} ${val}/100
`; }); } function renderEnvironment(aqiData, envScore) { const container = $('env-content'); const aqiBadge = $('aqi-badge'); if (!aqiData) { aqiBadge.textContent = 'Unavailable'; aqiBadge.style.background = 'rgba(107,114,128,0.1)'; aqiBadge.style.color = '#6b7280'; container.innerHTML = `
🌫️
Air Quality Data Unavailable
No OpenAQ monitoring stations found within 50km. Using estimated score based on location heuristics.
Estimated Score ${envScore}
`; return; } const cat = aqiCategory(aqiData.aqi); aqiBadge.textContent = cat.label; aqiBadge.style.background = cat.bg; aqiBadge.style.color = cat.color; const fmt = val => val !== null ? val.toFixed(1) : '—'; container.innerHTML = `
${aqiData.aqi ?? '—'}
AQI
${cat.label}
Source: ${aqiData.source}
${[ { label: 'PM2.5', val: fmt(aqiData.pm25), unit: 'μg/m³', color: '#f59e0b' }, { label: 'PM10', val: fmt(aqiData.pm10), unit: 'μg/m³', color: '#f97316' }, { label: 'NO₂', val: fmt(aqiData.no2), unit: 'μg/m³', color: '#8b5cf6' }, { label: 'SO₂', val: fmt(aqiData.so2), unit: 'μg/m³', color: '#ef4444' }, ].map(p => `
${p.label}
${p.val} ${p.val !== '—' ? p.unit : ''}
`).join('')}
`; } function renderInfrastructure(infraData) { $('infra-score-display').textContent = infraData.overall; const container = $('infra-content'); container.innerHTML = ''; Object.entries(infraData.breakdown).forEach(([label, data]) => { const col = data.score >= 70 ? '#10b981' : data.score >= 45 ? '#f59e0b' : '#f43f5e'; container.innerHTML += `
${data.icon}
${label} ${data.score}/100
`; }); } function renderRisk(riskData) { const container = $('risk-content'); container.innerHTML = ''; riskData.items.forEach(item => { const cls = item.risk === 'High' ? 'risk-high' : item.risk === 'Medium' ? 'risk-medium' : 'risk-low'; const dot = item.risk === 'High' ? '#f43f5e' : item.risk === 'Medium' ? '#f59e0b' : '#10b981'; container.innerHTML += `
${item.icon} ${item.risk}
${item.label}
${item.risk === 'High' ? 'Significant concern' : item.risk === 'Medium' ? 'Moderate concern' : 'Minimal concern'}
`; }); } function renderAIInsights(insights) { // Main cards const container = $('ai-insights-content'); container.innerHTML = ''; const cards = [ { title: 'Strengths', icon: '✅', color: '#10b981', content: insights.strengths.map(s => `
  • •${s}
  • `).join(''), }, { title: 'Weaknesses', icon: '⚠️', color: '#f59e0b', content: insights.weaknesses.map(w => `
  • •${w}
  • `).join(''), }, { title: 'Best For', icon: '🎯', color: '#3b82f6', content: insights.bestFor.map(b => `
  • →${b}
  • `).join(''), }, { title: `Grade: ${insights.grade.grade}`, icon: '🏆', color: insights.grade.color, content: `
    ${insights.grade.grade}
    ${insights.grade.label}
    `, }, ]; cards.forEach(card => { container.innerHTML += `
    ${card.icon} ${card.title}
      ${card.content}
    `; }); // Summary $('ai-summary').innerHTML = `
    📝 Location Summary

    ${insights.summary}

    `; // Best For tags $('ai-best-for').innerHTML = `
    👥 Recommended For
    ${insights.bestFor.map(b => `${b}`).join('')}
    `; // Investment const potColor = insights.investPotential === 'High' ? '#10b981' : insights.investPotential === 'Moderate' ? '#f59e0b' : '#6b7280'; $('ai-investment').innerHTML = `
    Investment Potential
    ${insights.investPotential}

    ${insights.investText}

    `; } // ─── MAIN ANALYSIS PIPELINE ────────────────────────── async function analyze(lat, lon, locName, locType) { $('empty-state').classList.add('hidden'); $('dashboard').classList.remove('hidden'); showLoading(true); setStatus('Analyzing…', '#f59e0b'); try { // Step 1: Geocode already done; just store state.location = { lat, lon, name: locName, type: locType }; // Step 2: Init map setLoadingMsg('Setting up Map', 'Initializing interactive map…'); initMap(lat, lon); // Step 3: Fetch amenities setLoadingMsg('Fetching Amenities', `Querying OSM for ${locName}…`); const overpassData = await fetchAmenities(lat, lon, state.radius); const counts = parseAmenities(overpassData); // Step 4: Fetch air quality setLoadingMsg('Checking Air Quality', 'Querying OpenAQ monitoring stations…'); const aqiRaw = await fetchAirQuality(lat, lon); const aqiData = parseAQI(aqiRaw); // Step 5: Calculate scores setLoadingMsg('Computing Scores', 'Running intelligence algorithms…'); const connectScore = calcConnectivityScore(counts); const livData = calcLivabilityScore(counts); const infraData = calcInfrastructureScore(counts, locName); const riskData = calcRiskScore(counts, aqiData, locName); const envScore = aqiData ? aqiCategory(aqiData.aqi).score : getEstimatedEnvScore(locName); const scores = { connectivity: connectScore, livability: livData.overall, environment: envScore, infrastructure: infraData.overall, risk: riskData.overall, }; const overall = calcOverallScore(scores); scores.overall = overall; state.scores = scores; // Step 6: Render setLoadingMsg('Rendering Dashboard', 'Preparing visualizations…'); renderLocationHeader(state.location, overall); renderScorePanel(scores); renderConnectivity(counts, connectScore); renderLivability(counts, livData); renderEnvironment(aqiData, envScore); renderInfrastructure(infraData); renderRisk(riskData); const insights = generateInsights(locName, counts, scores, aqiData, riskData); renderAIInsights(insights); plotMapMarkers(lat, lon, counts, state.radius); // Charts setTimeout(() => { buildGaugeChart(overall); buildRadarChart(scores); buildBarChart(scores); }, 100); showLoading(false); setStatus('Analysis Complete', '#10b981'); showToast(`${locName.split(',')[0]} analyzed successfully`, 'success'); } catch (err) { showLoading(false); setStatus('Error', '#f43f5e'); console.error('Analysis error:', err); showToast('Analysis failed. Check connection and try again.', 'error'); } } function getEstimatedEnvScore(locName) { const name = locName.toLowerCase(); const greenCities = ['hyderabad','bangalore','bengaluru','pune','chandigarh','mysore']; const pollutedCities = ['delhi','ghaziabad','kanpur','lucknow','agra','meerut','kolkata']; if (pollutedCities.some(c => name.includes(c))) return 35 + Math.floor(Math.random() * 15); if (greenCities.some(c => name.includes(c))) return 55 + Math.floor(Math.random() * 20); return 45 + Math.floor(Math.random() * 20); } // ─── SEARCH HANDLING ───────────────────────────────── async function handleSearch(query) { if (!query.trim()) { showToast('Please enter a location to search', 'error'); return; } setStatus('Searching…', '#f59e0b'); showLoading(true); setLoadingMsg('Geocoding Location', `Looking up "${query}"…`); try { const results = await geocode(query); if (!results || results.length === 0) { showLoading(false); setStatus('Not Found', '#f43f5e'); showToast('Location not found. Try a more specific search.', 'error'); return; } const top = results[0]; const lat = parseFloat(top.lat); const lon = parseFloat(top.lon); const locName = top.display_name.split(',').slice(0, 3).join(','); const locType = top.type || top.class || 'Location'; $('search-input').value = locName; hideSuggestions(); showLoading(false); await analyze(lat, lon, locName, locType); } catch (err) { showLoading(false); setStatus('Error', '#f43f5e'); showToast('Search failed. Please try again.', 'error'); } } function hideSuggestions() { const sug = $('search-suggestions'); sug.innerHTML = ''; sug.classList.add('hidden'); } function showSuggestions(results) { const sug = $('search-suggestions'); if (!results.length) { hideSuggestions(); return; } sug.innerHTML = results.map((r, i) => { const name = r.display_name.split(',').slice(0, 3).join(','); const extra = r.display_name.split(',').slice(3, 5).join(','); return `
    ${name}
    ${extra}
    ${i < results.length - 1 ? '
    ' : ''} `; }).join(''); sug.classList.remove('hidden'); sug.querySelectorAll('.suggestion-item').forEach(el => { el.addEventListener('click', () => { const r = results[parseInt(el.dataset.idx)]; $('search-input').value = r.display_name.split(',').slice(0, 3).join(','); hideSuggestions(); handleSearch($('search-input').value); }); }); } // ─── RADIUS SELECTOR ───────────────────────────────── function setupRadiusSelector() { document.querySelectorAll('.radius-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.radius-btn').forEach(b => { b.style.background = 'rgba(26,35,64,0.8)'; b.style.border = '1px solid #1e2d4a'; b.style.color = '#6b7280'; b.classList.remove('active-radius'); }); btn.style.background = 'rgba(59,130,246,0.2)'; btn.style.border = '1px solid rgba(59,130,246,0.4)'; btn.style.color = '#93c5fd'; btn.classList.add('active-radius'); const newRadius = parseInt(btn.dataset.radius); if (newRadius !== state.radius) { state.radius = newRadius; if (state.location) { analyze(state.location.lat, state.location.lon, state.location.name, state.location.type); } } }); }); } // ─── INIT ───────────────────────────────────────────── function init() { const searchInput = $('search-input'); const searchBtn = $('search-btn'); // Debounced suggestions searchInput.addEventListener('input', () => { clearTimeout(state.debounceTimer); const q = searchInput.value.trim(); if (q.length < 3) { hideSuggestions(); return; } state.debounceTimer = setTimeout(async () => { const results = await getSuggestions(q); showSuggestions(results); }, 350); }); searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') { hideSuggestions(); handleSearch(searchInput.value); } if (e.key === 'Escape') hideSuggestions(); }); searchBtn.addEventListener('click', () => handleSearch(searchInput.value)); // Close suggestions on outside click document.addEventListener('click', e => { if (!e.target.closest('#search-suggestions') && !e.target.closest('#search-input')) { hideSuggestions(); } }); setupRadiusSelector(); // Expose quickSearch for the hero buttons window.ZoneIQ = { quickSearch: (query) => { $('search-input').value = query; handleSearch(query); } }; } document.addEventListener('DOMContentLoaded', init); })(); Zone IQ — Know Every Location Before You Invest
    ZoneIQ
    Location Intelligence
    Location Intelligence Platform

    Know Every Location
    Before You Invest

    Enter any location in India to generate a complete intelligence report — connectivity, livability, environment, infrastructure, risk, and AI-powered insights.

    10+
    Data Layers
    Free
    No API Keys
    Live
    Real OSM Data