-
Notifications
You must be signed in to change notification settings - Fork 6
Stats #576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
nilspenzel
wants to merge
7
commits into
motis-project:master
Choose a base branch
from
nilspenzel:stats
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Stats #576
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
720d574
Add script for computing aggregated distances of tours
nilspenzel 067b0aa
Add script for viewing statistics
nilspenzel eec4593
Linting
nilspenzel 15422c5
wip
nilspenzel 80eb37b
wip
nilspenzel 38a8c4c
wip
nilspenzel 99b727e
wip
nilspenzel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| try { | ||
| await fetch('http://prima:3000/apiInternal/createStatistics', { | ||
| method: 'POST' | ||
| }); | ||
| } catch (err) { | ||
| console.error('Error:', err); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| export async function up(db) { | ||
| await db.schema | ||
| .alterTable('ride_share_tour') | ||
| .addColumn('approach_and_return_m', 'bigint') | ||
| .addColumn('fully_payed_m', 'bigint') | ||
| .addColumn('occupied_m', 'bigint') | ||
| .execute(); | ||
|
|
||
| await db.schema | ||
| .alterTable('tour') | ||
| .addColumn('approach_and_return_m', 'bigint') | ||
| .addColumn('fully_payed_m', 'bigint') | ||
| .addColumn('occupied_m', 'bigint') | ||
| .execute(); | ||
| } | ||
|
|
||
| export async function down() { } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { createStatistics } from '../src/lib/createStatistics'; | ||
|
|
||
| async function main(): Promise<void> { | ||
| createStatistics(); | ||
| } | ||
|
|
||
| // Run the main function | ||
| main().catch((error) => { | ||
| console.error('Error in main function:', error); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { db } from '../src/lib/server/db/index.js'; | ||
|
|
||
| async function getTours() { | ||
| return await db | ||
| .selectFrom('tour') | ||
| .where('tour.approachAndReturnM', 'is not', null) | ||
| .selectAll() | ||
| .execute(); | ||
| } | ||
|
|
||
| async function getRsTours() { | ||
| return await db | ||
| .selectFrom('rideShareTour') | ||
| .where('rideShareTour.approachAndReturnM', 'is not', null) | ||
| .selectAll() | ||
| .execute(); | ||
| } | ||
|
|
||
| type Tours = Awaited<ReturnType<typeof getTours>>; | ||
| type RsTours = Awaited<ReturnType<typeof getRsTours>>; | ||
|
|
||
| async function viewStatistics() { | ||
| const tours = await getTours(); | ||
| const tourEntries = { | ||
| all: createEntries(tours), | ||
| cancelled: createEntries(tours.filter((t) => t.cancelled)), | ||
| uncancelledTours: createEntries(tours.filter((t) => !t.cancelled)) | ||
| }; | ||
|
|
||
| const rsTours = await getRsTours(); | ||
| const rsTourEntries = { | ||
| all: createRsEntries(rsTours), | ||
| cancelled: createRsEntries(rsTours.filter((t) => t.cancelled)), | ||
| uncancelledTours: createRsEntries(rsTours.filter((t) => !t.cancelled)) | ||
| }; | ||
| console.log('TAXI'); | ||
| console.log(JSON.stringify(tourEntries, null, 2)); | ||
| console.log(); | ||
| console.log('RIDE SHARE'); | ||
| console.log(JSON.stringify(rsTourEntries, null, 2)); | ||
| } | ||
|
|
||
| function createEntries(tours: Tours) { | ||
| return { | ||
| count: tours.length, | ||
| approachAndReturnM: tours.reduce((prev, curr) => (prev += curr.approachAndReturnM!), 0), | ||
| fullyPayedM: tours.reduce((prev, curr) => (prev += curr.fullyPayedM!), 0), | ||
| occupiedM: tours.reduce((prev, curr) => (prev += curr.occupiedM!), 0) | ||
| }; | ||
| } | ||
|
|
||
| function createRsEntries(tours: RsTours) { | ||
| return { | ||
| count: tours.length, | ||
| approachAndReturnM: tours.reduce((prev, curr) => (prev += curr.approachAndReturnM!), 0), | ||
| fullyPayedM: tours.reduce((prev, curr) => (prev += curr.fullyPayedM!), 0), | ||
| occupiedM: tours.reduce((prev, curr) => (prev += curr.occupiedM!), 0) | ||
| }; | ||
| } | ||
|
|
||
| viewStatistics().catch((error) => { | ||
| console.error('Error in main function:', error); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { haversineDistance } from './createStatistics'; | ||
|
|
||
| describe('haversineDistance', () => { | ||
| it('returns 0 for identical coordinates', () => { | ||
| const berlin = { lat: 52.52, lng: 13.405 }; | ||
|
|
||
| expect(haversineDistance(berlin, berlin)).toBe(0); | ||
| }); | ||
|
|
||
| it('is symmetric', () => { | ||
| const berlin = { lat: 52.52, lng: 13.405 }; | ||
| const paris = { lat: 48.8566, lng: 2.3522 }; | ||
|
|
||
| const aToB = haversineDistance(berlin, paris); | ||
| const bToA = haversineDistance(paris, berlin); | ||
|
|
||
| expect(aToB).toBeCloseTo(bToA, 8); | ||
| }); | ||
|
|
||
| it('calculates Berlin to Paris distance in meters', () => { | ||
| const berlin = { lat: 52.52, lng: 13.405 }; | ||
| const paris = { lat: 48.8566, lng: 2.3522 }; | ||
|
|
||
| const distance = haversineDistance(berlin, paris); | ||
|
|
||
| // Great-circle distance is roughly 877 km | ||
| expect(distance).toBeGreaterThan(875_000); | ||
| expect(distance).toBeLessThan(879_000); | ||
| }); | ||
|
|
||
| it('calculates short distances reasonably', () => { | ||
| const a = { lat: 52.52, lng: 13.405 }; | ||
| const b = { lat: 52.5201, lng: 13.405 }; | ||
|
|
||
| const distance = haversineDistance(a, b); | ||
|
|
||
| // 0.0001 degrees latitude is about 11.1 meters | ||
| expect(distance).toBeGreaterThan(11); | ||
| expect(distance).toBeLessThan(12); | ||
| }); | ||
|
|
||
| it('handles crossing the antimeridian', () => { | ||
| const a = { lat: 0, lng: 179.9 }; | ||
| const b = { lat: 0, lng: -179.9 }; | ||
|
|
||
| const distance = haversineDistance(a, b); | ||
|
|
||
| // 0.2 degrees at equator ≈ 22.2 km | ||
| expect(distance).toBeGreaterThan(22_000); | ||
| expect(distance).toBeLessThan(22_300); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import { jsonArrayFrom } from 'kysely/helpers/postgres'; | ||
| import { db } from '$lib/server/db'; | ||
| import type { Coordinates } from '$lib/util/Coordinates'; | ||
| import { carRouting } from '$lib/util/carRouting'; | ||
| import type { Itinerary, Leg } from '$lib/openapi'; | ||
| import { polyLineToLatLngArray } from '$lib/util/polylineToGeoJSON'; | ||
|
|
||
| export async function createStatistics() { | ||
| await computeAndPersistStatistics('tour'); | ||
| await computeAndPersistStatistics('rideShareTour'); | ||
| } | ||
|
|
||
| async function tourQuery() { | ||
| return await db | ||
| .selectFrom('tour') | ||
| .innerJoin('request', 'request.tour', 'tour.id') | ||
| .innerJoin('vehicle', 'vehicle.id', 'tour.vehicle') | ||
| .innerJoin('company', 'company.id', 'vehicle.company') | ||
| .where('tour.arrival', '<', Date.now()) | ||
| .where('tour.approachAndReturnM', 'is', null) | ||
| .where('tour.cancelled', '=', false) | ||
| .select((eb) => [ | ||
| jsonArrayFrom( | ||
| eb | ||
| .selectFrom('request') | ||
| .innerJoin('event', 'event.request', 'request.id') | ||
| .innerJoin('eventGroup', 'event.eventGroupId', 'eventGroup.id') | ||
| .where('request.cancelled', '=', false) | ||
| .whereRef('request.tour', '=', 'tour.id') | ||
| .selectAll(['event', 'eventGroup']) | ||
| .select('request.passengers') | ||
| ).as('events'), | ||
| 'company.lat', | ||
| 'company.lng', | ||
| 'tour.id' | ||
| ]) | ||
| .execute(); | ||
| } | ||
|
|
||
| async function rideShareTourQuery() { | ||
| return ( | ||
| await db | ||
| .selectFrom('rideShareTour as tour') | ||
| .where('tour.approachAndReturnM', 'is', null) | ||
| .where('tour.cancelled', '=', false) | ||
| .where('tour.latestEnd', '<', Date.now()) | ||
| .select((eb) => [ | ||
| jsonArrayFrom( | ||
| eb | ||
| .selectFrom('request') | ||
| .innerJoin('event', 'event.request', 'request.id') | ||
| .innerJoin('eventGroup', 'event.eventGroupId', 'eventGroup.id') | ||
| .where('request.cancelled', '=', false) | ||
| .whereRef('request.tour', '=', 'tour.id') | ||
| .selectAll(['event', 'eventGroup']) | ||
| .select('request.passengers') | ||
| ).as('events') | ||
| ]) | ||
| .execute() | ||
| ).map((t) => ({ ...t, lat: 0, lng: 0, id: 0 })); | ||
| } | ||
|
|
||
| type Tours = Awaited<ReturnType<typeof tourQuery>>; | ||
| type Tour = Tours[0]; | ||
| type Event = Tour['events'][0]; | ||
|
|
||
| async function computeStatistics(events: Event[], company?: Coordinates) { | ||
| const routingQueries = new Array<Promise<Itinerary | undefined>>(events.length); | ||
| for (let i = 0; i < events.length - 1; ++i) { | ||
| routingQueries[i] = carRouting(events[i], events[i + 1]); | ||
| } | ||
| const routingResults = await Promise.all(routingQueries); | ||
| const distances = routingResults.map((r) => legsToTravelDistance(r?.legs)); | ||
|
|
||
| let currentPassengers = 0; | ||
| let occupiedM = 0; | ||
| let fullyPayedM = 0; | ||
| for (let i = 0; i != events.length; ++i) { | ||
| const curr = events[i]; | ||
| currentPassengers += curr.isPickup ? curr.passengers : -curr.passengers; | ||
| if (currentPassengers !== 0) { | ||
| occupiedM += distances[i]; | ||
| } | ||
| fullyPayedM += distances[i]; | ||
| } | ||
|
|
||
| let approachPlusReturn = 0; | ||
| if (company !== undefined) { | ||
| const routingResultApproach = await carRouting(company, events[0]); | ||
| const routingResultReturn = await carRouting(events[events.length - 1], company); | ||
| approachPlusReturn = | ||
| legsToTravelDistance(routingResultApproach?.legs) + | ||
| legsToTravelDistance(routingResultReturn?.legs); | ||
| } | ||
| return { | ||
| approachPlusReturn, | ||
| fullyPayedM, | ||
| occupiedM | ||
| }; | ||
| } | ||
|
|
||
| export function haversineDistance(c1: Coordinates, c2: Coordinates) { | ||
| function toRad(deg: number) { | ||
| return (deg * Math.PI) / 180; | ||
| } | ||
| const R = 6371000; // Earth radius in meters | ||
|
|
||
| const dLat = toRad(c2.lat - c1.lat); | ||
| const dLon = toRad(c2.lng - c1.lng); | ||
| const a = | ||
| Math.sin(dLat / 2) ** 2 + | ||
| Math.cos(toRad(c1.lat)) * Math.cos(toRad(c2.lat)) * Math.sin(dLon / 2) ** 2; | ||
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | ||
| return R * c; | ||
| } | ||
|
|
||
| function legToTravelDistance(leg: Leg): number { | ||
| const legGeometry = polyLineToLatLngArray(leg.legGeometry.points).map((v) => ({ | ||
| lat: v[0], | ||
| lng: v[1] | ||
| })); | ||
| let sum = 0; | ||
| for (let i = 1; i != legGeometry.length; ++i) { | ||
| const prev = legGeometry[i - 1]; | ||
| const curr = legGeometry[i]; | ||
| sum += haversineDistance(prev, curr); | ||
| } | ||
| return sum; | ||
| } | ||
|
|
||
| function legsToTravelDistance(legs: Leg[] | undefined) { | ||
| return legs === undefined | ||
| ? 0 | ||
| : legs.reduce((prev, curr) => (prev += legToTravelDistance(curr)), 0); | ||
| } | ||
|
|
||
| async function computeAndPersistStatistics(type: 'tour' | 'rideShareTour') { | ||
| const tours = type === 'tour' ? await tourQuery() : await rideShareTourQuery(); | ||
| const stats = await Promise.all( | ||
| tours.map( | ||
| async (t) => | ||
| await computeStatistics( | ||
| t.events.sort((e1, e2) => e1.scheduledTimeStart - e2.scheduledTimeStart), | ||
| type === 'tour' ? { lat: t.lat!, lng: t.lng! } : undefined | ||
| ) | ||
| ) | ||
| ); | ||
| for (let i = 0; i != tours.length; ++i) { | ||
| await db | ||
| .updateTable(type) | ||
| .set({ | ||
| fullyPayedM: Math.round(stats[i].fullyPayedM), | ||
| approachAndReturnM: Math.round(stats[i].approachPlusReturn), | ||
| occupiedM: Math.round(stats[i].occupiedM) | ||
| }) | ||
| .where('id', '=', tours[i].id) | ||
| .execute(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { createStatistics } from '$lib/createStatistics'; | ||
| import { json, type RequestEvent } from '@sveltejs/kit'; | ||
|
|
||
| export const POST = async (_: RequestEvent) => { | ||
| await createStatistics(); | ||
| return json({}); | ||
| }; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could also be interesting for cancelled tours?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. So far I avoided touching this since, #582 means there might be tours for which it is not trivial to tell which requests belonged to it before it was cancelled. So I will probably look at this issue before merging this.