Skip to content

Commit 8b546b4

Browse files
authored
plate filter work (#154)
fixing plate filters and camera day/night scheduling issues. fixing plate list pagination not working. fixed processing time, region, and possible plates for statistics page. optimized statistics db call.
1 parent ed9695d commit 8b546b4

40 files changed

Lines changed: 2921 additions & 2458 deletions

OpenAlprWebhookProcessor.Server/CameraUpdateService/SimpleCameraScheduler.cs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ private void ScheduleCameraTimer(
216216
var delay = toggleTime - DateTimeOffset.Now;
217217
if (delay <= TimeSpan.Zero)
218218
{
219-
_logger.LogWarning("Next toggle time for camera {CameraId} is in the past, skipping", cameraId);
219+
_logger.LogWarning("Next toggle time for camera {CameraId} is in the past (calculated: {ToggleTime}, current: {CurrentTime}, delay: {Delay}), skipping",
220+
cameraId, toggleTime, DateTimeOffset.Now, delay);
220221
return;
221222
}
222223

@@ -320,11 +321,14 @@ private static SunriseSunset DetermineCurrentDayNightMode(
320321
int sunsetOffset)
321322
{
322323
var currentTimeInTimezone = DateTimeOffset.UtcNow.AddHours(timeZoneOffset).DateTime;
324+
325+
// Add a small buffer to avoid race conditions at transition times
326+
var lookAheadTime = currentTimeInTimezone.AddMinutes(1);
323327

324328
var celestialTimes = Celestial.CalculateCelestialTimes(
325329
latitude,
326330
longitude,
327-
currentTimeInTimezone,
331+
lookAheadTime,
328332
timeZoneOffset);
329333

330334
DateTime nextToggleTime;
@@ -333,7 +337,7 @@ private static SunriseSunset DetermineCurrentDayNightMode(
333337
var nextSunset = Celestial.Get_Next_SunSet(
334338
latitude,
335339
longitude,
336-
currentTimeInTimezone,
340+
lookAheadTime,
337341
timeZoneOffset);
338342
nextToggleTime = nextSunset.AddMinutes(sunsetOffset);
339343
}
@@ -342,12 +346,48 @@ private static SunriseSunset DetermineCurrentDayNightMode(
342346
var nextSunrise = Celestial.Get_Next_SunRise(
343347
latitude,
344348
longitude,
345-
currentTimeInTimezone,
349+
lookAheadTime,
346350
timeZoneOffset);
347351
nextToggleTime = nextSunrise.AddMinutes(sunriseOffset);
348352
}
349353

350-
return new DateTimeOffset(nextToggleTime, TimeSpan.FromHours(timeZoneOffset));
354+
var calculatedTime = new DateTimeOffset(nextToggleTime, TimeSpan.FromHours(timeZoneOffset));
355+
356+
// Ensure the calculated time is at least 30 seconds in the future to avoid immediate past times
357+
var minimumFutureTime = DateTimeOffset.Now.AddSeconds(30);
358+
if (calculatedTime <= minimumFutureTime)
359+
{
360+
// If calculated time is too close or in the past, try calculating for tomorrow
361+
var tomorrowTime = lookAheadTime.AddDays(1);
362+
var tomorrowCelestialTimes = Celestial.CalculateCelestialTimes(
363+
latitude,
364+
longitude,
365+
tomorrowTime,
366+
timeZoneOffset);
367+
368+
if (tomorrowCelestialTimes.IsSunUp)
369+
{
370+
var nextSunset = Celestial.Get_Next_SunSet(
371+
latitude,
372+
longitude,
373+
tomorrowTime,
374+
timeZoneOffset);
375+
nextToggleTime = nextSunset.AddMinutes(sunsetOffset);
376+
}
377+
else
378+
{
379+
var nextSunrise = Celestial.Get_Next_SunRise(
380+
latitude,
381+
longitude,
382+
tomorrowTime,
383+
timeZoneOffset);
384+
nextToggleTime = nextSunrise.AddMinutes(sunriseOffset);
385+
}
386+
387+
calculatedTime = new DateTimeOffset(nextToggleTime, TimeSpan.FromHours(timeZoneOffset));
388+
}
389+
390+
return calculatedTime;
351391
}
352392

353393
public DateTimeOffset? GetNextScheduledExecutionTime(Guid cameraId)

OpenAlprWebhookProcessor.Server/Data/Repositories/PlateGroupRepository.cs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,28 +138,40 @@ public async Task<PlateStatisticsAggregation> GetPlateStatisticsAggregationAsync
138138
long last90DaysEpoch,
139139
CancellationToken cancellationToken = default)
140140
{
141-
var allEpochsQuery = _dbSet
141+
var bestNumberEpochs = await _dbSet
142142
.AsNoTracking()
143-
.Where(pg => pg.BestNumber == plateNumber || pg.PossibleNumbers.Any(pn => pn.Number == plateNumber))
144-
.Select(pg => pg.ReceivedOnEpoch);
143+
.Where(pg => pg.BestNumber == plateNumber)
144+
.Select(pg => pg.ReceivedOnEpoch)
145+
.ToListAsync(cancellationToken);
146+
147+
var possibleNumberEpochs = await _context.PlateGroupPossibleNumbers
148+
.AsNoTracking()
149+
.Where(pn => pn.Number == plateNumber)
150+
.Select(pn => pn.PlateGroup.ReceivedOnEpoch)
151+
.ToListAsync(cancellationToken);
145152

146-
var result = await allEpochsQuery
147-
.GroupBy(e => 1)
148-
.Select(g => new PlateStatisticsAggregation
153+
var allEpochs = bestNumberEpochs
154+
.Concat(possibleNumberEpochs)
155+
.Distinct()
156+
.ToList();
157+
158+
if (!allEpochs.Any())
159+
{
160+
return new PlateStatisticsAggregation
149161
{
150-
TotalCount = g.Count(),
151-
Last90DaysCount = g.Count(e => e > last90DaysEpoch),
152-
MinEpoch = g.Min(),
153-
MaxEpoch = g.Max()
154-
})
155-
.FirstOrDefaultAsync(cancellationToken);
156-
157-
return result ?? new PlateStatisticsAggregation
162+
TotalCount = 0,
163+
Last90DaysCount = 0,
164+
MinEpoch = 0,
165+
MaxEpoch = 0
166+
};
167+
}
168+
169+
return new PlateStatisticsAggregation
158170
{
159-
TotalCount = 0,
160-
Last90DaysCount = 0,
161-
MinEpoch = 0,
162-
MaxEpoch = 0
171+
TotalCount = allEpochs.Count,
172+
Last90DaysCount = allEpochs.Count(e => e > last90DaysEpoch),
173+
MinEpoch = allEpochs.Min(),
174+
MaxEpoch = allEpochs.Max()
163175
};
164176
}
165177

