Fix frame deduplication and chronological ordering in time series endpoint

- Fix scraping to wait for display label to change after step forward, preventing duplicate minutesAgo values within same folder
- Add hashset-based deduplication by AbsoluteObservationTime across cache folders
- Filter frames that are too close together (< 4 minutes apart)
- Preserve original folder structure while pruning filtered frames
- Sort folders by earliest frame time to maintain chronological order
- Add validation to detect folder overlaps that could cause out-of-order frames
This commit is contained in:
2025-12-08 01:20:09 +10:00
parent d7bdbdaebb
commit 2d9d45d430
18 changed files with 2734 additions and 366 deletions
+33
View File
@@ -0,0 +1,33 @@
namespace BomLocalService.Models;
/// <summary>
/// Information about a cache folder containing cached data for a location.
/// Each cache folder represents one capture session and can contain multiple data types.
/// </summary>
public class CacheFolder
{
/// <summary>
/// The folder name (e.g., "Pomona_QLD_20251207_000906").
/// </summary>
public string FolderName { get; set; } = string.Empty;
/// <summary>
/// The UTC timestamp when this cache folder was created (extracted from folder name).
/// </summary>
public DateTime CacheTimestamp { get; set; }
/// <summary>
/// The observation time from the metadata (when the weather data was captured by BOM).
/// </summary>
public DateTime ObservationTime { get; set; }
/// <summary>
/// List of data types available in this cache folder.
/// </summary>
public List<CachedDataType> AvailableDataTypes { get; set; } = new();
/// <summary>
/// Whether this cache folder is complete and ready to use.
/// </summary>
public bool IsComplete { get; set; }
}
+29
View File
@@ -0,0 +1,29 @@
namespace BomLocalService.Models;
/// <summary>
/// Information about the available cache range for a location.
/// Provides oldest and newest cache folder information to help clients understand available historical data.
/// </summary>
public class CacheRange
{
/// <summary>
/// Information about the oldest available cache folder.
/// </summary>
public CacheFolder? OldestCache { get; set; }
/// <summary>
/// Information about the newest available cache folder.
/// </summary>
public CacheFolder? NewestCache { get; set; }
/// <summary>
/// Total number of complete cache folders available.
/// </summary>
public int TotalCacheFolders { get; set; }
/// <summary>
/// The time span between oldest and newest cache (in minutes).
/// Null if there are fewer than 2 cache folders.
/// </summary>
public int? TimeSpanMinutes { get; set; }
}
+1
View File
@@ -50,6 +50,7 @@ public class CacheUpdateStatus
/// - "Cache is valid, no update needed" - Cache exists and is still fresh
/// - "Cache is stale, update triggered" - Cache exists but expired, update initiated
/// - "No cache exists, update triggered" - No cache found, update initiated
/// - "Cache update already in progress" - An update is currently running
/// </summary>
public string? Message { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
namespace BomLocalService.Models;
/// <summary>
/// Types of cached data available from BOM weather pages.
/// </summary>
public enum CachedDataType
{
/// <summary>
/// Rain radar images showing historical precipitation (40 min ago → 10 min ago).
/// Frames represent a time series and can be joined across cache folders for extended historical slideshows.
/// </summary>
Radar
// Future data types (not yet implemented):
// Temperature - forecast images showing current conditions and projected temperatures for the week ahead
// Wind - forecast images showing current conditions and projected wind patterns for the week ahead
}
+28
View File
@@ -0,0 +1,28 @@
namespace BomLocalService.Models;
/// <summary>
/// Base model representing a single cached frame image.
/// Extended by type-specific frame models (RadarFrame, etc.).
/// </summary>
public class CachedFrame
{
/// <summary>
/// Frame index within the frameset (0-based).
/// </summary>
public int FrameIndex { get; set; }
/// <summary>
/// Full file system path to the frame image file (server-side only).
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// URL endpoint to retrieve this frame image.
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// The data type this frame belongs to.
/// </summary>
public CachedDataType DataType { get; set; }
}
+17 -21
View File
@@ -1,33 +1,29 @@
namespace BomLocalService.Models;
/// <summary>
/// Represents a single frame from the radar slideshow (0-6).
/// Represents a single radar frame (historical precipitation data).
/// Frame 0 is oldest (40 minutes ago), Frame 6 is newest (10 minutes ago).
/// Radar frames can be joined across cache folders to create extended historical slideshows.
/// </summary>
public class RadarFrame
public class RadarFrame : CachedFrame
{
/// <summary>
/// Frame index (0-6), where 0 is the oldest frame (40 minutes ago) and 6 is the newest (10 minutes ago).
/// </summary>
public int FrameIndex { get; set; }
/// <summary>
/// Full file system path to the frame image file (server-side only).
/// Format: "{CacheDirectory}/{LocationKey}_{Timestamp}/frame_{FrameIndex}.png"
/// Example: "/app/cache/Pomona_QLD_20251207_000906/frame_0.png"
/// </summary>
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// URL endpoint to retrieve this frame image.
/// Format: "/api/radar/{Suburb}/{State}/frame/{FrameIndex}"
/// Example: "/api/radar/Pomona/QLD/frame/0"
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// Number of minutes ago this frame represents (40, 35, 30, 25, 20, 15, 10).
/// Frame 0 = 40 minutes ago, Frame 6 = 10 minutes ago.
/// This is relative to the cache folder's observation time.
/// </summary>
public int MinutesAgo { get; set; }
/// <summary>
/// The absolute UTC observation time for this frame.
/// Calculated as: ObservationTime - MinutesAgo.
/// This is set when frames are joined across cache folders in timeseries responses.
/// </summary>
public DateTime? AbsoluteObservationTime { get; set; }
public RadarFrame()
{
DataType = CachedDataType.Radar;
}
}
+58
View File
@@ -0,0 +1,58 @@
namespace BomLocalService.Models;
/// <summary>
/// Response containing radar frames from multiple cache folders (historical data).
/// Used when requesting radar data across a time range rather than just the most recent frames.
/// Radar frames can be joined across cache folders because they represent a historical time series.
/// </summary>
public class RadarTimeSeriesResponse
{
/// <summary>
/// List of cache folders, each containing radar frames from a single capture session.
/// Ordered chronologically (oldest first).
/// </summary>
public List<RadarCacheFolderFrames> CacheFolders { get; set; } = new();
/// <summary>
/// The start time of the requested range (UTC).
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// The end time of the requested range (UTC).
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// Total number of frames across all cache folders.
/// </summary>
public int TotalFrames { get; set; }
}
/// <summary>
/// A cache folder with its associated radar frames.
/// Represents all frames from a single radar capture session (from the radar subfolder).
/// </summary>
public class RadarCacheFolderFrames
{
/// <summary>
/// The cache folder name this frameset came from.
/// </summary>
public string CacheFolderName { get; set; } = string.Empty;
/// <summary>
/// The timestamp when this cache folder was created (UTC).
/// </summary>
public DateTime CacheTimestamp { get; set; }
/// <summary>
/// The observation time from metadata (UTC).
/// </summary>
public DateTime ObservationTime { get; set; }
/// <summary>
/// All radar frames from this cache folder's radar subfolder (typically 7 frames, 0-6).
/// Frame URLs are already generated and ready to use.
/// </summary>
public List<RadarFrame> Frames { get; set; } = new();
}