let selectedType = 'Appartement'; const steppers = { pieces: 3, lots: 1 }; // ── Three.js background ── function initThree() { const canvas = document.getElementById('bg-canvas'); const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.z = 30; const count = 1200; const positions = new Float32Array(count * 3); const colors = new Float32Array(count * 3); for (let i = 0; i < count; i++) { positions[i * 3] = (Math.random() - 0.5) * 80; positions[i * 3 + 1] = (Math.random() - 0.5) * 80; positions[i * 3 + 2] = (Math.random() - 0.5) * 40; const r = Math.random(); if (r < 0.5) { colors[i * 3] = 0.23; colors[i * 3 + 1] = 0.51; colors[i * 3 + 2] = 0.96; } else if (r < 0.8) { colors[i * 3] = 0.02; colors[i * 3 + 1] = 0.71; colors[i * 3 + 2] = 0.83; } else { colors[i * 3] = 0.55; colors[i * 3 + 1] = 0.58; colors[i * 3 + 2] = 0.70; } } const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); const mat = new THREE.PointsMaterial({ size: 0.12, vertexColors: true, transparent: true, opacity: 0.7, sizeAttenuation: true }); const points = new THREE.Points(geo, mat); scene.add(points); const lineMat = new THREE.LineBasicMaterial({ color: 0x1e3a5f, transparent: true, opacity: 0.15 }); for (let i = -5; i <= 5; i++) { const hGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(-40, i * 8, -10), new THREE.Vector3(40, i * 8, -10) ]); scene.add(new THREE.Line(hGeo, lineMat)); const vGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(i * 8, -40, -10), new THREE.Vector3(i * 8, 40, -10) ]); scene.add(new THREE.Line(vGeo, lineMat)); } let mouse = { x: 0, y: 0 }; document.addEventListener('mousemove', e => { mouse.x = (e.clientX / window.innerWidth - 0.5) * 0.3; mouse.y = (e.clientY / window.innerHeight - 0.5) * 0.3; }); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); let t = 0; function animate() { requestAnimationFrame(animate); t += 0.0005; points.rotation.y = t + mouse.x; points.rotation.x = mouse.y * 0.5; points.rotation.z = t * 0.3; renderer.render(scene, camera); } animate(); } // ── Compteur animé ── function animateCounters() { document.querySelectorAll('.stat-n').forEach(el => { const target = parseInt(el.dataset.target); const duration = 1500; const start = performance.now(); function update(now) { const progress = Math.min((now - start) / duration, 1); const ease = 1 - Math.pow(1 - progress, 3); const val = Math.floor(ease * target); el.textContent = target >= 1000000 ? (val / 1000000).toFixed(1) + 'M' : val.toLocaleString('fr-FR'); if (progress < 1) requestAnimationFrame(update); } requestAnimationFrame(update); }); } // ── Formulaire ── function selectType(btn) { document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); selectedType = btn.dataset.value; } function step(id, delta) { const min = id === 'pieces' ? 1 : 0; const max = id === 'pieces' ? 15 : 50; steppers[id] = Math.min(max, Math.max(min, steppers[id] + delta)); document.getElementById(id + '-val').textContent = steppers[id]; document.getElementById(id).value = steppers[id]; } function fmt(n) { return new Intl.NumberFormat('fr-FR').format(n) + ' €'; } async function loadDepartements() { const res = await fetch('/api/departements'); const depts = await res.json(); const sel = document.getElementById('departement'); sel.innerHTML = depts.map(d => ``).join(''); const idx = depts.indexOf('75'); if (idx >= 0) sel.selectedIndex = idx; loadCommunes(); } async function loadCommunes() { const dept = document.getElementById('departement').value; if (!dept) return; const res = await fetch(`/api/communes/${dept}`); const communes = await res.json(); const sel = document.getElementById('commune'); sel.innerHTML = communes.map(c => ``).join(''); } async function predict() { const btn = document.getElementById('cta-btn'); const label = document.getElementById('cta-label'); const icon = document.getElementById('cta-icon'); const loader = document.getElementById('cta-loader'); label.textContent = 'Analyse en cours'; icon.style.display = 'none'; loader.style.display = 'block'; btn.disabled = true; try { const commune = document.getElementById('commune').value; const dept = document.getElementById('departement').value; let latitude = 0, longitude = 0, code_postal = '00000'; try { const adresse = `${commune} ${dept} France`; const geoRes = await fetch( `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(adresse)}&limit=1` ); const geoData = await geoRes.json(); if (geoData.features && geoData.features.length > 0) { longitude = geoData.features[0].geometry.coordinates[0]; latitude = geoData.features[0].geometry.coordinates[1]; code_postal = geoData.features[0].properties.postcode || '00000'; } } catch (e) { console.log('Géocodage échoué, fallback centroïde'); } const res = await fetch('/api/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type_bien: selectedType, surface: document.getElementById('surface').value, nb_pieces: steppers.pieces, nb_lots: steppers.lots, surface_terrain: document.getElementById('terrain').value, commune: commune, departement: dept, latitude: latitude, longitude: longitude, code_postal: code_postal }) }); const data = await res.json(); document.getElementById('result-price').textContent = fmt(data.prix); document.getElementById('result-sub').textContent = selectedType + ' · ' + commune + ' (' + dept + ')' + ' · ' + data.dist_centre_km + ' km du centre · ' + new Intl.NumberFormat('fr-FR').format(data.revenu_median_zone) + '€ revenu médian'; document.getElementById('r-bas').textContent = fmt(data.fourchette_bas); document.getElementById('r-haut').textContent = fmt(data.fourchette_haut); document.getElementById('r-m2').textContent = new Intl.NumberFormat('fr-FR').format(data.prix_m2) + ' €/m²'; document.getElementById('r-precision').textContent = data.precision + '%'; document.getElementById('r-segment').textContent = data.segment; const result = document.getElementById('result'); result.style.display = 'block'; setTimeout(() => { document.getElementById('bar-fill').style.width = data.precision + '%'; }, 100); result.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) { alert('Erreur de connexion. Vérifiez que Flask tourne bien sur le port 5000.'); } finally { label.textContent = 'Estimer maintenant'; icon.style.display = 'block'; loader.style.display = 'none'; btn.disabled = false; } } // ── Init ── initThree(); animateCounters(); loadDepartements();