Lines changed: 78 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,79 @@
1-
using Mediator;
2-
using Microsoft.AspNetCore.Authorization;
3-
using Microsoft.AspNetCore.Mvc;
4-
using OpenAlprWebhookProcessor.Features.ImageRelay.GetCropImage;
5-
using OpenAlprWebhookProcessor.Features.ImageRelay.GetImage;
6-
using OpenAlprWebhookProcessor.Features.ImageRelay.SnapshotRelay;
7-
using System;
8-
using System.Threading;
9-
using System.Threading.Tasks;
10-
11-
namespace OpenAlprWebhookProcessor.Features.ImageRelay
12-
{
13-
[Authorize]
14-
[ApiController]
15-
[Route("api/images")]
16-
public class ImageRelayController : ControllerBase
17-
{
18-
private readonly IMediator _mediator;
19-
20-
public ImageRelayController(IMediator mediator)
21-
{
22-
_mediator = mediator;
23-
}
24-
25-
[HttpGet("{imageId}")]
26-
public async Task<IActionResult> GetImage(
27-
string imageId,
28-
CancellationToken cancellationToken)
29-
{
30-
try
31-
{
32-
var query = new GetImageQuery(imageId);
33-
var image = await _mediator.Send(query, cancellationToken);
34-
35-
return File(image, "image/jpeg");
36-
}
37-
catch
38-
{
39-
return NotFound();
40-
}
41-
}
42-
43-
[HttpGet("crop/{imageId}")]
44-
public async Task<IActionResult> GetCropImage(
45-
string imageId,
46-
CancellationToken cancellationToken)
47-
{
48-
try
49-
{
50-
var query = new GetCropImageQuery(imageId);
51-
var cropImage = await _mediator.Send(query, cancellationToken);
52-
53-
return File(cropImage, "image/jpeg");
54-
}
55-
catch
56-
{
57-
return NotFound();
58-
}
59-
}
60-
61-
[HttpGet("{CameraId}/snapshot")]
62-
public async Task<IActionResult> GetSnapshot(
63-
Guid cameraId,
64-
CancellationToken cancellationToken)
65-
{
66-
try
67-
{
68-
var query = new GetSnapshotQuery(cameraId);
69-
var snapshot = await _mediator.Send(query, cancellationToken);
70-
71-
return File(snapshot, "image/jpeg");
72-
}
73-
catch
74-
{
75-
return NotFound();
76-
}
77-
}
78-
}
1+
using Mediator;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Mvc;
4+
using OpenAlprWebhookProcessor.Features.ImageRelay.GetCropImage;
5+
using OpenAlprWebhookProcessor.Features.ImageRelay.GetImage;
6+
using OpenAlprWebhookProcessor.Features.ImageRelay.SnapshotRelay;
7+
using System;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace OpenAlprWebhookProcessor.Features.ImageRelay
12+
{
13+
[Authorize]
14+
[ApiController]
15+
[Route("api/images")]
16+
public class ImageRelayController : ControllerBase
17+
{
18+
private readonly IMediator _mediator;
19+
20+
public ImageRelayController(IMediator mediator)
21+
{
22+
_mediator = mediator;
23+
}
24+
25+
[HttpGet("{imageId}")]
26+
public async Task<IActionResult> GetImage(
27+
string imageId,
28+
CancellationToken cancellationToken)
29+
{
30+
try
31+
{
32+
var query = new GetImageQuery(imageId);
33+
var image = await _mediator.Send(query, cancellationToken);
34+
35+
return File(image, "image/jpeg");
36+
}
37+
catch
38+
{
39+
return NotFound();
40+
}
41+
}
42+
43+
[HttpGet("crop/{imageId}")]
44+
public async Task<IActionResult> GetCropImage(
45+
string imageId,
46+
CancellationToken cancellationToken)
47+
{
48+
try
49+
{
50+
var query = new GetCropImageQuery(imageId);
51+
var cropImage = await _mediator.Send(query, cancellationToken);
52+
53+
return File(cropImage, "image/jpeg");
54+
}
55+
catch
56+
{
57+
return NotFound();
58+
}
59+
}
60+
61+
[HttpGet("{CameraId}/snapshot")]
62+
public async Task<IActionResult> GetSnapshot(
63+
Guid cameraId,
64+
CancellationToken cancellationToken)
65+
{
66+
try
67+
{
68+
var query = new GetSnapshotQuery(cameraId);
69+
var snapshot = await _mediator.Send(query, cancellationToken);
70+
71+
return File(snapshot, "image/jpeg");
72+
}
73+
catch
74+
{
75+
return NotFound();
76+
}
77+
}
78+
}
7979
}

OpenAlprWebhookProcessor.Server/Features/LicensePlates/Queries/GetPlateFilters/GetLicensePlateFiltersResponse.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public class GetLicensePlateFiltersResponse
88

99
public List<string> VehicleModels { get; set; }
1010

11+
public Dictionary<string, List<string>> VehicleMakeModelMap { get; set; }
12+
1113
public List<string> VehicleTypes { get; set; }
1214

1315
public List<string> VehicleYears { get; set; }

OpenAlprWebhookProcessor.Server/Features/LicensePlates/Queries/GetPlateFilters/GetPlateFiltersQueryHandler.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Mediator;
22
using Microsoft.EntityFrameworkCore;
33
using OpenAlprWebhookProcessor.Data.Repositories;
4+
using System.Collections.Generic;
45
using System.Linq;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -29,13 +30,33 @@ public async ValueTask<GetLicensePlateFiltersResponse> Handle(
2930
.OrderBy(x => x)
3031
.ToListAsync(cancellationToken);
3132

32-
var vehicleMakes = await plateGroups
33+
var vehicleMakeModels = await plateGroups
3334
.Where(x => !string.IsNullOrEmpty(x.VehicleMakeModel))
3435
.Select(x => x.VehicleMakeModel)
3536
.Distinct()
36-
.OrderBy(x => x)
3737
.ToListAsync(cancellationToken);
3838

39+
var vehicleMakes = vehicleMakeModels
40+
.Select(x => CapitalizeName(x.Split('_')[0]))
41+
.Distinct()
42+
.OrderBy(x => x)
43+
.ToList();
44+
45+
var vehicleModels = vehicleMakeModels
46+
.Where(x => x.Contains('_'))
47+
.Select(x => CapitalizeName(x.Split('_', 2)[1]))
48+
.Distinct()
49+
.OrderBy(x => x)
50+
.ToList();
51+
52+
var vehicleMakeModelMap = vehicleMakeModels
53+
.Where(x => x.Contains('_'))
54+
.GroupBy(x => CapitalizeName(x.Split('_')[0]))
55+
.ToDictionary(
56+
g => g.Key,
57+
g => g.Select(x => CapitalizeName(x.Split('_', 2)[1])).Distinct().OrderBy(x => x).ToList()
58+
);
59+
3960
var vehicleTypes = await plateGroups
4061
.Where(x => !string.IsNullOrEmpty(x.VehicleType))
4162
.Select(x => x.VehicleType)
@@ -54,9 +75,19 @@ public async ValueTask<GetLicensePlateFiltersResponse> Handle(
5475
{
5576
VehicleColors = vehicleColors,
5677
VehicleMakes = vehicleMakes,
78+
VehicleModels = vehicleModels,
79+
VehicleMakeModelMap = vehicleMakeModelMap,
5780
VehicleTypes = vehicleTypes,
5881
VehicleRegions = vehicleRegions
5982
};
6083
}
84+
85+
private static string CapitalizeName(string name)
86+
{
87+
if (string.IsNullOrEmpty(name))
88+
return name;
89+
90+
return char.ToUpper(name[0]) + name.Substring(1).ToLower();
91+
}
6192
}
6293
}

0 commit comments

Comments
 (0)