fix for broken backend
All checks were successful
release-tag / release-image (push) Successful in 45s

This commit is contained in:
Иван Антонович Козлов 2025-05-28 16:07:45 +03:00
parent b9ab746cfd
commit f9ca0bdbd9
6 changed files with 421 additions and 671 deletions

View File

@ -122,6 +122,7 @@ body {
background: #806c58; background: #806c58;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
height: 50px; height: 50px;
width: 450px;
margin: 0 25px; margin: 0 25px;
color: #fff; color: #fff;
font-size: 18px; font-size: 18px;

File diff suppressed because one or more lines are too long

View File

@ -36,19 +36,25 @@ export default {
fullPolyline: null, fullPolyline: null,
passedPolyline: null, passedPolyline: null,
tramDirection: "right", tramDirection: "right",
routeId: null,
segmentLengths: [],
cumulativeDistances: [],
totalLength: 0,
sightPollTimer: null,
}; };
}, },
mounted() { async mounted() {
this.initializeMap(); this.initializeMap();
this.fetchRoute(); await this.fetchContext(); // obtain current routeId
this.fetchSights();
this.startTracking(); this.startTracking();
}, },
methods: { methods: {
async fetchSights() { async fetchSights() {
if (!this.routeId) return;
try { 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(); const data = await response.json();
console.log("Данные достопримечательностей:", data);
if (Array.isArray(data)) { if (Array.isArray(data)) {
const grouped = {}; const grouped = {};
@ -99,6 +105,30 @@ export default {
} catch (error) { } catch (error) {
console.error("Ошибка при получении достопримечательностей:", 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() { initializeMap() {
@ -120,11 +150,27 @@ export default {
}, },
async fetchRoute() { async fetchRoute() {
fetch(`${API_URL}/route/1`) if (!this.routeId) return;
fetch(`${API_URL}/route/${this.routeId}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.path && Array.isArray(data.path)) { if (data.path && Array.isArray(data.path)) {
this.routeLatlngs = data.path.map((coord) => [coord[0], coord[1]]); 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 ( if (
typeof data.scale_min === "number" && typeof data.scale_min === "number" &&
typeof data.scale_max === "number" typeof data.scale_max === "number"
@ -190,6 +236,7 @@ export default {
}, },
async fetchStations() { async fetchStations() {
if (!this.routeId) return;
this.stationMarkers.forEach(({ marker }) => { this.stationMarkers.forEach(({ marker }) => {
if (this.map.hasLayer(marker)) { if (this.map.hasLayer(marker)) {
this.map.removeLayer(marker); this.map.removeLayer(marker);
@ -199,8 +246,12 @@ export default {
try { try {
const [ruStations, enStations] = await Promise.all([ const [ruStations, enStations] = await Promise.all([
fetch(`${API_URL}/route/1/station`).then((r) => r.json()), fetch(`${API_URL}/route/${this.routeId}/station`).then((r) =>
fetch(`${API_URL}/route/1/station?lang=en`).then((r) => r.json()), r.json()
),
fetch(`${API_URL}/route/${this.routeId}/station?lang=en`).then((r) =>
r.json()
),
]); ]);
const enById = {}; const enById = {};
@ -368,39 +419,83 @@ export default {
setInterval(this.updateTramPosition, 500); 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() { async updateTramPosition() {
try { try {
const response = await fetch(`${GEO_URL}/v1/geolocation/context`); const response = await fetch(`${GEO_URL}/v1/geolocation/context`);
const data = await response.json(); const data = await response.json();
if (
data.routeId &&
data.routeId !== 0 &&
data.routeId !== this.routeId
) {
this.routeId = data.routeId;
}
// console.log("Текущие координаты трамвая:", data.currentCoordinates); // console.log("Текущие координаты трамвая:", data.currentCoordinates);
const { percentageCompleted } = data.routeProgress; 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), const targetDistance = percentageCompleted * this.totalLength;
this.routeLatlngs.length - 1
);
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.passedPolyline) this.map.removeLayer(this.passedPolyline);
if (this.fullPolyline) this.map.removeLayer(this.fullPolyline); if (this.fullPolyline) this.map.removeLayer(this.fullPolyline);
// Красная линия уже проехали // Пройденная часть (красная)
this.passedPolyline = L.polyline( this.passedPolyline = L.polyline(passedCoords, {
this.routeLatlngs.slice(0, progressIndex + 1), color: "red",
{ weight: 7,
color: "red", pane: "routePane",
weight: 7, }).addTo(this.map);
pane: "routePane",
}
).addTo(this.map);
// Белая линия ещё впереди // Оставшаяся часть (белая)
this.fullPolyline = L.polyline(this.routeLatlngs.slice(progressIndex), { this.fullPolyline = L.polyline(fullCoords, {
color: "white", color: "white",
weight: 7, weight: 7,
pane: "routePane", pane: "routePane",
@ -425,8 +520,9 @@ export default {
0 0
); );
const isPassed = stationIndex < progressIndex; const stationPassed =
const color = isPassed ? "red" : "white"; this.cumulativeDistances[stationIndex] <= targetDistance;
const color = stationPassed ? "red" : "white";
marker.setStyle({ marker.setStyle({
color: "black", 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> </script>

View File

@ -85,6 +85,8 @@ export default {
isStartScrolling: false, isStartScrolling: false,
isEndScrolling: false, isEndScrolling: false,
isEnScrolling: false, isEnScrolling: false,
pollId: null,
hasLoadedStops: false,
}; };
}, },
watch: { watch: {
@ -101,41 +103,69 @@ export default {
this.$nextTick(this.checkScroll); this.$nextTick(this.checkScroll);
}, },
}, },
async mounted() { mounted() {
const contextRes = await fetch(`${GEO_URL}/v1/geolocation/context`); this.startPollingContext();
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();
});
}, },
methods: { 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() { checkScroll() {
const threshold = 280; const threshold = 280;
if (this.$refs.startStopRuText) { if (this.$refs.startStopRuText) {

View File

@ -204,6 +204,7 @@ export default {
nextStopTransfers: null, nextStopTransfers: null,
stops: [], stops: [],
routeProgress: null, routeProgress: null,
routeId: null,
}; };
}, },
computed: { computed: {
@ -301,28 +302,44 @@ export default {
} }
}, },
async fetchSights() { async fetchSights() {
const geoRes = await axios.get(`${GEO_URL}/v1/geolocation/context`); // Prefer cached routeId, otherwise fetch context once
const routeId = geoRes.data.routeId; let routeId = this.routeId;
if (!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; return;
} }
const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`); try {
const rawSights = sightsRes.data; const sightsRes = await axios.get(`${API_URL}/route/${routeId}/sight`);
const detailedSights = await Promise.all( const rawSights = sightsRes.data;
rawSights.map(async (sight) => { const detailedSights = await Promise.all(
const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`); rawSights.map(async (sight) => {
const thumbnailUrl = detailRes.data.thumbnail const detailRes = await axios.get(`${API_URL}/sight/${sight.id}`);
? await this.getMediaBlobUrl(detailRes.data.thumbnail) const thumbnailUrl = detailRes.data.thumbnail
: ""; ? await this.getMediaBlobUrl(detailRes.data.thumbnail)
return { : "";
id: sight.id, return {
name: detailRes.data.name, id: sight.id,
thumbnailUrl, name: detailRes.data.name,
}; thumbnailUrl,
}) };
); })
this.sights = detailedSights; );
this.sights = detailedSights;
} catch (error) {
console.error("Error fetching sights:", error);
}
}, },
toggleSightsList() { toggleSightsList() {
this.showSightsList = !this.showSightsList; this.showSightsList = !this.showSightsList;
@ -414,10 +431,18 @@ export default {
try { try {
const response = await axios.get(`${GEO_URL}/v1/geolocation/context`); const response = await axios.get(`${GEO_URL}/v1/geolocation/context`);
this.routeProgress = response.data.routeProgress; this.routeProgress = response.data.routeProgress;
const stopsResponse = await axios.get( const newRouteId = response.data.routeId;
`${API_URL}/route/${response.data.routeId}/station` if (newRouteId && newRouteId !== this.routeId) {
); this.routeId = newRouteId;
this.stops = stopsResponse.data; }
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; let newSightId = response.data.nearestSightId;
if (!newSightId) { 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() { async mounted() {
await this.fetchSights(); await this.fetchSights();
await this.fetchGeolocationContext(); await this.fetchGeolocationContext();

View File

@ -96,7 +96,7 @@ export default {
isModalOpen: false, isModalOpen: false,
autoCloseTimer: null, autoCloseTimer: null,
imageUrl: "", imageUrl: "",
sightId: 14, sightId: null,
stopName: "", stopName: "",
articles: [], articles: [],
selectedArticleId: null, selectedArticleId: null,
@ -120,10 +120,16 @@ export default {
}, },
methods: { methods: {
async fetchSightInfo() { 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}`); const response = await axios.get(`${API_URL}/sight/${this.sightId}`);
this.stopName = response.data.name; this.stopName = response.data.name;
}, },
async fetchArticles() { async fetchArticles() {
// Do nothing if sightId is null, zero, or empty
if (!this.sightId) return;
const response = await axios.get( const response = await axios.get(
`${API_URL}/sight/${this.sightId}/article` `${API_URL}/sight/${this.sightId}/article`
); );