|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<title>DVF - Prix Immobiliers en France</title> |
|
|
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"> |
|
|
<link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet"> |
|
|
<script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script> |
|
|
<script src="https://unpkg.com/pmtiles@2.11.0/dist/index.js"></script> |
|
|
<style> |
|
|
body { margin: 0; padding: 0; } |
|
|
#map { position: absolute; top: 0; bottom: 0; width: 100%; } |
|
|
|
|
|
.map-overlay { |
|
|
position: absolute; |
|
|
background: rgba(255, 255, 255, 0.95); |
|
|
border-radius: 8px; |
|
|
padding: 15px; |
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.15); |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
font-size: 13px; |
|
|
} |
|
|
|
|
|
#info { |
|
|
top: 10px; |
|
|
left: 10px; |
|
|
min-width: 200px; |
|
|
} |
|
|
|
|
|
#info h3 { |
|
|
margin: 0 0 10px 0; |
|
|
font-size: 16px; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
#info .stat { |
|
|
margin: 5px 0; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
#info .stat strong { |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
#legend { |
|
|
bottom: 30px; |
|
|
left: 10px; |
|
|
padding: 10px 15px; |
|
|
} |
|
|
|
|
|
#legend h4 { |
|
|
margin: 0 0 10px 0; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.legend-scale { |
|
|
display: flex; |
|
|
height: 15px; |
|
|
border-radius: 3px; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.legend-scale div { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.legend-labels { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
margin-top: 5px; |
|
|
font-size: 11px; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
#controls { |
|
|
top: 10px; |
|
|
right: 10px; |
|
|
} |
|
|
|
|
|
#controls label { |
|
|
display: block; |
|
|
margin: 5px 0; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
#controls select { |
|
|
width: 100%; |
|
|
padding: 5px; |
|
|
margin-top: 10px; |
|
|
border-radius: 4px; |
|
|
border: 1px solid #ccc; |
|
|
} |
|
|
|
|
|
#zoom-level { |
|
|
position: absolute; |
|
|
bottom: 30px; |
|
|
right: 10px; |
|
|
background: rgba(255,255,255,0.9); |
|
|
padding: 5px 10px; |
|
|
border-radius: 4px; |
|
|
font-family: monospace; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
#top-cities { |
|
|
top: 80px; |
|
|
right: 10px; |
|
|
max-width: 220px; |
|
|
display: none; |
|
|
} |
|
|
|
|
|
#top-cities h4 { |
|
|
margin: 0 0 10px 0; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
#top-cities ol { |
|
|
margin: 0; |
|
|
padding-left: 20px; |
|
|
} |
|
|
|
|
|
#top-cities li { |
|
|
margin: 4px 0; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
#top-cities .city-name { |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
#top-cities .city-stats { |
|
|
color: #666; |
|
|
font-size: 11px; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="map"></div> |
|
|
|
|
|
<div class="map-overlay" id="info"> |
|
|
<h3>Prix Immobiliers</h3> |
|
|
<div id="hover-info"> |
|
|
<p style="color: #999;">Survolez une zone pour voir les prix</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="map-overlay" id="legend"> |
|
|
<h4>Prix médian (€/m²)</h4> |
|
|
<div class="legend-scale" id="legend-scale"></div> |
|
|
<div class="legend-labels" id="legend-labels"> |
|
|
<span>1 500</span> |
|
|
<span>2 500</span> |
|
|
<span>3 500</span> |
|
|
<span>5 000</span> |
|
|
<span>8 000+</span> |
|
|
</div> |
|
|
<div style="margin-top: 8px; display: flex; align-items: center; font-size: 11px; color: #666;"> |
|
|
<span style="display: inline-block; width: 16px; height: 12px; background: #999999; border-radius: 2px; margin-right: 6px;"></span> |
|
|
<span>Aucune transaction</span> |
|
|
</div> |
|
|
<div style="margin-top: 4px; display: flex; align-items: center; font-size: 11px; color: #666;"> |
|
|
<span style="display: inline-block; width: 16px; height: 12px; background: #cccccc; border-radius: 2px; margin-right: 6px;"></span> |
|
|
<span>< 5 transactions</span> |
|
|
</div> |
|
|
<div id="current-level" style="margin-top: 8px; font-size: 11px; color: #999;">Niveau: Pays</div> |
|
|
</div> |
|
|
|
|
|
<div class="map-overlay" id="controls"> |
|
|
<strong>Type de bien</strong> |
|
|
<select id="property-type"> |
|
|
<option value="all">Tous types</option> |
|
|
<option value="maison">Maisons</option> |
|
|
<option value="appartement">Appartements</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
<div id="zoom-level">Zoom: <span id="zoom-value">5</span></div> |
|
|
|
|
|
<div class="map-overlay" id="top-cities"> |
|
|
<h4>🏆 Top 10 Villes</h4> |
|
|
<div id="top-cities-list"></div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
console.log('Script starting...'); |
|
|
console.log('pmtiles defined?', typeof pmtiles !== 'undefined'); |
|
|
console.log('maplibregl defined?', typeof maplibregl !== 'undefined'); |
|
|
|
|
|
|
|
|
let pmtilesAvailable = false; |
|
|
try { |
|
|
if (typeof pmtiles !== 'undefined' && typeof maplibregl !== 'undefined') { |
|
|
const protocol = new pmtiles.Protocol(); |
|
|
maplibregl.addProtocol("pmtiles", protocol.tile); |
|
|
pmtilesAvailable = true; |
|
|
console.log('PMTiles protocol registered successfully'); |
|
|
} else { |
|
|
console.log('PMTiles or MapLibre library not loaded'); |
|
|
} |
|
|
} catch (e) { |
|
|
console.error('PMTiles registration error:', e); |
|
|
} |
|
|
|
|
|
|
|
|
const colors = ['#1a9850', '#a6d96a', '#ffdd00', '#f46d43', '#d73027']; |
|
|
|
|
|
|
|
|
const priceScales = { |
|
|
country: null, |
|
|
regions: null, |
|
|
departments: null, |
|
|
iris: null, |
|
|
communes: null, |
|
|
parcels: null |
|
|
}; |
|
|
|
|
|
|
|
|
const geoData = {}; |
|
|
|
|
|
|
|
|
function computePriceScale(features, priceField, level = 'default') { |
|
|
|
|
|
return [1500, 3125, 4750, 6375, 8000]; |
|
|
} |
|
|
|
|
|
|
|
|
const legendScale = document.getElementById('legend-scale'); |
|
|
colors.forEach(color => { |
|
|
const div = document.createElement('div'); |
|
|
div.style.backgroundColor = color; |
|
|
legendScale.appendChild(div); |
|
|
}); |
|
|
|
|
|
|
|
|
const map = new maplibregl.Map({ |
|
|
container: 'map', |
|
|
style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', |
|
|
center: [2.3, 46.5], |
|
|
zoom: 4, |
|
|
minZoom: 3, |
|
|
maxZoom: 18 |
|
|
}); |
|
|
|
|
|
|
|
|
let currentPropertyType = 'all'; |
|
|
let currentLevel = 'country'; |
|
|
let topCitiesData = []; |
|
|
|
|
|
function updateTopCitiesPanel() { |
|
|
const countField = getCountField(); |
|
|
const priceField = getPriceField(); |
|
|
|
|
|
|
|
|
const aggregated = {}; |
|
|
const bigCities = { |
|
|
'Paris': { pattern: /^Paris \d+e?r? Arrondissement$/i, code: '75' }, |
|
|
'Marseille': { pattern: /^Marseille \d+e?r? Arrondissement$/i, code: '13' }, |
|
|
'Lyon': { pattern: /^Lyon \d+e?r? Arrondissement$/i, code: '69' } |
|
|
}; |
|
|
|
|
|
topCitiesData.forEach(f => { |
|
|
const name = f.properties.nom_commune || f.properties.nom_commune_geo || ''; |
|
|
const count = f.properties[countField] || 0; |
|
|
const price = f.properties[priceField]; |
|
|
|
|
|
if (count === 0) return; |
|
|
|
|
|
|
|
|
let aggregateName = null; |
|
|
for (const [city, info] of Object.entries(bigCities)) { |
|
|
if (info.pattern.test(name)) { |
|
|
aggregateName = city; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
if (aggregateName) { |
|
|
|
|
|
if (!aggregated[aggregateName]) { |
|
|
aggregated[aggregateName] = { name: aggregateName, count: 0, priceSum: 0, priceCount: 0 }; |
|
|
} |
|
|
aggregated[aggregateName].count += count; |
|
|
if (price) { |
|
|
aggregated[aggregateName].priceSum += price * count; |
|
|
aggregated[aggregateName].priceCount += count; |
|
|
} |
|
|
} else { |
|
|
|
|
|
aggregated[name] = { name, count, priceSum: price ? price * count : 0, priceCount: price ? count : 0 }; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const sorted = Object.values(aggregated) |
|
|
.map(c => ({ |
|
|
name: c.name, |
|
|
count: c.count, |
|
|
price: c.priceCount > 0 ? c.priceSum / c.priceCount : null |
|
|
})) |
|
|
.sort((a, b) => b.count - a.count) |
|
|
.slice(0, 10); |
|
|
|
|
|
const listHtml = sorted.map((c, i) => { |
|
|
const priceStr = c.price ? Math.round(c.price).toLocaleString('fr-FR') + ' €/m²' : 'N/A'; |
|
|
return `<div style="margin: 6px 0;"><span class="city-name">${i+1}. ${c.name}</span><br><span class="city-stats">${c.count.toLocaleString('fr-FR')} transactions • ${priceStr}</span></div>`; |
|
|
}).join(''); |
|
|
|
|
|
document.getElementById('top-cities-list').innerHTML = listHtml; |
|
|
} |
|
|
|
|
|
function showTopCities(show) { |
|
|
document.getElementById('top-cities').style.display = show ? 'block' : 'none'; |
|
|
} |
|
|
|
|
|
function getPriceField() { |
|
|
switch(currentPropertyType) { |
|
|
case 'maison': return 'prix_m2_maison_median'; |
|
|
case 'appartement': return 'prix_m2_appart_median'; |
|
|
default: return 'prix_m2_median'; |
|
|
} |
|
|
} |
|
|
|
|
|
function getCountField() { |
|
|
switch(currentPropertyType) { |
|
|
case 'maison': return 'nb_maisons'; |
|
|
case 'appartement': return 'nb_appartements'; |
|
|
default: return 'nb_transactions'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function formatPrice(price) { |
|
|
if (price === null || price === undefined) return 'N/A'; |
|
|
return Math.round(price).toLocaleString('fr-FR') + ' €/m²'; |
|
|
} |
|
|
|
|
|
function formatCount(count) { |
|
|
if (count === null || count === undefined) return 'N/A'; |
|
|
return count.toLocaleString('fr-FR'); |
|
|
} |
|
|
|
|
|
|
|
|
function updateInfo(properties, level) { |
|
|
const priceField = getPriceField(); |
|
|
const countField = getCountField(); |
|
|
|
|
|
let name = ''; |
|
|
if (level === 'country') { |
|
|
name = properties.name || 'France'; |
|
|
} else if (level === 'regions') { |
|
|
name = properties.nom_region || properties.nom_region_geo || 'Région'; |
|
|
} else if (level === 'departments') { |
|
|
name = properties.nom_departement || 'Département'; |
|
|
} else if (level === 'iris') { |
|
|
const irisName = properties.nom_iris || ''; |
|
|
const communeName = properties.nom_commune_iris || ''; |
|
|
const irisCode = properties.code_iris || ''; |
|
|
|
|
|
if (irisName === communeName && irisCode) { |
|
|
name = `IRIS ${irisCode} (${communeName})`; |
|
|
} else if (irisName) { |
|
|
name = communeName && irisName !== communeName ? `${irisName} (${communeName})` : irisName; |
|
|
} else { |
|
|
name = irisCode ? `IRIS ${irisCode}` : 'IRIS'; |
|
|
} |
|
|
} else if (level === 'communes') { |
|
|
name = properties.nom_commune || properties.nom_commune_geo || 'Commune'; |
|
|
} else if (level === 'parcels') { |
|
|
const parcelId = properties.id_parcelle_unique || properties.id || ''; |
|
|
const commune = properties.nom_commune || ''; |
|
|
name = commune ? `Parcelle ${parcelId} (${commune})` : `Parcelle ${parcelId}`; |
|
|
} |
|
|
|
|
|
const price = properties[priceField]; |
|
|
const priceMean = properties[priceField.replace('median', 'mean')]; |
|
|
const count = properties[countField]; |
|
|
const priceQ25 = properties[priceField.replace('median', 'q25')]; |
|
|
const priceQ75 = properties[priceField.replace('median', 'q75')]; |
|
|
|
|
|
const noData = (count || 0) === 0; |
|
|
const insufficientData = (count || 0) > 0 && (count || 0) < 5; |
|
|
|
|
|
let dataNote = ''; |
|
|
if (noData) { |
|
|
dataNote = '<div class="stat" style="color: #888; font-style: italic;">⛔ Aucune transaction</div>'; |
|
|
} else if (insufficientData) { |
|
|
dataNote = '<div class="stat" style="color: #999; font-style: italic;">⚠️ Données insuffisantes (<5 transactions)</div>'; |
|
|
} |
|
|
|
|
|
document.getElementById('hover-info').innerHTML = ` |
|
|
<div class="stat"><strong>${name}</strong></div> |
|
|
${dataNote} |
|
|
<div class="stat">Prix médian: <strong>${noData ? 'N/A' : formatPrice(price)}</strong></div> |
|
|
<div class="stat">Prix moyen: ${noData ? 'N/A' : formatPrice(priceMean)}</div> |
|
|
<div class="stat">Fourchette: ${noData ? 'N/A' : formatPrice(priceQ25) + ' - ' + formatPrice(priceQ75)}</div> |
|
|
<div class="stat">Transactions: <strong>${formatCount(count)}</strong></div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function resetInfo() { |
|
|
document.getElementById('hover-info').innerHTML = |
|
|
'<p style="color: #999;">Survolez une zone pour voir les prix</p>'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function buildFillColor(level) { |
|
|
const priceField = getPriceField(); |
|
|
const countField = getCountField(); |
|
|
|
|
|
const defaultScale = [1500, 3125, 4750, 6375, 8000]; |
|
|
const scale = priceScales[level] || priceScales['communes'] || defaultScale; |
|
|
|
|
|
|
|
|
const stops = []; |
|
|
for (let i = 0; i < scale.length; i++) { |
|
|
stops.push(scale[i], colors[i]); |
|
|
} |
|
|
|
|
|
|
|
|
if (level === 'parcels') { |
|
|
return [ |
|
|
'interpolate', |
|
|
['linear'], |
|
|
['coalesce', ['get', priceField], 0], |
|
|
...stops |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
return [ |
|
|
'case', |
|
|
['==', ['coalesce', ['get', countField], 0], 0], |
|
|
'#999999', |
|
|
['<', ['coalesce', ['get', countField], 0], 5], |
|
|
'#cccccc', |
|
|
[ |
|
|
'interpolate', |
|
|
['linear'], |
|
|
['coalesce', ['get', priceField], 0], |
|
|
...stops |
|
|
] |
|
|
]; |
|
|
} |
|
|
|
|
|
|
|
|
function updateLegend(level) { |
|
|
const scale = priceScales[level] || [1500, 3125, 4750, 6375, 8000]; |
|
|
const labels = document.getElementById('legend-labels'); |
|
|
const levelNames = { |
|
|
country: 'Pays', |
|
|
regions: 'Régions', |
|
|
departments: 'Départements', |
|
|
communes: 'Communes', |
|
|
iris: 'IRIS (Quartiers)', |
|
|
parcels: 'Parcelles' |
|
|
}; |
|
|
|
|
|
|
|
|
const fmt = (n) => n >= 1000 ? `${(n/1000).toFixed(1)}k` : n.toString(); |
|
|
|
|
|
|
|
|
labels.innerHTML = ` |
|
|
<span>${fmt(scale[0])}</span> |
|
|
<span>${fmt(scale[1])}</span> |
|
|
<span>${fmt(scale[2])}</span> |
|
|
<span>${fmt(scale[3])}</span> |
|
|
<span>${fmt(scale[4])}</span> |
|
|
`; |
|
|
|
|
|
document.getElementById('current-level').textContent = `Niveau: ${levelNames[level]}`; |
|
|
} |
|
|
|
|
|
|
|
|
function updateScalesAndColors() { |
|
|
const priceField = getPriceField(); |
|
|
|
|
|
|
|
|
['country', 'regions', 'departments', 'iris', 'communes'].forEach(level => { |
|
|
if (geoData[level]) { |
|
|
priceScales[level] = computePriceScale(geoData[level].features, priceField, level); |
|
|
} |
|
|
}); |
|
|
|
|
|
priceScales['parcels'] = [1500, 3125, 4750, 6375, 8000]; |
|
|
|
|
|
|
|
|
['country', 'regions', 'departments', 'iris', 'communes'].forEach(level => { |
|
|
const layerId = `${level}-fill`; |
|
|
if (map.getLayer(layerId)) { |
|
|
map.setPaintProperty(layerId, 'fill-color', buildFillColor(level)); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
if (map.getLayer('parcels-fill')) { |
|
|
map.setPaintProperty('parcels-fill', 'fill-color', buildFillColor('parcels')); |
|
|
} |
|
|
|
|
|
|
|
|
updateLegend(currentLevel); |
|
|
} |
|
|
|
|
|
|
|
|
function updateLayerColors() { |
|
|
updateScalesAndColors(); |
|
|
} |
|
|
|
|
|
|
|
|
function getLevelFromZoom(zoom) { |
|
|
if (zoom < 5) return 'country'; |
|
|
if (zoom < 7) return 'regions'; |
|
|
if (zoom < 9) return 'departments'; |
|
|
if (zoom < 11) return 'communes'; |
|
|
if (zoom < 13) return 'iris'; |
|
|
return 'parcels'; |
|
|
} |
|
|
|
|
|
map.on('load', async () => { |
|
|
|
|
|
const levels = ['country', 'regions', 'departments', 'iris', 'communes']; |
|
|
const priceField = getPriceField(); |
|
|
|
|
|
const fetchPromises = levels.map(level => |
|
|
fetch(`data/${level}.geojson`).then(r => r.json()) |
|
|
); |
|
|
const results = await Promise.all(fetchPromises); |
|
|
|
|
|
levels.forEach((level, i) => { |
|
|
geoData[level] = results[i]; |
|
|
priceScales[level] = computePriceScale(geoData[level].features, priceField, level); |
|
|
console.log(`${level} scale:`, priceScales[level]); |
|
|
}); |
|
|
|
|
|
|
|
|
topCitiesData = geoData.communes.features; |
|
|
updateTopCitiesPanel(); |
|
|
showTopCities(true); |
|
|
|
|
|
|
|
|
map.addSource('country', { |
|
|
type: 'geojson', |
|
|
data: geoData.country |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'country-fill', |
|
|
type: 'fill', |
|
|
source: 'country', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('country'), |
|
|
'fill-opacity': 0.7 |
|
|
}, |
|
|
maxzoom: 5 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'country-line', |
|
|
type: 'line', |
|
|
source: 'country', |
|
|
paint: { |
|
|
'line-color': '#333', |
|
|
'line-width': 2 |
|
|
}, |
|
|
maxzoom: 5 |
|
|
}); |
|
|
|
|
|
|
|
|
map.addSource('regions', { |
|
|
type: 'geojson', |
|
|
data: geoData.regions |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'regions-fill', |
|
|
type: 'fill', |
|
|
source: 'regions', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('regions'), |
|
|
'fill-opacity': 0.7 |
|
|
}, |
|
|
minzoom: 5, |
|
|
maxzoom: 7 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'regions-line', |
|
|
type: 'line', |
|
|
source: 'regions', |
|
|
paint: { |
|
|
'line-color': '#333', |
|
|
'line-width': 1.5 |
|
|
}, |
|
|
minzoom: 5, |
|
|
maxzoom: 7 |
|
|
}); |
|
|
|
|
|
|
|
|
map.addSource('departments', { |
|
|
type: 'geojson', |
|
|
data: geoData.departments |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'departments-fill', |
|
|
type: 'fill', |
|
|
source: 'departments', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('departments'), |
|
|
'fill-opacity': 0.7 |
|
|
}, |
|
|
minzoom: 7, |
|
|
maxzoom: 9 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'departments-line', |
|
|
type: 'line', |
|
|
source: 'departments', |
|
|
paint: { |
|
|
'line-color': '#333', |
|
|
'line-width': 1 |
|
|
}, |
|
|
minzoom: 7, |
|
|
maxzoom: 9 |
|
|
}); |
|
|
|
|
|
|
|
|
map.addSource('iris', { |
|
|
type: 'geojson', |
|
|
data: geoData.iris |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'iris-fill', |
|
|
type: 'fill', |
|
|
source: 'iris', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('iris'), |
|
|
'fill-opacity': 0.7 |
|
|
}, |
|
|
minzoom: 11, |
|
|
maxzoom: 13 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'iris-line', |
|
|
type: 'line', |
|
|
source: 'iris', |
|
|
paint: { |
|
|
'line-color': '#555', |
|
|
'line-width': 0.5 |
|
|
}, |
|
|
minzoom: 11, |
|
|
maxzoom: 13 |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (pmtilesAvailable) { |
|
|
try { |
|
|
map.addSource('parcels', { |
|
|
type: 'vector', |
|
|
url: 'pmtiles://data/parcels.pmtiles' |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'parcels-fill', |
|
|
type: 'fill', |
|
|
source: 'parcels', |
|
|
'source-layer': 'parcels', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('parcels'), |
|
|
'fill-opacity': 0.8 |
|
|
}, |
|
|
minzoom: 13 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'parcels-line', |
|
|
type: 'line', |
|
|
source: 'parcels', |
|
|
'source-layer': 'parcels', |
|
|
paint: { |
|
|
'line-color': '#333', |
|
|
'line-width': 0.3 |
|
|
}, |
|
|
minzoom: 13 |
|
|
}); |
|
|
|
|
|
console.log('Parcels PMTiles layer added'); |
|
|
} catch (e) { |
|
|
console.log('Parcels PMTiles not available:', e.message); |
|
|
} |
|
|
} else { |
|
|
console.log('PMTiles not available, skipping parcels layer'); |
|
|
} |
|
|
|
|
|
|
|
|
map.addSource('communes', { |
|
|
type: 'geojson', |
|
|
data: geoData.communes |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'communes-fill', |
|
|
type: 'fill', |
|
|
source: 'communes', |
|
|
paint: { |
|
|
'fill-color': buildFillColor('communes'), |
|
|
'fill-opacity': 0.7 |
|
|
}, |
|
|
minzoom: 9, |
|
|
maxzoom: 11 |
|
|
}); |
|
|
|
|
|
map.addLayer({ |
|
|
id: 'communes-line', |
|
|
type: 'line', |
|
|
source: 'communes', |
|
|
paint: { |
|
|
'line-color': '#666', |
|
|
'line-width': 0.5 |
|
|
}, |
|
|
minzoom: 9, |
|
|
maxzoom: 11 |
|
|
}); |
|
|
|
|
|
|
|
|
['country', 'regions', 'departments', 'iris', 'communes'].forEach(layer => { |
|
|
map.on('mousemove', `${layer}-fill`, (e) => { |
|
|
map.getCanvas().style.cursor = 'pointer'; |
|
|
if (e.features.length > 0) { |
|
|
updateInfo(e.features[0].properties, layer); |
|
|
} |
|
|
}); |
|
|
|
|
|
map.on('mouseleave', `${layer}-fill`, () => { |
|
|
map.getCanvas().style.cursor = ''; |
|
|
resetInfo(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
if (map.getSource('parcels')) { |
|
|
map.on('mousemove', 'parcels-fill', (e) => { |
|
|
map.getCanvas().style.cursor = 'pointer'; |
|
|
if (e.features.length > 0) { |
|
|
updateInfo(e.features[0].properties, 'parcels'); |
|
|
} |
|
|
}); |
|
|
|
|
|
map.on('mouseleave', 'parcels-fill', () => { |
|
|
map.getCanvas().style.cursor = ''; |
|
|
resetInfo(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
updateLegend(getLevelFromZoom(map.getZoom())); |
|
|
}); |
|
|
|
|
|
|
|
|
map.on('zoom', () => { |
|
|
const zoom = map.getZoom(); |
|
|
document.getElementById('zoom-value').textContent = zoom.toFixed(1); |
|
|
|
|
|
const newLevel = getLevelFromZoom(zoom); |
|
|
if (newLevel !== currentLevel) { |
|
|
currentLevel = newLevel; |
|
|
updateLegend(currentLevel); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('property-type').addEventListener('change', (e) => { |
|
|
currentPropertyType = e.target.value; |
|
|
updateLayerColors(); |
|
|
updateTopCitiesPanel(); |
|
|
}); |
|
|
|
|
|
|
|
|
map.addControl(new maplibregl.NavigationControl()); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|