fix for broken backend
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				release-tag / release-image (push) Successful in 45s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	release-tag / release-image (push) Successful in 45s
				
			This commit is contained in:
		| @@ -122,6 +122,7 @@ body { | ||||
|   background: #806c58; | ||||
|   border-radius: 10px 10px 0 0; | ||||
|   height: 50px; | ||||
|   width: 450px; | ||||
|   margin: 0 25px; | ||||
|   color: #fff; | ||||
|   font-size: 18px; | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -36,19 +36,25 @@ export default { | ||||
|       fullPolyline: null, | ||||
|       passedPolyline: null, | ||||
|       tramDirection: "right", | ||||
|       routeId: null, | ||||
|       segmentLengths: [], | ||||
|       cumulativeDistances: [], | ||||
|       totalLength: 0, | ||||
|       sightPollTimer: null, | ||||
|     }; | ||||
|   }, | ||||
|   mounted() { | ||||
|   async mounted() { | ||||
|     this.initializeMap(); | ||||
|     this.fetchRoute(); | ||||
|     this.fetchSights(); | ||||
|     await this.fetchContext(); // obtain current routeId | ||||
|     this.startTracking(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async fetchSights() { | ||||
|       if (!this.routeId) return; | ||||
|       try { | ||||
|         const response = await fetch(`${API_URL}/route/1/sight`); | ||||
|         const response = await fetch(`${API_URL}/route/${this.routeId}/sight`); | ||||
|         const data = await response.json(); | ||||
|         console.log("Данные достопримечательностей:", data); | ||||
|  | ||||
|         if (Array.isArray(data)) { | ||||
|           const grouped = {}; | ||||
| @@ -99,6 +105,30 @@ export default { | ||||
|       } catch (error) { | ||||
|         console.error("Ошибка при получении достопримечательностей:", error); | ||||
|       } | ||||
|       // Stop polling once at least one sight marker exists | ||||
|       if (this.sightMarkers.length > 0 && this.sightPollTimer) { | ||||
|         clearInterval(this.sightPollTimer); | ||||
|         this.sightPollTimer = null; | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     // ── Poll fetchSights every 3 s until at least one sight is on the map ── | ||||
|     startSightPolling() { | ||||
|       if (this.sightPollTimer) return; // already polling | ||||
|       this.sightPollTimer = setInterval(() => { | ||||
|         this.fetchSights(); | ||||
|         if (this.sightMarkers.length > 0) { | ||||
|           clearInterval(this.sightPollTimer); | ||||
|           this.sightPollTimer = null; | ||||
|         } | ||||
|       }, 3000); | ||||
|     }, | ||||
|  | ||||
|     stopSightPolling() { | ||||
|       if (this.sightPollTimer) { | ||||
|         clearInterval(this.sightPollTimer); | ||||
|         this.sightPollTimer = null; | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     initializeMap() { | ||||
| @@ -120,11 +150,27 @@ export default { | ||||
|     }, | ||||
|  | ||||
|     async fetchRoute() { | ||||
|       fetch(`${API_URL}/route/1`) | ||||
|       if (!this.routeId) return; | ||||
|       fetch(`${API_URL}/route/${this.routeId}`) | ||||
|         .then((response) => response.json()) | ||||
|         .then((data) => { | ||||
|           if (data.path && Array.isArray(data.path)) { | ||||
|             this.routeLatlngs = data.path.map((coord) => [coord[0], coord[1]]); | ||||
|             // ── Pre-compute cumulative distances for distance-based progress ── | ||||
|             this.segmentLengths = []; | ||||
|             this.cumulativeDistances = [0]; | ||||
|             for (let i = 1; i < this.routeLatlngs.length; i++) { | ||||
|               const prev = L.latLng(this.routeLatlngs[i - 1]); | ||||
|               const curr = L.latLng(this.routeLatlngs[i]); | ||||
|               const segLen = prev.distanceTo(curr); // metres | ||||
|               this.segmentLengths.push(segLen); | ||||
|               this.cumulativeDistances.push( | ||||
|                 this.cumulativeDistances[i - 1] + segLen | ||||
|               ); | ||||
|             } | ||||
|             this.totalLength = | ||||
|               this.cumulativeDistances[this.cumulativeDistances.length - 1] || | ||||
|               0; | ||||
|             if ( | ||||
|               typeof data.scale_min === "number" && | ||||
|               typeof data.scale_max === "number" | ||||
| @@ -190,6 +236,7 @@ export default { | ||||
|     }, | ||||
|  | ||||
|     async fetchStations() { | ||||
|       if (!this.routeId) return; | ||||
|       this.stationMarkers.forEach(({ marker }) => { | ||||
|         if (this.map.hasLayer(marker)) { | ||||
|           this.map.removeLayer(marker); | ||||
| @@ -199,8 +246,12 @@ export default { | ||||
|  | ||||
|       try { | ||||
|         const [ruStations, enStations] = await Promise.all([ | ||||
|           fetch(`${API_URL}/route/1/station`).then((r) => r.json()), | ||||
|           fetch(`${API_URL}/route/1/station?lang=en`).then((r) => r.json()), | ||||
|           fetch(`${API_URL}/route/${this.routeId}/station`).then((r) => | ||||
|             r.json() | ||||
|           ), | ||||
|           fetch(`${API_URL}/route/${this.routeId}/station?lang=en`).then((r) => | ||||
|             r.json() | ||||
|           ), | ||||
|         ]); | ||||
|  | ||||
|         const enById = {}; | ||||
| @@ -368,39 +419,83 @@ export default { | ||||
|       setInterval(this.updateTramPosition, 500); | ||||
|     }, | ||||
|  | ||||
|     async fetchContext() { | ||||
|       try { | ||||
|         const response = await fetch(`${GEO_URL}/v1/geolocation/context`); | ||||
|         const data = await response.json(); | ||||
|         const ctxRouteId = | ||||
|           (data && data.routeId) || | ||||
|           (data.routeProgress && data.routeProgress.routeId) || | ||||
|           null; | ||||
|  | ||||
|         if (ctxRouteId && ctxRouteId !== this.routeId) { | ||||
|           this.routeId = ctxRouteId; | ||||
|         } | ||||
|       } catch (error) { | ||||
|         console.error("Ошибка при получении контекста геолокации:", error); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     async updateTramPosition() { | ||||
|       try { | ||||
|         const response = await fetch(`${GEO_URL}/v1/geolocation/context`); | ||||
|         const data = await response.json(); | ||||
|         if ( | ||||
|           data.routeId && | ||||
|           data.routeId !== 0 && | ||||
|           data.routeId !== this.routeId | ||||
|         ) { | ||||
|           this.routeId = data.routeId; | ||||
|         } | ||||
|         // console.log("Текущие координаты трамвая:", data.currentCoordinates); | ||||
|  | ||||
|         const { percentageCompleted } = data.routeProgress; | ||||
|  | ||||
|         if (this.routeLatlngs.length === 0) return; | ||||
|         if (this.totalLength === 0) return; | ||||
|  | ||||
|         const progressIndex = Math.min( | ||||
|           Math.floor(percentageCompleted * this.routeLatlngs.length), | ||||
|           this.routeLatlngs.length - 1 | ||||
|         ); | ||||
|         // Дистанция, пройденная трамваем (м) | ||||
|         const targetDistance = percentageCompleted * this.totalLength; | ||||
|  | ||||
|         const tramLatLng = this.routeLatlngs[progressIndex]; | ||||
|         // Определяем сегмент маршрута, в котором находится трамвай | ||||
|         let segIdx = 0; | ||||
|         while ( | ||||
|           segIdx < this.cumulativeDistances.length - 1 && | ||||
|           this.cumulativeDistances[segIdx + 1] < targetDistance | ||||
|         ) { | ||||
|           segIdx++; | ||||
|         } | ||||
|  | ||||
|         const segStart = this.routeLatlngs[segIdx]; | ||||
|         const segEnd = | ||||
|           this.routeLatlngs[Math.min(segIdx + 1, this.routeLatlngs.length - 1)]; | ||||
|         const segStartDist = this.cumulativeDistances[segIdx]; | ||||
|         const segLen = this.cumulativeDistances[segIdx + 1] - segStartDist || 1; | ||||
|  | ||||
|         // Интерполируем точку на сегменте | ||||
|         const t = segLen === 0 ? 0 : (targetDistance - segStartDist) / segLen; | ||||
|         const tramLatLng = [ | ||||
|           segStart[0] + (segEnd[0] - segStart[0]) * t, | ||||
|           segStart[1] + (segEnd[1] - segStart[1]) * t, | ||||
|         ]; | ||||
|  | ||||
|         // Формируем координаты пройденной и оставшейся части | ||||
|         const passedCoords = this.routeLatlngs.slice(0, segIdx + 1); | ||||
|         passedCoords.push(tramLatLng); | ||||
|         const fullCoords = [tramLatLng, ...this.routeLatlngs.slice(segIdx + 1)]; | ||||
|  | ||||
|         // Удаляем старые линии, если есть | ||||
|         if (this.passedPolyline) this.map.removeLayer(this.passedPolyline); | ||||
|         if (this.fullPolyline) this.map.removeLayer(this.fullPolyline); | ||||
|  | ||||
|         // Красная линия — уже проехали | ||||
|         this.passedPolyline = L.polyline( | ||||
|           this.routeLatlngs.slice(0, progressIndex + 1), | ||||
|           { | ||||
|             color: "red", | ||||
|             weight: 7, | ||||
|             pane: "routePane", | ||||
|           } | ||||
|         ).addTo(this.map); | ||||
|         // Пройденная часть (красная) | ||||
|         this.passedPolyline = L.polyline(passedCoords, { | ||||
|           color: "red", | ||||
|           weight: 7, | ||||
|           pane: "routePane", | ||||
|         }).addTo(this.map); | ||||
|  | ||||
|         // Белая линия — ещё впереди | ||||
|         this.fullPolyline = L.polyline(this.routeLatlngs.slice(progressIndex), { | ||||
|         // Оставшаяся часть (белая) | ||||
|         this.fullPolyline = L.polyline(fullCoords, { | ||||
|           color: "white", | ||||
|           weight: 7, | ||||
|           pane: "routePane", | ||||
| @@ -425,8 +520,9 @@ export default { | ||||
|             0 | ||||
|           ); | ||||
|  | ||||
|           const isPassed = stationIndex < progressIndex; | ||||
|           const color = isPassed ? "red" : "white"; | ||||
|           const stationPassed = | ||||
|             this.cumulativeDistances[stationIndex] <= targetDistance; | ||||
|           const color = stationPassed ? "red" : "white"; | ||||
|  | ||||
|           marker.setStyle({ | ||||
|             color: "black", | ||||
| @@ -461,5 +557,22 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     // When routeId becomes available or changes, (re)load dependent data | ||||
|     routeId(newVal, oldVal) { | ||||
|       if (newVal && newVal !== oldVal) { | ||||
|         this.fetchRoute(); | ||||
|         this.fetchSights(); | ||||
|         this.fetchStations(); | ||||
|         this.startSightPolling(); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   beforeUnmount() { | ||||
|     if (this.sightPollTimer) { | ||||
|       clearInterval(this.sightPollTimer); | ||||
|       this.sightPollTimer = null; | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -85,6 +85,8 @@ export default { | ||||
|       isStartScrolling: false, | ||||
|       isEndScrolling: false, | ||||
|       isEnScrolling: false, | ||||
|       pollId: null, | ||||
|       hasLoadedStops: false, | ||||
|     }; | ||||
|   }, | ||||
|   watch: { | ||||
| @@ -101,41 +103,69 @@ export default { | ||||
|       this.$nextTick(this.checkScroll); | ||||
|     }, | ||||
|   }, | ||||
|   async mounted() { | ||||
|     const contextRes = await fetch(`${GEO_URL}/v1/geolocation/context`); | ||||
|     const data = await contextRes.json(); | ||||
|     this.routeNumber = data.routeNumber; | ||||
|  | ||||
|     const startStopId = data.startStopId; | ||||
|     const endStopId = data.endStopId; | ||||
|  | ||||
|     const [startStopRes, endStopRes] = await Promise.all([ | ||||
|       fetch(`${API_URL}/station/${startStopId}`), | ||||
|       fetch(`${API_URL}/station/${endStopId}`), | ||||
|     ]); | ||||
|  | ||||
|     const startStopData = await startStopRes.json(); | ||||
|     const endStopData = await endStopRes.json(); | ||||
|  | ||||
|     this.startStopName = startStopData.name; | ||||
|     this.endStopName = endStopData.name; | ||||
|  | ||||
|     const [startStopEnRes, endStopEnRes] = await Promise.all([ | ||||
|       fetch(`${API_URL}/station/${startStopId}?lang=en`), | ||||
|       fetch(`${API_URL}/station/${endStopId}?lang=en`), | ||||
|     ]); | ||||
|  | ||||
|     const startStopEnData = await startStopEnRes.json(); | ||||
|     const endStopEnData = await endStopEnRes.json(); | ||||
|  | ||||
|     this.startStopNameEn = startStopEnData.name; | ||||
|     this.endStopNameEn = endStopEnData.name; | ||||
|  | ||||
|     this.$nextTick(() => { | ||||
|       this.checkScroll(); | ||||
|     }); | ||||
|   mounted() { | ||||
|     this.startPollingContext(); | ||||
|   }, | ||||
|   methods: { | ||||
|     startPollingContext() { | ||||
|       // first request immediately, then every 3 s | ||||
|       this.fetchContext(); | ||||
|       this.pollId = setInterval(this.fetchContext, 3000); | ||||
|     }, | ||||
|  | ||||
|     async fetchContext() { | ||||
|       try { | ||||
|         const res = await fetch(`${GEO_URL}/v1/geolocation/context`); | ||||
|         const data = await res.json(); | ||||
|  | ||||
|         if (data.routeNumber) { | ||||
|           this.routeNumber = data.routeNumber; | ||||
|         } | ||||
|  | ||||
|         const { startStopId, endStopId } = data; | ||||
|  | ||||
|         // when both IDs are available for the first time, load names and stop polling | ||||
|         if (startStopId && endStopId && !this.hasLoadedStops) { | ||||
|           await this.loadStopNames(startStopId, endStopId); | ||||
|           this.hasLoadedStops = true; | ||||
|           if (this.pollId) { | ||||
|             clearInterval(this.pollId); | ||||
|             this.pollId = null; | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.error("Failed to fetch geolocation context", err); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     async loadStopNames(startStopId, endStopId) { | ||||
|       try { | ||||
|         const [startRuRes, endRuRes] = await Promise.all([ | ||||
|           fetch(`${API_URL}/station/${startStopId}`), | ||||
|           fetch(`${API_URL}/station/${endStopId}`), | ||||
|         ]); | ||||
|         const startRu = await startRuRes.json(); | ||||
|         const endRu = await endRuRes.json(); | ||||
|  | ||||
|         this.startStopName = startRu.name; | ||||
|         this.endStopName = endRu.name; | ||||
|  | ||||
|         const [startEnRes, endEnRes] = await Promise.all([ | ||||
|           fetch(`${API_URL}/station/${startStopId}?lang=en`), | ||||
|           fetch(`${API_URL}/station/${endStopId}?lang=en`), | ||||
|         ]); | ||||
|         const startEn = await startEnRes.json(); | ||||
|         const endEn = await endEnRes.json(); | ||||
|  | ||||
|         this.startStopNameEn = startEn.name; | ||||
|         this.endStopNameEn = endEn.name; | ||||
|  | ||||
|         this.$nextTick(this.checkScroll); | ||||
|       } catch (err) { | ||||
|         console.error("Failed to load station names", err); | ||||
|       } | ||||
|     }, | ||||
|  | ||||
|     checkScroll() { | ||||
|       const threshold = 280; | ||||
|       if (this.$refs.startStopRuText) { | ||||
|   | ||||
| @@ -204,6 +204,7 @@ export default { | ||||
|       nextStopTransfers: null, | ||||
|       stops: [], | ||||
|       routeProgress: null, | ||||
|       routeId: null, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -301,28 +302,44 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     async fetchSights() { | ||||
|       const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`); | ||||
|       const routeId = geoRes.data.routeId; | ||||
|       // Prefer cached routeId, otherwise fetch context once | ||||
|       let routeId = this.routeId; | ||||
|       if (!routeId) { | ||||
|         console.warn("Missing routeId in geo context:", geoRes.data); | ||||
|         try { | ||||
|           const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`); | ||||
|           routeId = geoRes.data.routeId; | ||||
|           if (routeId && routeId !== this.routeId) { | ||||
|             this.routeId = routeId; | ||||
|           } | ||||
|         } catch (error) { | ||||
|           console.error("Failed to get routeId from context:", error); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|       if (!routeId) { | ||||
|         console.warn("Missing routeId — skipping fetchSights"); | ||||
|         return; | ||||
|       } | ||||
|       const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`); | ||||
|       const rawSights = sightsRes.data; | ||||
|       const detailedSights = await Promise.all( | ||||
|         rawSights.map(async (sight) => { | ||||
|           const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`); | ||||
|           const thumbnailUrl = detailRes.data.thumbnail | ||||
|             ? await this.getMediaBlobUrl(detailRes.data.thumbnail) | ||||
|             : ""; | ||||
|           return { | ||||
|             id: sight.id, | ||||
|             name: detailRes.data.name, | ||||
|             thumbnailUrl, | ||||
|           }; | ||||
|         }) | ||||
|       ); | ||||
|       this.sights = detailedSights; | ||||
|       try { | ||||
|         const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`); | ||||
|         const rawSights = sightsRes.data; | ||||
|         const detailedSights = await Promise.all( | ||||
|           rawSights.map(async (sight) => { | ||||
|             const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`); | ||||
|             const thumbnailUrl = detailRes.data.thumbnail | ||||
|               ? await this.getMediaBlobUrl(detailRes.data.thumbnail) | ||||
|               : ""; | ||||
|             return { | ||||
|               id: sight.id, | ||||
|               name: detailRes.data.name, | ||||
|               thumbnailUrl, | ||||
|             }; | ||||
|           }) | ||||
|         ); | ||||
|         this.sights = detailedSights; | ||||
|       } catch (error) { | ||||
|         console.error("Error fetching sights:", error); | ||||
|       } | ||||
|     }, | ||||
|     toggleSightsList() { | ||||
|       this.showSightsList = !this.showSightsList; | ||||
| @@ -414,10 +431,18 @@ export default { | ||||
|       try { | ||||
|         const response = await axios.get(`${GEO_URL}/v1/geolocation/context`); | ||||
|         this.routeProgress = response.data.routeProgress; | ||||
|         const stopsResponse = await axios.get( | ||||
|           `${API_URL}/route/${response.data.routeId}/station` | ||||
|         ); | ||||
|         this.stops = stopsResponse.data; | ||||
|         const newRouteId = response.data.routeId; | ||||
|         if (newRouteId && newRouteId !== this.routeId) { | ||||
|           this.routeId = newRouteId; | ||||
|         } | ||||
|         if (this.routeId) { | ||||
|           const stopsResponse = await axios.get( | ||||
|             `${API_URL}/route/${this.routeId}/station` | ||||
|           ); | ||||
|           this.stops = stopsResponse.data; | ||||
|         } else { | ||||
|           this.stops = []; | ||||
|         } | ||||
|         let newSightId = response.data.nearestSightId; | ||||
|  | ||||
|         if (!newSightId) { | ||||
| @@ -490,6 +515,14 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     routeId(newVal, oldVal) { | ||||
|       if (newVal && newVal !== oldVal) { | ||||
|         // Refresh sights because routeId changed | ||||
|         this.fetchSights(); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   async mounted() { | ||||
|     await this.fetchSights(); | ||||
|     await this.fetchGeolocationContext(); | ||||
|   | ||||
| @@ -96,7 +96,7 @@ export default { | ||||
|       isModalOpen: false, | ||||
|       autoCloseTimer: null, | ||||
|       imageUrl: "", | ||||
|       sightId: 14, | ||||
|       sightId: null, | ||||
|       stopName: "", | ||||
|       articles: [], | ||||
|       selectedArticleId: null, | ||||
| @@ -120,10 +120,16 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     async fetchSightInfo() { | ||||
|       // Do nothing if sightId is null, zero, or empty | ||||
|       if (!this.sightId) return; | ||||
|  | ||||
|       const response = await axios.get(`${API_URL}/sight/${this.sightId}`); | ||||
|       this.stopName = response.data.name; | ||||
|     }, | ||||
|     async fetchArticles() { | ||||
|       // Do nothing if sightId is null, zero, or empty | ||||
|       if (!this.sightId) return; | ||||
|  | ||||
|       const response = await axios.get( | ||||
|         `${API_URL}/sight/${this.sightId}/article` | ||||
|       ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user