Skip to content

Commit 10d1372

Browse files
committed
Refactor PeriodSegmentData construction into shared builder, remove tripCount, and sync base path data on segment times modal save
1 parent 71e49f1 commit 10d1372

9 files changed

Lines changed: 116 additions & 104 deletions

File tree

packages/transition-backend/src/services/path/PathGtfsGeographyGenerator.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
*/
77
// eslint-disable-next-line n/no-unpublished-import
88
import type * as GtfsTypes from 'gtfs-types';
9-
import { Path, PeriodSegmentData } from 'transition-common/lib/services/path/Path';
9+
import { Path, type PeriodSegmentData } from 'transition-common/lib/services/path/Path';
10+
import { buildPeriodSegmentData } from 'transition-common/lib/services/path/PathSegmentTimeUtils';
1011
import type { TimeAndDistance } from 'transition-common/lib/services/path/PathTypes';
1112
import { StopTime, TripStopTimesWithService } from '../gtfsImport/GtfsImportTypes';
1213
import { GtfsMessages } from 'transition-common/lib/services/gtfs/GtfsMessages';
@@ -160,21 +161,7 @@ export const computeSegmentTimesByServiceAndPeriod = (
160161
if (!result[serviceId]) {
161162
result[serviceId] = {};
162163
}
163-
result[serviceId][period] = {
164-
segments: segmentsData,
165-
dwellTimeSeconds: dwellTimeSecondsData,
166-
travelTimeWithoutDwellTimesSeconds: totalTravelTimeWithoutDwellTimesSeconds,
167-
operatingTimeWithoutLayoverTimeSeconds: totalTravelTimeWithDwellTimesSeconds,
168-
averageSpeedWithoutDwellTimesMetersPerSecond:
169-
totalTravelTimeWithoutDwellTimesSeconds > 0
170-
? Math.round((totalDistanceMeters / totalTravelTimeWithoutDwellTimesSeconds) * 100) / 100
171-
: 0,
172-
operatingSpeedMetersPerSecond:
173-
totalTravelTimeWithDwellTimesSeconds > 0
174-
? Math.round((totalDistanceMeters / totalTravelTimeWithDwellTimesSeconds) * 100) / 100
175-
: 0,
176-
tripCount: bucketTrips.length
177-
};
164+
result[serviceId][period] = buildPeriodSegmentData(segmentsData, dwellTimeSecondsData, totalDistanceMeters);
178165
}
179166

180167
return result;

packages/transition-backend/src/services/path/__tests__/PathGtfsGeographyGenerator.test.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -624,9 +624,7 @@ describe('computeSegmentTimesByServiceAndPeriod', () => {
624624
expect(Object.keys(result)).toEqual(['svcA', 'svcB']);
625625
expect(Object.keys(result['svcA'])).toEqual(['am_peak', 'midday']);
626626
expect(result['svcA']['am_peak'].segments[0].travelTimeSeconds).toEqual(100);
627-
expect(result['svcA']['am_peak'].tripCount).toEqual(1);
628627
expect(result['svcB']['am_peak'].segments[0].travelTimeSeconds).toEqual(120);
629-
expect(result['svcB']['am_peak'].tripCount).toEqual(1);
630628
expect(Object.keys(result['svcA'])).toEqual(['am_peak', 'midday']);
631629
expect(result['svcA']['midday'].segments[0].travelTimeSeconds).toEqual(80);
632630
});
@@ -637,7 +635,6 @@ describe('computeSegmentTimesByServiceAndPeriod', () => {
637635

638636
const result = computeSegmentTimesByServiceAndPeriod([trip1, trip2], [500], periods, 1000);
639637

640-
expect(result['svcA']['am_peak'].tripCount).toEqual(2);
641638
expect(result['svcA']['am_peak'].segments[0].travelTimeSeconds).toEqual(110);
642639
});
643640

@@ -649,7 +646,6 @@ describe('computeSegmentTimesByServiceAndPeriod', () => {
649646
const result = computeSegmentTimesByServiceAndPeriod([earlyTrip, amTrip], [null], periods, 1000);
650647

651648
expect(Object.keys(result)).toEqual(['svc1']);
652-
expect(result['svc1']['am_peak'].tripCount).toEqual(2);
653649
});
654650

