mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
fix(atlas): date a country's first and last trip from the trips that count
The Atlas tooltip took both dates from the start dates of every trip with a place in the country. One trip showed the month it began twice, the month it ended never appeared, and a trip only booked could become the last visit of a country already visited. Both now come from the trips that give the country its status, places and bookings alike: the start of the earliest and the end of the latest, with a trip still under way ending today. The optimistic mark on the client clears the dates the same way the server does. The second label rendered the raw key atlas.lastVisitLabel since #1908 removed it from the locales; the strings are back as they were. The month is built from the date's digits now, because a 'YYYY-MM-DD' string parses as UTC and showed the month before anywhere west of Greenwich. Closes #1535
This commit is contained in:
@@ -16,6 +16,7 @@ import { KNOWN_COUNTRIES } from './known-countries';
|
||||
import { cityFromAddress } from './city-from-address';
|
||||
import { transferEndpointIds } from './transfer-endpoints';
|
||||
import type { FlightEndpointRow } from './transfer-endpoints';
|
||||
import { countryVisitDates } from './visit-dates';
|
||||
import { haversineKm } from '../common/geo';
|
||||
|
||||
/**
|
||||
@@ -262,21 +263,16 @@ export class AtlasService {
|
||||
}
|
||||
}
|
||||
|
||||
const countries = [...countrySet.values()].map((c) => {
|
||||
const countryTrips = trips.filter((t) => c.tripIds.has(t.id));
|
||||
const dates = countryTrips
|
||||
.map((t) => t.start_date)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
return {
|
||||
code: c.code,
|
||||
placeCount: c.places.length,
|
||||
tripCount: c.tripIds.size,
|
||||
firstVisit: dates[0] || null,
|
||||
lastVisit: dates[dates.length - 1] || null,
|
||||
status: c.status,
|
||||
};
|
||||
});
|
||||
// The dates are filled in at the end, once a manual mark or a booking has had its
|
||||
// say on the status: they come only from the trips that match that final status.
|
||||
const countries = [...countrySet.values()].map((c) => ({
|
||||
code: c.code,
|
||||
placeCount: c.places.length,
|
||||
tripCount: c.tripIds.size,
|
||||
firstVisit: null as string | null,
|
||||
lastVisit: null as string | null,
|
||||
status: c.status,
|
||||
}));
|
||||
|
||||
const citySet = new Set<string>();
|
||||
for (const place of places) {
|
||||
@@ -367,14 +363,17 @@ export class AtlasService {
|
||||
|
||||
// Collapse to one entry per coordinate before resolving countries —
|
||||
// getCountryFromCoords is a point-in-polygon scan and used to run once per point.
|
||||
const endpointStatus = new Map<string, { lat: number; lng: number; status: VisitStatus }>();
|
||||
const endpointStatus = new Map<string, { lat: number; lng: number; status: VisitStatus; tripIds: Set<number> }>();
|
||||
for (const e of endpoints) {
|
||||
const status = tripStatus.get(e.trip_id) ?? 'idea';
|
||||
const key = `${e.lat},${e.lng}`;
|
||||
const seen = endpointStatus.get(key);
|
||||
if (seen) seen.status = strongerVisitStatus(seen.status, status);
|
||||
else endpointStatus.set(key, { lat: e.lat, lng: e.lng, status });
|
||||
if (seen) {
|
||||
seen.status = strongerVisitStatus(seen.status, status);
|
||||
seen.tripIds.add(e.trip_id);
|
||||
} else endpointStatus.set(key, { lat: e.lat, lng: e.lng, status, tripIds: new Set([e.trip_id]) });
|
||||
}
|
||||
const bookingTripIds = new Map<string, Set<number>>();
|
||||
for (const e of endpointStatus.values()) {
|
||||
const code = getCountryFromCoords(e.lat, e.lng);
|
||||
if (!code || hidden.has(code)) continue;
|
||||
@@ -382,6 +381,20 @@ export class AtlasService {
|
||||
if (existing) existing.status = strongerVisitStatus(existing.status, e.status);
|
||||
else
|
||||
countries.push({ code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null, status: e.status });
|
||||
const ids = bookingTripIds.get(code) ?? new Set<number>();
|
||||
for (const id of e.tripIds) ids.add(id);
|
||||
bookingTripIds.set(code, ids);
|
||||
}
|
||||
|
||||
// A booking can be what makes a country with places visited, so its trip dates the
|
||||
// country as well. A country reached by bookings alone has no trip in its tooltip
|
||||
// and stays without dates.
|
||||
for (const c of countries) {
|
||||
const fromPlaces = countrySet.get(c.code);
|
||||
if (!fromPlaces) continue;
|
||||
const fromBookings = bookingTripIds.get(c.code);
|
||||
const dated = trips.filter((t) => fromPlaces.tripIds.has(t.id) || fromBookings?.has(t.id));
|
||||
Object.assign(c, countryVisitDates(dated, c.status, now));
|
||||
}
|
||||
|
||||
// Everything below counts actual visits only. countries[] still carries planned and
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* The "First trip / Last trip" dates Atlas shows for a country (#1535).
|
||||
*
|
||||
* Both used to be the earliest and the latest start_date of every trip with a
|
||||
* place in the country. A single trip therefore showed its departure month twice,
|
||||
* the month you came home never appeared, and a trip you had only booked could
|
||||
* become the last visit of a country you had already been to.
|
||||
*
|
||||
* Now only the trips that give the country its status count: the visited ones for
|
||||
* a visited country, the planned ones for a planned country. firstVisit is where
|
||||
* the earliest of them starts and lastVisit where the latest ends. A trip you are
|
||||
* still on ends today, not on the day you plan to fly back.
|
||||
*
|
||||
* Dates stay 'YYYY-MM-DD' strings and are compared as such, like tripVisitStatus
|
||||
* does, so no timezone can shift them on the way.
|
||||
*/
|
||||
import { tripVisitStatus, type VisitStatus } from '@trek/shared';
|
||||
|
||||
export interface DatedTrip {
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
}
|
||||
|
||||
export function countryVisitDates(
|
||||
trips: DatedTrip[],
|
||||
status: VisitStatus,
|
||||
today: string,
|
||||
): { firstVisit: string | null; lastVisit: string | null } {
|
||||
let firstVisit: string | null = null;
|
||||
let lastVisit: string | null = null;
|
||||
for (const trip of trips) {
|
||||
const start = trip.start_date || trip.end_date;
|
||||
if (!start || tripVisitStatus(trip.start_date, trip.end_date, today) !== status) continue;
|
||||
let end = trip.end_date && trip.end_date > start ? trip.end_date : start;
|
||||
if (status === 'visited' && end > today) end = today;
|
||||
if (!firstVisit || start < firstVisit) firstVisit = start;
|
||||
if (!lastVisit || end > lastVisit) lastVisit = end;
|
||||
}
|
||||
return { firstVisit, lastVisit };
|
||||
}
|
||||
Reference in New Issue
Block a user