map.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. const MapView = (() => {
  2. let map = null;
  3. let trackLayers = {}; // trackId → { layer, color, markers }
  4. let currentTrackId = null;
  5. const COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22', '#34495e'];
  6. let colorIndex = 0;
  7. function init() {
  8. map = L.map('map');
  9. const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  10. attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
  11. maxZoom: 19
  12. });
  13. const topoLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
  14. attribution: '&copy; <a href="https://opentopomap.org">OpenTopoMap</a> contributors',
  15. maxZoom: 17
  16. });
  17. const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
  18. attribution: 'Tiles &copy; Esri &mdash; Source: Esri, USGS, AeroGRID, IGN, and the GIS User Community',
  19. maxZoom: 19
  20. });
  21. const baseMaps = {
  22. 'OpenStreetMap': osmLayer,
  23. 'Topographic': topoLayer,
  24. 'Satellite': satelliteLayer
  25. };
  26. osmLayer.addTo(map);
  27. L.control.layers(baseMaps).addTo(map);
  28. L.control.scale({ metric: true, imperial: false }).addTo(map);
  29. // Restore state from URL hash
  30. restoreFromHash();
  31. // Save state on map move/zoom
  32. map.on('moveend zoomend', saveToHash);
  33. return map;
  34. }
  35. function getNextColor() {
  36. const c = COLORS[colorIndex % COLORS.length];
  37. colorIndex++;
  38. return c;
  39. }
  40. function addTrack(trackData, trackId) {
  41. if (trackLayers[trackId]) return trackLayers[trackId].layer;
  42. const color = getNextColor();
  43. const lines = [];
  44. const addedMarkers = [];
  45. if (!trackData.segments || trackData.segments.length === 0) return null;
  46. for (const seg of trackData.segments) {
  47. if (!seg || seg.length === 0) continue;
  48. const latlngs = seg.map(p => [p[0], p[1]]);
  49. lines.push(latlngs);
  50. }
  51. if (lines.length === 0) return null;
  52. const layer = L.polyline(lines, { color, weight: 3, opacity: 0.8 });
  53. // Add start marker (green)
  54. const firstSeg = trackData.segments.find(s => s && s.length > 0);
  55. if (firstSeg) {
  56. const first = firstSeg[0];
  57. const startMarker = L.circleMarker([first[0], first[1]], {
  58. radius: 7,
  59. color: '#27ae60',
  60. fillColor: '#27ae60',
  61. fillOpacity: 1,
  62. weight: 2
  63. }).bindTooltip('Start: ' + (trackData.meta?.name || 'Track'));
  64. startMarker.addTo(map);
  65. addedMarkers.push(startMarker);
  66. }
  67. // Add end marker (red)
  68. const lastSeg = [...trackData.segments].reverse().find(s => s && s.length > 0);
  69. if (lastSeg) {
  70. const last = lastSeg[lastSeg.length - 1];
  71. const endMarker = L.circleMarker([last[0], last[1]], {
  72. radius: 7,
  73. color: '#e74c3c',
  74. fillColor: '#e74c3c',
  75. fillOpacity: 1,
  76. weight: 2
  77. }).bindTooltip('End: ' + (trackData.meta?.name || 'Track'));
  78. endMarker.addTo(map);
  79. addedMarkers.push(endMarker);
  80. }
  81. layer.addTo(map);
  82. trackLayers[trackId] = { layer, color, markers: addedMarkers };
  83. return layer;
  84. }
  85. function removeTrack(trackId) {
  86. if (trackLayers[trackId]) {
  87. map.removeLayer(trackLayers[trackId].layer);
  88. // Remove markers
  89. if (trackLayers[trackId].markers) {
  90. trackLayers[trackId].markers.forEach(m => map.removeLayer(m));
  91. }
  92. delete trackLayers[trackId];
  93. }
  94. }
  95. function clearTracks() {
  96. Object.keys(trackLayers).forEach(id => removeTrack(id));
  97. colorIndex = 0;
  98. }
  99. function fitTrack(trackId) {
  100. if (!trackLayers[trackId]) return;
  101. try {
  102. const bounds = trackLayers[trackId].layer.getBounds();
  103. if (bounds.isValid()) {
  104. map.fitBounds(bounds, { padding: [20, 20] });
  105. }
  106. } catch (e) {
  107. console.warn('fitTrack error:', e);
  108. }
  109. }
  110. function fitAll() {
  111. const layers = Object.values(trackLayers).map(t => t.layer);
  112. if (layers.length === 0) return;
  113. try {
  114. const group = L.featureGroup(layers);
  115. const bounds = group.getBounds();
  116. if (bounds.isValid()) {
  117. map.fitBounds(bounds, { padding: [20, 20] });
  118. }
  119. } catch (e) {
  120. console.warn('fitAll error:', e);
  121. }
  122. }
  123. function hasTrack(trackId) { return !!trackLayers[trackId]; }
  124. function saveToHash() {
  125. if (!map) return;
  126. const c = map.getCenter();
  127. const z = map.getZoom();
  128. const tracks = Object.keys(trackLayers).join(',');
  129. let hash = `#map=${c.lat.toFixed(5)},${c.lng.toFixed(5)},${z}`;
  130. if (tracks) hash += `&tracks=${tracks}`;
  131. if (currentTrackId !== null && currentTrackId !== undefined) hash += `&open=${currentTrackId}`;
  132. history.replaceState(null, '', hash);
  133. }
  134. function restoreFromHash() {
  135. const params = getHashParams();
  136. if (params.map) {
  137. const parts = params.map.split(',');
  138. if (parts.length === 3) {
  139. const lat = parseFloat(parts[0]);
  140. const lng = parseFloat(parts[1]);
  141. const zoom = parseInt(parts[2]);
  142. if (!isNaN(lat) && !isNaN(lng) && !isNaN(zoom)) {
  143. map.setView([lat, lng], zoom);
  144. return params;
  145. }
  146. }
  147. }
  148. // Default view
  149. map.setView([51.505, -0.09], 5);
  150. return params;
  151. }
  152. function getHashParams() {
  153. const hash = window.location.hash.slice(1);
  154. const params = {};
  155. if (!hash) return params;
  156. hash.split('&').forEach(part => {
  157. const eqIdx = part.indexOf('=');
  158. if (eqIdx > 0) {
  159. const k = part.slice(0, eqIdx);
  160. const v = part.slice(eqIdx + 1);
  161. if (k && v) params[k] = v;
  162. }
  163. });
  164. return params;
  165. }
  166. function setCurrentTrack(id) {
  167. currentTrackId = id;
  168. saveToHash();
  169. }
  170. function getMap() { return map; }
  171. return {
  172. init,
  173. addTrack,
  174. removeTrack,
  175. clearTracks,
  176. fitTrack,
  177. fitAll,
  178. hasTrack,
  179. saveToHash,
  180. restoreFromHash,
  181. getHashParams,
  182. setCurrentTrack,
  183. getMap
  184. };
  185. })();