/* ═══════════════════════════════════════════════════════
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 += `
`;
});
}
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}
`;
});
// 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}
`;
}
// ─── 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 `
${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 LocationBefore You Invest
Enter any location in India to generate a complete intelligence report — connectivity, livability, environment, infrastructure, risk, and AI-powered insights.
Kokapet, Hyderabad
Gachibowli, Hyderabad
Whitefield, Bangalore
Madhapur, Hyderabad
Baner, Pune
Visakhapatnam
Interactive Map
Nearby amenities & infrastructure
Hospital
School
Bank
Restaurant
Transit
Park
Connectivity Intelligence
Nearby amenities within selected radius
Livability Intelligence
Quality of life metrics by category
Environmental Intelligence
Air quality & pollution metrics
—
Infrastructure Intelligence
Access to critical infrastructure
Risk Intelligence
Derived risk assessment for this location
Category Comparison
Radar analysis of all intelligence layers
AI-Powered Insights
Automated intelligence analysis
Score Breakdown
Weighted component scores
Zone IQ — Built with OpenStreetMap, Overpass API & OpenAQ. Data is for informational purposes only.
Analyzing Location
Fetching location data…