Spaces:
Sleeping
Sleeping
| 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 => `<option value="${d}">${d}</option>`).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 => `<option value="${c}">${c}</option>`).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(); |