immo-predict / src /static /script.js
Grailleton's picture
Remove LFS tracking
da475f5
Raw
History Blame Contribute Delete
7.83 kB
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();