Fix map selector to load nearby stops from SL API
Replace hardcoded single-stop search with /api/sites/nearby endpoint that loads all transit stops within radius of map center. Stops load dynamically as user pans/zooms the map. Also unified marker layer management and consistent CSS class for select buttons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -709,77 +709,37 @@ class ConfigManager {
|
|||||||
if (e.target === mapModal) closeMap();
|
if (e.target === mapModal) closeMap();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load transit stops - search for common Stockholm areas
|
// Load nearby transit stops based on map center
|
||||||
const loadSitesOnMap = async () => {
|
const markersLayer = L.layerGroup().addTo(map);
|
||||||
try {
|
const loadedSiteIds = new Set();
|
||||||
// Start with focused search to avoid too many markers
|
|
||||||
const searchTerms = ['Ambassaderna'];
|
const loadNearbySites = async () => {
|
||||||
const allSites = new Map();
|
const center = map.getCenter();
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
// Scale radius based on zoom: wider view = larger radius
|
||||||
|
const radius = zoom >= 15 ? 500 : zoom >= 13 ? 1500 : zoom >= 11 ? 4000 : 8000;
|
||||||
|
|
||||||
for (const term of searchTerms) {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/sites/search?q=${encodeURIComponent(term)}`);
|
const response = await fetch(`/api/sites/nearby?lat=${center.lat}&lon=${center.lng}&radius=${radius}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(`Search "${term}" returned:`, data);
|
|
||||||
if (data.sites) {
|
if (data.sites) {
|
||||||
data.sites.forEach(site => {
|
data.sites.forEach(site => {
|
||||||
// Only add sites with valid coordinates from API
|
if (loadedSiteIds.has(site.id)) return;
|
||||||
const lat = site.lat || site.latitude;
|
const lat = parseFloat(site.lat);
|
||||||
const lon = site.lon || site.longitude;
|
const lon = parseFloat(site.lon);
|
||||||
|
if (!lat || !lon || isNaN(lat) || isNaN(lon)) return;
|
||||||
|
|
||||||
if (lat && lon && !isNaN(parseFloat(lat)) && !isNaN(parseFloat(lon))) {
|
loadedSiteIds.add(site.id);
|
||||||
if (!allSites.has(site.id)) {
|
|
||||||
allSites.set(site.id, {
|
|
||||||
id: site.id,
|
|
||||||
name: site.name,
|
|
||||||
lat: parseFloat(lat),
|
|
||||||
lon: parseFloat(lon)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`Site ${site.id} (${site.name}) missing coordinates, skipping`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error searching for ${term}:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add known site with coordinates as fallback
|
|
||||||
const knownSites = [
|
|
||||||
{ id: '1411', name: 'Ambassaderna', lat: 59.3293, lon: 18.0686 }
|
|
||||||
];
|
|
||||||
|
|
||||||
knownSites.forEach(site => {
|
|
||||||
if (!allSites.has(site.id)) {
|
|
||||||
allSites.set(site.id, site);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const sitesArray = Array.from(allSites.values());
|
|
||||||
console.log(`Loading ${sitesArray.length} sites on map with coordinates:`, sitesArray);
|
|
||||||
|
|
||||||
if (sitesArray.length > 0) {
|
|
||||||
const markers = [];
|
|
||||||
sitesArray.forEach(site => {
|
|
||||||
const lat = site.lat;
|
|
||||||
const lon = site.lon;
|
|
||||||
|
|
||||||
if (!lat || !lon || isNaN(lat) || isNaN(lon)) {
|
|
||||||
console.warn(`Invalid coordinates for site ${site.id}, skipping`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create custom icon
|
|
||||||
const customIcon = L.divIcon({
|
const customIcon = L.divIcon({
|
||||||
className: 'custom-marker',
|
className: 'custom-marker',
|
||||||
html: `<div style="background-color: #0061a1; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-weight: bold; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">🚌</div>`,
|
html: '<div style="background-color: #0061a1; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-weight: bold; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);">🚌</div>',
|
||||||
iconSize: [30, 30],
|
iconSize: [30, 30],
|
||||||
iconAnchor: [15, 15]
|
iconAnchor: [15, 15]
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = L.marker([lat, lon], { icon: customIcon }).addTo(map);
|
const marker = L.marker([lat, lon], { icon: customIcon });
|
||||||
|
|
||||||
const popupContent = `
|
const popupContent = `
|
||||||
<div style="min-width: 200px;">
|
<div style="min-width: 200px;">
|
||||||
@@ -793,36 +753,18 @@ class ConfigManager {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
marker.bindPopup(popupContent);
|
marker.bindPopup(popupContent);
|
||||||
markers.push(marker);
|
markersLayer.addLayer(marker);
|
||||||
|
|
||||||
// Make marker clickable to open popup
|
|
||||||
marker.on('click', function() {
|
|
||||||
this.openPopup();
|
|
||||||
});
|
});
|
||||||
});
|
console.log(`Loaded ${data.sites.length} nearby sites (${loadedSiteIds.size} total on map)`);
|
||||||
|
|
||||||
// Fit map to show all markers, or center on first marker
|
|
||||||
if (markers.length > 0) {
|
|
||||||
if (markers.length === 1) {
|
|
||||||
map.setView(markers[0].getLatLng(), 15);
|
|
||||||
} else {
|
|
||||||
const group = new L.featureGroup(markers);
|
|
||||||
map.fitBounds(group.getBounds().pad(0.1));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no markers, show message
|
|
||||||
console.log('No sites with coordinates found');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading sites on map:', error);
|
console.error('Error loading nearby sites:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load sites after map is initialized
|
// Load sites on init and when map is panned/zoomed
|
||||||
setTimeout(() => {
|
setTimeout(() => loadNearbySites(), 300);
|
||||||
loadSitesOnMap();
|
map.on('moveend', loadNearbySites);
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Handle site selection from map popup - use event delegation on the modal
|
// Handle site selection from map popup - use event delegation on the modal
|
||||||
mapModal.addEventListener('click', (e) => {
|
mapModal.addEventListener('click', (e) => {
|
||||||
@@ -874,73 +816,42 @@ class ConfigManager {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.sites && data.sites.length > 0) {
|
if (data.sites && data.sites.length > 0) {
|
||||||
// Clear existing markers
|
// Clear existing markers and reset tracking
|
||||||
map.eachLayer((layer) => {
|
markersLayer.clearLayers();
|
||||||
if (layer instanceof L.Marker) {
|
loadedSiteIds.clear();
|
||||||
map.removeLayer(layer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear existing markers
|
const searchMarkers = [];
|
||||||
map.eachLayer((layer) => {
|
|
||||||
if (layer instanceof L.Marker) {
|
|
||||||
map.removeLayer(layer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add markers for search results
|
|
||||||
const markers = [];
|
|
||||||
data.sites.forEach(site => {
|
data.sites.forEach(site => {
|
||||||
let lat = site.lat || site.latitude;
|
const lat = parseFloat(site.lat);
|
||||||
let lon = site.lon || site.longitude;
|
const lon = parseFloat(site.lon);
|
||||||
|
if (!lat || !lon || isNaN(lat) || isNaN(lon)) return;
|
||||||
|
|
||||||
// If no coordinates, use approximate location based on map center
|
loadedSiteIds.add(site.id);
|
||||||
if (!lat || !lon) {
|
|
||||||
const center = map.getCenter();
|
|
||||||
lat = center.lat + (Math.random() - 0.5) * 0.05;
|
|
||||||
lon = center.lon + (Math.random() - 0.5) * 0.05;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create custom icon
|
|
||||||
const customIcon = L.divIcon({
|
const customIcon = L.divIcon({
|
||||||
className: 'custom-transit-marker',
|
className: 'custom-marker',
|
||||||
html: `<div style="background-color: #0061a1; color: white; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-weight: bold; border: 3px solid white; box-shadow: 0 2px 8px rgba(0,0,0,0.4); font-size: 18px;">🚌</div>`,
|
html: '<div style="background-color: #28a745; color: white; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-weight: bold; border: 3px solid white; box-shadow: 0 2px 8px rgba(0,0,0,0.4); font-size: 18px;">🚌</div>',
|
||||||
iconSize: [32, 32],
|
iconSize: [32, 32],
|
||||||
iconAnchor: [16, 16],
|
iconAnchor: [16, 16]
|
||||||
popupAnchor: [0, -16]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const marker = L.marker([lat, lon], {
|
const marker = L.marker([lat, lon], { icon: customIcon });
|
||||||
icon: customIcon,
|
marker.bindPopup(`
|
||||||
title: site.name
|
<div style="min-width: 200px;">
|
||||||
}).addTo(map);
|
<strong style="font-size: 1.1em; color: #0061a1;">${site.name}</strong><br>
|
||||||
|
<span style="color: #666; font-size: 0.9em;">ID: ${site.id}</span><br>
|
||||||
const popupContent = document.createElement('div');
|
<button class="select-site-from-map" data-site-id="${site.id}" data-site-name="${site.name}"
|
||||||
popupContent.style.minWidth = '220px';
|
style="margin-top: 8px; padding: 8px 15px; width: 100%; background-color: #0061a1; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||||
popupContent.innerHTML = `
|
|
||||||
<div style="margin-bottom: 8px;">
|
|
||||||
<strong style="font-size: 1.1em; color: #0061a1; display: block; margin-bottom: 4px;">${site.name}</strong>
|
|
||||||
<span style="color: #666; font-size: 0.9em;">ID: ${site.id}</span>
|
|
||||||
</div>
|
|
||||||
<button class="select-site-from-map-btn"
|
|
||||||
data-site-id="${site.id}"
|
|
||||||
data-site-name="${site.name}"
|
|
||||||
style="margin-top: 8px; padding: 10px 15px; width: 100%; background-color: #0061a1; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1em;">
|
|
||||||
Select This Stop
|
Select This Stop
|
||||||
</button>
|
</button>
|
||||||
`;
|
</div>
|
||||||
|
`);
|
||||||
marker.bindPopup(popupContent);
|
markersLayer.addLayer(marker);
|
||||||
markers.push(marker);
|
searchMarkers.push(marker);
|
||||||
|
|
||||||
marker.on('click', function() {
|
|
||||||
this.openPopup();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit map to show results
|
if (searchMarkers.length > 0) {
|
||||||
if (markers.length > 0) {
|
const group = new L.featureGroup(searchMarkers);
|
||||||
const group = new L.featureGroup(markers);
|
|
||||||
map.fitBounds(group.getBounds().pad(0.1));
|
map.fitBounds(group.getBounds().pad(0.1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user