384 lines
11 KiB
JavaScript
384 lines
11 KiB
JavaScript
// Konfiguracja API - sparametryzowana
|
|
function getDefaultApiUrl() {
|
|
// Auto-detect: użyj hostname z którego serwowany jest frontend
|
|
const protocol = window.location.protocol; // http: lub https:
|
|
const hostname = window.location.hostname; // np. 192.168.1.100 lub nazwa hosta
|
|
|
|
// Jeśli otwarte jako file://, użyj localhost
|
|
if (protocol === 'file:') {
|
|
return 'http://localhost:5000/api';
|
|
}
|
|
|
|
// W przeciwnym razie użyj tego samego hosta (przez reverse proxy)
|
|
return `${window.location.origin}/api`;
|
|
}
|
|
|
|
function getApiUrl() {
|
|
// Sprawdź localStorage
|
|
const saved = localStorage.getItem('izochrona_api_url');
|
|
if (saved) {
|
|
return saved;
|
|
}
|
|
|
|
// Użyj auto-detected
|
|
return getDefaultApiUrl();
|
|
}
|
|
|
|
function setApiUrl(url) {
|
|
localStorage.setItem('izochrona_api_url', url);
|
|
location.reload(); // Przeładuj stronę
|
|
}
|
|
|
|
let API_URL = getApiUrl();
|
|
|
|
// Stan aplikacji
|
|
let map;
|
|
let selectedStop = null;
|
|
let isochroneLayers = [];
|
|
let stopsLayer = null;
|
|
|
|
// Kolory dla różnych przedziałów czasowych
|
|
const ISOCHRONE_COLORS = {
|
|
30: '#0080ff',
|
|
60: '#00c864',
|
|
90: '#ffc800',
|
|
120: '#ff6400',
|
|
180: '#ff0064'
|
|
};
|
|
|
|
// Funkcja do generowania koloru dla dowolnego czasu
|
|
function getColorForTime(time) {
|
|
// Jeśli jest w predefiniowanych, użyj go
|
|
if (ISOCHRONE_COLORS[time]) {
|
|
return ISOCHRONE_COLORS[time];
|
|
}
|
|
|
|
// W przeciwnym razie generuj kolor na podstawie czasu
|
|
// Im dłuższy czas, tym bardziej czerwony
|
|
const hue = Math.max(0, 220 - (time * 1.2)); // Od niebieskiego (220) do czerwonego (0)
|
|
return `hsl(${hue}, 80%, 50%)`;
|
|
}
|
|
|
|
// Inicjalizacja mapy
|
|
function initMap() {
|
|
map = L.map('map').setView([52.0, 19.0], 7); // Polska
|
|
|
|
// Warstwa OSM
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 18
|
|
}).addTo(map);
|
|
|
|
console.log('Mapa zainicjalizowana');
|
|
}
|
|
|
|
// Wyszukiwanie stacji
|
|
let searchTimeout;
|
|
document.getElementById('station-search').addEventListener('input', (e) => {
|
|
const query = e.target.value.trim();
|
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
if (query.length < 2) {
|
|
document.getElementById('search-results').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
searchStations(query);
|
|
}, 300);
|
|
});
|
|
|
|
async function searchStations(query) {
|
|
try {
|
|
const response = await fetch(`${API_URL}/stops/search?q=${encodeURIComponent(query)}`);
|
|
const data = await response.json();
|
|
|
|
displaySearchResults(data.stops);
|
|
} catch (error) {
|
|
console.error('Błąd wyszukiwania:', error);
|
|
document.getElementById('search-results').innerHTML =
|
|
'<p style="color: red; padding: 8px;">Błąd połączenia z serwerem</p>';
|
|
}
|
|
}
|
|
|
|
function displaySearchResults(stops) {
|
|
const resultsDiv = document.getElementById('search-results');
|
|
|
|
if (!stops || stops.length === 0) {
|
|
resultsDiv.innerHTML = '<p style="padding: 8px; color: #6c757d;">Brak wyników</p>';
|
|
return;
|
|
}
|
|
|
|
resultsDiv.innerHTML = stops.slice(0, 10).map(stop => `
|
|
<div class="search-result-item" data-stop-id="${stop.stop_id}">
|
|
<strong>${stop.stop_name}</strong>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Dodaj event listenery
|
|
resultsDiv.querySelectorAll('.search-result-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const stopId = item.dataset.stopId;
|
|
const stopName = item.querySelector('strong').textContent;
|
|
const stop = stops.find(s => s.stop_id === stopId);
|
|
selectStation(stop);
|
|
});
|
|
});
|
|
}
|
|
|
|
function selectStation(stop) {
|
|
selectedStop = stop;
|
|
|
|
// Ukryj wyniki wyszukiwania
|
|
document.getElementById('search-results').innerHTML = '';
|
|
document.getElementById('station-search').value = '';
|
|
|
|
// Pokaż wybraną stację
|
|
document.getElementById('selected-station').style.display = 'block';
|
|
document.getElementById('station-name').textContent = stop.stop_name;
|
|
|
|
// Pokaż kontrolki
|
|
document.getElementById('time-controls').style.display = 'block';
|
|
|
|
// Wycentruj mapę na stacji
|
|
map.setView([stop.stop_lat, stop.stop_lon], 10);
|
|
|
|
// Dodaj marker
|
|
if (stopsLayer) {
|
|
map.removeLayer(stopsLayer);
|
|
}
|
|
|
|
stopsLayer = L.marker([stop.stop_lat, stop.stop_lon], {
|
|
icon: L.divIcon({
|
|
className: 'custom-marker',
|
|
html: '📍',
|
|
iconSize: [30, 30]
|
|
})
|
|
}).addTo(map);
|
|
|
|
stopsLayer.bindPopup(`<strong>${stop.stop_name}</strong>`).openPopup();
|
|
|
|
console.log('Wybrano stację:', stop);
|
|
}
|
|
|
|
// Wyczyść wybór
|
|
document.getElementById('clear-selection').addEventListener('click', () => {
|
|
selectedStop = null;
|
|
document.getElementById('selected-station').style.display = 'none';
|
|
document.getElementById('time-controls').style.display = 'none';
|
|
document.getElementById('legend').style.display = 'none';
|
|
document.getElementById('stats').style.display = 'none';
|
|
|
|
clearIsochrones();
|
|
|
|
if (stopsLayer) {
|
|
map.removeLayer(stopsLayer);
|
|
stopsLayer = null;
|
|
}
|
|
});
|
|
|
|
// Oblicz izochrony
|
|
document.getElementById('calculate-btn').addEventListener('click', async () => {
|
|
if (!selectedStop) return;
|
|
|
|
// Pobierz wartości z checkboxów
|
|
const checkboxes = document.querySelectorAll('.checkbox-group input[type="checkbox"]:checked');
|
|
const timeIntervals = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
|
|
// Pobierz własne wartości
|
|
const customInput = document.getElementById('custom-times').value.trim();
|
|
if (customInput) {
|
|
const customTimes = customInput
|
|
.split(',')
|
|
.map(t => parseInt(t.trim()))
|
|
.filter(t => !isNaN(t) && t > 0); // Filtruj poprawne liczby
|
|
|
|
timeIntervals.push(...customTimes);
|
|
}
|
|
|
|
// Usuń duplikaty i posortuj
|
|
const uniqueIntervals = [...new Set(timeIntervals)].sort((a, b) => a - b);
|
|
|
|
if (uniqueIntervals.length === 0) {
|
|
alert('Wybierz przynajmniej jeden przedział czasowy lub wpisz własną wartość');
|
|
return;
|
|
}
|
|
|
|
await calculateIsochrones(selectedStop.stop_id, uniqueIntervals);
|
|
});
|
|
|
|
async function calculateIsochrones(stopId, timeIntervals) {
|
|
// Pokaż loading
|
|
document.getElementById('loading').style.display = 'block';
|
|
document.getElementById('calculate-btn').disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/isochrones`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
origin_stop_id: stopId,
|
|
time_intervals: timeIntervals
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
displayIsochrones(data.isochrones, data.reachable_stops);
|
|
} else {
|
|
alert(`Błąd: ${data.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Błąd:', error);
|
|
alert('Błąd połączenia z serwerem');
|
|
} finally {
|
|
document.getElementById('loading').style.display = 'none';
|
|
document.getElementById('calculate-btn').disabled = false;
|
|
}
|
|
}
|
|
|
|
function displayIsochrones(isochrones, reachableStops) {
|
|
// Usuń poprzednie warstwy
|
|
clearIsochrones();
|
|
|
|
// Sortuj od największego do najmniejszego (żeby mniejsze były na wierzchu)
|
|
isochrones.sort((a, b) => b.time - a.time);
|
|
|
|
// Dodaj wielokąty izochron
|
|
isochrones.forEach(iso => {
|
|
if (!iso.geometry) return;
|
|
|
|
const color = getColorForTime(iso.time);
|
|
|
|
const layer = L.geoJSON(iso.geometry, {
|
|
style: {
|
|
color: color,
|
|
weight: 2,
|
|
opacity: 0.8,
|
|
fillColor: color,
|
|
fillOpacity: 0.2
|
|
}
|
|
}).addTo(map);
|
|
|
|
layer.bindPopup(`<strong>${iso.time} min</strong><br>${iso.stop_count} stacji`);
|
|
|
|
isochroneLayers.push(layer);
|
|
});
|
|
|
|
// Dodaj markery dla osiągalnych stacji
|
|
reachableStops.forEach(stop => {
|
|
const marker = L.circleMarker([stop.lat, stop.lon], {
|
|
radius: 4,
|
|
fillColor: '#0080ff',
|
|
color: 'white',
|
|
weight: 1,
|
|
opacity: 1,
|
|
fillOpacity: 0.8
|
|
}).addTo(map);
|
|
|
|
marker.bindPopup(`
|
|
<strong>${stop.name}</strong><br>
|
|
Czas: ${stop.time} min
|
|
`);
|
|
|
|
isochroneLayers.push(marker);
|
|
});
|
|
|
|
// Aktualizuj legendę dynamicznie
|
|
updateLegend(isochrones);
|
|
|
|
// Pokaż statystyki
|
|
const statsContent = `
|
|
<strong>Osiągalne stacje:</strong> ${reachableStops.length}<br>
|
|
<strong>Izochrony:</strong> ${isochrones.length}<br>
|
|
<strong>Najdalsza stacja:</strong> ${reachableStops[reachableStops.length - 1]?.name || 'brak'}
|
|
(${reachableStops[reachableStops.length - 1]?.time || 0} min)
|
|
`;
|
|
document.getElementById('stats-content').innerHTML = statsContent;
|
|
document.getElementById('stats').style.display = 'block';
|
|
|
|
console.log('Wyświetlono izochrony:', isochrones.length);
|
|
}
|
|
|
|
function clearIsochrones() {
|
|
isochroneLayers.forEach(layer => map.removeLayer(layer));
|
|
isochroneLayers = [];
|
|
}
|
|
|
|
function updateLegend(isochrones) {
|
|
const legend = document.getElementById('legend');
|
|
const legendContainer = legend.querySelector('.legend-items') || createLegendContainer(legend);
|
|
|
|
// Wyczyść starą legendę
|
|
legendContainer.innerHTML = '';
|
|
|
|
// Dodaj wpisy dla każdej izochrony
|
|
isochrones.forEach(iso => {
|
|
const color = getColorForTime(iso.time);
|
|
const item = document.createElement('div');
|
|
item.className = 'legend-item';
|
|
item.innerHTML = `
|
|
<span class="legend-color" style="background: ${color}; opacity: 0.6;"></span>
|
|
<span>${iso.time} min</span>
|
|
`;
|
|
legendContainer.appendChild(item);
|
|
});
|
|
|
|
legend.style.display = 'block';
|
|
}
|
|
|
|
function createLegendContainer(legend) {
|
|
// Znajdź h3 i dodaj kontener po nim
|
|
const h3 = legend.querySelector('h3');
|
|
const container = document.createElement('div');
|
|
container.className = 'legend-items';
|
|
|
|
// Usuń stare statyczne wpisy jeśli istnieją
|
|
const oldItems = legend.querySelectorAll('.legend-item');
|
|
oldItems.forEach(item => item.remove());
|
|
|
|
legend.appendChild(container);
|
|
return container;
|
|
}
|
|
|
|
// Konfiguracja API URL
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const input = document.getElementById('api-url-input');
|
|
const saveBtn = document.getElementById('api-url-save');
|
|
const resetBtn = document.getElementById('api-url-reset');
|
|
const status = document.getElementById('api-url-status');
|
|
|
|
// Pokaż aktualny URL
|
|
input.value = API_URL;
|
|
status.textContent = `Aktualny: ${API_URL}`;
|
|
|
|
// Zapisz nowy URL
|
|
saveBtn.addEventListener('click', () => {
|
|
const newUrl = input.value.trim();
|
|
if (!newUrl) {
|
|
status.textContent = 'Błąd: Adres nie może być pusty';
|
|
status.style.color = 'red';
|
|
return;
|
|
}
|
|
|
|
setApiUrl(newUrl);
|
|
});
|
|
|
|
// Reset do auto-detect
|
|
resetBtn.addEventListener('click', () => {
|
|
localStorage.removeItem('izochrona_api_url');
|
|
location.reload();
|
|
});
|
|
});
|
|
|
|
// Inicjalizacja przy starcie
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
initMap();
|
|
console.log('Aplikacja gotowa');
|
|
console.log('API URL:', API_URL);
|
|
});
|