<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bus La Rochelle – recherche adresse</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
html, body { height: 100%; margin: 0; }
#map { width: 100%; height: 100vh; }
#searchBox {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
background: rgba(255,255,255,0.95); padding: 8px 12px; border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 1000; display: flex; flex-direction: column;
width: 280px; font-family: sans-serif;
}
#searchBox input { padding: 6px; border-radius: 6px; border: 1px solid #ccc; }
#suggestions {
max-height: 180px; overflow-y: auto; margin-top: 4px;
border-radius: 6px; border: 1px solid #ccc; background: white;
}
#suggestions div {
padding: 6px 8px; cursor: pointer;
}
#suggestions div:hover { background: #f0f0f0; }
.popup-hours { font-size: 0.9em; margin-top: 4px; }
</style>
</head>
<body>
<div id="map"></div>
<div id="searchBox">
<input type="text" id="address" placeholder="Chercher une adresse..." autocomplete="off">
<div id="suggestions"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([46.1603, -1.1511], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '© OpenStreetMap contributors'
}).addTo(map);
// --- Icône bus ---
const busSvg = `
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" fill="yellow"/>
<path fill="black" d="M4,16c0,0.88 0.39,1.67 1,2.22L5,20c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h8v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1.78c0.61,-0.55 1,-1.34 1,-2.22L20,6c0,-3.5 -3.58,-4 -8,-4s-8,0.5 -8,4v10zM7.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5S6.67,14 7.5,14s1.5,0.67 1.5,1.5S8.33,17 7.5,17zM16.5,17c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM18,11L6,11L6,6h12v5z"/>
</svg>`;
const busIcon = L.icon({
iconUrl: "data:image/svg+xml;base64," + btoa(busSvg),
iconSize: [20, 20], iconAnchor: [10, 20], popupAnchor: [0, -20]
});
const formatTime = t => t.split(":").slice(0,2).join(":");
let allStops = []; // stockage pour filtrage proximité
async function loadStopsAndTimes() {
const stopsUrl = "https://opendata.agglo-larochelle.fr/d4c/api/records/1.0/search/?dataset=transport_yelo___gtfs_stop_des_bus&rows=4000";
const timesUrl = "https://opendata.agglo-larochelle.fr/d4c/api/records/1.0/search/?dataset=transport_yelo___gtfs_stop_times_des_bus&rows=90000";
const tripsUrl = "https://opendata.agglo-larochelle.fr/d4c/api/records/1.0/search/?dataset=transport_yelo___gtfs_trips_des_bus&rows=4000";
const [stopsRes, timesRes, tripsRes] = await Promise.all([fetch(stopsUrl), fetch(timesUrl), fetch(tripsUrl)]);
const stopsData = await stopsRes.json();
const timesData = await timesRes.json();
const tripsData = await tripsRes.json();
const tripsMap = {};
tripsData.records.forEach(r => { tripsMap[r.fields.trip_id] = r.fields; });
const stopTimesMap = {};
timesData.records.forEach(r => {
const f = r.fields;
if (!f.stop_id || !f.arrival_time || !f.trip_id) return;
if (!stopTimesMap[f.stop_id]) stopTimesMap[f.stop_id] = [];
stopTimesMap[f.stop_id].push(f);
});
allStops = stopsData.records;
const logicalStops = stopsData.records.filter(r => r.fields.location_type === "1");
const physicalStops = stopsData.records.filter(r => r.fields.location_type === "0");
const stopNameToPhysical = {};
physicalStops.forEach(r => {
const stop = r.fields;
if (!stopNameToPhysical[stop.stop_name]) stopNameToPhysical[stop.stop_name] = [];
stopNameToPhysical[stop.stop_name].push(stop);
});
const now = new Date();
const currentTime = now.toTimeString().split(" ")[0];
logicalStops.forEach(logStop => {
const stopName = logStop.fields.stop_name;
const lat = logStop.fields.stop_lat, lon = logStop.fields.stop_lon;
if (!lat || !lon) return;
const physicals = stopNameToPhysical[stopName] || [];
let upcoming = [];
physicals.forEach(phys => {
const times = stopTimesMap[phys.stop_id] || [];
times.forEach(t => {
if (t.arrival_time >= currentTime) {
const trip = tripsMap[t.trip_id];
if (trip) {
const line = trip.route_id || "?";
const dir = trip.headsign || trip.trip_headsign || "";
upcoming.push({ line, dir, time: t.arrival_time });
}
}
});
});
upcoming = upcoming.sort((a,b)=>a.time.localeCompare(b.time)).slice(0,2);
const popup = `<b>${stopName}</b><br>${upcoming.map(u=>`${u.line} → ${u.dir}: ${formatTime(u.time)}`).join("<br>")||"Aucun passage"}`;
L.marker([lat, lon], {icon: busIcon}).addTo(map).bindPopup(popup);
});
}
// --- RECHERCHE D’ADRESSE ---
const input = document.getElementById("address");
const suggestionsDiv = document.getElementById("suggestions");
input.addEventListener("input", async e => {
const q = e.target.value.trim();
if (q.length < 3) { suggestionsDiv.innerHTML = ""; return; }
const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&addressdetails=1&limit=5&countrycodes=fr`;
const res = await fetch(url);
const data = await res.json();
suggestionsDiv.innerHTML = "";
data.forEach(place => {
const div = document.createElement("div");
div.textContent = place.display_name;
div.onclick = () => {
map.setView([place.lat, place.lon], 15);
L.marker([place.lat, place.lon], {color:"blue"}).addTo(map).bindPopup(place.display_name).openPopup();
suggestionsDiv.innerHTML = "";
input.value = place.display_name;
showNearbyStops(place.lat, place.lon);
};
suggestionsDiv.appendChild(div);
});
});
// --- Afficher arrêts proches ---
function distance(lat1, lon1, lat2, lon2) {
const R = 6371e3;
const φ1 = lat1*Math.PI/180, φ2 = lat2*Math.PI/180;
const Δφ = (lat2-lat1)*Math.PI/180;
const Δλ = (lon2-lon1)*Math.PI/180;
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function showNearbyStops(lat, lon) {
const near = allStops.filter(r => {
const s = r.fields;
return s.location_type === "1" && distance(lat, lon, s.stop_lat, s.stop_lon) < 500;
});
near.forEach(r => {
L.circleMarker([r.fields.stop_lat, r.fields.stop_lon], {radius:6,color:"red"}).addTo(map)
.bindPopup(`<b>${r.fields.stop_name}</b><br>Arrêt à proximité`);
});
}
// --- GEOLOCALISATION MEMORISÉE ---
if (localStorage.getItem("gpsConsent") === "granted") enableGps();
else if (localStorage.getItem("gpsConsent") !== "denied") {
if (confirm("Autoriser la géolocalisation ?")) {
localStorage.setItem("gpsConsent","granted"); enableGps();
} else localStorage.setItem("gpsConsent","denied");
}
function enableGps() {
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(pos => {
const {latitude, longitude} = pos.coords;
map.setView([latitude, longitude], 15);
L.circleMarker([latitude, longitude], {radius:8,fillColor:"blue",color:"white",weight:2,fillOpacity:0.8})
.addTo(map).bindPopup("Vous êtes ici").openPopup();
});
}
}
loadStopsAndTimes();
</script>
</body>
</html>