655651
test('should assign trip shortly after last period to last period', () => {
@@ -659,7 +655,6 @@ describe('computeSegmentTimesByServiceAndPeriod', () => {
659655
const result = computeSegmentTimesByServiceAndPeriod([lateTrip], [null], periods, 1000);
660656

661657
expect(Object.keys(result)).toEqual(['svc1']);
662-
expect(result['svc1']['pm_peak'].tripCount).toEqual(1);
663658
});
664659

665660
test('should skip trip far beyond last period overflow threshold', () => {

packages/transition-common/src/services/path/Path.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ export type PeriodSegmentData = {
7070
operatingTimeWithoutLayoverTimeSeconds: number;
7171
averageSpeedWithoutDwellTimesMetersPerSecond: number;
7272
operatingSpeedMetersPerSecond: number;
73-
tripCount: number;
7473
};
7574

7675
export interface PathAttributesData {
@@ -943,20 +942,15 @@ export class Path extends MapObject<GeoJSON.LineString, PathAttributes> implemen
943942
operatingSpeedMetersPerSecond:
944943
operatingTimeWithoutLayoverTimeSeconds > 0
945944
? Math.round((totalDistanceMeters / operatingTimeWithoutLayoverTimeSeconds) * 100) / 100
946-
: 0,
947-
tripCount: 0
945+
: 0
948946
};
949947
}
950948

951-
/** Compute a trip-count-weighted average across multiple services' PeriodSegmentData for the same period. */
949+
/** Compute an equal-weight average across multiple services' PeriodSegmentData for the same period. */
952950
private _averagePeriodSegmentData(dataArray: PeriodSegmentData[]): PeriodSegmentData {
953951
const numSegments = Math.min(...dataArray.map((d) => d.segments.length));
954952
const numStops = Math.min(...dataArray.map((d) => d.dwellTimeSeconds.length));
955-
const totalTripCount = dataArray.reduce((sum, d) => sum + d.tripCount, 0);
956-
// When all entries have tripCount 0 (e.g. synthetic fallback data), use equal weights
957-
const useEqualWeights = totalTripCount === 0;
958-
const weight = (d: PeriodSegmentData) => (useEqualWeights ? 1 : d.tripCount);
959-
const divisor = useEqualWeights ? dataArray.length : totalTripCount;
953+
const count = dataArray.length;
960954

961955
const avgSegments: TimeAndDistance[] = [];
962956
const avgDwell: number[] = [];
@@ -966,24 +960,24 @@ export class Path extends MapObject<GeoJSON.LineString, PathAttributes> implemen
966960
let distSum = 0;
967961
let hasDistance = false;
968962
for (const d of dataArray) {
969-
travelSum += d.segments[i].travelTimeSeconds * weight(d);
963+
travelSum += d.segments[i].travelTimeSeconds;
970964
if (d.segments[i].distanceMeters !== null) {
971-
distSum += d.segments[i].distanceMeters! * weight(d);
965+
distSum += d.segments[i].distanceMeters!;
972966
hasDistance = true;
973967
}
974968
}
975969
avgSegments.push({
976-
travelTimeSeconds: Math.round(travelSum / divisor),
977-
distanceMeters: hasDistance ? Math.round(distSum / divisor) : null
970+
travelTimeSeconds: Math.round(travelSum / count),
971+
distanceMeters: hasDistance ? Math.round(distSum / count) : null
978972
});
979973
}
980974

981975
for (let i = 0; i < numStops; i++) {
982976
let dwellSum = 0;
983977
for (const d of dataArray) {
984-
dwellSum += d.dwellTimeSeconds[i] * weight(d);
978+
dwellSum += d.dwellTimeSeconds[i];
985979
}
986-
avgDwell.push(Math.round(dwellSum / divisor));
980+
avgDwell.push(Math.round(dwellSum / count));
987981
}
988982

989983
const travelTimeWithoutDwellTimesSeconds = avgSegments.reduce((sum, s) => sum + s.travelTimeSeconds, 0);
@@ -1003,11 +997,38 @@ export class Path extends MapObject<GeoJSON.LineString, PathAttributes> implemen
1003997
operatingSpeedMetersPerSecond:
1004998
operatingTimeWithoutLayoverTimeSeconds > 0
1005999
? Math.round((totalDistanceMeters / operatingTimeWithoutLayoverTimeSeconds) * 100) / 100
1006-
: 0,
1007-
tripCount: totalTripCount
1000+
: 0
10081001
};
10091002
}
10101003

1004+
/** Recompute base segment data (data.segments, data.dwellTimeSeconds, and derived stats)
1005+
* as a weighted average across all entries in segmentsByServiceAndPeriod.
1006+
* Also refreshes derived statistics. Creates a single history entry. */
1007+
updateBaseFromServicePeriodData() {
1008+
const byServiceAndPeriod = this.attributes.data.segmentsByServiceAndPeriod;
1009+
if (!byServiceAndPeriod) return;
1010+
1011+
const allPeriodData: PeriodSegmentData[] = [];
1012+
for (const serviceEntries of Object.values(byServiceAndPeriod)) {
1013+
for (const periodData of Object.values(serviceEntries)) {
1014+
allPeriodData.push(periodData);
1015+
}
1016+
}
1017+
if (allPeriodData.length === 0) return;
1018+
1019+
const avg = allPeriodData.length === 1 ? allPeriodData[0] : this._averagePeriodSegmentData(allPeriodData);
1020+
1021+
this.attributes.data.segments = avg.segments;
1022+
this.attributes.data.dwellTimeSeconds = avg.dwellTimeSeconds;
1023+
this.attributes.data.travelTimeWithoutDwellTimesSeconds = avg.travelTimeWithoutDwellTimesSeconds;
1024+
this.attributes.data.operatingTimeWithoutLayoverTimeSeconds = avg.operatingTimeWithoutLayoverTimeSeconds;
1025+
this.attributes.data.averageSpeedWithoutDwellTimesMetersPerSecond =
1026+
avg.averageSpeedWithoutDwellTimesMetersPerSecond;
1027+
this.attributes.data.operatingSpeedMetersPerSecond = avg.operatingSpeedMetersPerSecond;
1028+
this.refreshStats();
1029+
this._updateHistory();
1030+
}
1031+
10111032
emptyGeography() {
10121033
const newData = {
10131034
segments: null, // the last segment is the return back to first stop

packages/transition-common/src/services/path/PathGeographyGenerator.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
kphToMps
1616
} from 'chaire-lib-common/lib/utils/PhysicsUtils';
1717
import Path, { type PeriodSegmentData } from './Path';
18+
import { buildPeriodSegmentData } from './PathSegmentTimeUtils';
1819
import type { TimeAndDistance, TypeNodeChange, SegmentChangeInfo } from './PathTypes';
1920

2021
const MIN_TRAVEL_TIME_FOR_DWELL_SECONDS = 15;
@@ -901,26 +902,9 @@ const remapPeriodSegmentData = (
901902

902903
remappedDwellTimes.push(getDwellTimeSecondsForNode(path, nodeIds[nodeIds.length - 1]));
903904

904-
const travelTimeWithoutDwellTimesSeconds = remappedSegments.reduce((sum, seg) => sum + seg.travelTimeSeconds, 0);
905-
const totalDwellTimeSeconds = remappedDwellTimes.reduce((sum, d) => sum + d, 0);
906-
const operatingTimeWithoutLayoverTimeSeconds = travelTimeWithoutDwellTimesSeconds + totalDwellTimeSeconds;
907905
const totalDistanceMeters = remappedSegments.reduce((sum, seg) => sum + (seg.distanceMeters || 0), 0);
908906

909-
return {
910-
segments: remappedSegments,
911-
dwellTimeSeconds: remappedDwellTimes,
912-
travelTimeWithoutDwellTimesSeconds,
913-
operatingTimeWithoutLayoverTimeSeconds,
914-
averageSpeedWithoutDwellTimesMetersPerSecond:
915-
travelTimeWithoutDwellTimesSeconds > 0
916-
? Math.round((totalDistanceMeters / travelTimeWithoutDwellTimesSeconds) * 100) / 100
917-
: 0,
918-
operatingSpeedMetersPerSecond:
919-
operatingTimeWithoutLayoverTimeSeconds > 0
920-
? Math.round((totalDistanceMeters / operatingTimeWithoutLayoverTimeSeconds) * 100) / 100
921-
: 0,
922-
tripCount: initialPeriod.tripCount
923-
};
907+
return buildPeriodSegmentData(remappedSegments, remappedDwellTimes, totalDistanceMeters);
924908
};
925909

926910
/**

packages/transition-common/src/services/path/PathSegmentTimeUtils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* License text available at https://opensource.org/licenses/MIT
66
*/
77

8+
import type { PeriodSegmentData } from './Path';
9+
810
// === Types ===
911

1012
export type Checkpoint = {
@@ -49,3 +51,25 @@ export const formatSeconds = (seconds: number): string => {
4951
const secs = rounded % 60;
5052
return `${mins}m${secs < 10 ? '0' : ''}${secs}s`;
5153
};
54+
55+
/** Build a PeriodSegmentData object from segments, dwell times, and total distance.
56+
* Computes travel/operating totals and speed metrics. */
57+
export const buildPeriodSegmentData = (
58+
segments: { travelTimeSeconds: number; distanceMeters: number | null }[],
59+
dwellTimeSeconds: number[],
60+
totalDistanceMeters: number
61+
): PeriodSegmentData => {
62+
const travelTotal = segments.reduce((sum, s) => sum + s.travelTimeSeconds, 0);
63+
const dwellTotal = dwellTimeSeconds.reduce((sum, d) => sum + d, 0);
64+
const operatingTotal = travelTotal + dwellTotal;
65+
return {
66+
segments,
67+
dwellTimeSeconds,
68+
travelTimeWithoutDwellTimesSeconds: travelTotal,
69+
operatingTimeWithoutLayoverTimeSeconds: operatingTotal,
70+
averageSpeedWithoutDwellTimesMetersPerSecond:
71+
travelTotal > 0 ? Math.round((totalDistanceMeters / travelTotal) * 100) / 100 : 0,
72+
operatingSpeedMetersPerSecond:
73+
operatingTotal > 0 ? Math.round((totalDistanceMeters / operatingTotal) * 100) / 100 : 0
74+
};
75+
};

packages/transition-common/src/services/path/__tests__/Path.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ describe('getSegmentTravelTimeForPeriod', () => {
816816
operatingTimeWithoutLayoverTimeSeconds: 480,
817817
averageSpeedWithoutDwellTimesMetersPerSecond: 3.06,
818818
operatingSpeedMetersPerSecond: 3.06,
819-
tripCount: 1
819+
820820
}
821821
}
822822
};
@@ -842,7 +842,7 @@ describe('getSegmentTravelTimeForPeriod', () => {
842842
operatingTimeWithoutLayoverTimeSeconds: 480,
843843
averageSpeedWithoutDwellTimesMetersPerSecond: 3.06,
844844
operatingSpeedMetersPerSecond: 3.06,
845-
tripCount: 1
845+
846846
}
847847
}
848848
};
@@ -896,7 +896,7 @@ describe('getSegmentTravelTimesForPeriod', () => {
896896
operatingTimeWithoutLayoverTimeSeconds: 480,
897897
averageSpeedWithoutDwellTimesMetersPerSecond: 3.06,
898898
operatingSpeedMetersPerSecond: 3.06,
899-
tripCount: 1
899+
900900
}
901901
}
902902
};
@@ -920,7 +920,7 @@ describe('getSegmentTravelTimesForPeriod', () => {
920920
operatingTimeWithoutLayoverTimeSeconds: 480,
921921
averageSpeedWithoutDwellTimesMetersPerSecond: 3.06,
922922
operatingSpeedMetersPerSecond: 3.06,
923-
tripCount: 1
923+
924924
}
925925
}
926926
};
@@ -936,10 +936,10 @@ describe('getSegmentTravelTimesForPeriod', () => {
936936
});
937937
});
938938

