// Initialize map const map = L.map('map').setView([55.7558, 37.6173], 5); // Add OpenStreetMap tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 18 }).addTo(map); // Layers to store map features let routeLayers = L.layerGroup().addTo(map); let transferMarkers = L.layerGroup().addTo(map); // Transport colors const transportColors = { 'plane': '#ff9800', 'train': '#1976d2', 'bus': '#cddc39', 'other': '#9e9e9e' }; // Get transport color function getTransportColor(feature) { const transportType = feature.properties.transport_type || feature.properties.transport || 'other'; return transportColors[transportType] || transportColors.other; } // Get line style based on feature properties function getLineStyle(feature) { if (feature.properties && feature.properties.synthetic === 'true') { return { color: getTransportColor(feature), weight: 2, dashArray: '5, 5', opacity: 0.7 }; } return { color: getTransportColor(feature), weight: 3, opacity: 0.8 }; } // Get marker style based on feature properties function getMarkerStyle(feature) { const color = feature.properties.stroke_color || getTransportColor(feature); const width = feature.properties.stroke_width || 3; return { color: color, weight: width, fillColor: '#fff', fillOpacity: 1 }; } // Format duration from seconds to readable format function formatDuration(seconds) { if (!seconds || seconds === 0) return '0h'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; } // Format connection time function formatConnectionTime(connectionTime) { if (!connectionTime) return 'N/A'; return formatDuration(connectionTime); } // Render GeoJSON on map function renderGeoJSON(geojsonData) { // Clear existing layers routeLayers.clearLayers(); transferMarkers.clearLayers(); if (!geojsonData || !geojsonData.features) { return; } // Process features geojsonData.features.forEach(feature => { const kind = feature.properties?.kind || feature.type; if (kind === 'LineString' || feature.geometry?.type === 'LineString') { // LineString feature - route segment const style = getLineStyle(feature); L.geoJSON(feature, { style: function(feature) { return getLineStyle(feature); }, onEachFeature: function(feature, layer) { layer.addTo(routeLayers); } }).addTo(routeLayers); } else if (kind === 'Point' || feature.geometry?.type === 'Point') { // Point feature - transfer marker const markerType = feature.properties?.marker_type; if (markerType === 'transfer' || feature.properties?.is_transfer === 'true') { const style = getMarkerStyle(feature); const marker = L.circleMarker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]], { color: style.color, weight: style.weight, fillColor: style.fillColor, fillOpacity: style.fillOpacity, radius: 6 }); // Create popup content let popupContent = `

${feature.properties?.title || 'Transfer'}

`; if (feature.properties?.connection_time_formatted) { popupContent += `

Connection time: ${feature.properties.connection_time_formatted}

`; } else if (feature.properties?.connection_time) { popupContent += `

Connection time: ${formatConnectionTime(feature.properties.connection_time)}

`; } if (feature.properties?.transfer_type) { popupContent += `

Transfer type: ${feature.properties.transfer_type}

`; } marker.bindPopup(popupContent); marker.addTo(transferMarkers); } } }); // Fit map to bounds if there are features if (routeLayers.getLayers().length > 0 || transferMarkers.getLayers().length > 0) { const group = new L.featureGroup([...routeLayers.getLayers(), ...transferMarkers.getLayers()]); map.fitBounds(group.getBounds().pad(0.1)); } } // Show loading overlay function showLoading() { document.getElementById('loading-overlay').classList.remove('hidden'); } // Hide loading overlay function hideLoading() { document.getElementById('loading-overlay').classList.add('hidden'); } // Show error message function showError(message) { const errorEl = document.getElementById('error-message'); errorEl.textContent = message; errorEl.classList.remove('hidden'); setTimeout(() => { errorEl.classList.add('hidden'); }, 5000); } // Hide error message function hideError() { document.getElementById('error-message').classList.add('hidden'); } // Render routes list function renderRoutesList(routes) { const routesListEl = document.getElementById('routes-list'); if (!routes || routes.length === 0) { routesListEl.innerHTML = '

No routes found

'; return; } let html = ''; routes.forEach((route, index) => { const duration = formatDuration(route.duration_seconds || route.duration || 0); const transfers = route.transfers || route.transfer_count || 0; const cost = route.cost || 0; html += `
${duration} ${cost > 0 ? cost + ' units' : 'N/A'}
${transfers} transfer${transfers !== 1 ? 's' : ''}
`; }); routesListEl.innerHTML = html; // Add click handlers to route cards document.querySelectorAll('.route-card').forEach(card => { card.addEventListener('click', function() { // Remove selected class from all cards document.querySelectorAll('.route-card').forEach(c => c.classList.remove('selected')); this.classList.add('selected'); // Fetch and render GeoJSON for this route const searchId = this.dataset.searchId; const routeId = this.dataset.routeId; fetchGeoJSON(searchId, routeId); }); }); } // Fetch GeoJSON for a route async function fetchGeoJSON(searchId, routeId) { try { const response = await fetch(`/v1/routes/${searchId}/${routeId}/geojson`); if (!response.ok) { throw new Error(`Failed to fetch GeoJSON: ${response.statusText}`); } const geojsonData = await response.json(); renderGeoJSON(geojsonData); } catch (error) { console.error('Error fetching GeoJSON:', error); showError('Failed to load route map'); } } // Search for routes async function searchRoutes(formData) { showLoading(); hideError(); try { const response = await fetch('/v1/routes/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ from: formData.get('from'), to: formData.get('to'), date: formData.get('date') }) }); if (!response.ok) { throw new Error(`Search failed: ${response.statusText}`); } const searchData = await response.json(); // Render routes list if (searchData.routes) { renderRoutesList(searchData.routes); } else if (searchData.routes === null || searchData.routes === undefined) { document.getElementById('routes-list').innerHTML = '

No routes found

'; } // If there's only one route, select it automatically const routes = searchData.routes || []; if (routes.length === 1) { const firstCard = document.querySelector('.route-card'); if (firstCard) { firstCard.classList.add('selected'); fetchGeoJSON(routes[0].search_id, routes[0].id); } } } catch (error) { console.error('Error searching routes:', error); showError('Failed to search routes. Please try again.'); document.getElementById('routes-list').innerHTML = '

Search failed

'; } finally { hideLoading(); } } // Initialize search form document.getElementById('search-form').addEventListener('submit', function(e) { e.preventDefault(); const formData = new FormData(this); searchRoutes(formData); }); // Set default date to today const dateInput = document.getElementById('travel-date'); const today = new Date().toISOString().split('T')[0]; dateInput.value = today; dateInput.min = today;