Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/docs/features/public-transit.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ place's stops get the same warning. Lines, alerts and facilities that cannot be
loaded are left out rather than kept as "none", and are loaded again on a later
visit or within about a minute.

A place's board includes the stops within 1 km whose name is the place's name.
Sources spell it differently, with or without the town ("Neumarkt", "Köln
Neumarkt", "Marienplatz, München"). It also includes stops whose name says
they belong to the station, such as "Köln Dom/Hbf" at Köln Hauptbahnhof or
"S+U Alexanderplatz Bhf/Gontardstr." at Alexanderplatz. A stop that only shares
the district is left out: "Köln Deutz Suevenstr." is not on the board for
"Köln Messe/Deutz". Some bus stops beside a station are left out too, when
their name doesn't say they belong to it.

A place's stops can have different names, for example a main station and a bus
stop beside it. A departure from a stop other than the place's main one shows
that stop's name next to its line, so you know where to board. When you open a
Expand Down
2 changes: 1 addition & 1 deletion integrations/transit/__tests__/merged-board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ describe("the stop a merged row leaves from", () => {
"ms:bf": [dep("ms:t1", "RE1", "Aachen Hbf", "10:00:00")],
"ms:bus": [dep("ms:u1", "150", "Chorweiler", "10:01:00")],
},
{ "ms:bf": "Köln Messe/Deutz Bf", "ms:bus": "Köln Messe/Deutz Ottoplatz Bussteig" },
{ "ms:bf": "Köln Messe/Deutz Bf", "ms:bus": "Köln Bf Deutz/Messe Ottoplatz Bussteig" },
"Köln Messe/Deutz",
);
expect(rows.map((row) => row.atOtherStop ?? false)).toEqual([false, true]);
Expand Down
140 changes: 138 additions & 2 deletions integrations/transit/__tests__/place-link-names.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const station: TransitStop[] = [
];

/** A board over a cache that keeps what it is given, and a stop search returning `station`. */
function setup(options: PlaceTransitOptions = {}) {
function setup(options: PlaceTransitOptions = {}, stops: TransitStop[] = station) {
const values = new Map<string, unknown>();
const ctx = {
cache: {
Expand All @@ -33,7 +33,7 @@ function setup(options: PlaceTransitOptions = {}) {
},
} as unknown as IntegrationContext;
const search = vi.fn(async () => ({
data: station,
data: stops,
attributions: [],
freshness: freshnessNow(),
}));
Expand Down Expand Up @@ -95,3 +95,139 @@ describe("names that link a place's stops", () => {
expect(search.mock.calls.length).toBeGreaterThan(calls);
});
});

/** A stop `meters` north of (lat, lng): 0.001° of latitude is about 111 m. */
function stopAt(id: string, name: string, lat: number, lng: number, meters = 100): TransitStop {
return { id, provider: id.split(":")[0], name, lat: lat + meters / 111_000, lng, modes: ["bus"] };
}

async function linkedNames(place: { lat: number; lng: number; name: string }, names: string[]) {
const stops = names.map((name, i) => stopAt(`p:${i}`, name, place.lat, place.lng));
const { transit } = setup({}, stops);
const linked = await transit.getLinkedStops(place);
return linked.data.map((stop) => stop.name);
}

describe("which stops a place's name links", () => {
// Names live providers give stops within 1 km of Köln Messe/Deutz.
const deutz = { lat: 50.940833, lng: 6.975, name: "Köln Messe/Deutz" };

it("links the station under each source's spelling", async () => {
const station = [
"Köln Messe/Deutz",
"Köln Messe/Deutz Bf",
"Köln Messe/Deutz Bahnhof",
"Messe/Deutz Bahnhof, Köln",
"Messe/Deutz",
"Köln Messe/Deutz Gl. 11-12",
];
expect(await linkedNames(deutz, station)).toEqual(station);
});

it("links a stop named as the station's own", async () => {
const own = ["Köln Bf Deutz/Messe LANXESS arena", "Köln, Deutz Bf Deutz/Messe LANXESS arena"];
expect(await linkedNames(deutz, own)).toEqual(own);
});

it("doesn't link stops that only share the district or the town", async () => {
const others = [
"Köln Deutz Suevenstr.",
"Köln, Deutz Gummersbacher Straße",
"Köln, Deutz Deutzer Freiheit",
"Köln Deutz Technische Hochschule",
"Köln Deutz Koelnmesse",
"Köln Bf Deutz/Messeplatz",
"KÖLN",
];
expect(await linkedNames(deutz, others)).toEqual([]);
});

it("links a stop named with or without its town", async () => {
const neumarkt = { lat: 50.9364, lng: 6.9469, name: "Neumarkt" };
expect(
await linkedNames(neumarkt, ["Köln Neumarkt", "Neumarkt", "Köln Neumarkt Galerie"]),
).toEqual(["Köln Neumarkt", "Neumarkt"]);
const friedrichstrasse = { lat: 52.5203, lng: 13.3869, name: "Berlin Friedrichstraße" };
expect(
await linkedNames(friedrichstrasse, [
"S+U Friedrichstr. Bhf (Berlin)",
"Friedrichstr./Reinhardtstr. (Berlin)",
]),
).toEqual(["S+U Friedrichstr. Bhf (Berlin)"]);
});

it("links a stop whose town a comma sets apart, but not another stop of that district", async () => {
const pasing = { lat: 48.1497, lng: 11.4614, name: "München-Pasing" };
expect(
await linkedNames(pasing, [
"Pasing, München",
"Pasing Bahnhof, München",
"Krankenhaus Pasing, München",
"Rathaus Pasing, München",
]),
).toEqual(["Pasing, München", "Pasing Bahnhof, München"]);
const marienplatz = { lat: 48.1372, lng: 11.5755, name: "Marienplatz" };
expect(await linkedNames(marienplatz, ["Marienplatz, München"])).toEqual([
"Marienplatz, München",
]);
});

it("links a station named for its side of town", async () => {
const ost = { lat: 48.1274, lng: 11.6047, name: "München Ost" };
expect(
await linkedNames(ost, [
"Ostbahnhof, München",
"München Ostbahnhof (Orleansplatz)",
"München Ostfriedhof",
]),
).toEqual(["Ostbahnhof, München", "München Ostbahnhof (Orleansplatz)"]);
});

it("links the stops a main station's name marks as its own", async () => {
const hbf = { lat: 50.9432, lng: 6.9586, name: "Köln Hauptbahnhof" };
expect(
await linkedNames(hbf, ["Köln Hbf", "Köln Dom/Hbf", "Köln Breslauer Platz/Hbf", "Köln Dom"]),
).toEqual(["Köln Hbf", "Köln Dom/Hbf", "Köln Breslauer Platz/Hbf"]);
const alex = { lat: 52.5219, lng: 13.4132, name: "Berlin Alexanderplatz" };
expect(
await linkedNames(alex, [
"S+U Alexanderplatz Bhf/Gontardstr. (Berlin)",
"U Alexanderplatz/Karl-Liebknecht-Str. (Berlin)",
]),
).toEqual(["S+U Alexanderplatz Bhf/Gontardstr. (Berlin)"]);
});

it("links only the station of a place named like its town", async () => {
const homburg = { lat: 50.2193, lng: 8.6211, name: "Bad Homburg" };
expect(
await linkedNames(homburg, ["Bad Homburg Bahnhof", "Bad Homburg Kurhaus", "Bad Homburg"]),
).toEqual(["Bad Homburg Bahnhof", "Bad Homburg"]);
});

it("doesn't read a comma as setting the town apart when the place is a town", async () => {
const bruehl = { lat: 50.8285, lng: 6.9036, name: "Brühl" };
expect(
await linkedNames(bruehl, [
"Brühl Bf",
"Brühl Mitte",
"Brühl Nord",
"Brühl Am Stern",
"Brühl, Giesler Galerie",
"Rathaus, Brühl",
]),
).toEqual(["Brühl Bf"]);
});

it("keeps different places with a shared last word apart", async () => {
const tor = { lat: 52.4991, lng: 13.4181, name: "Kottbusser Tor" };
expect(await linkedNames(tor, ["U Kottbusser Tor (Berlin)", "Schlesisches Tor"])).toEqual([
"U Kottbusser Tor (Berlin)",
]);
});

it("doesn't link a stop more than 1 km away", async () => {
const stops = [stopAt("p:far", "Köln Messe/Deutz", deutz.lat, deutz.lng, 1_200)];
const { transit } = setup({}, stops);
expect((await transit.getLinkedStops(deutz)).data).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("linked stop discovery failures", () => {
it("keeps the stops found by one name variant when another variant's search fails", async () => {
const search = vi.fn(async (query: string) => {
if (query === "Aachen Hauptbahnhof") throw new Error("Transit stop sources unavailable");
return result(stops);
return result(stops.map((stop) => ({ ...stop, name: "Aachen Hbf" })));
});
const api = discovering(search as unknown as TransitOrchestrator["searchByNameRaw"]);

Expand Down
Loading
Loading