Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/12-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ await _databaseService.MarkLocationServerRejectedAsync(id, "threshold");
int deleted = await _databaseService.PurgeSyncedLocationsAsync(daysOld: 7);
```

### Trip Cache Operations
### Downloaded Trip Data Operations

```csharp
// Get downloaded trips
Expand Down
10 changes: 0 additions & 10 deletions src/WayfarerMobile.Core/Interfaces/IApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,6 @@ Task<bool> DeleteRegionAsync(
TripUpdateRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets trip geographic boundary for tile download calculation.
/// </summary>
/// <param name="tripId">The trip ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Trip boundary response with bounding box, or null if not found.</returns>
Task<TripBoundaryResponse?> GetTripBoundaryAsync(
Guid tripId,
CancellationToken cancellationToken = default);

#endregion

#region Segment Operations
Expand Down
88 changes: 0 additions & 88 deletions src/WayfarerMobile.Core/Models/TripModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,94 +1031,6 @@ public class DownloadedTrip
public bool IsFullyDownloaded => Status == "complete";
}

/// <summary>
/// Trip boundary response from server for tile download calculation.
/// </summary>
public class TripBoundaryResponse
{
/// <summary>
/// Gets or sets the trip ID.
/// </summary>
[JsonPropertyName("tripId")]
public Guid TripId { get; set; }

/// <summary>
/// Gets or sets the trip name.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the bounding box.
/// </summary>
[JsonPropertyName("boundingBox")]
public BoundingBox BoundingBox { get; set; } = new();
}

/// <summary>
/// Tile coordinate for download.
/// Implements IEquatable for proper collection operations (e.g., List.Remove).
/// This class is immutable after construction (init-only properties).
/// </summary>
public class TileCoordinate : IEquatable<TileCoordinate>
{
/// <summary>
/// Gets the zoom level.
/// </summary>
public int Zoom { get; init; }

/// <summary>
/// Gets the X coordinate.
/// </summary>
public int X { get; init; }

/// <summary>
/// Gets the Y coordinate.
/// </summary>
public int Y { get; init; }

/// <summary>
/// Gets the tile URL from a server template.
/// </summary>
/// <param name="urlTemplate">URL template with {z}, {x}, {y} placeholders.</param>
/// <returns>Full tile URL.</returns>
public string GetTileUrl(string urlTemplate) =>
urlTemplate.Replace("{z}", Zoom.ToString())
.Replace("{x}", X.ToString())
.Replace("{y}", Y.ToString());

/// <summary>
/// Gets a unique identifier for this tile.
/// </summary>
public string Id => $"{Zoom}-{X}-{Y}";

/// <inheritdoc />
public bool Equals(TileCoordinate? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Zoom == other.Zoom && X == other.X && Y == other.Y;
}

/// <inheritdoc />
public override bool Equals(object? obj) => Equals(obj as TileCoordinate);

/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(Zoom, X, Y);

/// <summary>
/// Equality operator.
/// </summary>
public static bool operator ==(TileCoordinate? left, TileCoordinate? right) =>
left?.Equals(right) ?? right is null;

/// <summary>
/// Inequality operator.
/// </summary>
public static bool operator !=(TileCoordinate? left, TileCoordinate? right) =>
!(left == right);
}

/// <summary>
/// Location from the timeline API with full details.
/// </summary>
Expand Down
14 changes: 3 additions & 11 deletions src/WayfarerMobile/Interfaces/ITripContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ public interface ITripContentService
Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync();

