Текстовый файл из учебного комплекта, не свидетельство установленного runtime. Вернуться к учебнику · Исходный файл
1 import { 2 Map as MapLibreMap, 3 NavigationControl, 4 setWorkerUrl, 5 type GeoJSONSource, 6 type LngLatBoundsLike, 7 } from "maplibre-gl"; 8 import mapLibreWorkerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"; 9 10 setWorkerUrl(mapLibreWorkerUrl); 11 12 export type Airport = { 13 id?: string; 14 code?: string; 15 name?: string; 16 lat: number; 17 lon: number; 18 status?: string; 19 }; 20 21 export type RouteArtifact = { 22 from?: Record<string, unknown>; 23 to?: Record<string, unknown>; 24 geometry: [number, number][]; 25 distance_km?: number; 26 }; 27 28 export class MapController { 29 private map: MapLibreMap | null = null; 30 private ready = false; 31 private pendingAirports: Airport[] | null = null; 32 private pendingPos: { lat: number; lon: number } | null = null; 33 private pendingRoute: RouteArtifact | null = null; 34 35 init(containerId: string): void { 36 if (this.map) return; 37 38 const map = new MapLibreMap({ 39 container: containerId, 40 style: "https://demotiles.maplibre.org/style.json", 41 center: [37.6173, 55.7558], 42 zoom: 6, 43 }); 44 this.map = map; 45 46 map.addControl(new NavigationControl({ showCompass: true }), "top-left"); 47 48 map.on("load", () => { 49 if (this.map !== map) return; 50 51 map.addSource("airports", { 52 type: "geojson", 53 data: { type: "FeatureCollection", features: [] }, 54 }); 55 map.addLayer({ 56 id: "airports", 57 type: "circle", 58 source: "airports", 59 paint: { 60 "circle-radius": 5, 61 "circle-color": "#ffb020", 62 "circle-stroke-color": "#1d1d1f", 63 "circle-stroke-width": 1, 64 }, 65 }); 66 67 map.addSource("position", { 68 type: "geojson", 69 data: { type: "FeatureCollection", features: [] }, 70 }); 71 map.addLayer({ 72 id: "position", 73 type: "circle", 74 source: "position", 75 paint: { 76 "circle-radius": 7, 77 "circle-color": "#007aff", 78 "circle-stroke-color": "#ffffff", 79 "circle-stroke-width": 2, 80 }, 81 }); 82 83 map.addSource("route", { 84 type: "geojson", 85 data: { type: "FeatureCollection", features: [] }, 86 }); 87 map.addLayer({ 88 id: "route", 89 type: "line", 90 source: "route", 91 paint: { 92 "line-color": "#007aff", 93 "line-width": 4, 94 }, 95 }); 96 97 this.ready = true; 98 if (this.pendingAirports) this.setAirports(this.pendingAirports); 99 if (this.pendingPos) this.setCurrentPosition(this.pendingPos.lat, this.pendingPos.lon); 100 if (this.pendingRoute) this.drawRoute(this.pendingRoute); 101 }); 102 } 103 104 setAirports(airports: Airport[]): void { 105 if (!this.map || !this.ready) { 106 this.pendingAirports = airports; 107 return; 108 } 109 const source = this.map.getSource("airports") as GeoJSONSource; 110 source.setData({ 111 type: "FeatureCollection", 112 features: airports.map((a) => ({ 113 type: "Feature", 114 geometry: { type: "Point", coordinates: [a.lon, a.lat] }, 115 properties: { 116 id: a.id ?? "", 117 code: a.code ?? "", 118 name: a.name ?? "", 119 status: a.status ?? "", 120 }, 121 })), 122 }); 123 } 124 125 setCurrentPosition(lat: number, lon: number): void { 126 if (!this.map || !this.ready) { 127 this.pendingPos = { lat, lon }; 128 return; 129 } 130 const source = this.map.getSource("position") as GeoJSONSource; 131 source.setData({ 132 type: "FeatureCollection", 133 features: [ 134 { 135 type: "Feature", 136 geometry: { type: "Point", coordinates: [lon, lat] }, 137 properties: {}, 138 }, 139 ], 140 }); 141 } 142 143 drawRoute(route: RouteArtifact): void { 144 if (!this.map || !this.ready) { 145 this.pendingRoute = route; 146 return; 147 } 148 const source = this.map.getSource("route") as GeoJSONSource; 149 source.setData({ 150 type: "FeatureCollection", 151 features: [ 152 { 153 type: "Feature", 154 geometry: { type: "LineString", coordinates: route.geometry }, 155 properties: { distance_km: route.distance_km ?? null }, 156 }, 157 ], 158 }); 159 160 const bounds = this.boundsForCoords(route.geometry); 161 if (bounds) { 162 this.map.fitBounds(bounds, { padding: 60, duration: 800 }); 163 } 164 } 165 166 private boundsForCoords(coords: [number, number][]): LngLatBoundsLike | null { 167 if (!coords.length) return null; 168 let minLon = coords[0][0]; 169 let maxLon = coords[0][0]; 170 let minLat = coords[0][1]; 171 let maxLat = coords[0][1]; 172 for (const [lon, lat] of coords) { 173 minLon = Math.min(minLon, lon); 174 maxLon = Math.max(maxLon, lon); 175 minLat = Math.min(minLat, lat); 176 maxLat = Math.max(maxLat, lat); 177 } 178 return [ 179 [minLon, minLat], 180 [maxLon, maxLat], 181 ]; 182 } 183 }