-
Notifications
You must be signed in to change notification settings - Fork 2
/
CityBikesDataSource.cs
282 lines (249 loc) · 12.4 KB
/
CityBikesDataSource.cs
1
2
3
4
5
6
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
using Esri.ArcGISRuntime.Data;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.RealTime;
using System.Diagnostics;
using System.Text.Json;
namespace BikeAvailability;
internal class CityBikesDataSource : DynamicEntityDataSource
{
// Timer to request updates at a given interval.
private readonly IDispatcherTimer _getBikeUpdatesTimer = Application.Current.Dispatcher.CreateTimer();
// REST endpoint for one of the cities described by the CityBikes API (http://api.citybik.es/).
private readonly string _cityBikesUrl;
// Dictionary of previous observations for bike stations (to evaluate change in inventory).
private readonly Dictionary<string, Dictionary<string, object>> _previousObservations = new();
// Name of the city.
private readonly string _cityName;
// Timer and related variables used to display observations at a consitent interval.
private readonly IDispatcherTimer _addBikeUpdatesTimer = Application.Current.Dispatcher.CreateTimer();
private readonly List<Tuple<MapPoint, Dictionary<string, object>>> _currentObservations = new();
private readonly bool _showSmoothUpdates;
public CityBikesDataSource(string cityName, string cityBikesUrl,
int updateIntervalSeconds, bool smoothUpdateDisplay = true)
{
// Store the name of the city.
_cityName = cityName;
// Store the timer interval (how often to request updates from the URL).
_getBikeUpdatesTimer.Interval = TimeSpan.FromSeconds(updateIntervalSeconds);
// URL for a specific city's bike rental stations.
_cityBikesUrl = cityBikesUrl;
// Set the function that will run at each timer interval.
_getBikeUpdatesTimer.Tick += (s, e) => _ = PullBikeUpdates();
// Store whether updates should be shown consitently over time or when the first arrive.
_showSmoothUpdates = smoothUpdateDisplay;
if (smoothUpdateDisplay)
{
// _addBikeUpdatesTimer.Interval = TimeSpan.FromSeconds(3);
_addBikeUpdatesTimer.Tick += (s, e) => AddBikeObservations();
}
}
protected override Task OnConnectAsync(CancellationToken cancellationToken)
{
// Start the timer to pull updates periodically.
_getBikeUpdatesTimer.Start();
return Task.CompletedTask;
}
protected override Task OnDisconnectAsync()
{
// Stop the timers (suspend update requests).
_getBikeUpdatesTimer.Stop();
_addBikeUpdatesTimer.Stop();
// Clear the dictionary of previous observations.
_previousObservations.Clear();
return Task.CompletedTask;
}
protected override Task<DynamicEntityDataSourceInfo> OnLoadAsync()
{
// When the data source is loaded, create metadata that defines:
// - A schema (fields) for the entities (bike stations) and their observations
// - Which field uniquely identifies entities (StationID)
// - The spatial reference for the station locations (WGS84)
var fields = new List<Field>
{
new Field(FieldType.Text, "StationID", "", 50),
new Field(FieldType.Text, "StationName", "", 125),
new Field(FieldType.Text, "Address", "", 125),
new Field(FieldType.Date, "TimeStamp", "", 0),
new Field(FieldType.Float32, "Longitude", "", 0),
new Field(FieldType.Float32, "Latitude", "", 0),
new Field(FieldType.Int32, "BikesAvailable", "", 0),
new Field(FieldType.Int32, "EBikesAvailable", "", 0),
new Field(FieldType.Int32, "EmptySlots", "", 0),
new Field(FieldType.Text, "ObservationID", "", 50),
new Field(FieldType.Int32, "InventoryChange", "", 0),
new Field(FieldType.Text, "ImageUrl", "", 255),
new Field(FieldType.Text, "CityName", "", 50)
};
var info = new DynamicEntityDataSourceInfo("StationID", fields)
{
SpatialReference = SpatialReferences.Wgs84
};
return Task.FromResult(info);
}
private async Task PullBikeUpdates()
{
// Exit if the data source is not connected.
if (this.ConnectionStatus != ConnectionStatus.Connected) { return; }
try
{
// Stop the timer that adds observations while getting updates.
_addBikeUpdatesTimer.Stop();
// If showing consistent updates, process any remaining ones from the last update.
if (_showSmoothUpdates)
{
for (int i = _currentObservations.Count - 1; i > 0; i--)
{
var obs = _currentObservations[i];
AddObservation(obs.Item1, obs.Item2);
_currentObservations.Remove(obs);
}
}
// Call a function to get a set of bike stations (locations and attributes).
var bikeUpdates = await GetDeserializedCityBikeResponse();
var updatedStationCount = 0;
var totalInventoryChange = 0;
// Iterate the info for each station.
foreach (var update in bikeUpdates)
{
// Get the location, attributes, and ID for this station.
var location = update.Item1;
var attributes = update.Item2;
var id = attributes["StationID"].ToString();
// Get the last set of values for this station (if they exist).
_previousObservations.TryGetValue(id, out Dictionary<string, object> lastObservation);
if (lastObservation is not null)
{
// Check if the new update has different values for BikesAvailable or EBikesAvailable.
if ((int)attributes["BikesAvailable"] != (int)lastObservation["BikesAvailable"] ||
(int)attributes["EBikesAvailable"] != (int)lastObservation["EBikesAvailable"])
{
// Calculate the change in inventory.
var stationInventoryChange = (int)attributes["BikesAvailable"] - (int)lastObservation["BikesAvailable"];
attributes["InventoryChange"] = stationInventoryChange;
totalInventoryChange += stationInventoryChange;
updatedStationCount++;
// If showing updates immediately, add the update to the data source.
if (!_showSmoothUpdates)
{
AddObservation(location, attributes);
}
else
{
// If showing smooth (consistent) updates, add to the current observations list for processing.
var observation = new Tuple<MapPoint, Dictionary<string, object>>(location, attributes);
_currentObservations.Add(observation);
}
}
// Update the latest update for this station.
_previousObservations[id] = attributes;
}
}
// If showing consistent updates, set up the timer for adding observations to the data source.
if (_showSmoothUpdates)
{
if (_currentObservations.Count > 0)
{
var updatesPerSecond = (int)Math.Ceiling(_currentObservations.Count / _getBikeUpdatesTimer.Interval.TotalSeconds);
if (updatesPerSecond > 0)
{
long ticksPerUpdate = 10000000 / updatesPerSecond;
_addBikeUpdatesTimer.Interval = TimeSpan.FromTicks(ticksPerUpdate);
_addBikeUpdatesTimer.Start(); // Tick event will add one update.
}
Debug.WriteLine($"**** Stations from this update = {updatedStationCount}, total to process = {_currentObservations.Count}");
}
}
Debug.WriteLine($"**** Total inventory change: {totalInventoryChange} for {updatedStationCount} stations");
}
catch (Exception ex)
{
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
}
private void AddBikeObservations()
{
// Add one observation on the timer interval.
// The interval was determined to spread these additions over the span required to get the next updates.
if (_currentObservations.Count > 0)
{
var obs = _currentObservations[^1];
AddObservation(obs.Item1, obs.Item2);
_currentObservations.Remove(obs);
}
}
public async Task GetInitialBikeStations()
{
// Exit if the data source is not connected.
if (this.ConnectionStatus != ConnectionStatus.Connected) { return; }
try
{
// Call a function to get a set of bike stations (locations and attributes).
var bikeUpdates = await GetDeserializedCityBikeResponse();
// Iterate the info for each station.
foreach (var update in bikeUpdates)
{
var location = update.Item1;
var attributes = update.Item2;
// Update the latest update for this station.
_previousObservations[attributes["StationID"].ToString()] = attributes;
// Add the update to the data source.
AddObservation(location, attributes);
}
}
catch (Exception ex)
{
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
}
private async Task<List<Tuple<MapPoint, Dictionary<string, object>>>> GetDeserializedCityBikeResponse()
{
// Deserialize a response from CityBikes as a list of bike station locations and attributes.
List<Tuple<MapPoint, Dictionary<string, object>>> bikeInfo = new();
try
{
// Get a JSON response from the REST service.
var client = new HttpClient();
HttpResponseMessage response = await client.GetAsync(new Uri(_cityBikesUrl));
if (response.IsSuccessStatusCode)
{
// Read the JSON response for this bike network (including all stations).
var cityBikeJson = await response.Content.ReadAsStringAsync();
// Get the "stations" portion of the JSON and deserialize the list of stations.
var stationsStartPos = cityBikeJson.IndexOf(@"""stations"":[") + 11;
var stationsEndPos = cityBikeJson.LastIndexOf(@"]") + 1;
var stationsJson = cityBikeJson[stationsStartPos..stationsEndPos];
var bikeUpdates = JsonSerializer.Deserialize<List<BikeStation>>(stationsJson);
// Iterate the info for each station.
foreach (var update in bikeUpdates)
{
// Build a dictionary of attributes from the response.
var attributes = new Dictionary<string, object>
{
{ "StationID", update.StationInfo.StationID },
{ "StationName", update.StationName },
{ "Address", update.StationInfo.Address },
{ "TimeStamp", DateTime.Parse(update.TimeStamp) },
{ "Longitude", update.Longitude },
{ "Latitude", update.Latitude },
{ "BikesAvailable", update.BikesAvailable },
{ "EBikesAvailable", update.StationInfo.EBikesAvailable },
{ "EmptySlots", update.EmptySlots },
{ "ObservationID", update.ObservationID },
{ "InventoryChange", 0 },
{ "ImageUrl", "https://static.arcgis.com/images/Symbols/Transportation/esriDefaultMarker_189.png" },
{ "CityName", _cityName }
};
// Create a map point from the longitude (x) and latitude (y) values.
var location = new MapPoint(update.Longitude, update.Latitude, SpatialReferences.Wgs84);
// Add this bike station's info to the list.
bikeInfo.Add(new Tuple<MapPoint, Dictionary<string, object>>(location, attributes));
}
}
}
catch (Exception ex)
{
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return bikeInfo;
}
}