/// <summary>
/// Syncs trip metadata with the server (updates places, segments, areas).
/// Does not handle tile downloads - returns whether bounding box changed.
/// Syncs downloaded Trip metadata with the server, including its bounding box.
/// </summary>
/// <param name="tripServerId">The server-side trip ID.</param>
/// <param name="forceSync">If true, sync regardless of version.</param>
/// <param name="progress">Optional progress reporter.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Tuple of (updated trip entity or null, whether bounding box changed significantly).</returns>
Task<(DownloadedTripEntity? Trip, bool BoundingBoxChanged)> SyncTripMetadataAsync(
/// <returns>The updated Trip entity, or null when synchronization cannot complete.</returns>
Task<DownloadedTripEntity?> SyncTripMetadataAsync(
Guid tripServerId,
bool forceSync = false,
IProgress<DownloadProgressEventArgs>? progress = null,
Expand Down Expand Up @@ -67,11 +66,4 @@ public interface ITripContentService
/// <returns>List of trip segments.</returns>
Task<List<TripSegment>> GetOfflineSegmentsAsync(Guid tripServerId);

/// <summary>
/// Checks if bounding box has changed significantly (more than ~1km at equator).
/// </summary>
/// <param name="trip">The local trip entity.</param>
/// <param name="serverBoundingBox">The server bounding box.</param>
/// <returns>True if bounding box changed significantly.</returns>
bool HasBoundingBoxChangedSignificantly(DownloadedTripEntity trip, BoundingBox serverBoundingBox);
}
2 changes: 1 addition & 1 deletion src/WayfarerMobile/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ private static void ConfigureHttpClients(IServiceCollection services)
client.DefaultRequestHeaders.Add("User-Agent", "WayfarerMobile/1.0 (Location tracking app)");
});

// Tiles - map tile downloads with 60s timeout
// Tiles - request-driven interactive OpenStreetMap transport with 60s timeout
services.AddHttpClient("Tiles", client =>
{
client.Timeout = TimeSpan.FromSeconds(60);
Expand Down
50 changes: 0 additions & 50 deletions src/WayfarerMobile/Services/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,51 +263,6 @@ public async Task<List<TripSummary>> GetTripsAsync(CancellationToken cancellatio
}
}

/// <summary>
/// Gets trip boundary for tile download calculation.
/// </summary>
/// <param name="tripId">The trip ID.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Trip boundary response or null if not found.</returns>
public async Task<TripBoundaryResponse?> GetTripBoundaryAsync(Guid tripId, CancellationToken cancellationToken = default)
{
if (!IsConfigured)
{
_logger.LogWarning("Cannot get trip boundary - API is not configured");
return null;
}

try
{
var response = await ExecuteWithRetryAsync(
() => CreateRequest(HttpMethod.Get, $"/api/trips/{tripId}/boundary"),
cancellationToken);

if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TripBoundaryResponse>(JsonOptions, cancellationToken);
}

_logger.LogWarning("Failed to get trip boundary: {StatusCode}", response.StatusCode);
return null;
}
catch (HttpRequestException ex)
{
_logger.LogNetworkWarningIfOnline("Network error getting trip boundary for {TripId}: {Message}", tripId, ex.Message);
return null;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Request timed out getting trip boundary for {TripId}", tripId);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error getting trip boundary for {TripId}", tripId);
return null;
}
}

#region Place CRUD

