diff --git a/package.json b/package.json index 08bbbb4b..8bfc7712 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test:e2e": "playwright test", "test:unit": "./setup-db.sh && vitest --fileParallelism false", "test:gen": "./setup-db.sh && vitest src/lib/server/booking/tests/generatedTests/gen.test.ts --fileParallelism false", - "test:one": "./setup-db.sh && vitest src/lib/server/booking/rideShare/tests/bookToRSTour.test.ts --fileParallelism false", + "test:one": "./setup-db.sh && vitest src/lib/server/booking/taxi/tests/scenarios/scenario1.test.ts --fileParallelism false", "sim": "pnpx vite-node --options.transformMode.ssr='/.*/' scripts/simulation/script.ts", "sim:health": "pnpx vite-node --options.transformMode.ssr='/.*/' scripts/simulation/script.ts -- --ongoing --health --restrict --bu --wl", "sim:nohealth": "pnpx vite-node --options.transformMode.ssr='/.*/' scripts/simulation/script.ts -- --ongoing --restrict --bu", diff --git a/scripts/reproduce/callBookingApi.ts b/scripts/reproduce/callBookingApi.ts index 1d3c2a7e..046e265f 100644 --- a/scripts/reproduce/callBookingApi.ts +++ b/scripts/reproduce/callBookingApi.ts @@ -5,27 +5,28 @@ import { bookingApi } from '../../src/lib/server/booking/taxi/bookingApi'; const parameters = { capacities: { - passengers: 1, + passengers: 2, bikes: 0, luggage: 0, wheelchairs: 0 }, connection1: { start: { - lat: 51.505444, + lat: 51.505442, lng: 14.638026999999997, address: 'Weißwasser Bahnhof' }, target: { - lat: 51.4417032, - lng: 14.6895141, - address: 'Körnerplatz' + lat: 51.4804411, + lng: 14.7708978, + address: 'Feuerlöschteich 4' }, - startTime: 1754619540000, - targetTime: 1754621400000, - signature: 'cf9dcc63e0c95aacc17524783c750f35f3af63e9b607cdd3e25121d3db430632', + startTime: 1761456300000, + targetTime: 1761458160000, + signature: '043d3df015b5e08882f15fc83d2930582db6d51c9cc7a3544b07c38e1945dcf6', startFixed: true, - requestedTime: 1754609100000 + requestedTime: 1761438000000, + mode: 1 }, connection2: null }; diff --git a/scripts/reproduce/callWhitelist.ts b/scripts/reproduce/callWhitelist.ts index 4053c2fe..72802ed7 100644 --- a/scripts/reproduce/callWhitelist.ts +++ b/scripts/reproduce/callWhitelist.ts @@ -2,397 +2,83 @@ import 'dotenv/config'; import { whitelist } from '../../src/routes/api/whitelist/whitelist'; -import { type WhitelistRequest } from '../../src/routes/api/whitelist/WhitelistRequest'; +import { type WhitelistRequest } from '../../src/lib/server/util/whitelistRequest'; import { Insertion } from '../../src/lib/server/booking/taxi/insertion'; +import { Coordinates } from '../../src/lib/util/Coordinates'; +import { BusStop } from '../../src/lib/server/booking/taxi/BusStop'; +import { Capacities } from '../../src/lib/util/booking/Capacities'; -const params = { +const params: { + start: Coordinates; + target: Coordinates; + startBusStops: BusStop[]; + targetBusStops: BusStop[]; + directTimes: string[]; + startFixed: boolean; + capacities: Capacities; +} = { start: { - lat: 51.5338373, - lng: 14.5364242 + lat: 51.5393992, + lng: 14.5316092 }, target: { - lat: 51.5566873, - lng: 14.6996425 + lat: 51.4804411, + lng: 14.7708978 }, - startBusStops: [ - { - lat: 51.505424, - lng: 14.639178999999999, - times: [ - '2025-08-06T11:35:00.000Z', - '2025-08-06T12:35:00.000Z', - '2025-08-06T13:35:00.000Z', - '2025-08-06T14:35:00.000Z', - '2025-08-06T15:35:00.000Z', - '2025-08-06T16:35:00.000Z', - '2025-08-06T17:35:00.000Z', - '2025-08-06T18:35:00.000Z', - '2025-08-07T03:35:00.000Z', - '2025-08-07T04:35:00.000Z', - '2025-08-07T05:35:00.000Z', - '2025-08-07T06:35:00.000Z', - '2025-08-07T07:35:00.000Z', - '2025-08-07T08:35:00.000Z', - '2025-08-07T09:35:00.000Z', - '2025-08-07T10:35:00.000Z', - '2025-08-07T11:35:00.000Z' - ] - }, - { - lat: 51.5058, - lng: 14.642610999999999, - times: [ - '2025-08-06T11:36:00.000Z', - '2025-08-06T12:36:00.000Z', - '2025-08-06T13:36:00.000Z', - '2025-08-06T14:36:00.000Z', - '2025-08-06T15:36:00.000Z', - '2025-08-06T16:36:00.000Z', - '2025-08-06T17:36:00.000Z', - '2025-08-06T18:36:00.000Z', - '2025-08-07T03:36:00.000Z', - '2025-08-07T04:36:00.000Z', - '2025-08-07T05:36:00.000Z', - '2025-08-07T06:36:00.000Z', - '2025-08-07T07:36:00.000Z', - '2025-08-07T08:36:00.000Z', - '2025-08-07T09:36:00.000Z', - '2025-08-07T10:36:00.000Z', - '2025-08-07T11:36:00.000Z', - '2025-08-07T12:36:00.000Z' - ] - } - ], + startBusStops: [], targetBusStops: [ { - lat: 51.541595, - lng: 14.533987, - times: ['2025-08-06T11:29:00.000Z', '2025-08-07T05:31:00.000Z', '2025-08-07T11:29:00.000Z'] - }, - { - lat: 51.50438, - lng: 14.635010999999999, - times: ['2025-08-07T03:12:00.000Z'] - }, - { - lat: 51.5058, - lng: 14.642610999999999, - times: [ - '2025-08-06T12:06:00.000Z', - '2025-08-06T13:57:00.000Z', - '2025-08-06T15:57:00.000Z', - '2025-08-07T03:57:00.000Z', - '2025-08-07T05:57:00.000Z', - '2025-08-07T07:57:00.000Z', - '2025-08-07T09:57:00.000Z', - '2025-08-07T12:06:00.000Z' - ] - }, - { - lat: 51.50830500000001, - lng: 14.644146999999998, + lat: 51.505442, + lng: 14.638026999999997, times: [ - '2025-08-06T12:04:00.000Z', - '2025-08-06T13:04:00.000Z', - '2025-08-06T14:04:00.000Z', - '2025-08-06T15:04:00.000Z', - '2025-08-06T16:04:00.000Z', - '2025-08-06T17:04:00.000Z', - '2025-08-07T04:04:00.000Z', - '2025-08-07T05:04:00.000Z', - '2025-08-07T06:04:00.000Z', - '2025-08-07T07:04:00.000Z', - '2025-08-07T08:04:00.000Z', - '2025-08-07T09:04:00.000Z', - '2025-08-07T10:04:00.000Z', - '2025-08-07T11:04:00.000Z', - '2025-08-07T12:04:00.000Z', - '2025-08-07T13:04:00.000Z' + '2025-10-25T17:40:00.000Z', + '2025-10-25T18:40:00.000Z', + '2025-10-25T19:40:00.000Z', + '2025-10-25T20:40:00.000Z', + '2025-10-26T05:25:00.000Z' ] }, { - lat: 51.505444, - lng: 14.638026999999997, - times: [ - '2025-08-06T11:40:00.000Z', - '2025-08-06T12:40:00.000Z', - '2025-08-06T13:40:00.000Z', - '2025-08-06T14:40:00.000Z', - '2025-08-06T15:40:00.000Z', - '2025-08-06T16:40:00.000Z', - '2025-08-06T17:40:00.000Z', - '2025-08-06T18:40:00.000Z', - '2025-08-06T19:40:00.000Z', - '2025-08-06T20:40:00.000Z', - '2025-08-07T02:24:00.000Z', - '2025-08-07T03:40:00.000Z', - '2025-08-07T04:40:00.000Z', - '2025-08-07T05:40:00.000Z', - '2025-08-07T06:40:00.000Z', - '2025-08-07T07:40:00.000Z', - '2025-08-07T08:40:00.000Z', - '2025-08-07T09:40:00.000Z', - '2025-08-07T10:40:00.000Z', - '2025-08-07T11:40:00.000Z', - '2025-08-07T12:40:00.000Z' - ] + lat: 51.516359, + lng: 14.718338999999999, + times: ['2025-10-25T17:56:00.000Z', '2025-10-25T19:56:00.000Z'] } ], directTimes: [ - '2025-08-06T10:54:00.000Z', - '2025-08-06T10:59:00.000Z', - '2025-08-06T11:04:00.000Z', - '2025-08-06T11:09:00.000Z', - '2025-08-06T11:14:00.000Z', - '2025-08-06T11:19:00.000Z', - '2025-08-06T11:24:00.000Z', - '2025-08-06T11:29:00.000Z', - '2025-08-06T11:34:00.000Z', - '2025-08-06T11:39:00.000Z', - '2025-08-06T11:44:00.000Z', - '2025-08-06T11:49:00.000Z', - '2025-08-06T11:54:00.000Z', - '2025-08-06T11:59:00.000Z', - '2025-08-06T12:04:00.000Z', - '2025-08-06T12:09:00.000Z', - '2025-08-06T12:14:00.000Z', - '2025-08-06T12:19:00.000Z', - '2025-08-06T12:24:00.000Z', - '2025-08-06T12:29:00.000Z', - '2025-08-06T12:34:00.000Z', - '2025-08-06T12:39:00.000Z', - '2025-08-06T12:44:00.000Z', - '2025-08-06T12:49:00.000Z', - '2025-08-06T12:54:00.000Z', - '2025-08-06T12:59:00.000Z', - '2025-08-06T13:04:00.000Z', - '2025-08-06T13:09:00.000Z', - '2025-08-06T13:14:00.000Z', - '2025-08-06T13:19:00.000Z', - '2025-08-06T13:24:00.000Z', - '2025-08-06T13:29:00.000Z', - '2025-08-06T13:34:00.000Z', - '2025-08-06T13:39:00.000Z', - '2025-08-06T13:44:00.000Z', - '2025-08-06T13:49:00.000Z', - '2025-08-06T13:54:00.000Z', - '2025-08-06T13:59:00.000Z', - '2025-08-06T14:04:00.000Z', - '2025-08-06T14:09:00.000Z', - '2025-08-06T14:14:00.000Z', - '2025-08-06T14:19:00.000Z', - '2025-08-06T14:24:00.000Z', - '2025-08-06T14:29:00.000Z', - '2025-08-06T14:34:00.000Z', - '2025-08-06T14:39:00.000Z', - '2025-08-06T14:44:00.000Z', - '2025-08-06T14:49:00.000Z', - '2025-08-06T14:54:00.000Z', - '2025-08-06T14:59:00.000Z', - '2025-08-06T15:04:00.000Z', - '2025-08-06T15:09:00.000Z', - '2025-08-06T15:14:00.000Z', - '2025-08-06T15:19:00.000Z', - '2025-08-06T15:24:00.000Z', - '2025-08-06T15:29:00.000Z', - '2025-08-06T15:34:00.000Z', - '2025-08-06T15:39:00.000Z', - '2025-08-06T15:44:00.000Z', - '2025-08-06T15:49:00.000Z', - '2025-08-06T15:54:00.000Z', - '2025-08-06T15:59:00.000Z', - '2025-08-06T16:04:00.000Z', - '2025-08-06T16:09:00.000Z', - '2025-08-06T16:14:00.000Z', - '2025-08-06T16:19:00.000Z', - '2025-08-06T16:24:00.000Z', - '2025-08-06T16:29:00.000Z', - '2025-08-06T16:34:00.000Z', - '2025-08-06T16:39:00.000Z', - '2025-08-06T16:44:00.000Z', - '2025-08-06T16:49:00.000Z', - '2025-08-06T16:54:00.000Z', - '2025-08-06T16:59:00.000Z', - '2025-08-06T17:04:00.000Z', - '2025-08-06T17:09:00.000Z', - '2025-08-06T17:14:00.000Z', - '2025-08-06T17:19:00.000Z', - '2025-08-06T17:24:00.000Z', - '2025-08-06T17:29:00.000Z', - '2025-08-06T17:34:00.000Z', - '2025-08-06T17:39:00.000Z', - '2025-08-06T17:44:00.000Z', - '2025-08-06T17:49:00.000Z', - '2025-08-06T17:54:00.000Z', - '2025-08-06T17:59:00.000Z', - '2025-08-06T18:04:00.000Z', - '2025-08-06T18:09:00.000Z', - '2025-08-06T18:14:00.000Z', - '2025-08-06T18:19:00.000Z', - '2025-08-06T18:24:00.000Z', - '2025-08-06T18:29:00.000Z', - '2025-08-06T18:34:00.000Z', - '2025-08-06T18:39:00.000Z', - '2025-08-06T18:44:00.000Z', - '2025-08-06T18:49:00.000Z', - '2025-08-06T18:54:00.000Z', - '2025-08-06T18:59:00.000Z', - '2025-08-06T19:04:00.000Z', - '2025-08-06T19:09:00.000Z', - '2025-08-06T19:14:00.000Z', - '2025-08-06T19:19:00.000Z', - '2025-08-06T19:24:00.000Z', - '2025-08-06T19:29:00.000Z', - '2025-08-06T19:34:00.000Z', - '2025-08-06T19:39:00.000Z', - '2025-08-06T19:44:00.000Z', - '2025-08-06T19:49:00.000Z', - '2025-08-06T19:54:00.000Z', - '2025-08-06T19:59:00.000Z', - '2025-08-06T20:04:00.000Z', - '2025-08-06T20:09:00.000Z', - '2025-08-06T20:14:00.000Z', - '2025-08-06T20:19:00.000Z', - '2025-08-06T20:24:00.000Z', - '2025-08-06T20:29:00.000Z', - '2025-08-06T20:34:00.000Z', - '2025-08-06T20:39:00.000Z', - '2025-08-06T20:44:00.000Z', - '2025-08-06T20:49:00.000Z', - '2025-08-06T20:54:00.000Z', - '2025-08-06T20:59:00.000Z', - '2025-08-07T01:54:00.000Z', - '2025-08-07T01:59:00.000Z', - '2025-08-07T02:04:00.000Z', - '2025-08-07T02:09:00.000Z', - '2025-08-07T02:14:00.000Z', - '2025-08-07T02:19:00.000Z', - '2025-08-07T02:24:00.000Z', - '2025-08-07T02:29:00.000Z', - '2025-08-07T02:34:00.000Z', - '2025-08-07T02:39:00.000Z', - '2025-08-07T02:44:00.000Z', - '2025-08-07T02:49:00.000Z', - '2025-08-07T02:54:00.000Z', - '2025-08-07T02:59:00.000Z', - '2025-08-07T03:04:00.000Z', - '2025-08-07T03:09:00.000Z', - '2025-08-07T03:14:00.000Z', - '2025-08-07T03:19:00.000Z', - '2025-08-07T03:24:00.000Z', - '2025-08-07T03:29:00.000Z', - '2025-08-07T03:34:00.000Z', - '2025-08-07T03:39:00.000Z', - '2025-08-07T03:44:00.000Z', - '2025-08-07T03:49:00.000Z', - '2025-08-07T03:54:00.000Z', - '2025-08-07T03:59:00.000Z', - '2025-08-07T04:04:00.000Z', - '2025-08-07T04:09:00.000Z', - '2025-08-07T04:14:00.000Z', - '2025-08-07T04:19:00.000Z', - '2025-08-07T04:24:00.000Z', - '2025-08-07T04:29:00.000Z', - '2025-08-07T04:34:00.000Z', - '2025-08-07T04:39:00.000Z', - '2025-08-07T04:44:00.000Z', - '2025-08-07T04:49:00.000Z', - '2025-08-07T04:54:00.000Z', - '2025-08-07T04:59:00.000Z', - '2025-08-07T05:04:00.000Z', - '2025-08-07T05:09:00.000Z', - '2025-08-07T05:14:00.000Z', - '2025-08-07T05:19:00.000Z', - '2025-08-07T05:24:00.000Z', - '2025-08-07T05:29:00.000Z', - '2025-08-07T05:34:00.000Z', - '2025-08-07T05:39:00.000Z', - '2025-08-07T05:44:00.000Z', - '2025-08-07T05:49:00.000Z', - '2025-08-07T05:54:00.000Z', - '2025-08-07T05:59:00.000Z', - '2025-08-07T06:04:00.000Z', - '2025-08-07T06:09:00.000Z', - '2025-08-07T06:14:00.000Z', - '2025-08-07T06:19:00.000Z', - '2025-08-07T06:24:00.000Z', - '2025-08-07T06:29:00.000Z', - '2025-08-07T06:34:00.000Z', - '2025-08-07T06:39:00.000Z', - '2025-08-07T06:44:00.000Z', - '2025-08-07T06:49:00.000Z', - '2025-08-07T06:54:00.000Z', - '2025-08-07T06:59:00.000Z', - '2025-08-07T07:04:00.000Z', - '2025-08-07T07:09:00.000Z', - '2025-08-07T07:14:00.000Z', - '2025-08-07T07:19:00.000Z', - '2025-08-07T07:24:00.000Z', - '2025-08-07T07:29:00.000Z', - '2025-08-07T07:34:00.000Z', - '2025-08-07T07:39:00.000Z', - '2025-08-07T07:44:00.000Z', - '2025-08-07T07:49:00.000Z', - '2025-08-07T07:54:00.000Z', - '2025-08-07T07:59:00.000Z', - '2025-08-07T08:04:00.000Z', - '2025-08-07T08:09:00.000Z', - '2025-08-07T08:14:00.000Z', - '2025-08-07T08:19:00.000Z', - '2025-08-07T08:24:00.000Z', - '2025-08-07T08:29:00.000Z', - '2025-08-07T08:34:00.000Z', - '2025-08-07T08:39:00.000Z', - '2025-08-07T08:44:00.000Z', - '2025-08-07T08:49:00.000Z', - '2025-08-07T08:54:00.000Z', - '2025-08-07T08:59:00.000Z', - '2025-08-07T09:04:00.000Z', - '2025-08-07T09:09:00.000Z', - '2025-08-07T09:14:00.000Z', - '2025-08-07T09:19:00.000Z', - '2025-08-07T09:24:00.000Z', - '2025-08-07T09:29:00.000Z', - '2025-08-07T09:34:00.000Z', - '2025-08-07T09:39:00.000Z', - '2025-08-07T09:44:00.000Z', - '2025-08-07T09:49:00.000Z', - '2025-08-07T09:54:00.000Z', - '2025-08-07T09:59:00.000Z', - '2025-08-07T10:04:00.000Z', - '2025-08-07T10:09:00.000Z', - '2025-08-07T10:14:00.000Z', - '2025-08-07T10:19:00.000Z', - '2025-08-07T10:24:00.000Z', - '2025-08-07T10:29:00.000Z', - '2025-08-07T10:34:00.000Z', - '2025-08-07T10:39:00.000Z', - '2025-08-07T10:44:00.000Z', - '2025-08-07T10:49:00.000Z', - '2025-08-07T10:54:00.000Z', - '2025-08-07T10:59:00.000Z', - '2025-08-07T11:04:00.000Z', - '2025-08-07T11:09:00.000Z', - '2025-08-07T11:14:00.000Z', - '2025-08-07T11:19:00.000Z', - '2025-08-07T11:24:00.000Z', - '2025-08-07T11:29:00.000Z', - '2025-08-07T11:34:00.000Z', - '2025-08-07T11:39:00.000Z', - '2025-08-07T11:44:00.000Z', - '2025-08-07T11:49:00.000Z', - '2025-08-07T11:54:00.000Z', - '2025-08-07T11:59:00.000Z', - '2025-08-07T12:04:00.000Z', - '2025-08-07T12:09:00.000Z', - '2025-08-07T12:14:00.000Z', - '2025-08-07T12:19:00.000Z' + '2025-10-25T18:40:00.000Z', + '2025-10-25T18:45:00.000Z', + '2025-10-25T18:50:00.000Z', + '2025-10-25T18:55:00.000Z', + '2025-10-25T19:00:00.000Z', + '2025-10-25T19:05:00.000Z', + '2025-10-25T19:10:00.000Z', + '2025-10-25T19:15:00.000Z', + '2025-10-25T19:20:00.000Z', + '2025-10-25T19:25:00.000Z', + '2025-10-25T19:30:00.000Z', + '2025-10-25T19:35:00.000Z', + '2025-10-25T19:40:00.000Z', + '2025-10-25T19:45:00.000Z', + '2025-10-25T19:50:00.000Z', + '2025-10-25T19:55:00.000Z', + '2025-10-25T20:00:00.000Z', + '2025-10-25T20:05:00.000Z', + '2025-10-25T20:10:00.000Z', + '2025-10-25T20:15:00.000Z', + '2025-10-25T20:20:00.000Z', + '2025-10-25T20:25:00.000Z', + '2025-10-25T20:30:00.000Z', + '2025-10-25T20:35:00.000Z', + '2025-10-25T20:40:00.000Z', + '2025-10-25T20:45:00.000Z', + '2025-10-25T20:50:00.000Z', + '2025-10-25T20:55:00.000Z' ], startFixed: true, capacities: { wheelchairs: 0, bikes: 0, - passengers: 1, + passengers: 2, luggage: 0 } }; @@ -401,10 +87,10 @@ async function main() { const p: WhitelistRequest = { ...params, startBusStops: params.startBusStops.map((b) => { - return { ...b, times: b.times.map((t) => new Date(t).getTime()) }; + return { ...b, times: b.times.map((t: string) => new Date(t).getTime()) }; }), targetBusStops: params.targetBusStops.map((b) => { - return { ...b, times: b.times.map((t) => new Date(t).getTime()) }; + return { ...b, times: b.times.map((t: string) => new Date(t).getTime()) }; }), directTimes: params.directTimes.map((t) => new Date(t).getTime()) }; diff --git a/scripts/simulation/generateBookingParameters.ts b/scripts/simulation/generateBookingParameters.ts index ae9356ff..3843fb81 100644 --- a/scripts/simulation/generateBookingParameters.ts +++ b/scripts/simulation/generateBookingParameters.ts @@ -1,5 +1,5 @@ import { type BookingParameters } from '../../src/lib/server/booking/taxi/bookingApi'; -import type { ExpectedConnection } from '../../src/lib/server/booking/taxi/bookRide'; +import type { ExpectedConnection } from '../../src/lib/server/booking/expectedConnection'; import type { Capacities } from '../../src/lib/util/booking/Capacities'; import { type Coordinates } from '../../src/lib/util/Coordinates'; import { HOUR, MINUTE, DAY } from '../../src/lib/util/time'; diff --git a/scripts/simulation/script.ts b/scripts/simulation/script.ts index 2f8fb795..0b86ec58 100644 --- a/scripts/simulation/script.ts +++ b/scripts/simulation/script.ts @@ -230,6 +230,17 @@ async function bookingFull( { isDirect }, { requestedTime1: new Date(requestedTime1).toISOString() }, { startFixed: parameters.connection1.startFixed }, + { + legs: chosenItinerary.legs.map((l) => { + return { + start: new Date(l.scheduledStartTime).toISOString(), + end: new Date(l.scheduledEndTime).toISOString(), + mode: l.mode, + from: JSON.stringify(l.from, null, 2), + to: JSON.stringify(l.to, null, 2) + }; + }) + }, { time: new Date( parameters.connection1.startFixed diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9b6652ae..a4b133cc 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -6,7 +6,6 @@ export const MIN_PREP = HOUR; export const MAX_TRAVEL = HOUR; export const SCHEDULED_TIME_BUFFER_PICKUP = 1 * MINUTE; export const SCHEDULED_TIME_BUFFER_DROPOFF_RELATIVE = 0.35; -export const SCHEDULED_TIME_BUFFER_DROPOFF = SCHEDULED_TIME_BUFFER_DROPOFF_RELATIVE * MAX_TRAVEL; export const MAX_PASSENGER_WAITING_TIME_PICKUP = 20 * MINUTE; export const MAX_PASSENGER_WAITING_TIME_DROPOFF = 20 * MINUTE; export const WGS84 = 4326; diff --git a/src/lib/server/booking/expectedConnection.ts b/src/lib/server/booking/expectedConnection.ts index 1d90e17c..0f90fa5a 100644 --- a/src/lib/server/booking/expectedConnection.ts +++ b/src/lib/server/booking/expectedConnection.ts @@ -1,8 +1,8 @@ import type { Leg } from '$lib/openapi'; import { Mode } from '$lib/server/booking/mode'; +import { isOdmLeg, isTaxiLeg } from '$lib/util/booking/checkLegType'; import type { Coordinates } from '$lib/util/Coordinates'; import type { UnixtimeMs } from '$lib/util/UnixtimeMs'; -import { isOdmLeg, isTaxiLeg } from '../../../routes/(customer)/routing/utils'; export type ExpectedConnection = { start: Coordinates; diff --git a/src/lib/server/booking/rideShare/getRideShareInfo.ts b/src/lib/server/booking/rideShare/getRideShareInfo.ts index d5228233..5c6a41be 100644 --- a/src/lib/server/booking/rideShare/getRideShareInfo.ts +++ b/src/lib/server/booking/rideShare/getRideShareInfo.ts @@ -1,6 +1,6 @@ import type { Itinerary } from '$lib/openapi'; import { db } from '$lib/server/db'; -import { isRideShareLeg } from '../../../../routes/(customer)/routing/utils'; +import { isRideShareLeg } from '$lib/util/booking/checkLegType'; export async function getRideShareInfo(tourId: number) { return await db diff --git a/src/lib/server/booking/rideShare/insertion.ts b/src/lib/server/booking/rideShare/insertion.ts index 2791a34a..e7efdfec 100644 --- a/src/lib/server/booking/rideShare/insertion.ts +++ b/src/lib/server/booking/rideShare/insertion.ts @@ -76,8 +76,8 @@ type SingleInsertionEvaluation = { }; type Evaluations = { - busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][]; - userChosenEvaluations: (SingleInsertionEvaluation | undefined)[]; + busStopEvaluations: SingleInsertionEvaluation[][][][]; + userChosenEvaluations: SingleInsertionEvaluation[][]; bothEvaluations: Insertion[][][]; }; @@ -407,19 +407,31 @@ export function evaluateSingleInsertions( travelDurations: (number | undefined)[], promisedTimes?: PromisedTimesRideShare ): Evaluations { + const insertionIdxCount = rideShareTours.reduce( + (acc, curr) => + (acc += + insertionRanges + .get(curr.rideShareTour) + ?.reduce((acc, curr) => (acc += curr.latestDropoff + 1 - curr.earliestPickup), 0) ?? 0), + 0 + ); const bothEvaluations: Insertion[][][] = []; - const userChosenEvaluations: (SingleInsertionEvaluation | undefined)[] = []; - const busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][] = new Array< - (SingleInsertionEvaluation | undefined)[][] + const userChosenEvaluations: SingleInsertionEvaluation[][] = []; + const busStopEvaluations: SingleInsertionEvaluation[][][][] = new Array< + SingleInsertionEvaluation[][][] >(busStopTimes.length); + for (let i = 0; i != insertionIdxCount + 1; i++) { + userChosenEvaluations[i] = new Array(); + } for (let i = 0; i != busStopTimes.length; ++i) { - busStopEvaluations[i] = new Array<(SingleInsertionEvaluation | undefined)[]>( - busStopTimes[i].length - ); + busStopEvaluations[i] = new Array(busStopTimes[i].length); bothEvaluations[i] = new Array(busStopTimes[i].length); for (let j = 0; j != busStopTimes[i].length; ++j) { - busStopEvaluations[i][j] = new Array(); + busStopEvaluations[i][j] = new Array(); bothEvaluations[i][j] = new Array(); + for (let k = 0; k != insertionIdxCount; ++k) { + busStopEvaluations[i][j][k] = new Array(); + } } } const prepTime = Date.now() + MIN_PREP; @@ -502,7 +514,9 @@ export function evaluateSingleInsertions( next, promisedTimes ); - busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx] = resultBus; // TODO + if (resultBus !== undefined) { + busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx].push(resultBus); + } } } insertionCase.what = InsertWhat.USER_CHOSEN; @@ -517,7 +531,9 @@ export function evaluateSingleInsertions( next, promisedTimes ); - userChosenEvaluations[insertionInfo.insertionIdx] = resultUserChosen; // TODO + if (resultUserChosen != undefined) { + userChosenEvaluations[insertionInfo.insertionIdx].push(resultUserChosen); + } }); return { busStopEvaluations, userChosenEvaluations, bothEvaluations }; } @@ -527,8 +543,8 @@ export function evaluatePairInsertions( startFixed: boolean, insertionRanges: Map, busStopTimes: Interval[][], - busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][], - userChosenEvaluations: (SingleInsertionEvaluation | undefined)[], + busStopEvaluations: SingleInsertionEvaluation[][][][], + userChosenEvaluations: SingleInsertionEvaluation[][], whitelist?: boolean ): Insertion[][][] { const bestEvaluations = new Array(busStopTimes.length); @@ -551,152 +567,168 @@ export function evaluatePairInsertions( ) { for (let busStopIdx = 0; busStopIdx != busStopTimes.length; ++busStopIdx) { for (let timeIdx = 0; timeIdx != busStopTimes[busStopIdx].length; ++timeIdx) { - const pickup = startFixed + const pickupCases = startFixed ? busStopEvaluations[busStopIdx][timeIdx][insertionInfo.insertionIdx] : userChosenEvaluations[insertionInfo.insertionIdx]; - if (pickup == undefined) { + if (pickupCases == undefined) { break; } - const dropoff = startFixed + const dropoffCases = startFixed ? userChosenEvaluations[insertionInfo.insertionIdx + dropoffIdx - pickupIdx] : busStopEvaluations[busStopIdx][timeIdx][ insertionInfo.insertionIdx + dropoffIdx - pickupIdx ]; - if (dropoff == undefined) { + if (dropoffCases == undefined) { continue; } const prevDropoff = events[dropoffIdx - 1]; const nextDropoff = events[dropoffIdx]; const twoAfterDropoff = events[dropoffIdx + 1]; - const communicatedPickupTime = Math.max( - pickup.window.endTime - SCHEDULED_TIME_BUFFER_PICKUP, - pickup.window.startTime - ); - const communicatedDropoffTime = Math.min( - dropoff.window.startTime + - getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), - dropoff.window.endTime - ); - - // Determine the scheduled times for pickup and dropoff - const leewayBetweenPickupDropoff = - communicatedDropoffTime - - communicatedPickupTime - - pickup.nextLegDuration - - dropoff.prevLegDuration; - const pickupScheduledShift = Math.min( - pickup.window.size(), - SCHEDULED_TIME_BUFFER_PICKUP, - leewayBetweenPickupDropoff - ); - const scheduledPickupTime = communicatedPickupTime + pickupScheduledShift; - const scheduledDropoffTime = - communicatedDropoffTime - - Math.min( - dropoff.window.size(), - getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), - leewayBetweenPickupDropoff - pickupScheduledShift - ); - - // Compute the delta of the taxi's time spend driving for the tour containing the new request - const approachPlusReturnDurationDelta = - pickup.approachPlusReturnDurationDelta + dropoff.approachPlusReturnDurationDelta; - const fullyPayedDurationDelta = - pickup.fullyPayedDurationDelta + dropoff.fullyPayedDurationDelta; - - // Compute the delta of the taxi's waiting time - const newDeparture = - prevPickup.tourId !== twoBeforePickup?.tourId - ? Math.min( - communicatedPickupTime - pickup.prevLegDuration, - getScheduledEventTime(prevPickup) - ) - prevPickup.prevLegDuration - : prevPickup.departure; - const newArrival = - nextDropoff.tourId !== twoAfterDropoff?.tourId - ? Math.max( - communicatedDropoffTime + dropoff.nextLegDuration, - getScheduledEventTime(nextDropoff) - ) + nextDropoff.nextLegDuration - : nextDropoff.arrival; - const relevantEvents = events.slice(pickupIdx, dropoffIdx); - const tours = new Set(); - let oldTourDurationSum = 0; - relevantEvents.forEach((e) => { - if (!tours.has(e.tourId)) { - oldTourDurationSum += e.arrival - e.departure; - tours.add(e.tourId); + for (const pickup of pickupCases) { + for (const dropoff of dropoffCases) { + const communicatedPickupTime = Math.max( + pickup.window.endTime - SCHEDULED_TIME_BUFFER_PICKUP, + pickup.window.startTime + ); + const communicatedDropoffTime = Math.min( + Math.max( + dropoff.window.startTime, + communicatedPickupTime + pickup.nextLegDuration + dropoff.prevLegDuration + ) + getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), + dropoff.window.endTime + ); + + // Verify, that the shift induced to other events by pickup and dropoff are mutually compatible + const availableDistance = + communicatedDropoffTime - + communicatedPickupTime - + dropoff.prevLegDuration - + pickup.nextLegDuration; + if (availableDistance < 0) { + continue; + } + + // Determine the scheduled times for pickup and dropoff + const leewayBetweenPickupDropoff = + communicatedDropoffTime - + communicatedPickupTime - + pickup.nextLegDuration - + dropoff.prevLegDuration; + const pickupScheduledShift = Math.min( + pickup.window.size(), + SCHEDULED_TIME_BUFFER_PICKUP, + leewayBetweenPickupDropoff + ); + const scheduledPickupTime = communicatedPickupTime + pickupScheduledShift; + const scheduledDropoffTime = + communicatedDropoffTime - + Math.min( + dropoff.window.size(), + getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), + leewayBetweenPickupDropoff - pickupScheduledShift + ); + + // Compute the delta of the taxi's time spend driving for the tour containing the new request + const approachPlusReturnDurationDelta = + pickup.approachPlusReturnDurationDelta + dropoff.approachPlusReturnDurationDelta; + const fullyPayedDurationDelta = + pickup.fullyPayedDurationDelta + dropoff.fullyPayedDurationDelta; + + // Compute the delta of the taxi's waiting time + const newDeparture = + prevPickup.tourId !== twoBeforePickup?.tourId + ? Math.min( + communicatedPickupTime - pickup.prevLegDuration, + getScheduledEventTime(prevPickup) + ) - prevPickup.prevLegDuration + : prevPickup.departure; + const newArrival = + nextDropoff.tourId !== twoAfterDropoff?.tourId + ? Math.max( + communicatedDropoffTime + dropoff.nextLegDuration, + getScheduledEventTime(nextDropoff) + ) + nextDropoff.nextLegDuration + : nextDropoff.arrival; + const relevantEvents = events.slice(pickupIdx, dropoffIdx); + const tours = new Set(); + let oldTourDurationSum = 0; + relevantEvents.forEach((e) => { + if (!tours.has(e.tourId)) { + oldTourDurationSum += e.arrival - e.departure; + tours.add(e.tourId); + } + }); + const tourDurationDelta = newArrival - newDeparture - oldTourDurationSum; + const waitingTime = + tourDurationDelta - approachPlusReturnDurationDelta - fullyPayedDurationDelta; + + const payedDurationDelta = scheduledDropoffTime - scheduledPickupTime; + + const profit = getProfit( + payedDurationDelta, + tourDurationDelta - waitingTime, + waitingTime + ); + if (profit < MINIMUM_PROFIT) { + console.log( + 'insertion considered not feasible for driver,', + 'pickup: ', + printInsertionType(pickup.case), + 'dropoff: ', + printInsertionType(dropoff.case), + { prevPickupId: prevPickup.eventId }, + { nextPickupId: nextPickup.eventId }, + { prevDropoffId: prevDropoff.eventId }, + { nextDropoffId: nextDropoff.eventId }, + { waitingTime }, + { payedDurationDelta }, + { profit } + ); + return undefined; + } + console.log( + whitelist ? 'WHITELIST' : 'BOOKING API', + 'feasible insertion found,', + 'pickup: ', + printInsertionType(pickup.case), + 'dropoff: ', + printInsertionType(dropoff.case), + { prevPickupId: prevPickup.eventId }, + { nextPickupId: nextPickup.eventId }, + { prevDropoffId: prevDropoff.eventId }, + { nextDropoffId: nextDropoff.eventId }, + { waitingTime }, + { profit } + ); + if (bestEvaluations[busStopIdx][timeIdx] == undefined) { + const tour = events[pickupIdx].tourId; + bestEvaluations[busStopIdx][timeIdx].push({ + pickupTime: communicatedPickupTime, + dropoffTime: communicatedDropoffTime, + scheduledPickupTimeEnd: scheduledPickupTime, + scheduledPickupTimeStart: communicatedPickupTime, + scheduledDropoffTimeStart: scheduledDropoffTime, + scheduledDropoffTimeEnd: communicatedDropoffTime, + pickupCase: structuredClone(pickup.case), + dropoffCase: structuredClone(dropoff.case), + pickupIdx, + dropoffIdx, + waitingTime, + tour, + pickupPrevLegDuration: pickup.prevLegDuration, + pickupNextLegDuration: pickup.nextLegDuration, + dropoffPrevLegDuration: dropoff.prevLegDuration, + dropoffNextLegDuration: dropoff.nextLegDuration, + prevPickupId: pickup.prevId, + nextPickupId: pickup.nextId, + prevDropoffId: dropoff.prevId, + nextDropoffId: dropoff.nextId, + pickupIdxInEvents: pickup.idxInEvents, + dropoffIdxInEvents: dropoff.idxInEvents, + profit + }); + } } - }); - const tourDurationDelta = newArrival - newDeparture - oldTourDurationSum; - const waitingTime = - tourDurationDelta - approachPlusReturnDurationDelta - fullyPayedDurationDelta; - - const payedDurationDelta = scheduledDropoffTime - scheduledPickupTime; - - const profit = getProfit( - payedDurationDelta, - tourDurationDelta - waitingTime, - waitingTime - ); - if (profit < MINIMUM_PROFIT) { - console.log( - 'insertion considered not feasible for driver,', - 'pickup: ', - printInsertionType(pickup.case), - 'dropoff: ', - printInsertionType(dropoff.case), - { prevPickupId: prevPickup.eventId }, - { nextPickupId: nextPickup.eventId }, - { prevDropoffId: prevDropoff.eventId }, - { nextDropoffId: nextDropoff.eventId }, - { waitingTime }, - { payedDurationDelta }, - { profit } - ); - return undefined; - } - console.log( - whitelist ? 'WHITELIST' : 'BOOKING API', - 'feasible insertion found,', - 'pickup: ', - printInsertionType(pickup.case), - 'dropoff: ', - printInsertionType(dropoff.case), - { prevPickupId: prevPickup.eventId }, - { nextPickupId: nextPickup.eventId }, - { prevDropoffId: prevDropoff.eventId }, - { nextDropoffId: nextDropoff.eventId }, - { waitingTime }, - { profit } - ); - if (bestEvaluations[busStopIdx][timeIdx] == undefined) { - const tour = events[pickupIdx].tourId; - bestEvaluations[busStopIdx][timeIdx].push({ - pickupTime: communicatedPickupTime, - dropoffTime: communicatedDropoffTime, - scheduledPickupTimeEnd: scheduledPickupTime, - scheduledPickupTimeStart: communicatedPickupTime, - scheduledDropoffTimeStart: scheduledDropoffTime, - scheduledDropoffTimeEnd: communicatedDropoffTime, - pickupCase: structuredClone(pickup.case), - dropoffCase: structuredClone(dropoff.case), - pickupIdx, - dropoffIdx, - waitingTime, - tour, - pickupPrevLegDuration: pickup.prevLegDuration, - pickupNextLegDuration: pickup.nextLegDuration, - dropoffPrevLegDuration: dropoff.prevLegDuration, - dropoffNextLegDuration: dropoff.nextLegDuration, - prevPickupId: pickup.prevId, - nextPickupId: pickup.nextId, - prevDropoffId: dropoff.prevId, - nextDropoffId: dropoff.nextId, - pickupIdxInEvents: pickup.idxInEvents, - dropoffIdxInEvents: dropoff.idxInEvents, - profit - }); } } } diff --git a/src/lib/server/booking/taxi/durations.ts b/src/lib/server/booking/taxi/durations.ts index 5822f071..bd32c6b7 100644 --- a/src/lib/server/booking/taxi/durations.ts +++ b/src/lib/server/booking/taxi/durations.ts @@ -88,7 +88,8 @@ export function getAllowedOperationTimes( next: Event | undefined, expandedSearchInterval: Interval, prepTime: UnixtimeMs, - vehicle: VehicleWithInterval + vehicle: VehicleWithInterval, + allowedTimes: Interval[] ): Interval[] { console.assert( implication(!returnsToCompany(insertionCase), next !== undefined), @@ -149,9 +150,12 @@ export function getAllowedOperationTimes( !(insertionCase.how != InsertHow.NEW_TOUR && relevantAvailabilities.length > 1), `Found ${relevantAvailabilities.length} intervals, which are supposed to be disjoint, containing the same timestamp.` ); - const finalWindow = relevantAvailabilities - .map((availability) => new Interval(availability).intersect(window)) - .filter((availability) => availability != undefined); + const finalWindow = Interval.intersect( + relevantAvailabilities + .map((availability) => new Interval(availability).intersect(window)) + .filter((availability) => availability != undefined), + allowedTimes + ).filter((availability) => availability != undefined); return finalWindow; } @@ -161,15 +165,11 @@ export function getArrivalWindow( directDuration: number, busStopWindow: Interval | undefined, prevLegDuration: number, - nextLegDuration: number, - allowedTimes: Interval[] + nextLegDuration: number ): Interval | undefined { - const directWindows = Interval.intersect( - allowedTimes, - windows - .map((window) => window.shrink(prevLegDuration, nextLegDuration)) - .filter((window) => window != undefined) - ); + const directWindows = windows + .map((window) => window.shrink(prevLegDuration, nextLegDuration)) + .filter((window) => window != undefined); let arrivalWindows = directWindows .map((window) => diff --git a/src/lib/server/booking/taxi/insertion.ts b/src/lib/server/booking/taxi/insertion.ts index 274a9974..5115be64 100644 --- a/src/lib/server/booking/taxi/insertion.ts +++ b/src/lib/server/booking/taxi/insertion.ts @@ -91,8 +91,8 @@ type SingleInsertionEvaluation = { }; type Evaluations = { - busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][]; - userChosenEvaluations: (SingleInsertionEvaluation | undefined)[]; + busStopEvaluations: SingleInsertionEvaluation[][][][]; + userChosenEvaluations: SingleInsertionEvaluation[][]; bothEvaluations: (Insertion | undefined)[][]; }; @@ -167,8 +167,7 @@ export function evaluateSingleInsertion( 0, busStopWindow, prevLegDuration, - nextLegDuration, - allowedTimes + nextLegDuration ); if (arrivalWindow == undefined) { return undefined; @@ -289,7 +288,6 @@ export function evaluateBothInsertion( busStopIdx: number | undefined, prev: Event | undefined, next: Event | undefined, - allowedTimes: Interval[], passengerCountNewRequest: number, promisedTimes?: PromisedTimes ): InsertionEvaluation | undefined { @@ -324,8 +322,7 @@ export function evaluateBothInsertion( passengerDuration, busStopWindow, prevLegDuration, - nextLegDuration, - allowedTimes + nextLegDuration ); if (arrivalWindow == undefined) { console.log( @@ -337,7 +334,6 @@ export function evaluateBothInsertion( { busStopWindow: busStopWindow?.toString() }, { prevLegDuration: prevLegDuration.toString() }, { nextLegDuration: nextLegDuration.toString() }, - { allowedTimes: allowedTimes.toString() }, { prev: prev?.id }, { next: next?.id } ); @@ -566,7 +562,8 @@ export function evaluateNewTours( undefined, expandedSearchInterval, prepTime, - vehicle + vehicle, + allowedTimes ); for (let busStopIdx = 0; busStopIdx != busStopTimes.length; ++busStopIdx) { for (let busTimeIdx = 0; busTimeIdx != busStopTimes[busStopIdx].length; ++busTimeIdx) { @@ -580,7 +577,6 @@ export function evaluateNewTours( busStopIdx, undefined, undefined, - allowedTimes, required.passengers, promisedTimes ); @@ -623,17 +619,34 @@ export function evaluateSingleInsertions( allowedTimes: Interval[], promisedTimes?: PromisedTimes ): Evaluations { + const insertionIdxCount = companies.reduce( + (acc, curr) => + (acc += curr.vehicles.reduce( + (acc, curr) => + (acc += + insertionRanges + .get(curr.id) + ?.reduce((acc, curr) => (acc += curr.latestDropoff + 1 - curr.earliestPickup), 0) ?? + 0), + 0 + )), + 0 + ); const bothEvaluations: (Insertion | undefined)[][] = []; - const userChosenEvaluations: (SingleInsertionEvaluation | undefined)[] = []; - const busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][] = new Array< - (SingleInsertionEvaluation | undefined)[][] + const userChosenEvaluations: SingleInsertionEvaluation[][] = []; + for (let i = 0; i != insertionIdxCount + 1; i++) { + userChosenEvaluations[i] = new Array(); + } + const busStopEvaluations: SingleInsertionEvaluation[][][][] = new Array< + SingleInsertionEvaluation[][][] >(busStopTimes.length); for (let i = 0; i != busStopTimes.length; ++i) { - busStopEvaluations[i] = new Array<(SingleInsertionEvaluation | undefined)[]>( - busStopTimes[i].length - ); + busStopEvaluations[i] = new Array(busStopTimes[i].length); for (let j = 0; j != busStopTimes[i].length; ++j) { - busStopEvaluations[i][j] = new Array(); + busStopEvaluations[i][j] = new Array(); + for (let k = 0; k != insertionIdxCount + 1; k++) { + busStopEvaluations[i][j][k] = new Array(); + } } bothEvaluations[i] = new Array(busStopTimes[i].length); } @@ -673,7 +686,8 @@ export function evaluateSingleInsertions( next, expandedSearchInterval, prepTime, - insertionInfo.vehicle + insertionInfo.vehicle, + allowedTimes ); // Ensure shifting the previous or next events' scheduledTime does not cause the whole tour to be prolonged too much @@ -707,7 +721,6 @@ export function evaluateSingleInsertions( busStopIdx, prev, next, - allowedTimes, required.passengers, promisedTimes ); @@ -749,15 +762,8 @@ export function evaluateSingleInsertions( allowedTimes, promisedTimes ); - if ( - resultBus != undefined && - (busStopEvaluations[busStopIdx][busTimeIdx] == undefined || - busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx] == undefined || - resultBus.cost < - busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx]!.cost) && - !waitsTooLong(resultBus.taxiWaitingTime) - ) { - busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx] = resultBus; + if (resultBus != undefined) { + busStopEvaluations[busStopIdx][busTimeIdx][insertionInfo.insertionIdx].push(resultBus); } } } @@ -777,13 +783,8 @@ export function evaluateSingleInsertions( allowedTimes, promisedTimes ); - if ( - resultUserChosen != undefined && - (userChosenEvaluations[insertionInfo.insertionIdx] == undefined || - resultUserChosen.cost < userChosenEvaluations[insertionInfo.insertionIdx]!.cost) && - !waitsTooLong(resultUserChosen.taxiWaitingTime) - ) { - userChosenEvaluations[insertionInfo.insertionIdx] = resultUserChosen; + if (resultUserChosen != undefined) { + userChosenEvaluations[insertionInfo.insertionIdx].push(resultUserChosen); } }); }); @@ -795,8 +796,8 @@ export function evaluatePairInsertions( startFixed: boolean, insertionRanges: Map, busStopTimes: Interval[][], - busStopEvaluations: (SingleInsertionEvaluation | undefined)[][][], - userChosenEvaluations: (SingleInsertionEvaluation | undefined)[], + busStopEvaluations: SingleInsertionEvaluation[][][][], + userChosenEvaluations: SingleInsertionEvaluation[][], required: Capacities, whitelist?: boolean ): (Insertion | undefined)[][] { @@ -824,15 +825,11 @@ export function evaluatePairInsertions( return; } let cumulatedTaxiDrivingDelta = 0; - let pickupInvalid = false; for ( let dropoffIdx = pickupIdx + 1; dropoffIdx != insertionInfo.currentRange.latestDropoff + 1; ++dropoffIdx ) { - if (pickupInvalid) { - break; - } const prevDropoffIdx = dropoffIdx - 1; if ( dropoffIdx > 1 && @@ -850,238 +847,238 @@ export function evaluatePairInsertions( events[dropoffIdx - 2].nextLegDuration; } for (let busStopIdx = 0; busStopIdx != busStopTimes.length; ++busStopIdx) { - if (pickupInvalid) { - break; - } for (let timeIdx = 0; timeIdx != busStopTimes[busStopIdx].length; ++timeIdx) { - const pickup = startFixed + const pickupCases = startFixed ? busStopEvaluations[busStopIdx][timeIdx][insertionInfo.insertionIdx] : userChosenEvaluations[insertionInfo.insertionIdx]; - if (pickup == undefined) { - pickupInvalid = true; - break; + if (pickupCases.length === 0) { + continue; } - const dropoff = startFixed + const dropoffCases = startFixed ? userChosenEvaluations[insertionInfo.insertionIdx + dropoffIdx - pickupIdx] : busStopEvaluations[busStopIdx][timeIdx][ insertionInfo.insertionIdx + dropoffIdx - pickupIdx ]; - if (dropoff == undefined) { + if (dropoffCases.length === 0) { continue; } const prevDropoff = events[dropoffIdx - 1]; const nextDropoff = events[dropoffIdx]; const twoAfterDropoff = events[dropoffIdx + 1]; - const communicatedPickupTime = Math.max( - pickup.window.endTime - SCHEDULED_TIME_BUFFER_PICKUP, - pickup.window.startTime - ); - const communicatedDropoffTime = Math.min( - dropoff.window.startTime + - getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), - dropoff.window.endTime - ); + for (const pickup of pickupCases) { + for (const dropoff of dropoffCases) { + const communicatedPickupTime = Math.max( + pickup.window.endTime - SCHEDULED_TIME_BUFFER_PICKUP, + pickup.window.startTime + ); + const communicatedDropoffTime = Math.min( + Math.max( + dropoff.window.startTime, + communicatedPickupTime + pickup.nextLegDuration + dropoff.prevLegDuration + ) + getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), + dropoff.window.endTime + ); - // Verify, that the shift induced to other events by pickup and dropoff are mutually compatible - if (dropoffIdx < pickupIdx + 3) { - let availableDistance = - communicatedDropoffTime - - communicatedPickupTime - - dropoff.prevLegDuration - - pickup.nextLegDuration; - if (pickupIdx + 2 === dropoffIdx) { - availableDistance -= - nextPickup.tourId !== prevDropoff.tourId - ? (prevDropoff.directDuration ?? Number.MAX_SAFE_INTEGER / 2) - : prevDropoff.prevLegDuration; - } - if (availableDistance - 2 < 0) { - continue; - } - } + // Verify, that the shift induced to other events by pickup and dropoff are mutually compatible + const availableDistance = + communicatedDropoffTime - + communicatedPickupTime - + dropoff.prevLegDuration - + pickup.nextLegDuration; + if (availableDistance < 0) { + continue; + } - // Determine the scheduled times for pickup and dropoff - const leewayBetweenPickupDropoff = - communicatedDropoffTime - - communicatedPickupTime - - pickup.nextLegDuration - - dropoff.prevLegDuration; - const pickupScheduledShift = Math.min( - pickup.window.size(), - SCHEDULED_TIME_BUFFER_PICKUP, - leewayBetweenPickupDropoff - ); - const scheduledPickupTime = - communicatedPickupTime + - (pickup.case.how === InsertHow.APPEND ? 0 : pickupScheduledShift); - const scheduledDropoffTime = - communicatedDropoffTime - - (dropoff.case.how === InsertHow.PREPEND - ? 0 - : Math.min( - dropoff.window.size(), - getScheduledTimeBufferDropoff(dropoff.window.startTime - pickup.window.endTime), - leewayBetweenPickupDropoff - pickupScheduledShift - )); + // Determine the scheduled times for pickup and dropoff + const leewayBetweenPickupDropoff = + communicatedDropoffTime - + communicatedPickupTime - + pickup.nextLegDuration - + dropoff.prevLegDuration; + const pickupScheduledShift = Math.min( + pickup.window.size(), + SCHEDULED_TIME_BUFFER_PICKUP, + leewayBetweenPickupDropoff + ); + const scheduledPickupTime = + communicatedPickupTime + + (pickup.case.how === InsertHow.APPEND ? 0 : pickupScheduledShift); + const scheduledDropoffTime = + communicatedDropoffTime - + (dropoff.case.how === InsertHow.PREPEND + ? 0 + : Math.min( + dropoff.window.size(), + getScheduledTimeBufferDropoff( + dropoff.window.startTime - pickup.window.endTime + ), + leewayBetweenPickupDropoff - pickupScheduledShift + )); - // Compute the delta of the taxi's time spend driving for the tour containing the new request - const approachPlusReturnDurationDelta = - pickup.approachPlusReturnDurationDelta + dropoff.approachPlusReturnDurationDelta; - const fullyPayedDurationDelta = - pickup.fullyPayedDurationDelta + - dropoff.fullyPayedDurationDelta + - cumulatedTaxiDrivingDelta; + // Compute the delta of the taxi's time spend driving for the tour containing the new request + const approachPlusReturnDurationDelta = + pickup.approachPlusReturnDurationDelta + dropoff.approachPlusReturnDurationDelta; + const fullyPayedDurationDelta = + pickup.fullyPayedDurationDelta + + dropoff.fullyPayedDurationDelta + + cumulatedTaxiDrivingDelta; - // Compute the delta of the taxi's waiting time - const newDeparture = comesFromCompany(pickup.case) - ? scheduledPickupTime - pickup.prevLegDuration - : prevPickup.tourId !== twoBeforePickup?.tourId - ? Math.min( - communicatedPickupTime - pickup.prevLegDuration, - getScheduledEventTime(prevPickup) - ) - prevPickup.prevLegDuration - : prevPickup.departure; - const newArrival = returnsToCompany(dropoff.case) - ? scheduledDropoffTime + dropoff.nextLegDuration - : nextDropoff.tourId !== twoAfterDropoff?.tourId - ? Math.max( - communicatedDropoffTime + dropoff.nextLegDuration, - getScheduledEventTime(nextDropoff) - ) + nextDropoff.nextLegDuration - : nextDropoff.arrival; - const relevantEvents = events.slice( - pickup.case.how === InsertHow.CONNECT ? pickupIdx - 1 : pickupIdx, - dropoff.case.how === InsertHow.CONNECT ? dropoffIdx + 1 : dropoffIdx - ); - const tours = new Set(); - let oldTourDurationSum = 0; - relevantEvents.forEach((e) => { - if (!tours.has(e.tourId)) { - oldTourDurationSum += e.arrival - e.departure; - tours.add(e.tourId); - } - }); - const tourDurationDelta = newArrival - newDeparture - oldTourDurationSum; - const taxiWaitingTime = - tourDurationDelta - approachPlusReturnDurationDelta - fullyPayedDurationDelta; - if (waitsTooLong(taxiWaitingTime)) { - continue; - } + // Compute the delta of the taxi's waiting time + const newDeparture = comesFromCompany(pickup.case) + ? scheduledPickupTime - pickup.prevLegDuration + : prevPickup.tourId !== twoBeforePickup?.tourId + ? Math.min( + communicatedPickupTime - pickup.prevLegDuration, + getScheduledEventTime(prevPickup) + ) - prevPickup.prevLegDuration + : prevPickup.departure; + const newArrival = returnsToCompany(dropoff.case) + ? scheduledDropoffTime + dropoff.nextLegDuration + : nextDropoff.tourId !== twoAfterDropoff?.tourId + ? Math.max( + communicatedDropoffTime + dropoff.nextLegDuration, + getScheduledEventTime(nextDropoff) + ) + nextDropoff.nextLegDuration + : nextDropoff.arrival; + const relevantEvents = events.slice( + pickup.case.how === InsertHow.CONNECT ? pickupIdx - 1 : pickupIdx, + dropoff.case.how === InsertHow.CONNECT ? dropoffIdx + 1 : dropoffIdx + ); + const tours = new Set(); + let oldTourDurationSum = 0; + relevantEvents.forEach((e) => { + if (!tours.has(e.tourId)) { + oldTourDurationSum += e.arrival - e.departure; + tours.add(e.tourId); + } + }); + const tourDurationDelta = newArrival - newDeparture - oldTourDurationSum; + const taxiWaitingTime = + tourDurationDelta - approachPlusReturnDurationDelta - fullyPayedDurationDelta; + if (waitsTooLong(taxiWaitingTime)) { + continue; + } - // Compute the delta of the duration spend by passengers in the taxi - let prevShiftPickup = 0; - if (!comesFromCompany(pickup.case) && prevPickup!.isPickup) { - prevShiftPickup = Math.max( - 0, - getScheduledEventTime(prevPickup!) - communicatedPickupTime + pickup.prevLegDuration - ); - } - let nextShiftPickup = 0; - if (!returnsToCompany(pickup.case) && !nextPickup!.isPickup) { - nextShiftPickup = Math.max( - 0, - scheduledPickupTime + pickup.nextLegDuration - getScheduledEventTime(nextPickup!) - ); - } - let prevShiftDropoff = 0; - if (!comesFromCompany(dropoff.case) && prevDropoff!.isPickup) { - prevShiftDropoff = Math.max( - 0, - getScheduledEventTime(prevDropoff!) - scheduledDropoffTime + dropoff.prevLegDuration - ); - } - let nextShiftDropoff = 0; - if (!returnsToCompany(dropoff.case) && !nextDropoff!.isPickup) { - nextShiftDropoff = Math.max( - 0, - communicatedDropoffTime + - dropoff.nextLegDuration - - getScheduledEventTime(nextDropoff!) - ); - } + // Compute the delta of the duration spend by passengers in the taxi + let prevShiftPickup = 0; + if (!comesFromCompany(pickup.case) && prevPickup!.isPickup) { + prevShiftPickup = Math.max( + 0, + getScheduledEventTime(prevPickup!) - + communicatedPickupTime + + pickup.prevLegDuration + ); + } + let nextShiftPickup = 0; + if (!returnsToCompany(pickup.case) && !nextPickup!.isPickup) { + nextShiftPickup = Math.max( + 0, + scheduledPickupTime + pickup.nextLegDuration - getScheduledEventTime(nextPickup!) + ); + } + let prevShiftDropoff = 0; + if (!comesFromCompany(dropoff.case) && prevDropoff!.isPickup) { + prevShiftDropoff = Math.max( + 0, + getScheduledEventTime(prevDropoff!) - + scheduledDropoffTime + + dropoff.prevLegDuration + ); + } + let nextShiftDropoff = 0; + if (!returnsToCompany(dropoff.case) && !nextDropoff!.isPickup) { + nextShiftDropoff = Math.max( + 0, + communicatedDropoffTime + + dropoff.nextLegDuration - + getScheduledEventTime(nextDropoff!) + ); + } - let weightedPassengerDuration = - required.passengers * (scheduledDropoffTime - scheduledPickupTime); - weightedPassengerDuration += getWeightedPassengerDurationDelta( - pickup.case, - prevPickup, - nextPickup, - prevShiftPickup, - nextShiftPickup - ); - weightedPassengerDuration += getWeightedPassengerDurationDelta( - dropoff.case, - prevDropoff, - nextDropoff, - prevShiftDropoff, - nextShiftDropoff - ); + let weightedPassengerDuration = + required.passengers * (scheduledDropoffTime - scheduledPickupTime); + weightedPassengerDuration += getWeightedPassengerDurationDelta( + pickup.case, + prevPickup, + nextPickup, + prevShiftPickup, + nextShiftPickup + ); + weightedPassengerDuration += getWeightedPassengerDurationDelta( + dropoff.case, + prevDropoff, + nextDropoff, + prevShiftDropoff, + nextShiftDropoff + ); - // Compute the cost used to compare to other insertion options - const cost = computeCost( - weightedPassengerDuration, - approachPlusReturnDurationDelta, - fullyPayedDurationDelta, - taxiWaitingTime - ); + // Compute the cost used to compare to other insertion options + const cost = computeCost( + weightedPassengerDuration, + approachPlusReturnDurationDelta, + fullyPayedDurationDelta, + taxiWaitingTime + ); - console.log( - whitelist ? 'WHITELIST' : 'BOOKING API', - 'valid insertion found,', - 'pickup: ', - printInsertionType(pickup.case), - 'dropoff: ', - printInsertionType(dropoff.case), - { prevPickupId: prevPickup?.id }, - { nextPickupId: nextPickup?.id }, - { prevDropoffId: prevDropoff?.id }, - { nextDropoffId: nextDropoff?.id }, - { cost }, - { weightedPassengerDuration }, - { taxiWaitingTime } - ); - if ( - bestEvaluations[busStopIdx][timeIdx] == undefined || - cost < bestEvaluations[busStopIdx][timeIdx]!.cost - ) { - const tour = events[pickupIdx].tourId; - bestEvaluations[busStopIdx][timeIdx] = { - pickupTime: communicatedPickupTime, - dropoffTime: communicatedDropoffTime, - scheduledPickupTimeEnd: scheduledPickupTime, - scheduledPickupTimeStart: communicatedPickupTime, - scheduledDropoffTimeStart: scheduledDropoffTime, - scheduledDropoffTimeEnd: communicatedDropoffTime, - pickupCase: structuredClone(pickup.case), - dropoffCase: structuredClone(dropoff.case), - pickupIdx, - dropoffIdx, - taxiWaitingTime, - approachPlusReturnDurationDelta, - fullyPayedDurationDelta, - passengerDuration: weightedPassengerDuration, - cost, - company: insertionInfo.companyIdx, - vehicle: insertionInfo.vehicle.id, - tour, - departure: comesFromCompany(pickup.case) - ? new Date(scheduledPickupTime - pickup.prevLegDuration).getTime() - : undefined, - arrival: returnsToCompany(dropoff.case) - ? new Date(scheduledDropoffTime + dropoff.nextLegDuration).getTime() - : undefined, - pickupPrevLegDuration: pickup.prevLegDuration, - pickupNextLegDuration: pickup.nextLegDuration, - dropoffPrevLegDuration: dropoff.prevLegDuration, - dropoffNextLegDuration: dropoff.nextLegDuration, - prevPickupId: pickup.prevId, - nextPickupId: pickup.nextId, - prevDropoffId: dropoff.prevId, - nextDropoffId: dropoff.nextId, - pickupIdxInEvents: pickup.idxInEvents, - dropoffIdxInEvents: dropoff.idxInEvents - }; + console.log( + whitelist ? 'WHITELIST' : 'BOOKING API', + 'valid insertion found,', + 'pickup: ', + printInsertionType(pickup.case), + 'dropoff: ', + printInsertionType(dropoff.case), + { prevPickupId: prevPickup?.id }, + { nextPickupId: nextPickup?.id }, + { prevDropoffId: prevDropoff?.id }, + { nextDropoffId: nextDropoff?.id }, + { cost }, + { weightedPassengerDuration }, + { taxiWaitingTime } + ); + if ( + bestEvaluations[busStopIdx][timeIdx] == undefined || + cost < bestEvaluations[busStopIdx][timeIdx]!.cost + ) { + const tour = events[pickupIdx].tourId; + bestEvaluations[busStopIdx][timeIdx] = { + pickupTime: communicatedPickupTime, + dropoffTime: communicatedDropoffTime, + scheduledPickupTimeEnd: scheduledPickupTime, + scheduledPickupTimeStart: communicatedPickupTime, + scheduledDropoffTimeStart: scheduledDropoffTime, + scheduledDropoffTimeEnd: communicatedDropoffTime, + pickupCase: structuredClone(pickup.case), + dropoffCase: structuredClone(dropoff.case), + pickupIdx, + dropoffIdx, + taxiWaitingTime, + approachPlusReturnDurationDelta, + fullyPayedDurationDelta, + passengerDuration: weightedPassengerDuration, + cost, + company: insertionInfo.companyIdx, + vehicle: insertionInfo.vehicle.id, + tour, + departure: comesFromCompany(pickup.case) + ? new Date(scheduledPickupTime - pickup.prevLegDuration).getTime() + : undefined, + arrival: returnsToCompany(dropoff.case) + ? new Date(scheduledDropoffTime + dropoff.nextLegDuration).getTime() + : undefined, + pickupPrevLegDuration: pickup.prevLegDuration, + pickupNextLegDuration: pickup.nextLegDuration, + dropoffPrevLegDuration: dropoff.prevLegDuration, + dropoffNextLegDuration: dropoff.nextLegDuration, + prevPickupId: pickup.prevId, + nextPickupId: pickup.nextId, + prevDropoffId: dropoff.prevId, + nextDropoffId: dropoff.nextId, + pickupIdxInEvents: pickup.idxInEvents, + dropoffIdxInEvents: dropoff.idxInEvents + }; + } + } } } } diff --git a/src/lib/server/booking/taxi/tests/concatenations/pairInsertion.test.ts b/src/lib/server/booking/taxi/tests/concatenations/pairInsertion.test.ts new file mode 100644 index 00000000..995e6562 --- /dev/null +++ b/src/lib/server/booking/taxi/tests/concatenations/pairInsertion.test.ts @@ -0,0 +1,97 @@ +import { inXMinutes, prepareTest, white } from '$lib/server/booking/testUtils'; +import { addCompany, addTaxi, getTours, setAvailability, Zone } from '$lib/testHelpers'; +import { describe, it, expect } from 'vitest'; +import type { ExpectedConnection } from '$lib/server/booking/expectedConnection'; +import { bookingApi } from '$lib/server/booking/taxi/bookingApi'; +import { isSamePlace } from '$lib/server/booking/isSamePlace'; +import { Mode } from '$lib/server/booking/mode'; + +const inWW1 = { lng: 14.643847884365528, lat: 51.507181621441845 }; +const inKringelsdorf = { lng: 14.606555746634228, lat: 51.38851958039794 }; +const inBoxberg = { lng: 14.577917469763548, lat: 51.40877145079591 }; +const inNochten = { + lng: 14.600164657165266, + lat: 51.43191720040872 +}; +const inWW2 = { lng: 14.63490818370542, lat: 51.499039246023926 }; +const capacities = { + passengers: 1, + wheelchairs: 0, + bikes: 0, + luggage: 0 +}; + +describe('Concatenation tests', () => { + it('create tour concetanation, insert pickup - append dropoff', async () => { + const mockUserId = await prepareTest(); + const company = await addCompany(Zone.WEIßWASSER, inWW1); + const taxi = await addTaxi(company, { passengers: 3, bikes: 0, wheelchairs: 0, luggage: 0 }); + await setAvailability(taxi, inXMinutes(0), inXMinutes(600)); + const body = JSON.stringify({ + start: inWW2, + target: inBoxberg, + startBusStops: [], + targetBusStops: [], + directTimes: [inXMinutes(70)], + startFixed: false, + capacities + }); + const whiteResponse = await white(body).then((r) => r.json()); + const connection1: ExpectedConnection = { + start: { ...inWW2, address: 'weisswasser' }, + target: { ...inBoxberg, address: 'boxberg' }, + startTime: whiteResponse.direct[0].pickupTime, + targetTime: whiteResponse.direct[0].dropoffTime, + signature: '', + startFixed: false, + requestedTime: inXMinutes(70), + mode: Mode.TAXI + }; + const bookingBody = { + connection1, + connection2: null, + capacities + }; + + await bookingApi(bookingBody, mockUserId, false, true, 0, 0, 0, true); + const tours = await getTours(); + expect(tours.length).toBe(1); + expect(tours[0].requests.length).toBe(1); + const body2 = JSON.stringify({ + start: inNochten, + target: inKringelsdorf, + startBusStops: [], + targetBusStops: [], + directTimes: [inXMinutes(90)], + startFixed: false, + capacities + }); + const whiteResponse2 = await white(body2).then((r) => r.json()); + const claspConnection: ExpectedConnection = { + start: { ...inNochten, address: 'nochten' }, + target: { ...inKringelsdorf, address: 'kringelsdorf' }, + startTime: whiteResponse2.direct[0].pickupTime, + targetTime: whiteResponse2.direct[0].dropoffTime, + signature: '', + startFixed: false, + requestedTime: inXMinutes(90), + mode: Mode.TAXI + }; + const bookingBodyAppend = { + connection1: claspConnection, + connection2: null, + capacities + }; + await bookingApi(bookingBodyAppend, mockUserId, false, true, 0, 0, 0, true); + const tours2 = await getTours(); + expect(tours2.length).toBe(1); + expect(tours2[0].requests.length).toBe(2); + const events = tours2 + .flatMap((t) => t.requests.flatMap((r) => r.events)) + .sort((e1, e2) => e1.scheduledTimeStart - e2.scheduledTimeStart); + expect(isSamePlace(events[0], inWW2)).toBe(true); + expect(isSamePlace(events[1], inNochten)).toBe(true); + expect(isSamePlace(events[2], inBoxberg)).toBe(true); + expect(isSamePlace(events[3], inKringelsdorf)).toBe(true); + }, 15000); +}); diff --git a/src/lib/server/booking/taxi/tests/scenarios/scenario1.test.ts b/src/lib/server/booking/taxi/tests/scenarios/scenario1.test.ts new file mode 100644 index 00000000..edd3cce0 --- /dev/null +++ b/src/lib/server/booking/taxi/tests/scenarios/scenario1.test.ts @@ -0,0 +1,175 @@ +import { inXMinutes, prepareTest, white } from '$lib/server/booking/testUtils'; +import { addCompany, addTaxi, getTours, setAvailability, Zone } from '$lib/testHelpers'; +import { describe, it, expect } from 'vitest'; +import type { ExpectedConnection } from '$lib/server/booking/expectedConnection'; +import { bookingApi } from '$lib/server/booking/taxi/bookingApi'; +import { isSamePlace } from '$lib/server/booking/isSamePlace'; +import { Mode } from '$lib/server/booking/mode'; +import type { Coordinates } from '$lib/util/Coordinates'; +import type { Capacities } from '$lib/util/booking/Capacities'; + +async function whiteAndBook( + start: Coordinates, + target: Coordinates, + user: number, + time: number, + startFixed: boolean, + capacities: Capacities, + startAddress?: string, + targetAddress?: string +) { + const body = JSON.stringify({ + start, + target, + startBusStops: [], + targetBusStops: [], + directTimes: [time], + startFixed, + capacities + }); + const whiteResponse = await white(body).then((r) => r.json()); + const connection1: ExpectedConnection = { + start: { ...start, address: startAddress ?? 'a' }, + target: { ...target, address: targetAddress ?? 'b' }, + startTime: whiteResponse.direct[0].pickupTime, + targetTime: whiteResponse.direct[0].dropoffTime, + signature: '', + startFixed, + requestedTime: time, + mode: Mode.TAXI + }; + const bookingBody = { + connection1, + connection2: null, + capacities + }; + await bookingApi(bookingBody, user, false, true, 0, 0, 0, false); +} + +// 1 +const inWW = { lng: 14.63490818370542, lat: 51.499039246023926 }; +const inBoxberg = { lng: 14.577917469763548, lat: 51.40877145079591 }; + +// 2 +const wwBhf = { lng: 14.637890411515059, lat: 51.50577743128434 }; +const boxbergDiesterweg = { lng: 14.57974388256821, lat: 51.40416900732731 }; + +// 3 +const wwBautzener = { lng: 14.632576396815239, lat: 51.49688776336018 }; +const boxbergAlteBautzener = { lng: 14.577521488787994, lat: 51.40861954813761 }; + +// 4 +const inKringelsdorf = { lng: 14.606555746634228, lat: 51.38851958039794 }; +const inNochten = { + lng: 14.600164657165266, + lat: 51.43191720040872 +}; + +const companyCentral = { lng: 14.643847884365528, lat: 51.507181621441845 }; +const capacities = { + passengers: 1, + wheelchairs: 0, + bikes: 0, + luggage: 0 +}; + +describe('Concatenation tests', () => { + it('concatenate 1 and 4', async () => { + await testScenario(inWW, inBoxberg, inNochten, inKringelsdorf, [ + inWW, + inNochten, + inBoxberg, + inKringelsdorf + ]); + }, 15000); + + it('concatenate 1 and 3', async () => { + await testScenario(inWW, inBoxberg, wwBautzener, boxbergAlteBautzener, [ + inWW, + wwBautzener, + inBoxberg, + boxbergAlteBautzener + ]); + }, 15000); + + it('concatenate 1 and 2', async () => { + await testScenario(inWW, inBoxberg, wwBhf, boxbergDiesterweg, [ + wwBhf, + inWW, + inBoxberg, + boxbergDiesterweg + ]); + }, 15000); + + it('concatenate 2 and 4', async () => { + await testScenario(wwBhf, boxbergDiesterweg, inNochten, inKringelsdorf, [ + wwBhf, + inNochten, + boxbergDiesterweg, + inKringelsdorf + ]); + }, 15000); + + it('concatenate 2 and 3', async () => { + await testScenario( + wwBhf, + boxbergDiesterweg, + wwBautzener, + boxbergAlteBautzener, + [wwBhf, wwBautzener, boxbergAlteBautzener, boxbergDiesterweg], + 0 + ); + }, 15000); + + it('concatenate 3 and 4', async () => { + await testScenario(wwBautzener, boxbergAlteBautzener, inNochten, inKringelsdorf, [ + wwBautzener, + inNochten, + boxbergAlteBautzener, + inKringelsdorf + ]); + }, 15000); +}); + +async function checkEventOrder(expectedCoordinates: (Coordinates | undefined)[]) { + const tours = await getTours(); + expect(tours.length).toBe(1); + expect(tours[0].requests.length).toBe(2); + const events = tours + .flatMap((t) => t.requests.flatMap((r) => r.events)) + .sort((e1, e2) => e1.scheduledTimeStart - e2.scheduledTimeStart); + expect(expectedCoordinates).toHaveLength(events.length); + for (let i = 0; i != expectedCoordinates.length; ++i) { + if (expectedCoordinates[i] !== undefined) { + expect(isSamePlace(events[i], expectedCoordinates[i]!)).toBe(true); + } + } +} + +async function testScenario( + start1: Coordinates, + target1: Coordinates, + start2: Coordinates, + target2: Coordinates, + expectedCoordinates: (Coordinates | undefined)[], + timeDifference?: number +) { + const mockUserId = await prepareTest(); + const company = await addCompany(Zone.WEIßWASSER, companyCentral); + const taxi = await addTaxi(company, { passengers: 3, bikes: 0, wheelchairs: 0, luggage: 0 }); + await setAvailability(taxi, inXMinutes(0), inXMinutes(600)); + + await whiteAndBook(start1, target1, mockUserId, inXMinutes(70), false, capacities); + const tours = await getTours(); + expect(tours.length).toBe(1); + expect(tours[0].requests.length).toBe(1); + await whiteAndBook( + start2, + target2, + mockUserId, + inXMinutes(70 + (timeDifference ?? 20)), + false, + capacities + ); + await checkEventOrder(expectedCoordinates); +} diff --git a/src/lib/server/util/healthCheck.ts b/src/lib/server/util/healthCheck.ts index f2960522..241501d6 100644 --- a/src/lib/server/util/healthCheck.ts +++ b/src/lib/server/util/healthCheck.ts @@ -4,13 +4,10 @@ import { groupBy } from '../../util/groupBy'; import { Interval } from '../../util/interval'; import { HOUR } from '../../util/time'; import { isSamePlace } from '../booking/isSamePlace'; -import { - SCHEDULED_TIME_BUFFER_DROPOFF, - SCHEDULED_TIME_BUFFER_PICKUP, - PASSENGER_CHANGE_DURATION -} from '$lib/constants'; +import { SCHEDULED_TIME_BUFFER_PICKUP, PASSENGER_CHANGE_DURATION } from '$lib/constants'; import { sortEventsByTime } from '$lib/testHelpers'; import { reverseGeo } from '$lib/server/util/reverseGeocode'; +import { getScheduledTimeBufferDropoff } from '$lib/util/getScheduledTimeBuffer'; function validateRequestHas2Events(tours: ToursWithRequests): boolean { let fail = false; @@ -265,12 +262,20 @@ function validateScheduledTimeStartBeforeEnd(tours: ToursWithRequests): boolean function validateScheduledIntervalSize(tours: ToursWithRequests): boolean { let fail = false; console.log('Validating scheduled time intervals are not growing...'); - for (const event of tours.flatMap((t) => t.requests.flatMap((r) => r.events))) { - if ( - event.scheduledTimeEnd - event.scheduledTimeStart > - (event.isPickup ? SCHEDULED_TIME_BUFFER_PICKUP : SCHEDULED_TIME_BUFFER_DROPOFF) // TODO - ) { - console.log('Found an event where the scheduled time interval grew, eventId: ', event.id); + for (const request of tours.flatMap((t) => t.requests)) { + if (request.events.length !== 2) { + console.log('Found a request with not exactly 2 events requestId: ', request.requestId); + } + const pickup = request.events.find((e) => e.isPickup)!; + const dropoff = request.events.find((e) => !e.isPickup)!; + const durationApprox = dropoff.scheduledTimeStart - pickup.scheduledTimeEnd; + const bufferUpperBound = getScheduledTimeBufferDropoff(durationApprox); + if (pickup.scheduledTimeEnd - pickup.scheduledTimeStart > SCHEDULED_TIME_BUFFER_PICKUP) { + console.log('Found an event where the scheduled time interval grew, eventId: ', pickup.id); + fail = true; + } + if (dropoff.scheduledTimeEnd - dropoff.scheduledTimeStart > bufferUpperBound) { + console.log('Found an event where the scheduled time interval grew, eventId: ', dropoff.id); fail = true; } } diff --git a/src/lib/server/util/rediscoverWhitelistRequestTimes.ts b/src/lib/server/util/rediscoverWhitelistRequestTimes.ts index cee57be7..f69c8417 100644 --- a/src/lib/server/util/rediscoverWhitelistRequestTimes.ts +++ b/src/lib/server/util/rediscoverWhitelistRequestTimes.ts @@ -1,5 +1,6 @@ import { DIRECT_FREQUENCY, MOTIS_SHIFT } from '$lib/constants'; import type { Leg } from '$lib/openapi'; +import { isOdmLeg } from '$lib/util/booking/checkLegType'; export function rediscoverWhitelistRequestTimes( startFixed: boolean, @@ -29,15 +30,24 @@ export function rediscoverWhitelistRequestTimes( } } else { const legAdjacentToOdm = - firstOdmIndex === 0 - ? legs.slice(firstOdmIndex + 1).find((l) => l.mode !== 'WALK') - : legs.slice(0, firstOdmIndex).findLast((l) => l.mode !== 'WALK'); - requestedTime1 = - firstOdmIndex === 0 - ? new Date(legAdjacentToOdm!.scheduledStartTime).getTime() - MOTIS_SHIFT - : new Date(legAdjacentToOdm!.scheduledEndTime).getTime() + MOTIS_SHIFT; + firstOdmIndex === 0 ? legs.find((l) => !isOdmLeg(l)) : legs.findLast((l) => !isOdmLeg(l)); + console.assert( + legAdjacentToOdm!.mode === 'WALK', + `leg adjacent to odm does not have mode WALK ${legAdjacentToOdm}` + ); + if (legAdjacentToOdm!.duration * 1000 > MOTIS_SHIFT) { + requestedTime1 = + firstOdmIndex === 0 + ? new Date(legAdjacentToOdm!.scheduledEndTime).getTime() + : new Date(legAdjacentToOdm!.scheduledStartTime).getTime(); + } else { + requestedTime1 = + firstOdmIndex === 0 + ? new Date(legAdjacentToOdm!.scheduledStartTime).getTime() + : new Date(legAdjacentToOdm!.scheduledEndTime).getTime(); + } if (firstOdmIndex !== lastOdmIndex) { - const legBeforeOdm = legs.slice(0, firstOdmIndex).findLast((l) => l.mode !== 'WALK'); + const legBeforeOdm = legs.findLast((l) => !isOdmLeg(l)); requestedTime2 = new Date(legBeforeOdm!.scheduledStartTime).getTime(); } } diff --git a/src/lib/util/booking/checkLegType.ts b/src/lib/util/booking/checkLegType.ts new file mode 100644 index 00000000..e9c5e696 --- /dev/null +++ b/src/lib/util/booking/checkLegType.ts @@ -0,0 +1,14 @@ +import type { Leg } from '$lib/openapi/types.gen'; +import type { LegLike } from '$lib/ui/modeStyle'; + +export function isRideShareLeg(l: Leg | LegLike) { + return l.mode == 'RIDE_SHARING'; +} + +export function isTaxiLeg(l: Leg | LegLike) { + return l.mode == 'ODM'; +} + +export function isOdmLeg(l: Leg | LegLike) { + return l.mode == 'ODM' || l.mode == 'RIDE_SHARING'; +} diff --git a/src/lib/util/getAllowedTimes.ts b/src/lib/util/getAllowedTimes.ts index 5850ca55..f6d4b5cb 100644 --- a/src/lib/util/getAllowedTimes.ts +++ b/src/lib/util/getAllowedTimes.ts @@ -16,13 +16,12 @@ export function getAllowedTimes( const earliestDay = roundToUnit(earliest, DAY, Math.floor); const latestDay = roundToUnit(latest, DAY, Math.floor) + DAY; - const noonEarliestDay = new Date(earliestDay + 12 * HOUR); - const allowedTimes: Array = []; for (let t = earliestDay; t < latestDay; t += DAY) { - const offset = getOffset(noonEarliestDay.getTime()); + const noon = new Date(t + 12 * HOUR); + const offset = getOffset(noon.getTime()); allowedTimes.push(new Interval(t + startOnDay - offset, t + endOnDay - offset)); - noonEarliestDay.setHours(noonEarliestDay.getHours() + 24); + noon.setHours(noon.getHours() + 24); } return allowedTimes; } diff --git a/src/lib/util/odmPrice.ts b/src/lib/util/odmPrice.ts index c9b6f79b..86a353c5 100644 --- a/src/lib/util/odmPrice.ts +++ b/src/lib/util/odmPrice.ts @@ -2,7 +2,7 @@ import { LOCALE } from '$lib/constants'; import { env } from '$env/dynamic/public'; import type { Itinerary, Leg } from '$lib/openapi'; -import { isTaxiLeg } from '../../routes/(customer)/routing/utils'; +import { isTaxiLeg } from './booking/checkLegType'; export const odmPrice = (it: Itinerary, passengers: number, kids: number) => it.legs.reduce( diff --git a/src/routes/(customer)/bookings/[slug]/+page.svelte b/src/routes/(customer)/bookings/[slug]/+page.svelte index c737809d..2f3ebc50 100644 --- a/src/routes/(customer)/bookings/[slug]/+page.svelte +++ b/src/routes/(customer)/bookings/[slug]/+page.svelte @@ -20,7 +20,7 @@ import { MapIcon } from 'lucide-svelte'; import PopupMap from '$lib/ui/PopupMap.svelte'; import { page } from '$app/state'; - import { isOdmLeg, isTaxiLeg } from '../../routing/utils'; + import { isOdmLeg, isTaxiLeg } from '$lib/util/booking/checkLegType'; const { data } = $props(); diff --git a/src/routes/(customer)/routing/+page.server.ts b/src/routes/(customer)/routing/+page.server.ts index 5ad1fa58..1d0c153e 100644 --- a/src/routes/(customer)/routing/+page.server.ts +++ b/src/routes/(customer)/routing/+page.server.ts @@ -16,7 +16,7 @@ import Prom from 'prom-client'; import { rediscoverWhitelistRequestTimes } from '$lib/server/util/rediscoverWhitelistRequestTimes'; import { rideShareApi } from '$lib/server/booking/index'; import { expectedConnectionFromLeg } from '$lib/server/booking/expectedConnection'; -import { isOdmLeg } from './utils'; +import { isOdmLeg } from '$lib/util/booking/checkLegType'; let booking_errors: Prom.Counter | undefined; let booking_attempts: Prom.Counter | undefined; diff --git a/src/routes/(customer)/routing/+page.svelte b/src/routes/(customer)/routing/+page.svelte index 345ad47b..f6fd87de 100644 --- a/src/routes/(customer)/routing/+page.svelte +++ b/src/routes/(customer)/routing/+page.svelte @@ -43,8 +43,8 @@ import { planAndSign, type SignedPlanResponse } from '$lib/planAndSign'; import logo from '$lib/assets/logo-alpha.png'; - import { isOdmLeg, isRideShareLeg } from './utils'; import Footer from '$lib/ui/Footer.svelte'; + import { isOdmLeg, isRideShareLeg } from '$lib/util/booking/checkLegType'; type LuggageType = 'none' | 'light' | 'heavy'; diff --git a/src/routes/(customer)/routing/ConnectionDetail.svelte b/src/routes/(customer)/routing/ConnectionDetail.svelte index e749b679..86c48bc4 100644 --- a/src/routes/(customer)/routing/ConnectionDetail.svelte +++ b/src/routes/(customer)/routing/ConnectionDetail.svelte @@ -11,7 +11,7 @@ import { getModeName } from './getModeName'; import Route from './Route.svelte'; import { routeBorderColor, routeColor } from '$lib/ui/modeStyle'; - import { isOdmLeg, isRideShareLeg } from './utils'; + import { isOdmLeg, isRideShareLeg } from '$lib/util/booking/checkLegType'; import type { SignedItinerary } from '$lib/planAndSign'; import ProfileBadge from '$lib/ui/ProfileBadge.svelte'; diff --git a/src/routes/(customer)/routing/ItineraryList.svelte b/src/routes/(customer)/routing/ItineraryList.svelte index 54cc6d36..0b0d5159 100644 --- a/src/routes/(customer)/routing/ItineraryList.svelte +++ b/src/routes/(customer)/routing/ItineraryList.svelte @@ -8,7 +8,7 @@ import DirectConnection from './DirectConnection.svelte'; import { odmPrice, getEuroString } from '$lib/util/odmPrice'; import { planAndSign, type SignedItinerary, type SignedPlanResponse } from '$lib/planAndSign'; - import { isOdmLeg, isRideShareLeg, isTaxiLeg } from './utils'; + import { isOdmLeg, isRideShareLeg, isTaxiLeg } from '$lib/util/booking/checkLegType'; let { routingResponses, diff --git a/src/routes/(customer)/routing/ItinerarySummary.svelte b/src/routes/(customer)/routing/ItinerarySummary.svelte index ff8c6327..4e6be7dd 100644 --- a/src/routes/(customer)/routing/ItinerarySummary.svelte +++ b/src/routes/(customer)/routing/ItinerarySummary.svelte @@ -8,7 +8,7 @@ import { t } from '$lib/i18n/translation'; import type { Snippet } from 'svelte'; import DisplayAddresses from '$lib/ui/DisplayAddresses.svelte'; - import { isRideShareLeg } from './utils'; + import { isRideShareLeg } from '$lib/util/booking/checkLegType'; const { it, diff --git a/src/routes/(customer)/routing/Route.svelte b/src/routes/(customer)/routing/Route.svelte index 2e1d2643..d3d4018c 100644 --- a/src/routes/(customer)/routing/Route.svelte +++ b/src/routes/(customer)/routing/Route.svelte @@ -1,7 +1,8 @@