const MapView = (() => {
let map = null;
let trackLayers = {}; // trackId → { layer, color, markers }
let currentTrackId = null;
const COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22', '#34495e'];
let colorIndex = 0;
function init() {
map = L.map('map');
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
});
const topoLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: '© OpenTopoMap contributors',
maxZoom: 17
});
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Source: Esri, USGS, AeroGRID, IGN, and the GIS User Community',
maxZoom: 19
});
const baseMaps = {
'OpenStreetMap': osmLayer,
'Topographic': topoLayer,
'Satellite': satelliteLayer
};
osmLayer.addTo(map);
L.control.layers(baseMaps).addTo(map);
L.control.scale({ metric: true, imperial: false }).addTo(map);
// Restore state from URL hash
restoreFromHash();
// Save state on map move/zoom
map.on('moveend zoomend', saveToHash);
return map;
}
function getNextColor() {
const c = COLORS[colorIndex % COLORS.length];
colorIndex++;
return c;
}
function addTrack(trackData, trackId) {
if (trackLayers[trackId]) return trackLayers[trackId].layer;
const color = getNextColor();
const lines = [];
const addedMarkers = [];
if (!trackData.segments || trackData.segments.length === 0) return null;
for (const seg of trackData.segments) {
if (!seg || seg.length === 0) continue;
const latlngs = seg.map(p => [p[0], p[1]]);
lines.push(latlngs);
}
if (lines.length === 0) return null;
const layer = L.polyline(lines, { color, weight: 3, opacity: 0.8 });
// Add start marker (green)
const firstSeg = trackData.segments.find(s => s && s.length > 0);
if (firstSeg) {
const first = firstSeg[0];
const startMarker = L.circleMarker([first[0], first[1]], {
radius: 7,
color: '#27ae60',
fillColor: '#27ae60',
fillOpacity: 1,
weight: 2
}).bindTooltip('Start: ' + (trackData.meta?.name || 'Track'));
startMarker.addTo(map);
addedMarkers.push(startMarker);
}
// Add end marker (red)
const lastSeg = [...trackData.segments].reverse().find(s => s && s.length > 0);
if (lastSeg) {
const last = lastSeg[lastSeg.length - 1];
const endMarker = L.circleMarker([last[0], last[1]], {
radius: 7,
color: '#e74c3c',
fillColor: '#e74c3c',
fillOpacity: 1,
weight: 2
}).bindTooltip('End: ' + (trackData.meta?.name || 'Track'));
endMarker.addTo(map);
addedMarkers.push(endMarker);
}
layer.addTo(map);
trackLayers[trackId] = { layer, color, markers: addedMarkers };
return layer;
}
function removeTrack(trackId) {
if (trackLayers[trackId]) {
map.removeLayer(trackLayers[trackId].layer);
// Remove markers
if (trackLayers[trackId].markers) {
trackLayers[trackId].markers.forEach(m => map.removeLayer(m));
}
delete trackLayers[trackId];
}
}
function clearTracks() {
Object.keys(trackLayers).forEach(id => removeTrack(id));
colorIndex = 0;
}
function fitTrack(trackId) {
if (!trackLayers[trackId]) return;
try {
const bounds = trackLayers[trackId].layer.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [20, 20] });
}
} catch (e) {
console.warn('fitTrack error:', e);
}
}
function fitAll() {
const layers = Object.values(trackLayers).map(t => t.layer);
if (layers.length === 0) return;
try {
const group = L.featureGroup(layers);
const bounds = group.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [20, 20] });
}
} catch (e) {
console.warn('fitAll error:', e);
}
}
function hasTrack(trackId) { return !!trackLayers[trackId]; }
function saveToHash() {
if (!map) return;
const c = map.getCenter();
const z = map.getZoom();
const tracks = Object.keys(trackLayers).join(',');
let hash = `#map=${c.lat.toFixed(5)},${c.lng.toFixed(5)},${z}`;
if (tracks) hash += `&tracks=${tracks}`;
if (currentTrackId !== null && currentTrackId !== undefined) hash += `&open=${currentTrackId}`;
history.replaceState(null, '', hash);
}
function restoreFromHash() {
const params = getHashParams();
if (params.map) {
const parts = params.map.split(',');
if (parts.length === 3) {
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
const zoom = parseInt(parts[2]);
if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
map.setView([lat, lng], zoom);
return params;
}
}
}
// Default view
map.setView([51.505, -0.09], 5);
return params;
}
function getHashParams() {
const hash = window.location.hash.slice(1);
const params = {};
if (!hash) return params;
hash.split('&').forEach(part => {
const eqIdx = part.indexOf('=');
if (eqIdx > 0) {
const k = part.slice(0, eqIdx);
const v = part.slice(eqIdx + 1);
if (k && v) params[k] = v;
}
});
return params;
}
function setCurrentTrack(id) {
currentTrackId = id;
saveToHash();
}
function getMap() { return map; }
return {
init,
addTrack,
removeTrack,
clearTracks,
fitTrack,
fitAll,
hasTrack,
saveToHash,
restoreFromHash,
getHashParams,
setCurrentTrack,
getMap
};
})();