/// <summary>
Expand Down Expand Up @@ -1069,11 +1024,6 @@ public async Task<bool> DeleteRegionAsync(Guid regionId, CancellationToken cance

#endregion

/// <summary>
/// Gets the underlying HttpClient for direct tile downloads.
/// </summary>
public HttpClient HttpClient => HttpClientInstance;

/// <inheritdoc/>
public async Task<TimelineResponse?> GetTimelineLocationsAsync(
string dateType,
Expand Down
38 changes: 9 additions & 29 deletions src/WayfarerMobile/Services/TripContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
}

/// <inheritdoc/>
public async Task<(DownloadedTripEntity? Trip, bool BoundingBoxChanged)> SyncTripMetadataAsync(
public async Task<DownloadedTripEntity?> SyncTripMetadataAsync(
Guid tripServerId,
bool forceSync = false,
IProgress<DownloadProgressEventArgs>? progress = null,
Expand All @@ -124,13 +124,13 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
if (localTrip == null)
{
_logger.LogWarning("Cannot sync trip {TripId} - not downloaded", tripServerId);
return (null, false);
return null;
}

if (!IsNetworkAvailable())
{
_logger.LogWarning("Cannot sync trip - no network connection");
return (null, false);
return null;
}

_logger.LogInformation("Starting metadata sync for trip: {TripName}", localTrip.Name);
Expand All @@ -141,14 +141,14 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
if (serverTrip == null)
{
_logger.LogWarning("Failed to fetch trip details for sync: {TripId}", tripServerId);
return (null, false);
return null;
}

// Check if update is needed (unless force sync)
if (!forceSync && serverTrip.Version <= localTrip.Version)
{
_logger.LogInformation("Trip {TripName} is already up to date (v{Version})", localTrip.Name, localTrip.Version);
return (localTrip, false);
return localTrip;
}

RaiseProgress(progress, localTrip.Id, 15, "Updating regions...");
Expand Down Expand Up @@ -179,21 +179,12 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
await _areaRepository.SaveOfflinePolygonsAsync(localTrip.Id, polygons);
localTrip.AreaCount = polygons.Count;

RaiseProgress(progress, localTrip.Id, 75, "Checking map coverage...");

// Check if bounding box changed significantly (caller will handle tile re-download)
var boundingBoxChanged = serverTrip.BoundingBox != null &&
HasBoundingBoxChangedSignificantly(localTrip, serverTrip.BoundingBox);

if (boundingBoxChanged && serverTrip.BoundingBox != null)
if (serverTrip.BoundingBox != null)
{
// Update bounding box metadata from server
localTrip.BoundingBoxNorth = serverTrip.BoundingBox.North;
localTrip.BoundingBoxSouth = serverTrip.BoundingBox.South;
localTrip.BoundingBoxEast = serverTrip.BoundingBox.East;
localTrip.BoundingBoxWest = serverTrip.BoundingBox.West;

_logger.LogInformation("Bounding box changed for trip {TripName}", localTrip.Name);
}

// Update version and timestamps
Expand All @@ -208,7 +199,7 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
_logger.LogInformation("Trip metadata synced: {TripName} (v{Version}, {PlaceCount} places, {SegmentCount} segments)",
localTrip.Name, localTrip.Version, places.Count, segments.Count);

return (localTrip, boundingBoxChanged);
return localTrip;
}
catch (OperationCanceledException)
{
Expand All @@ -218,7 +209,7 @@ public async Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync()
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync trip metadata: {TripId}", tripServerId);
return (null, false);
return null;
}
}

Expand Down Expand Up @@ -250,7 +241,7 @@ public async Task<int> SyncAllTripsMetadataAsync(CancellationToken cancellationT

try
{
var (result, _) = await SyncTripMetadataAsync(trip.ServerId, forceSync: false, cancellationToken: cancellationToken);
var result = await SyncTripMetadataAsync(trip.ServerId, forceSync: false, cancellationToken: cancellationToken);
if (result != null)
{
syncedCount++;
Expand Down Expand Up @@ -487,17 +478,6 @@ public async Task<List<TripSegment>> GetOfflineSegmentsAsync(Guid tripServerId)
}).ToList();
}

/// <inheritdoc/>
public bool HasBoundingBoxChangedSignificantly(DownloadedTripEntity trip, BoundingBox serverBoundingBox)
{
const double threshold = 0.01; // ~1km at equator

return Math.Abs(trip.BoundingBoxNorth - serverBoundingBox.North) > threshold ||
Math.Abs(trip.BoundingBoxSouth - serverBoundingBox.South) > threshold ||
Math.Abs(trip.BoundingBoxEast - serverBoundingBox.East) > threshold ||
Math.Abs(trip.BoundingBoxWest - serverBoundingBox.West) > threshold;
}

/// <summary>
/// Checks if network is available.
/// </summary>
Expand Down
3 changes: 1 addition & 2 deletions src/WayfarerMobile/Services/TripSyncCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ public TripSyncCoordinator(ITripContentService content, ITripRepository trips, I
public async Task<DownloadedTripEntity?> SyncTripAsync(Guid tripServerId, bool forceSync = false, CancellationToken cancellationToken = default)
{
var progress = new Progress<DownloadProgressEventArgs>(e => ProgressChanged?.Invoke(this, e));
var (trip, _) = await _content.SyncTripMetadataAsync(tripServerId, forceSync, progress, cancellationToken);
return trip;
return await _content.SyncTripMetadataAsync(tripServerId, forceSync, progress, cancellationToken);
}

public Task<List<DownloadedTripEntity>> GetTripsNeedingUpdateAsync() => _content.GetTripsNeedingUpdateAsync();
Expand Down
2 changes: 0 additions & 2 deletions src/WayfarerMobile/ViewModels/TripDownloadViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ private void OnProgressChanged(object? sender, Core.Interfaces.DownloadProgressE
if (_serverId is { } id) _callbacks?.UpdateItemProgress(id, DownloadProgress, e.ProgressPercent < 100);
}

public Task RefreshPausedDownloadsCountAsync() => Task.CompletedTask;

public void Dispose()
{
_downloads.ProgressChanged -= OnProgressChanged;
Expand Down
Loading
Loading