1+ using SMOBackend . Models ;
2+ using SMOBackend . Services ;
3+ using SMOBackend . Utils ;
4+
5+ namespace SMOBackend . Analytics ;
6+
7+ /// <summary>
8+ /// Service that maintains a dictionary of train numbers to their train types.
9+ /// </summary>
10+ public class TrainTypeService : IHostedService
11+ {
12+ private static readonly string DataDirectory = Path . Combine ( AppContext . BaseDirectory , "data" , "train-types" ) ;
13+ private static readonly string TrainTypesFile = Path . Combine ( DataDirectory , "train-types.bin" ) ;
14+
15+ private readonly ILogger < TrainTypeService > _logger ;
16+ private readonly TimetableDataService _timetableDataService ;
17+
18+ private readonly TtlCache < string , string > _trainTypeCache =
19+ new ( TimeSpan . FromHours ( 24 ) , "TrainTypeCache" , maxEntries : 10000 ) ;
20+
21+ private TimedFunction ? _autoSaveFunction ;
22+
23+ public TrainTypeService (
24+ ILogger < TrainTypeService > logger ,
25+ TimetableDataService timetableDataService )
26+ {
27+ _logger = logger ;
28+ _timetableDataService = timetableDataService ;
29+ }
30+
31+ /// <inheritdoc />
32+ public Task StartAsync ( CancellationToken cancellationToken )
33+ {
34+ _logger . LogInformation ( "Starting train type service..." ) ;
35+
36+ try
37+ {
38+ Directory . CreateDirectory ( DataDirectory ) ;
39+ if ( File . Exists ( TrainTypesFile ) )
40+ {
41+ _trainTypeCache . LoadFromFile ( TrainTypesFile ) ;
42+ _logger . LogInformation ( "Loaded {Count} train types from {FilePath}" ,
43+ _trainTypeCache . Count , TrainTypesFile ) ;
44+ }
45+ }
46+ catch ( Exception e )
47+ {
48+ _logger . LogError ( e , "Failed to load train types from file" ) ;
49+ }
50+
51+ _timetableDataService . PerServerDataReceived += OnTimetableDataReceived ;
52+
53+ // Auto-save every 5 minutes
54+ _autoSaveFunction = new ( SaveTrainTypes , TimeSpan . FromMinutes ( 5 ) ) ;
55+
56+ return Task . CompletedTask ;
57+ }
58+
59+ /// <inheritdoc />
60+ public async Task StopAsync ( CancellationToken cancellationToken )
61+ {
62+ _logger . LogInformation ( "Stopping train type service..." ) ;
63+
64+ _timetableDataService . PerServerDataReceived -= OnTimetableDataReceived ;
65+ _autoSaveFunction ? . Dispose ( ) ;
66+
67+ await SaveTrainTypesAsync ( ) . NoContext ( ) ;
68+ }
69+
70+ private void OnTimetableDataReceived ( PerServerData < Timetable [ ] > data )
71+ {
72+ try
73+ {
74+ var added = 0 ;
75+ var updated = 0 ;
76+
77+ foreach ( var timetable in data . Data )
78+ {
79+ if ( string . IsNullOrWhiteSpace ( timetable . TrainNoLocal ) )
80+ continue ;
81+
82+ // Extract train type from the first timetable entry
83+ var trainType = timetable . TimetableEntries . FirstOrDefault ( ) ? . TrainType ;
84+
85+ if ( string . IsNullOrWhiteSpace ( trainType ) )
86+ continue ;
87+
88+ if ( _trainTypeCache . TryGetValue ( timetable . TrainNoLocal , out var existingType ) )
89+ {
90+ if ( existingType == trainType ) continue ;
91+
92+ _trainTypeCache . Set ( timetable . TrainNoLocal , trainType ) ;
93+ updated ++ ;
94+ }
95+ else
96+ {
97+ _trainTypeCache . Add ( timetable . TrainNoLocal , trainType ) ;
98+ added ++ ;
99+ }
100+ }
101+
102+ if ( added > 0 || updated > 0 )
103+ {
104+ _logger . LogInformation (
105+ "Updated train type cache for server {ServerCode}: {Added} added, {Updated} updated" ,
106+ data . ServerCode , added , updated ) ;
107+ }
108+ }
109+ catch ( Exception ex )
110+ {
111+ _logger . LogError ( ex , "Error processing timetable data for train types" ) ;
112+ }
113+ }
114+
115+ /// <summary>
116+ /// Gets the train type for a given train number.
117+ /// </summary>
118+ /// <param name="trainNoLocal">The local train number.</param>
119+ /// <returns>The train type, or null if not found.</returns>
120+ public string ? GetTrainType ( string trainNoLocal ) =>
121+ _trainTypeCache . TryGetValue ( trainNoLocal , out var trainType ) ? trainType : null ;
122+
123+ private void SaveTrainTypes ( )
124+ {
125+ try
126+ {
127+ _trainTypeCache . SaveToFileAsync ( TrainTypesFile ) . Wait ( ) ;
128+ _logger . LogDebug ( "Saved {Count} train types to {FilePath}" ,
129+ _trainTypeCache . Count , TrainTypesFile ) ;
130+ }
131+ catch ( Exception ex )
132+ {
133+ _logger . LogError ( ex , "Failed to save train types to file" ) ;
134+ }
135+ }
136+
137+ private async Task SaveTrainTypesAsync ( )
138+ {
139+ try
140+ {
141+ await _trainTypeCache . SaveToFileAsync ( TrainTypesFile ) . NoContext ( ) ;
142+ _logger . LogInformation ( "Saved {Count} train types to {FilePath}" ,
143+ _trainTypeCache . Count , TrainTypesFile ) ;
144+ }
145+ catch ( Exception ex )
146+ {
147+ _logger . LogError ( ex , "Failed to save train types to file" ) ;
148+ }
149+ }
150+ }
0 commit comments