939-
describe('getSegmentsForPeriod - averaging with tripCount 0', () => {
939+
describe('getSegmentsForPeriod - averaging across services', () => {
940940
const lineId = uuidV4();
941941

942-
test('should average correctly when all services have tripCount 0', () => {
942+
test('should average correctly across services with equal weight', () => {
943943
const attributes = getPathAttributesWithData(true, { lineId });
944944
(attributes.data as any).totalDistanceMeters = 1000;
945945
(attributes.data as any).segmentsByServiceAndPeriod = {
@@ -954,7 +954,7 @@ describe('getSegmentsForPeriod - averaging with tripCount 0', () => {
954954
operatingTimeWithoutLayoverTimeSeconds: 325,
955955
averageSpeedWithoutDwellTimesMetersPerSecond: 3.33,
956956
operatingSpeedMetersPerSecond: 3.08,
957-
tripCount: 0
957+
958958
}
959959
},
960960
service2: {
@@ -968,7 +968,7 @@ describe('getSegmentsForPeriod - averaging with tripCount 0', () => {
968968
operatingTimeWithoutLayoverTimeSeconds: 645,
969969
averageSpeedWithoutDwellTimesMetersPerSecond: 1.67,
970970
operatingSpeedMetersPerSecond: 1.55,
971-
tripCount: 0
971+
972972
}
973973
}
974974
};

0 commit comments

Comments
 (0)