@trafficgroup/knex-rel 0.1.21 → 0.1.23-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -2
- package/CLAUDE.md +2 -11
- package/dist/constants/folder.constants.d.ts +2 -2
- package/dist/constants/folder.constants.js +6 -15
- package/dist/constants/folder.constants.js.map +1 -1
- package/dist/constants/study.constants.d.ts +1 -1
- package/dist/constants/study.constants.js +1 -1
- package/dist/constants/video.constants.d.ts +2 -2
- package/dist/constants/video.constants.js +5 -9
- package/dist/constants/video.constants.js.map +1 -1
- package/dist/dao/VideoMinuteResultDAO.d.ts +19 -2
- package/dist/dao/VideoMinuteResultDAO.js +75 -29
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/auth/auth.dao.js +1 -4
- package/dist/dao/auth/auth.dao.js.map +1 -1
- package/dist/dao/batch/batch.dao.d.ts +0 -10
- package/dist/dao/batch/batch.dao.js +14 -51
- package/dist/dao/batch/batch.dao.js.map +1 -1
- package/dist/dao/camera/camera.dao.js +7 -10
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/chat/chat.dao.d.ts +1 -1
- package/dist/dao/chat/chat.dao.js +40 -27
- package/dist/dao/chat/chat.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +7 -18
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/location/location.dao.js +9 -16
- package/dist/dao/location/location.dao.js.map +1 -1
- package/dist/dao/message/message.dao.d.ts +1 -1
- package/dist/dao/message/message.dao.js +26 -18
- package/dist/dao/message/message.dao.js.map +1 -1
- package/dist/dao/report-configuration/report-configuration.dao.js +68 -39
- package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
- package/dist/dao/study/study.dao.d.ts +8 -1
- package/dist/dao/study/study.dao.js +20 -15
- package/dist/dao/study/study.dao.js.map +1 -1
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.d.ts +2 -2
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.js +26 -26
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.js.map +1 -1
- package/dist/dao/user/user.dao.js +1 -4
- package/dist/dao/user/user.dao.js.map +1 -1
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
- package/dist/dao/video/video.dao.js +28 -30
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +10 -9
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
- package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
- package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
- package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
- package/dist/interfaces/message/message.interfaces.d.ts +2 -2
- package/dist/interfaces/report-configuration/report-configuration.interfaces.d.ts +1 -0
- package/dist/interfaces/study/study.interfaces.d.ts +2 -8
- package/dist/interfaces/user/user.interfaces.d.ts +1 -1
- package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
- package/dist/interfaces/video/video.interfaces.d.ts +2 -2
- package/migrations/20250717160737_migration.ts +1 -1
- package/migrations/20250717160908_migration.ts +2 -5
- package/migrations/20250717161310_migration.ts +1 -1
- package/migrations/20250717161406_migration.ts +3 -3
- package/migrations/20250717162431_migration.ts +1 -1
- package/migrations/20250717173228_migration.ts +2 -2
- package/migrations/20250717204731_migration.ts +1 -1
- package/migrations/20250722210109_migration.ts +4 -8
- package/migrations/20250722211019_migration.ts +1 -1
- package/migrations/20250723153852_migration.ts +10 -13
- package/migrations/20250723162257_migration.ts +7 -4
- package/migrations/20250723171109_migration.ts +7 -4
- package/migrations/20250723205331_migration.ts +9 -6
- package/migrations/20250724191345_migration.ts +11 -8
- package/migrations/20250730180932_migration.ts +13 -14
- package/migrations/20250730213625_migration.ts +11 -8
- package/migrations/20250804124509_migration.ts +21 -26
- package/migrations/20250804132053_migration.ts +8 -5
- package/migrations/20250804164518_migration.ts +7 -7
- package/migrations/20250823223016_migration.ts +21 -32
- package/migrations/20250910015452_migration.ts +6 -18
- package/migrations/20250911000000_migration.ts +4 -18
- package/migrations/20250917144153_migration.ts +7 -14
- package/migrations/20250930200521_migration.ts +4 -8
- package/migrations/20251010143500_migration.ts +6 -27
- package/migrations/20251020225758_migration.ts +15 -51
- package/migrations/20251112120000_migration.ts +2 -10
- package/migrations/20251112120200_migration.ts +7 -19
- package/migrations/20251112120300_migration.ts +2 -7
- package/migrations/20260109140000_migration.ts +2 -7
- package/package.json +3 -3
- package/src/constants/folder.constants.ts +8 -17
- package/src/constants/study.constants.ts +1 -1
- package/src/constants/video.constants.ts +7 -11
- package/src/d.types.ts +14 -18
- package/src/dao/VideoMinuteResultDAO.ts +127 -83
- package/src/dao/auth/auth.dao.ts +55 -58
- package/src/dao/batch/batch.dao.ts +100 -145
- package/src/dao/camera/camera.dao.ts +121 -124
- package/src/dao/chat/chat.dao.ts +45 -45
- package/src/dao/folder/folder.dao.ts +90 -105
- package/src/dao/location/location.dao.ts +87 -109
- package/src/dao/message/message.dao.ts +32 -32
- package/src/dao/reconciliation-log/reconciliation-log.dao.ts +1 -1
- package/src/dao/report-configuration/report-configuration.dao.ts +381 -370
- package/src/dao/study/study.dao.ts +83 -94
- package/src/dao/systemConfiguration/SystemConfigurationDAO.ts +35 -41
- package/src/dao/user/user.dao.ts +50 -52
- package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +48 -80
- package/src/dao/video/video.dao.ts +345 -396
- package/src/entities/BaseEntity.ts +1 -1
- package/src/index.ts +30 -43
- package/src/interfaces/auth/auth.interfaces.ts +10 -10
- package/src/interfaces/batch/batch.interfaces.ts +1 -1
- package/src/interfaces/camera/camera.interfaces.ts +9 -9
- package/src/interfaces/chat/chat.interfaces.ts +4 -4
- package/src/interfaces/folder/folder.interfaces.ts +2 -2
- package/src/interfaces/location/location.interfaces.ts +7 -7
- package/src/interfaces/message/message.interfaces.ts +3 -3
- package/src/interfaces/report-configuration/report-configuration.interfaces.ts +17 -16
- package/src/interfaces/study/study.interfaces.ts +3 -10
- package/src/interfaces/user/user.interfaces.ts +9 -9
- package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
- package/src/interfaces/video/video.interfaces.ts +34 -34
|
@@ -23,6 +23,14 @@ interface ITMCVehicles {
|
|
|
23
23
|
[vehicleClass: string]: ITMCDirections;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
interface ICrosswalkResults {
|
|
27
|
+
[crosswalkName: string]: {
|
|
28
|
+
[entityClass: string]: {
|
|
29
|
+
[direction: string]: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
interface ITMCResult {
|
|
27
35
|
vehicles: ITMCVehicles;
|
|
28
36
|
counts: {
|
|
@@ -33,6 +41,8 @@ interface ITMCResult {
|
|
|
33
41
|
totalcount: number;
|
|
34
42
|
detected_classes: { [vehicleClass: string]: number };
|
|
35
43
|
study_type: "TMC";
|
|
44
|
+
crosswalks?: ICrosswalkResults;
|
|
45
|
+
crosswalk_totals?: { pedestrian: number; bicycle: number };
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
interface IATRVehicles {
|
|
@@ -69,15 +79,15 @@ interface IGroupedResponse {
|
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
interface IStudyTimeGroupResult {
|
|
72
|
-
absoluteTime: string;
|
|
82
|
+
absoluteTime: string; // ISO 8601 start of bucket
|
|
73
83
|
groupIndex: number;
|
|
74
|
-
startMinute: number;
|
|
75
|
-
endMinute: number;
|
|
76
|
-
label: string;
|
|
84
|
+
startMinute: number; // Start minute number (0-based from video start)
|
|
85
|
+
endMinute: number; // End minute number (0-based from video start)
|
|
86
|
+
label: string; // Formatted label
|
|
77
87
|
results: ITMCResult | IATRResult;
|
|
78
88
|
minuteCount: number;
|
|
79
89
|
videoCount: number;
|
|
80
|
-
contributingVideos: string[];
|
|
90
|
+
contributingVideos: string[]; // Video UUIDs
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
interface IGroupedStudyResponse {
|
|
@@ -87,7 +97,7 @@ interface IGroupedStudyResponse {
|
|
|
87
97
|
study: {
|
|
88
98
|
uuid: string;
|
|
89
99
|
name: string;
|
|
90
|
-
type:
|
|
100
|
+
type: 'TMC' | 'ATR';
|
|
91
101
|
status: string;
|
|
92
102
|
};
|
|
93
103
|
videoCount: number;
|
|
@@ -406,11 +416,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
406
416
|
groupIndex: row.minute_number,
|
|
407
417
|
startMinute: row.minute_number,
|
|
408
418
|
endMinute: row.minute_number,
|
|
409
|
-
label: this.formatTimeLabel(
|
|
410
|
-
row.minute_number,
|
|
411
|
-
row.minute_number,
|
|
412
|
-
video.recordingStartedAt,
|
|
413
|
-
),
|
|
419
|
+
label: this.formatTimeLabel(row.minute_number, row.minute_number, video.recordingStartedAt),
|
|
414
420
|
results: row.results,
|
|
415
421
|
minuteCount: 1,
|
|
416
422
|
})),
|
|
@@ -427,41 +433,37 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
427
433
|
// Use Knex query builder for safe parameter binding
|
|
428
434
|
const groupingQuery = this.knex(this.tableName)
|
|
429
435
|
.select(
|
|
430
|
-
this.knex.raw(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.knex.raw(
|
|
434
|
-
this.knex.raw(
|
|
435
|
-
this.knex.raw("COUNT(*) as minute_count"),
|
|
436
|
-
this.knex.raw(
|
|
437
|
-
"array_agg(results ORDER BY minute_number) as all_results",
|
|
438
|
-
),
|
|
436
|
+
this.knex.raw('FLOOR(minute_number / ?) as group_index', [groupingMinutes]),
|
|
437
|
+
this.knex.raw('MIN(minute_number) as start_minute'),
|
|
438
|
+
this.knex.raw('MAX(minute_number) as end_minute'),
|
|
439
|
+
this.knex.raw('COUNT(*) as minute_count'),
|
|
440
|
+
this.knex.raw('array_agg(results ORDER BY minute_number) as all_results')
|
|
439
441
|
)
|
|
440
|
-
.where(
|
|
442
|
+
.where('video_id', video.id);
|
|
441
443
|
|
|
442
444
|
if (startMinute !== undefined) {
|
|
443
|
-
groupingQuery.where(
|
|
445
|
+
groupingQuery.where('minute_number', '>=', startMinute);
|
|
444
446
|
}
|
|
445
447
|
|
|
446
448
|
if (endMinute !== undefined) {
|
|
447
|
-
groupingQuery.where(
|
|
449
|
+
groupingQuery.where('minute_number', '<=', endMinute);
|
|
448
450
|
}
|
|
449
451
|
|
|
450
452
|
const rows = await groupingQuery
|
|
451
|
-
.groupBy(
|
|
452
|
-
.orderBy(
|
|
453
|
+
.groupBy('group_index')
|
|
454
|
+
.orderBy('group_index');
|
|
453
455
|
|
|
454
456
|
// Aggregate the results in TypeScript based on video type
|
|
455
457
|
const aggregatedGroups: IGroupedResult[] = rows.map((row: any) => {
|
|
456
|
-
if (!row || typeof row !==
|
|
457
|
-
throw new Error(
|
|
458
|
+
if (!row || typeof row !== 'object') {
|
|
459
|
+
throw new Error('Invalid row data received from database query');
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
const allResults = Array.isArray(row.all_results) ? row.all_results : [];
|
|
461
|
-
|
|
463
|
+
|
|
462
464
|
// Determine video type based on multiple factors
|
|
463
|
-
let studyType = video.videoType ||
|
|
464
|
-
|
|
465
|
+
let studyType = video.videoType || 'ATR'; // Default fallback to ATR
|
|
466
|
+
|
|
465
467
|
// Check if minute data has study_type field (ATR usually does)
|
|
466
468
|
if (allResults.length > 0 && allResults[0].study_type) {
|
|
467
469
|
studyType = allResults[0].study_type;
|
|
@@ -473,24 +475,20 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
473
475
|
const vehicleKeys = Object.keys(sampleResult.vehicles);
|
|
474
476
|
if (vehicleKeys.length > 0) {
|
|
475
477
|
const firstVehicleType = sampleResult.vehicles[vehicleKeys[0]];
|
|
476
|
-
if (firstVehicleType && typeof firstVehicleType ===
|
|
478
|
+
if (firstVehicleType && typeof firstVehicleType === 'object') {
|
|
477
479
|
const directions = Object.keys(firstVehicleType);
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
directions.includes("EAST") ||
|
|
482
|
-
directions.includes("WEST")
|
|
483
|
-
) {
|
|
484
|
-
studyType = "TMC";
|
|
480
|
+
if (directions.includes('NORTH') || directions.includes('SOUTH') ||
|
|
481
|
+
directions.includes('EAST') || directions.includes('WEST')) {
|
|
482
|
+
studyType = 'TMC';
|
|
485
483
|
}
|
|
486
484
|
}
|
|
487
485
|
}
|
|
488
486
|
}
|
|
489
487
|
}
|
|
490
|
-
|
|
488
|
+
|
|
491
489
|
// Aggregate based on determined video type
|
|
492
490
|
let aggregatedResult;
|
|
493
|
-
if (studyType ===
|
|
491
|
+
if (studyType === 'TMC') {
|
|
494
492
|
aggregatedResult = this.aggregateTMCResults(allResults);
|
|
495
493
|
} else {
|
|
496
494
|
aggregatedResult = this.aggregateATRResults(allResults);
|
|
@@ -500,11 +498,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
500
498
|
groupIndex: row.group_index,
|
|
501
499
|
startMinute: row.start_minute,
|
|
502
500
|
endMinute: row.end_minute,
|
|
503
|
-
label: this.formatTimeLabel(
|
|
504
|
-
row.start_minute,
|
|
505
|
-
row.end_minute,
|
|
506
|
-
video.recordingStartedAt,
|
|
507
|
-
),
|
|
501
|
+
label: this.formatTimeLabel(row.start_minute, row.end_minute, video.recordingStartedAt),
|
|
508
502
|
results: aggregatedResult,
|
|
509
503
|
minuteCount: row.minute_count,
|
|
510
504
|
};
|
|
@@ -585,8 +579,8 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
585
579
|
"v.uuid as videoUuid",
|
|
586
580
|
"v.recordingStartedAt",
|
|
587
581
|
this.knex.raw(
|
|
588
|
-
'"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
|
|
589
|
-
)
|
|
582
|
+
'"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
|
|
583
|
+
)
|
|
590
584
|
)
|
|
591
585
|
.whereIn("v.id", videoIds)
|
|
592
586
|
.orderBy("absoluteTime", "asc");
|
|
@@ -624,7 +618,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
624
618
|
for (const minute of minuteResults) {
|
|
625
619
|
const absoluteTime = new Date(minute.absoluteTime);
|
|
626
620
|
const minutesSinceEarliest = Math.floor(
|
|
627
|
-
(absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
|
|
621
|
+
(absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
|
|
628
622
|
);
|
|
629
623
|
const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
|
|
630
624
|
|
|
@@ -632,7 +626,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
632
626
|
// Calculate bucket start time
|
|
633
627
|
const bucketStartMinutes = bucketIndex * groupingMinutes;
|
|
634
628
|
const bucketStartTime = new Date(
|
|
635
|
-
earliestTime.getTime() + bucketStartMinutes * 60 * 1000
|
|
629
|
+
earliestTime.getTime() + bucketStartMinutes * 60 * 1000
|
|
636
630
|
);
|
|
637
631
|
|
|
638
632
|
buckets.set(bucketIndex, {
|
|
@@ -649,14 +643,12 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
649
643
|
}
|
|
650
644
|
|
|
651
645
|
// Step 5: Aggregate using existing methods based on study type
|
|
652
|
-
const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
|
|
653
|
-
buckets.values(),
|
|
654
|
-
)
|
|
646
|
+
const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(buckets.values())
|
|
655
647
|
.sort((a, b) => a.groupIndex - b.groupIndex)
|
|
656
648
|
.map((bucket) => {
|
|
657
649
|
let aggregatedResult: ITMCResult | IATRResult;
|
|
658
650
|
|
|
659
|
-
if (study.type ===
|
|
651
|
+
if (study.type === 'TMC') {
|
|
660
652
|
aggregatedResult = this.aggregateTMCResults(bucket.results);
|
|
661
653
|
} else {
|
|
662
654
|
aggregatedResult = this.aggregateATRResults(bucket.results);
|
|
@@ -667,10 +659,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
667
659
|
groupIndex: bucket.groupIndex,
|
|
668
660
|
startMinute: bucket.groupIndex * groupingMinutes,
|
|
669
661
|
endMinute: (bucket.groupIndex + 1) * groupingMinutes - 1,
|
|
670
|
-
label: this.formatStudyTimeLabel(
|
|
671
|
-
bucket.absoluteTime,
|
|
672
|
-
groupingMinutes,
|
|
673
|
-
),
|
|
662
|
+
label: this.formatStudyTimeLabel(bucket.absoluteTime, groupingMinutes),
|
|
674
663
|
results: aggregatedResult,
|
|
675
664
|
minuteCount: bucket.results.length,
|
|
676
665
|
videoCount: bucket.videoUuids.size,
|
|
@@ -733,27 +722,25 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
733
722
|
vehicles: {},
|
|
734
723
|
counts: {
|
|
735
724
|
total_vehicles: 0,
|
|
736
|
-
entry_vehicles: 0
|
|
725
|
+
entry_vehicles: 0
|
|
737
726
|
},
|
|
738
727
|
total: 0,
|
|
739
728
|
totalcount: 0,
|
|
740
729
|
detected_classes: {},
|
|
741
|
-
study_type: "TMC"
|
|
730
|
+
study_type: "TMC"
|
|
742
731
|
};
|
|
743
732
|
|
|
744
733
|
for (const minute of minutes) {
|
|
745
734
|
const results = minute; // minute is already the results object from array_agg
|
|
746
735
|
|
|
747
736
|
// Aggregate vehicle movements by class and direction
|
|
748
|
-
if (results.vehicles && typeof results.vehicles ===
|
|
749
|
-
for (const [vehicleClass, directions] of Object.entries(
|
|
750
|
-
results.vehicles,
|
|
751
|
-
)) {
|
|
737
|
+
if (results.vehicles && typeof results.vehicles === 'object') {
|
|
738
|
+
for (const [vehicleClass, directions] of Object.entries(results.vehicles)) {
|
|
752
739
|
// Skip the 'total' pseudo vehicle class - validate it's actually aggregate data
|
|
753
|
-
if (vehicleClass ===
|
|
740
|
+
if (vehicleClass === 'total' && typeof directions === 'number') {
|
|
754
741
|
continue; // This is aggregate total data, not a vehicle class
|
|
755
742
|
}
|
|
756
|
-
|
|
743
|
+
|
|
757
744
|
if (!aggregated.vehicles[vehicleClass]) {
|
|
758
745
|
aggregated.vehicles[vehicleClass] = {};
|
|
759
746
|
}
|
|
@@ -776,12 +763,11 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
776
763
|
for (const [turnType, count] of Object.entries(turns)) {
|
|
777
764
|
const turnCount = (count as number) || 0;
|
|
778
765
|
aggregated.vehicles[vehicleClass][direction][turnType] =
|
|
779
|
-
(aggregated.vehicles[vehicleClass][direction][turnType] ||
|
|
780
|
-
|
|
781
|
-
|
|
766
|
+
(aggregated.vehicles[vehicleClass][direction][turnType] || 0) + turnCount;
|
|
767
|
+
|
|
782
768
|
// Add to detected_classes count for this vehicle type
|
|
783
769
|
aggregated.detected_classes[vehicleClass] += turnCount;
|
|
784
|
-
|
|
770
|
+
|
|
785
771
|
// Add to total counts
|
|
786
772
|
aggregated.total += turnCount;
|
|
787
773
|
aggregated.totalcount += turnCount;
|
|
@@ -789,12 +775,10 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
789
775
|
}
|
|
790
776
|
}
|
|
791
777
|
}
|
|
792
|
-
|
|
778
|
+
|
|
793
779
|
// Also process the 'total' entry for validation but don't count it as a vehicle class
|
|
794
780
|
if (results.vehicles.total) {
|
|
795
|
-
for (const [direction, turns] of Object.entries(
|
|
796
|
-
results.vehicles.total as any,
|
|
797
|
-
)) {
|
|
781
|
+
for (const [direction, turns] of Object.entries(results.vehicles.total as any)) {
|
|
798
782
|
if (typeof turns === "object" && turns !== null) {
|
|
799
783
|
for (const [turnType, count] of Object.entries(turns)) {
|
|
800
784
|
const turnCount = (count as number) || 0;
|
|
@@ -807,12 +791,75 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
807
791
|
}
|
|
808
792
|
|
|
809
793
|
// Set entry_vehicles same as total_vehicles for TMC
|
|
810
|
-
aggregated.counts.entry_vehicles =
|
|
811
|
-
|
|
794
|
+
aggregated.counts.entry_vehicles = aggregated.counts.total_vehicles || aggregated.total;
|
|
795
|
+
|
|
796
|
+
// Aggregate crosswalk results if any minute contains crosswalk data
|
|
797
|
+
const crosswalkResult = this.aggregateCrosswalkResults(minutes);
|
|
798
|
+
if (crosswalkResult.crosswalks) {
|
|
799
|
+
aggregated.crosswalks = crosswalkResult.crosswalks;
|
|
800
|
+
aggregated.crosswalk_totals = crosswalkResult.crosswalk_totals;
|
|
801
|
+
}
|
|
812
802
|
|
|
813
803
|
return aggregated;
|
|
814
804
|
}
|
|
815
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Aggregate crosswalk results from minute-level data
|
|
808
|
+
* Sums counts by crosswalk name, entity class, and direction
|
|
809
|
+
*/
|
|
810
|
+
private aggregateCrosswalkResults(minutes: any[]): {
|
|
811
|
+
crosswalks?: ICrosswalkResults;
|
|
812
|
+
crosswalk_totals?: { pedestrian: number; bicycle: number };
|
|
813
|
+
} {
|
|
814
|
+
let hasCrosswalkData = false;
|
|
815
|
+
const aggregated: ICrosswalkResults = {};
|
|
816
|
+
const totals = { pedestrian: 0, bicycle: 0 };
|
|
817
|
+
|
|
818
|
+
for (const minute of minutes) {
|
|
819
|
+
const results = minute;
|
|
820
|
+
if (!results.crosswalks || typeof results.crosswalks !== 'object') {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
hasCrosswalkData = true;
|
|
825
|
+
|
|
826
|
+
for (const [crosswalkName, classData] of Object.entries(results.crosswalks)) {
|
|
827
|
+
if (!aggregated[crosswalkName]) {
|
|
828
|
+
aggregated[crosswalkName] = {};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (typeof classData !== 'object' || classData === null) continue;
|
|
832
|
+
|
|
833
|
+
for (const [entityClass, directions] of Object.entries(classData as Record<string, any>)) {
|
|
834
|
+
if (!aggregated[crosswalkName][entityClass]) {
|
|
835
|
+
aggregated[crosswalkName][entityClass] = {};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (typeof directions !== 'object' || directions === null) continue;
|
|
839
|
+
|
|
840
|
+
for (const [direction, count] of Object.entries(directions as Record<string, any>)) {
|
|
841
|
+
const numericCount = typeof count === 'number' ? count : 0;
|
|
842
|
+
aggregated[crosswalkName][entityClass][direction] =
|
|
843
|
+
(aggregated[crosswalkName][entityClass][direction] || 0) + numericCount;
|
|
844
|
+
|
|
845
|
+
// Accumulate totals by entity class
|
|
846
|
+
if (entityClass === 'pedestrian') {
|
|
847
|
+
totals.pedestrian += numericCount;
|
|
848
|
+
} else if (entityClass === 'bicycle') {
|
|
849
|
+
totals.bicycle += numericCount;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!hasCrosswalkData) {
|
|
857
|
+
return {};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return { crosswalks: aggregated, crosswalk_totals: totals };
|
|
861
|
+
}
|
|
862
|
+
|
|
816
863
|
/**
|
|
817
864
|
* Aggregate ATR (Automatic Traffic Recorder) results
|
|
818
865
|
*/
|
|
@@ -832,7 +879,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
832
879
|
if (results.vehicles) {
|
|
833
880
|
for (const [vehicleClass, lanes] of Object.entries(results.vehicles)) {
|
|
834
881
|
// Skip 'total' pseudo-class if present
|
|
835
|
-
if (vehicleClass ===
|
|
882
|
+
if (vehicleClass === 'total') {
|
|
836
883
|
continue;
|
|
837
884
|
}
|
|
838
885
|
|
|
@@ -841,8 +888,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
841
888
|
}
|
|
842
889
|
|
|
843
890
|
for (const [laneId, count] of Object.entries(lanes as any)) {
|
|
844
|
-
const numericCount =
|
|
845
|
-
typeof count === "number" ? count : parseInt(String(count)) || 0;
|
|
891
|
+
const numericCount = typeof count === 'number' ? count : (parseInt(String(count)) || 0);
|
|
846
892
|
aggregated.vehicles[vehicleClass][laneId] =
|
|
847
893
|
(aggregated.vehicles[vehicleClass][laneId] || 0) + numericCount;
|
|
848
894
|
}
|
|
@@ -939,7 +985,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
939
985
|
private formatTimeLabel(
|
|
940
986
|
startMinute: number,
|
|
941
987
|
endMinute: number,
|
|
942
|
-
recordingStartedAt?: Date | string | null
|
|
988
|
+
recordingStartedAt?: Date | string | null
|
|
943
989
|
): string {
|
|
944
990
|
if (recordingStartedAt) {
|
|
945
991
|
// Calculate actual wall-clock times using recordingStartedAt
|
|
@@ -978,10 +1024,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
978
1024
|
* Used when results are already grouped by date in the UI
|
|
979
1025
|
* Uses UTC time for consistency with absoluteTime field
|
|
980
1026
|
*/
|
|
981
|
-
private formatStudyTimeLabel(
|
|
982
|
-
startTime: Date,
|
|
983
|
-
durationMinutes: number,
|
|
984
|
-
): string {
|
|
1027
|
+
private formatStudyTimeLabel(startTime: Date, durationMinutes: number): string {
|
|
985
1028
|
const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
|
|
986
1029
|
|
|
987
1030
|
const formatTime = (date: Date): string => {
|
|
@@ -1002,6 +1045,7 @@ export type {
|
|
|
1002
1045
|
IGroupedResult,
|
|
1003
1046
|
ITMCResult,
|
|
1004
1047
|
IATRResult,
|
|
1048
|
+
ICrosswalkResults,
|
|
1005
1049
|
};
|
|
1006
1050
|
|
|
1007
1051
|
export default VideoMinuteResultDAO;
|
package/src/dao/auth/auth.dao.ts
CHANGED
|
@@ -4,61 +4,58 @@ import { IAuth } from "../../interfaces/auth/auth.interfaces";
|
|
|
4
4
|
import KnexManager from "../../KnexConnection";
|
|
5
5
|
|
|
6
6
|
export class AuthDAO implements IBaseDAO<IAuth> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
7
|
+
private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
|
|
8
|
+
|
|
9
|
+
async create(item: IAuth): Promise<IAuth> {
|
|
10
|
+
const [createdAuth] = await this._knex("auth").insert(item).returning("*");
|
|
11
|
+
return createdAuth;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getById(id: number): Promise<IAuth | null> {
|
|
15
|
+
const auth = await this._knex("auth").where({ id }).first();
|
|
16
|
+
return auth || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getByUuid(uuid: string): Promise<IAuth | null> {
|
|
20
|
+
// Auth table doesn't have uuid, so we'll return null or throw error
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getByUserId(userId: number): Promise<IAuth | null> {
|
|
25
|
+
const auth = await this._knex("auth").where({ userId }).first();
|
|
26
|
+
return auth || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getByEmailToken(emailToken: string): Promise<IAuth | null> {
|
|
30
|
+
const auth = await this._knex("auth").where({ emailToken }).first();
|
|
31
|
+
return auth || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async update(id: number, item: Partial<IAuth>): Promise<IAuth | null> {
|
|
35
|
+
const [updatedAuth] = await this._knex("auth").where({ id }).update(item).returning("*");
|
|
36
|
+
return updatedAuth || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async delete(id: number): Promise<boolean> {
|
|
40
|
+
const result = await this._knex("auth").where({ id }).del();
|
|
41
|
+
return result > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getAll(page: number, limit: number): Promise<IDataPaginator<IAuth>> {
|
|
45
|
+
const offset = (page - 1) * limit;
|
|
46
|
+
|
|
47
|
+
const [countResult] = await this._knex("auth").count("* as count");
|
|
48
|
+
const totalCount = +countResult.count;
|
|
49
|
+
const auths = await this._knex("auth").limit(limit).offset(offset);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
data: auths,
|
|
54
|
+
page,
|
|
55
|
+
limit,
|
|
56
|
+
count: auths.length,
|
|
57
|
+
totalCount,
|
|
58
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|