mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-04-22 07:16:39 +00:00
geo fences - wip
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/.vs/
|
||||||
|
**/.vscode/
|
||||||
|
**/*.user
|
||||||
|
**/node_modules/
|
||||||
|
logs/
|
||||||
|
tiles/
|
||||||
|
ready/
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
@@ -23,9 +23,9 @@ builder.Services.Configure<MapConfig>(builder.Configuration.GetSection("MapConfi
|
|||||||
builder.Services.Configure<StorageConfig>(builder.Configuration.GetSection("StorageConfig"));
|
builder.Services.Configure<StorageConfig>(builder.Configuration.GetSection("StorageConfig"));
|
||||||
builder.Services.Configure<ProcessingConfig>(builder.Configuration.GetSection("ProcessingConfig"));
|
builder.Services.Configure<ProcessingConfig>(builder.Configuration.GetSection("ProcessingConfig"));
|
||||||
|
|
||||||
builder.Services.AddSingleton<ITileRepository>(sp => new TileRepository(connectionString));
|
builder.Services.AddSingleton<ITileRepository>(sp => new TileRepository(connectionString, sp.GetRequiredService<ILogger<TileRepository>>()));
|
||||||
builder.Services.AddSingleton<IRegionRepository>(sp => new RegionRepository(connectionString));
|
builder.Services.AddSingleton<IRegionRepository>(sp => new RegionRepository(connectionString, sp.GetRequiredService<ILogger<RegionRepository>>()));
|
||||||
builder.Services.AddSingleton<IRouteRepository>(sp => new RouteRepository(connectionString));
|
builder.Services.AddSingleton<IRouteRepository>(sp => new RouteRepository(connectionString, sp.GetRequiredService<ILogger<RouteRepository>>()));
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddSingleton<GoogleMapsDownloaderV2>();
|
builder.Services.AddSingleton<GoogleMapsDownloaderV2>();
|
||||||
@@ -42,6 +42,12 @@ builder.Services.AddHostedService<RegionProcessingService>();
|
|||||||
builder.Services.AddSingleton<IRouteService, RouteService>();
|
builder.Services.AddSingleton<IRouteService, RouteService>();
|
||||||
builder.Services.AddHostedService<RouteProcessingService>();
|
builder.Services.AddHostedService<RouteProcessingService>();
|
||||||
|
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
|
||||||
|
options.SerializerOptions.PropertyNameCaseInsensitive = true;
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
builder.Services.AddSwaggerGen(c =>
|
||||||
{
|
{
|
||||||
@@ -236,8 +242,35 @@ async Task<IResult> CreateRoute([FromBody] CreateRouteRequest request, IRouteSer
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogInformation("Route creation request: ID={Id}, Name={Name}, Points={PointCount}, RegionSize={RegionSize}m, Zoom={Zoom}",
|
logger.LogInformation("Route creation request: ID={Id}, Name={Name}, Points={PointCount}, RegionSize={RegionSize}m, Zoom={Zoom}, RequestMaps={RequestMaps}",
|
||||||
request.Id, request.Name, request.Points.Count, request.RegionSizeMeters, request.ZoomLevel);
|
request.Id, request.Name, request.Points.Count, request.RegionSizeMeters, request.ZoomLevel, request.RequestMaps);
|
||||||
|
|
||||||
|
if (request.Points.Count > 0)
|
||||||
|
{
|
||||||
|
var firstPoint = request.Points[0];
|
||||||
|
logger.LogInformation("First point: Lat={Lat}, Lon={Lon}", firstPoint.Latitude, firstPoint.Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Geofences != null)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Geofences object received: Polygons count = {Count}", request.Geofences.Polygons?.Count ?? 0);
|
||||||
|
|
||||||
|
if (request.Geofences.Polygons != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < request.Geofences.Polygons.Count; i++)
|
||||||
|
{
|
||||||
|
var polygon = request.Geofences.Polygons[i];
|
||||||
|
logger.LogInformation("Geofence {Index}: NorthWest = {NW}, SouthEast = {SE}",
|
||||||
|
i,
|
||||||
|
polygon.NorthWest is not null ? $"({polygon.NorthWest.Lat}, {polygon.NorthWest.Lon})" : "null",
|
||||||
|
polygon.SouthEast is not null ? $"({polygon.SouthEast.Lat}, {polygon.SouthEast.Lon})" : "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("No geofences provided (Geofences is null)");
|
||||||
|
}
|
||||||
|
|
||||||
var route = await routeService.CreateRouteAsync(request);
|
var route = await routeService.CreateRouteAsync(request);
|
||||||
return Results.Ok(route);
|
return Results.Ok(route);
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
### Create Route with Geofences
|
||||||
|
POST http://localhost:5000/api/satellite/route
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "{{$guid}}",
|
||||||
|
"name": "Route with Geofences",
|
||||||
|
"description": "Test route with two geofence regions",
|
||||||
|
"regionSizeMeters": 500,
|
||||||
|
"zoomLevel": 18,
|
||||||
|
"requestMaps": true,
|
||||||
|
"geofences": {
|
||||||
|
"polygons": [
|
||||||
|
{
|
||||||
|
"northWest": {
|
||||||
|
"lat": 48.28022277841604,
|
||||||
|
"lon": 37.37548828125001
|
||||||
|
},
|
||||||
|
"southEast": {
|
||||||
|
"lat": 48.2720540660028,
|
||||||
|
"lon": 37.3901653289795
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"northWest": {
|
||||||
|
"lat": 48.2614270732573,
|
||||||
|
"lon": 37.35239982604981
|
||||||
|
},
|
||||||
|
"southEast": {
|
||||||
|
"lat": 48.24988342757033,
|
||||||
|
"lon": 37.37943649291993
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"lat": 48.276067180586544,
|
||||||
|
"lon": 37.38445758819581
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.27074009522731,
|
||||||
|
"lon": 37.374029159545906
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.263312668696855,
|
||||||
|
"lon": 37.37707614898682
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.26539817051818,
|
||||||
|
"lon": 37.36587524414063
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.25851283439989,
|
||||||
|
"lon": 37.35952377319337
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.254426906081555,
|
||||||
|
"lon": 37.374801635742195
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.25914140977405,
|
||||||
|
"lon": 37.39068031311036
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.25354110233028,
|
||||||
|
"lon": 37.401752471923835
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.25902712391726,
|
||||||
|
"lon": 37.416257858276374
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.26828345053738,
|
||||||
|
"lon": 37.402009963989265
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
### Create Route without Geofences (backward compatible)
|
||||||
|
POST http://localhost:5000/api/satellite/route
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "{{$guid}}",
|
||||||
|
"name": "Simple Route",
|
||||||
|
"description": "Route without geofences",
|
||||||
|
"regionSizeMeters": 500,
|
||||||
|
"zoomLevel": 18,
|
||||||
|
"requestMaps": true,
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"lat": 48.276067180586544,
|
||||||
|
"lon": 37.38445758819581
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": 48.27074009522731,
|
||||||
|
"lon": 37.374029159545906
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SatelliteProvider.Common.DTO;
|
namespace SatelliteProvider.Common.DTO;
|
||||||
|
|
||||||
public class CreateRouteRequest
|
public class CreateRouteRequest
|
||||||
@@ -7,7 +9,12 @@ public class CreateRouteRequest
|
|||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public double RegionSizeMeters { get; set; }
|
public double RegionSizeMeters { get; set; }
|
||||||
public int ZoomLevel { get; set; }
|
public int ZoomLevel { get; set; }
|
||||||
|
|
||||||
public List<RoutePoint> Points { get; set; } = new();
|
public List<RoutePoint> Points { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("geofences")]
|
||||||
|
public Geofences? Geofences { get; set; }
|
||||||
|
|
||||||
public bool RequestMaps { get; set; } = false;
|
public bool RequestMaps { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SatelliteProvider.Common.DTO;
|
namespace SatelliteProvider.Common.DTO;
|
||||||
|
|
||||||
public class GeoPoint
|
public class GeoPoint
|
||||||
{
|
{
|
||||||
const double PRECISION_TOLERANCE = 0.00005;
|
const double PRECISION_TOLERANCE = 0.00005;
|
||||||
public double Lat { get; }
|
|
||||||
public double Lon { get; }
|
[JsonPropertyName("lat")]
|
||||||
|
public double Lat { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lon")]
|
||||||
|
public double Lon { get; set; }
|
||||||
|
|
||||||
public GeoPoint() { }
|
public GeoPoint() { }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SatelliteProvider.Common.DTO;
|
||||||
|
|
||||||
|
public class GeofencePolygon
|
||||||
|
{
|
||||||
|
[JsonPropertyName("northWest")]
|
||||||
|
public GeoPoint? NorthWest { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("southEast")]
|
||||||
|
public GeoPoint? SouthEast { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Geofences
|
||||||
|
{
|
||||||
|
[JsonPropertyName("polygons")]
|
||||||
|
public List<GeofencePolygon> Polygons { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace SatelliteProvider.Common.DTO;
|
namespace SatelliteProvider.Common.DTO;
|
||||||
|
|
||||||
public class RoutePoint
|
public class RoutePoint
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("lat")]
|
||||||
public double Latitude { get; set; }
|
public double Latitude { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lon")]
|
||||||
public double Longitude { get; set; }
|
public double Longitude { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,16 @@ public static class GeoUtils
|
|||||||
{
|
{
|
||||||
return p1.DirectionTo(p2).Distance;
|
return p1.DirectionTo(p2).Distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GeoPoint CalculateCenter(GeoPoint northWest, GeoPoint southEast)
|
||||||
|
{
|
||||||
|
var centerLat = (northWest.Lat + southEast.Lat) / 2.0;
|
||||||
|
var centerLon = (northWest.Lon + southEast.Lon) / 2.0;
|
||||||
|
return new GeoPoint(centerLat, centerLon);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double CalculatePolygonDiagonalDistance(GeoPoint northWest, GeoPoint southEast)
|
||||||
|
{
|
||||||
|
return CalculateDistance(northWest, southEast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE route_regions ADD COLUMN is_geofence BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
@@ -10,8 +10,9 @@ public interface IRouteRepository
|
|||||||
Task InsertRoutePointsAsync(IEnumerable<RoutePointEntity> points);
|
Task InsertRoutePointsAsync(IEnumerable<RoutePointEntity> points);
|
||||||
Task<int> UpdateRouteAsync(RouteEntity route);
|
Task<int> UpdateRouteAsync(RouteEntity route);
|
||||||
Task<int> DeleteRouteAsync(Guid id);
|
Task<int> DeleteRouteAsync(Guid id);
|
||||||
Task LinkRouteToRegionAsync(Guid routeId, Guid regionId);
|
Task LinkRouteToRegionAsync(Guid routeId, Guid regionId, bool isGeofence = false);
|
||||||
Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId);
|
Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId);
|
||||||
|
Task<IEnumerable<Guid>> GetGeofenceRegionIdsByRouteAsync(Guid routeId);
|
||||||
Task<IEnumerable<RouteEntity>> GetRoutesWithPendingMapsAsync();
|
Task<IEnumerable<RouteEntity>> GetRoutesWithPendingMapsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using SatelliteProvider.DataAccess.Models;
|
using SatelliteProvider.DataAccess.Models;
|
||||||
|
|
||||||
@@ -7,10 +8,12 @@ namespace SatelliteProvider.DataAccess.Repositories;
|
|||||||
public class RegionRepository : IRegionRepository
|
public class RegionRepository : IRegionRepository
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger<RegionRepository> _logger;
|
||||||
|
|
||||||
public RegionRepository(string connectionString)
|
public RegionRepository(string connectionString, ILogger<RegionRepository> logger)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RegionEntity?> GetByIdAsync(Guid id)
|
public async Task<RegionEntity?> GetByIdAsync(Guid id)
|
||||||
@@ -26,7 +29,15 @@ public class RegionRepository : IRegionRepository
|
|||||||
FROM regions
|
FROM regions
|
||||||
WHERE id = @Id";
|
WHERE id = @Id";
|
||||||
|
|
||||||
return await connection.QuerySingleOrDefaultAsync<RegionEntity>(sql, new { Id = id });
|
var region = await connection.QuerySingleOrDefaultAsync<RegionEntity>(sql, new { Id = id });
|
||||||
|
|
||||||
|
if (region != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RegionRepository - Read region {RegionId} from DB: Lat={Lat:F12}, Lon={Lon:F12}, Status={Status}",
|
||||||
|
id, region.Latitude, region.Longitude, region.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return region;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<RegionEntity>> GetByStatusAsync(string status)
|
public async Task<IEnumerable<RegionEntity>> GetByStatusAsync(string status)
|
||||||
@@ -60,6 +71,9 @@ public class RegionRepository : IRegionRepository
|
|||||||
@CreatedAt, @UpdatedAt)
|
@CreatedAt, @UpdatedAt)
|
||||||
RETURNING id";
|
RETURNING id";
|
||||||
|
|
||||||
|
_logger.LogInformation("RegionRepository - Inserting region {RegionId} to DB: Lat={Lat:F12}, Lon={Lon:F12}, Status={Status}",
|
||||||
|
region.Id, region.Latitude, region.Longitude, region.Status);
|
||||||
|
|
||||||
return await connection.ExecuteScalarAsync<Guid>(sql, region);
|
return await connection.ExecuteScalarAsync<Guid>(sql, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using SatelliteProvider.DataAccess.Models;
|
using SatelliteProvider.DataAccess.Models;
|
||||||
|
|
||||||
@@ -7,10 +8,12 @@ namespace SatelliteProvider.DataAccess.Repositories;
|
|||||||
public class RouteRepository : IRouteRepository
|
public class RouteRepository : IRouteRepository
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger<RouteRepository> _logger;
|
||||||
|
|
||||||
public RouteRepository(string connectionString)
|
public RouteRepository(string connectionString, ILogger<RouteRepository> logger)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RouteEntity?> GetByIdAsync(Guid id)
|
public async Task<RouteEntity?> GetByIdAsync(Guid id)
|
||||||
@@ -40,7 +43,16 @@ public class RouteRepository : IRouteRepository
|
|||||||
WHERE route_id = @RouteId
|
WHERE route_id = @RouteId
|
||||||
ORDER BY sequence_number";
|
ORDER BY sequence_number";
|
||||||
|
|
||||||
return await connection.QueryAsync<RoutePointEntity>(sql, new { RouteId = routeId });
|
var points = (await connection.QueryAsync<RoutePointEntity>(sql, new { RouteId = routeId })).ToList();
|
||||||
|
|
||||||
|
if (points.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RouteRepository - Read {Count} points from DB for route {RouteId}. First: Lat={Lat:F12}, Lon={Lon:F12}, Last: Lat={LastLat:F12}, Lon={LastLon:F12}",
|
||||||
|
points.Count, routeId, points[0].Latitude, points[0].Longitude,
|
||||||
|
points[^1].Latitude, points[^1].Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Guid> InsertRouteAsync(RouteEntity route)
|
public async Task<Guid> InsertRouteAsync(RouteEntity route)
|
||||||
@@ -69,7 +81,15 @@ public class RouteRepository : IRouteRepository
|
|||||||
VALUES (@Id, @RouteId, @SequenceNumber, @Latitude, @Longitude,
|
VALUES (@Id, @RouteId, @SequenceNumber, @Latitude, @Longitude,
|
||||||
@PointType, @SegmentIndex, @DistanceFromPrevious, @CreatedAt)";
|
@PointType, @SegmentIndex, @DistanceFromPrevious, @CreatedAt)";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sql, points);
|
var pointsList = points.ToList();
|
||||||
|
if (pointsList.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RouteRepository - Inserting {Count} points to DB. First: Lat={Lat:F12}, Lon={Lon:F12}, Last: Lat={LastLat:F12}, Lon={LastLon:F12}",
|
||||||
|
pointsList.Count, pointsList[0].Latitude, pointsList[0].Longitude,
|
||||||
|
pointsList[^1].Latitude, pointsList[^1].Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(sql, pointsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> UpdateRouteAsync(RouteEntity route)
|
public async Task<int> UpdateRouteAsync(RouteEntity route)
|
||||||
@@ -101,15 +121,15 @@ public class RouteRepository : IRouteRepository
|
|||||||
return await connection.ExecuteAsync(sql, new { Id = id });
|
return await connection.ExecuteAsync(sql, new { Id = id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LinkRouteToRegionAsync(Guid routeId, Guid regionId)
|
public async Task LinkRouteToRegionAsync(Guid routeId, Guid regionId, bool isGeofence = false)
|
||||||
{
|
{
|
||||||
using var connection = new NpgsqlConnection(_connectionString);
|
using var connection = new NpgsqlConnection(_connectionString);
|
||||||
const string sql = @"
|
const string sql = @"
|
||||||
INSERT INTO route_regions (route_id, region_id, created_at)
|
INSERT INTO route_regions (route_id, region_id, is_geofence, created_at)
|
||||||
VALUES (@RouteId, @RegionId, @CreatedAt)
|
VALUES (@RouteId, @RegionId, @IsGeofence, @CreatedAt)
|
||||||
ON CONFLICT (route_id, region_id) DO NOTHING";
|
ON CONFLICT (route_id, region_id) DO NOTHING";
|
||||||
|
|
||||||
await connection.ExecuteAsync(sql, new { RouteId = routeId, RegionId = regionId, CreatedAt = DateTime.UtcNow });
|
await connection.ExecuteAsync(sql, new { RouteId = routeId, RegionId = regionId, IsGeofence = isGeofence, CreatedAt = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId)
|
public async Task<IEnumerable<Guid>> GetRegionIdsByRouteAsync(Guid routeId)
|
||||||
@@ -118,7 +138,18 @@ public class RouteRepository : IRouteRepository
|
|||||||
const string sql = @"
|
const string sql = @"
|
||||||
SELECT region_id
|
SELECT region_id
|
||||||
FROM route_regions
|
FROM route_regions
|
||||||
WHERE route_id = @RouteId";
|
WHERE route_id = @RouteId AND (is_geofence = false OR is_geofence IS NULL)";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Guid>> GetGeofenceRegionIdsByRouteAsync(Guid routeId)
|
||||||
|
{
|
||||||
|
using var connection = new NpgsqlConnection(_connectionString);
|
||||||
|
const string sql = @"
|
||||||
|
SELECT region_id
|
||||||
|
FROM route_regions
|
||||||
|
WHERE route_id = @RouteId AND is_geofence = true";
|
||||||
|
|
||||||
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
|
return await connection.QueryAsync<Guid>(sql, new { RouteId = routeId });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using SatelliteProvider.DataAccess.Models;
|
using SatelliteProvider.DataAccess.Models;
|
||||||
|
|
||||||
@@ -7,10 +8,12 @@ namespace SatelliteProvider.DataAccess.Repositories;
|
|||||||
public class TileRepository : ITileRepository
|
public class TileRepository : ITileRepository
|
||||||
{
|
{
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger<TileRepository> _logger;
|
||||||
|
|
||||||
public TileRepository(string connectionString)
|
public TileRepository(string connectionString, ILogger<TileRepository> logger)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TileEntity?> GetByIdAsync(Guid id)
|
public async Task<TileEntity?> GetByIdAsync(Guid id)
|
||||||
@@ -24,7 +27,15 @@ public class TileRepository : ITileRepository
|
|||||||
FROM tiles
|
FROM tiles
|
||||||
WHERE id = @Id";
|
WHERE id = @Id";
|
||||||
|
|
||||||
return await connection.QuerySingleOrDefaultAsync<TileEntity>(sql, new { Id = id });
|
var tile = await connection.QuerySingleOrDefaultAsync<TileEntity>(sql, new { Id = id });
|
||||||
|
|
||||||
|
if (tile != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("TileRepository - Read tile {TileId} from DB: Lat={Lat:F12}, Lon={Lon:F12}, Zoom={Zoom}",
|
||||||
|
id, tile.Latitude, tile.Longitude, tile.ZoomLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TileEntity?> FindExistingTileAsync(double latitude, double longitude, double tileSizeMeters, int zoomLevel, int version)
|
public async Task<TileEntity?> FindExistingTileAsync(double latitude, double longitude, double tileSizeMeters, int zoomLevel, int version)
|
||||||
@@ -105,6 +116,9 @@ public class TileRepository : ITileRepository
|
|||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
RETURNING id";
|
RETURNING id";
|
||||||
|
|
||||||
|
_logger.LogInformation("TileRepository - Inserting tile {TileId} to DB: Lat={Lat:F12}, Lon={Lon:F12}, Zoom={Zoom}",
|
||||||
|
tile.Id, tile.Latitude, tile.Longitude, tile.ZoomLevel);
|
||||||
|
|
||||||
return await connection.ExecuteScalarAsync<Guid>(sql, tile);
|
return await connection.ExecuteScalarAsync<Guid>(sql, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,32 @@ public record RegionStatusResponse
|
|||||||
|
|
||||||
public class RoutePointInput
|
public class RoutePointInput
|
||||||
{
|
{
|
||||||
public double Latitude { get; set; }
|
[System.Text.Json.Serialization.JsonPropertyName("lat")]
|
||||||
public double Longitude { get; set; }
|
public double Lat { get; set; }
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("lon")]
|
||||||
|
public double Lon { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeoPointInput
|
||||||
|
{
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("lat")]
|
||||||
|
public double Lat { get; set; }
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("lon")]
|
||||||
|
public double Lon { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeofencePolygonInput
|
||||||
|
{
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("northWest")]
|
||||||
|
public GeoPointInput? NorthWest { get; set; }
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("southEast")]
|
||||||
|
public GeoPointInput? SouthEast { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeofencesInput
|
||||||
|
{
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("polygons")]
|
||||||
|
public List<GeofencePolygonInput> Polygons { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateRouteRequest
|
public class CreateRouteRequest
|
||||||
@@ -51,6 +75,8 @@ public class CreateRouteRequest
|
|||||||
public double RegionSizeMeters { get; set; }
|
public double RegionSizeMeters { get; set; }
|
||||||
public int ZoomLevel { get; set; }
|
public int ZoomLevel { get; set; }
|
||||||
public List<RoutePointInput> Points { get; set; } = new();
|
public List<RoutePointInput> Points { get; set; } = new();
|
||||||
|
[System.Text.Json.Serialization.JsonPropertyName("geofences")]
|
||||||
|
public GeofencesInput? Geofences { get; set; }
|
||||||
public bool RequestMaps { get; set; } = false;
|
public bool RequestMaps { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,21 +24,21 @@ class Program
|
|||||||
Console.WriteLine("✓ API is ready");
|
Console.WriteLine("✓ API is ready");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
await TileTests.RunGetTileByLatLonTest(httpClient);
|
// await TileTests.RunGetTileByLatLonTest(httpClient);
|
||||||
|
|
||||||
await RegionTests.RunRegionProcessingTest_200m_Zoom18(httpClient);
|
// await RegionTests.RunRegionProcessingTest_200m_Zoom18(httpClient);
|
||||||
|
|
||||||
await RegionTests.RunRegionProcessingTest_400m_Zoom17(httpClient);
|
// await RegionTests.RunRegionProcessingTest_400m_Zoom17(httpClient);
|
||||||
|
|
||||||
await RegionTests.RunRegionProcessingTest_500m_Zoom18(httpClient);
|
// await RegionTests.RunRegionProcessingTest_500m_Zoom18(httpClient);
|
||||||
|
|
||||||
await RouteTests.RunSimpleRouteTest(httpClient);
|
// await RouteTests.RunSimpleRouteTest(httpClient);
|
||||||
|
|
||||||
await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient);
|
// await RouteTests.RunRouteWithRegionProcessingAndStitching(httpClient);
|
||||||
|
|
||||||
await RouteTests.RunComplexRouteWithStitching(httpClient);
|
await RouteTests.RunComplexRouteWithStitching(httpClient);
|
||||||
|
|
||||||
await RouteTests.RunExtendedRouteEast(httpClient);
|
// await RouteTests.RunExtendedRouteEast(httpClient);
|
||||||
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("=========================");
|
Console.WriteLine("=========================");
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ public static class RouteTests
|
|||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonWriteOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
public static async Task RunSimpleRouteTest(HttpClient httpClient)
|
public static async Task RunSimpleRouteTest(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Test: Create Simple Route with Two Points");
|
Console.WriteLine("Test: Create Simple Route with Two Points");
|
||||||
@@ -25,19 +30,19 @@ public static class RouteTests
|
|||||||
ZoomLevel = 18,
|
ZoomLevel = 18,
|
||||||
Points = new List<RoutePointInput>
|
Points = new List<RoutePointInput>
|
||||||
{
|
{
|
||||||
new() { Latitude = 48.276067180586544, Longitude = 37.38445758819581 },
|
new() { Lat = 48.276067180586544, Lon = 37.38445758819581 },
|
||||||
new() { Latitude = 48.27074009522731, Longitude = 37.374029159545906 }
|
new() { Lat = 48.27074009522731, Lon = 37.374029159545906 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Console.WriteLine($"Creating route with 2 points:");
|
Console.WriteLine($"Creating route with 2 points:");
|
||||||
Console.WriteLine($" Start: ({request.Points[0].Latitude}, {request.Points[0].Longitude})");
|
Console.WriteLine($" Start: ({request.Points[0].Lat}, {request.Points[0].Lon})");
|
||||||
Console.WriteLine($" End: ({request.Points[1].Latitude}, {request.Points[1].Longitude})");
|
Console.WriteLine($" End: ({request.Points[1].Lat}, {request.Points[1].Lon})");
|
||||||
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
||||||
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var response = await httpClient.PostAsJsonAsync("/api/satellite/route", request);
|
var response = await httpClient.PostAsJsonAsync("/api/satellite/route", request, JsonWriteOptions);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -136,20 +141,20 @@ public static class RouteTests
|
|||||||
RequestMaps = true,
|
RequestMaps = true,
|
||||||
Points = new List<RoutePointInput>
|
Points = new List<RoutePointInput>
|
||||||
{
|
{
|
||||||
new() { Latitude = 48.276067180586544, Longitude = 37.38445758819581 },
|
new() { Lat = 48.276067180586544, Lon = 37.38445758819581 },
|
||||||
new() { Latitude = 48.27074009522731, Longitude = 37.374029159545906 }
|
new() { Lat = 48.27074009522731, Lon = 37.374029159545906 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Console.WriteLine("Step 1: Creating route with RequestMaps=true");
|
Console.WriteLine("Step 1: Creating route with RequestMaps=true");
|
||||||
Console.WriteLine($" Start: ({request.Points[0].Latitude}, {request.Points[0].Longitude})");
|
Console.WriteLine($" Start: ({request.Points[0].Lat}, {request.Points[0].Lon})");
|
||||||
Console.WriteLine($" End: ({request.Points[1].Latitude}, {request.Points[1].Longitude})");
|
Console.WriteLine($" End: ({request.Points[1].Lat}, {request.Points[1].Lon})");
|
||||||
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
||||||
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
||||||
Console.WriteLine($" Request Maps: {request.RequestMaps}");
|
Console.WriteLine($" Request Maps: {request.RequestMaps}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request);
|
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request, JsonWriteOptions);
|
||||||
|
|
||||||
if (!routeResponse.IsSuccessStatusCode)
|
if (!routeResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -287,48 +292,72 @@ public static class RouteTests
|
|||||||
public static async Task RunComplexRouteWithStitching(HttpClient httpClient)
|
public static async Task RunComplexRouteWithStitching(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("Test: Complex Route with 10 Points - Region Processing and Stitching");
|
Console.WriteLine("Test: Complex Route with 10 Points + 2 Geofences - Region Processing and Stitching");
|
||||||
Console.WriteLine("=======================================================================");
|
Console.WriteLine("======================================================================================");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var routeId = Guid.NewGuid();
|
var routeId = Guid.NewGuid();
|
||||||
var request = new CreateRouteRequest
|
var request = new CreateRouteRequest
|
||||||
{
|
{
|
||||||
Id = routeId,
|
Id = routeId,
|
||||||
Name = "Complex Route with 10 Points",
|
Name = "Complex Route with 10 Points + 2 Geofences",
|
||||||
Description = "Test route with 10 action points for complex map stitching",
|
Description = "Test route with 10 action points and 2 geofence regions for complex map stitching",
|
||||||
RegionSizeMeters = 300.0,
|
RegionSizeMeters = 300.0,
|
||||||
ZoomLevel = 18,
|
ZoomLevel = 18,
|
||||||
RequestMaps = true,
|
RequestMaps = true,
|
||||||
Points = new List<RoutePointInput>
|
Points = new List<RoutePointInput>
|
||||||
{
|
{
|
||||||
new() { Latitude = 48.276067180586544, Longitude = 37.38445758819581 },
|
new() { Lat = 48.276067180586544, Lon = 37.38445758819581 },
|
||||||
new() { Latitude = 48.27074009522731, Longitude = 37.374029159545906 },
|
new() { Lat = 48.27074009522731, Lon = 37.374029159545906 },
|
||||||
new() { Latitude = 48.263312668696855, Longitude = 37.37707614898682 },
|
new() { Lat = 48.263312668696855, Lon = 37.37707614898682 },
|
||||||
new() { Latitude = 48.26539817051818, Longitude = 37.36587524414063 },
|
new() { Lat = 48.26539817051818, Lon = 37.36587524414063 },
|
||||||
new() { Latitude = 48.25851283439989, Longitude = 37.35952377319337 },
|
new() { Lat = 48.25851283439989, Lon = 37.35952377319337 },
|
||||||
new() { Latitude = 48.254426906081555, Longitude = 37.374801635742195 },
|
new() { Lat = 48.254426906081555, Lon = 37.374801635742195 },
|
||||||
new() { Latitude = 48.25914140977405, Longitude = 37.39068031311036 },
|
new() { Lat = 48.25914140977405, Lon = 37.39068031311036 },
|
||||||
new() { Latitude = 48.25354110233028, Longitude = 37.401752471923835 },
|
new() { Lat = 48.25354110233028, Lon = 37.401752471923835 },
|
||||||
new() { Latitude = 48.25902712391726, Longitude = 37.416257858276374 },
|
new() { Lat = 48.25902712391726, Lon = 37.416257858276374 },
|
||||||
new() { Latitude = 48.26828345053738, Longitude = 37.402009963989265 }
|
new() { Lat = 48.26828345053738, Lon = 37.402009963989265 }
|
||||||
|
},
|
||||||
|
Geofences = new GeofencesInput
|
||||||
|
{
|
||||||
|
Polygons = new List<GeofencePolygonInput>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
NorthWest = new GeoPointInput { Lat = 48.28022277841604, Lon = 37.37548828125001 },
|
||||||
|
SouthEast = new GeoPointInput { Lat = 48.2720540660028, Lon = 37.3901653289795 }
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
NorthWest = new GeoPointInput { Lat = 48.2614270732573, Lon = 37.35239982604981 },
|
||||||
|
SouthEast = new GeoPointInput { Lat = 48.24988342757033, Lon = 37.37943649291993 }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Console.WriteLine("Step 1: Creating complex route with RequestMaps=true");
|
Console.WriteLine("Step 1: Creating complex route with RequestMaps=true and 2 geofences");
|
||||||
Console.WriteLine($" Action Points: {request.Points.Count}");
|
Console.WriteLine($" Action Points: {request.Points.Count}");
|
||||||
|
Console.WriteLine($" Geofence Regions: {request.Geofences.Polygons.Count}");
|
||||||
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
Console.WriteLine($" Region Size: {request.RegionSizeMeters}m");
|
||||||
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
Console.WriteLine($" Zoom Level: {request.ZoomLevel}");
|
||||||
Console.WriteLine($" Request Maps: {request.RequestMaps}");
|
Console.WriteLine($" Request Maps: {request.RequestMaps}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Geofence Regions:");
|
||||||
|
for (int i = 0; i < request.Geofences.Polygons.Count; i++)
|
||||||
|
{
|
||||||
|
var polygon = request.Geofences.Polygons[i];
|
||||||
|
Console.WriteLine($" {i + 1}. NW: ({polygon.NorthWest.Lat}, {polygon.NorthWest.Lon}) -> SE: ({polygon.SouthEast.Lat}, {polygon.SouthEast.Lon})");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
Console.WriteLine("Route Path:");
|
Console.WriteLine("Route Path:");
|
||||||
for (int i = 0; i < request.Points.Count; i++)
|
for (int i = 0; i < request.Points.Count; i++)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {i + 1}. ({request.Points[i].Latitude}, {request.Points[i].Longitude})");
|
Console.WriteLine($" {i + 1}. ({request.Points[i].Lat}, {request.Points[i].Lon})");
|
||||||
}
|
}
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request);
|
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request, JsonWriteOptions);
|
||||||
|
|
||||||
if (!routeResponse.IsSuccessStatusCode)
|
if (!routeResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -387,7 +416,7 @@ public static class RouteTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Step 2: Waiting for complex route maps to be ready");
|
Console.WriteLine("Step 2: Waiting for complex route maps to be ready");
|
||||||
Console.WriteLine(" (Processing 56 regions SEQUENTIALLY to avoid API throttling)");
|
Console.WriteLine(" (Processing route point regions + 2 geofence regions SEQUENTIALLY to avoid API throttling)");
|
||||||
Console.WriteLine(" (This will take several minutes as each region is processed one at a time)");
|
Console.WriteLine(" (This will take several minutes as each region is processed one at a time)");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
@@ -486,13 +515,14 @@ public static class RouteTests
|
|||||||
Console.WriteLine($" Total Points: {finalRoute.TotalPoints}");
|
Console.WriteLine($" Total Points: {finalRoute.TotalPoints}");
|
||||||
Console.WriteLine($" Action Points: {actionPoints}");
|
Console.WriteLine($" Action Points: {actionPoints}");
|
||||||
Console.WriteLine($" Distance: {finalRoute.TotalDistanceMeters:F2}m");
|
Console.WriteLine($" Distance: {finalRoute.TotalDistanceMeters:F2}m");
|
||||||
|
Console.WriteLine($" Geofence Regions: 2");
|
||||||
Console.WriteLine($" Unique Tiles: {uniqueTileCount}");
|
Console.WriteLine($" Unique Tiles: {uniqueTileCount}");
|
||||||
Console.WriteLine($" Maps Ready: {finalRoute.MapsReady}");
|
Console.WriteLine($" Maps Ready: {finalRoute.MapsReady}");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
if (uniqueTileCount < 10)
|
if (uniqueTileCount < 10)
|
||||||
{
|
{
|
||||||
throw new Exception($"Expected at least 10 unique tiles for complex route, got {uniqueTileCount}");
|
throw new Exception($"Expected at least 10 unique tiles for complex route with geofences, got {uniqueTileCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stitchedInfo.Length < 1024)
|
if (stitchedInfo.Length < 1024)
|
||||||
@@ -500,7 +530,7 @@ public static class RouteTests
|
|||||||
throw new Exception($"Stitched image seems too small: {stitchedInfo.Length} bytes");
|
throw new Exception($"Stitched image seems too small: {stitchedInfo.Length} bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("✓ Complex Route with 10 Points Test: PASSED");
|
Console.WriteLine("✓ Complex Route with 10 Points + 2 Geofences Test: PASSED");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task RunExtendedRouteEast(HttpClient httpClient)
|
public static async Task RunExtendedRouteEast(HttpClient httpClient)
|
||||||
@@ -521,26 +551,26 @@ public static class RouteTests
|
|||||||
RequestMaps = true,
|
RequestMaps = true,
|
||||||
Points = new List<RoutePointInput>
|
Points = new List<RoutePointInput>
|
||||||
{
|
{
|
||||||
new() { Latitude = 48.276067180586544, Longitude = 37.51945758819581 },
|
new() { Lat = 48.276067180586544, Lon = 37.51945758819581 },
|
||||||
new() { Latitude = 48.27074009522731, Longitude = 37.509029159545906 },
|
new() { Lat = 48.27074009522731, Lon = 37.509029159545906 },
|
||||||
new() { Latitude = 48.263312668696855, Longitude = 37.51207614898682 },
|
new() { Lat = 48.263312668696855, Lon = 37.51207614898682 },
|
||||||
new() { Latitude = 48.26539817051818, Longitude = 37.50087524414063 },
|
new() { Lat = 48.26539817051818, Lon = 37.50087524414063 },
|
||||||
new() { Latitude = 48.25851283439989, Longitude = 37.49452377319337 },
|
new() { Lat = 48.25851283439989, Lon = 37.49452377319337 },
|
||||||
new() { Latitude = 48.254426906081555, Longitude = 37.509801635742195 },
|
new() { Lat = 48.254426906081555, Lon = 37.509801635742195 },
|
||||||
new() { Latitude = 48.25914140977405, Longitude = 37.52568031311036 },
|
new() { Lat = 48.25914140977405, Lon = 37.52568031311036 },
|
||||||
new() { Latitude = 48.25354110233028, Longitude = 37.536752471923835 },
|
new() { Lat = 48.25354110233028, Lon = 37.536752471923835 },
|
||||||
new() { Latitude = 48.25902712391726, Longitude = 37.551257858276374 },
|
new() { Lat = 48.25902712391726, Lon = 37.551257858276374 },
|
||||||
new() { Latitude = 48.26828345053738, Longitude = 37.537009963989265 },
|
new() { Lat = 48.26828345053738, Lon = 37.537009963989265 },
|
||||||
new() { Latitude = 48.27421563182974, Longitude = 37.52345758819581 },
|
new() { Lat = 48.27421563182974, Lon = 37.52345758819581 },
|
||||||
new() { Latitude = 48.26889854647051, Longitude = 37.513029159545906 },
|
new() { Lat = 48.26889854647051, Lon = 37.513029159545906 },
|
||||||
new() { Latitude = 48.26147111993905, Longitude = 37.51607614898682 },
|
new() { Lat = 48.26147111993905, Lon = 37.51607614898682 },
|
||||||
new() { Latitude = 48.26355662176038, Longitude = 37.50487524414063 },
|
new() { Lat = 48.26355662176038, Lon = 37.50487524414063 },
|
||||||
new() { Latitude = 48.25667128564209, Longitude = 37.49852377319337 },
|
new() { Lat = 48.25667128564209, Lon = 37.49852377319337 },
|
||||||
new() { Latitude = 48.25258535732375, Longitude = 37.513801635742195 },
|
new() { Lat = 48.25258535732375, Lon = 37.513801635742195 },
|
||||||
new() { Latitude = 48.25729986101625, Longitude = 37.52968031311036 },
|
new() { Lat = 48.25729986101625, Lon = 37.52968031311036 },
|
||||||
new() { Latitude = 48.25169955357248, Longitude = 37.540752471923835 },
|
new() { Lat = 48.25169955357248, Lon = 37.540752471923835 },
|
||||||
new() { Latitude = 48.25718557515946, Longitude = 37.555257858276374 },
|
new() { Lat = 48.25718557515946, Lon = 37.555257858276374 },
|
||||||
new() { Latitude = 48.26644190177958, Longitude = 37.541009963989265 }
|
new() { Lat = 48.26644190177958, Lon = 37.541009963989265 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -554,16 +584,16 @@ public static class RouteTests
|
|||||||
Console.WriteLine("Route Path (first 5 and last 5 points):");
|
Console.WriteLine("Route Path (first 5 and last 5 points):");
|
||||||
for (int i = 0; i < Math.Min(5, request.Points.Count); i++)
|
for (int i = 0; i < Math.Min(5, request.Points.Count); i++)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {i + 1}. ({request.Points[i].Latitude}, {request.Points[i].Longitude})");
|
Console.WriteLine($" {i + 1}. ({request.Points[i].Lat}, {request.Points[i].Lon})");
|
||||||
}
|
}
|
||||||
Console.WriteLine(" ...");
|
Console.WriteLine(" ...");
|
||||||
for (int i = Math.Max(5, request.Points.Count - 5); i < request.Points.Count; i++)
|
for (int i = Math.Max(5, request.Points.Count - 5); i < request.Points.Count; i++)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {i + 1}. ({request.Points[i].Latitude}, {request.Points[i].Longitude})");
|
Console.WriteLine($" {i + 1}. ({request.Points[i].Lat}, {request.Points[i].Lon})");
|
||||||
}
|
}
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request);
|
var routeResponse = await httpClient.PostAsJsonAsync("/api/satellite/route", request, JsonWriteOptions);
|
||||||
|
|
||||||
if (!routeResponse.IsSuccessStatusCode)
|
if (!routeResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -262,6 +262,12 @@ public class GoogleMapsDownloaderV2
|
|||||||
{
|
{
|
||||||
var tileCenter = GeoUtils.TileToWorldPos(x, y, zoomLevel);
|
var tileCenter = GeoUtils.TileToWorldPos(x, y, zoomLevel);
|
||||||
|
|
||||||
|
if ((x == xMin && y == yMin) || (x == xMax && y == yMax))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("GoogleMapsDownloader - Tile ({X}, {Y}) center calculated: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
x, y, tileCenter.Lat, tileCenter.Lon);
|
||||||
|
}
|
||||||
|
|
||||||
var existingTile = existingTiles.FirstOrDefault(t =>
|
var existingTile = existingTiles.FirstOrDefault(t =>
|
||||||
Math.Abs(t.Latitude - tileCenter.Lat) < 0.0001 &&
|
Math.Abs(t.Latitude - tileCenter.Lat) < 0.0001 &&
|
||||||
Math.Abs(t.Longitude - tileCenter.Lon) < 0.0001 &&
|
Math.Abs(t.Longitude - tileCenter.Lon) < 0.0001 &&
|
||||||
@@ -373,9 +379,7 @@ public class GoogleMapsDownloaderV2
|
|||||||
int totalTiles,
|
int totalTiles,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Tile ({X},{Y}) [{Index}/{Total}]: Waiting for semaphore slot", x, y, tileIndex + 1, totalTiles);
|
|
||||||
await _downloadSemaphore.WaitAsync(token);
|
await _downloadSemaphore.WaitAsync(token);
|
||||||
_logger.LogDebug("Tile ({X},{Y}) [{Index}/{Total}]: Acquired semaphore slot, starting download", x, y, tileIndex + 1, totalTiles);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -451,7 +455,6 @@ public class GoogleMapsDownloaderV2
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Tile ({X},{Y}) [{Index}/{Total}]: Releasing semaphore slot", x, y, tileIndex + 1, totalTiles);
|
|
||||||
_downloadSemaphore.Release();
|
_downloadSemaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,8 @@ public class RegionRequestQueue : IRegionRequestQueue
|
|||||||
|
|
||||||
public async ValueTask EnqueueAsync(RegionRequest request, CancellationToken cancellationToken = default)
|
public async ValueTask EnqueueAsync(RegionRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var queueDepthBefore = Count;
|
|
||||||
_totalEnqueued++;
|
_totalEnqueued++;
|
||||||
_logger?.LogDebug("Enqueuing region {RegionId} (queue depth before: {Depth}, total enqueued: {Total})",
|
|
||||||
request.Id, queueDepthBefore, _totalEnqueued);
|
|
||||||
|
|
||||||
await _queue.Writer.WriteAsync(request, cancellationToken);
|
await _queue.Writer.WriteAsync(request, cancellationToken);
|
||||||
|
|
||||||
var queueDepthAfter = Count;
|
|
||||||
_logger?.LogDebug("Enqueued region {RegionId} (queue depth after: {Depth})",
|
|
||||||
request.Id, queueDepthAfter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<RegionRequest?> DequeueAsync(CancellationToken cancellationToken = default)
|
public async ValueTask<RegionRequest?> DequeueAsync(CancellationToken cancellationToken = default)
|
||||||
@@ -44,9 +36,6 @@ public class RegionRequestQueue : IRegionRequestQueue
|
|||||||
if (_queue.Reader.TryRead(out var request))
|
if (_queue.Reader.TryRead(out var request))
|
||||||
{
|
{
|
||||||
_totalDequeued++;
|
_totalDequeued++;
|
||||||
var queueDepth = Count;
|
|
||||||
_logger?.LogDebug("Dequeued region {RegionId} (queue depth: {Depth}, total dequeued: {Total})",
|
|
||||||
request.Id, queueDepth, _totalDequeued);
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public class RegionService : IRegionService
|
|||||||
|
|
||||||
public async Task<RegionStatus> RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false)
|
public async Task<RegionStatus> RequestRegionAsync(Guid id, double latitude, double longitude, double sizeMeters, int zoomLevel, bool stitchTiles = false)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("RegionService - Requesting region {RegionId}: Lat={Lat:F12}, Lon={Lon:F12}, Size={Size}m, Zoom={Zoom}",
|
||||||
|
id, latitude, longitude, sizeMeters, zoomLevel);
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var region = new RegionEntity
|
var region = new RegionEntity
|
||||||
{
|
{
|
||||||
@@ -353,6 +356,12 @@ public class RegionService : IRegionService
|
|||||||
{
|
{
|
||||||
var orderedTiles = tiles.OrderByDescending(t => t.Latitude).ThenBy(t => t.Longitude).ToList();
|
var orderedTiles = tiles.OrderByDescending(t => t.Latitude).ThenBy(t => t.Longitude).ToList();
|
||||||
|
|
||||||
|
if (orderedTiles.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RegionService - Writing CSV with {Count} tiles. First tile: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
orderedTiles.Count, orderedTiles[0].Latitude, orderedTiles[0].Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
using var writer = new StreamWriter(filePath);
|
using var writer = new StreamWriter(filePath);
|
||||||
await writer.WriteLineAsync("latitude,longitude,file_path");
|
await writer.WriteLineAsync("latitude,longitude,file_path");
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ public class RouteProcessingService : BackgroundService
|
|||||||
{
|
{
|
||||||
var pendingRoutes = await GetRoutesWithPendingMapsAsync();
|
var pendingRoutes = await GetRoutesWithPendingMapsAsync();
|
||||||
|
|
||||||
|
if (pendingRoutes.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Processing {Count} route(s) with pending maps: {RouteIds}",
|
||||||
|
pendingRoutes.Count, string.Join(", ", pendingRoutes.Select(r => r.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var route in pendingRoutes)
|
foreach (var route in pendingRoutes)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
@@ -84,16 +90,37 @@ public class RouteProcessingService : BackgroundService
|
|||||||
private async Task ProcessRouteSequentiallyAsync(Guid routeId, CancellationToken cancellationToken)
|
private async Task ProcessRouteSequentiallyAsync(Guid routeId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var route = await _routeRepository.GetByIdAsync(routeId);
|
var route = await _routeRepository.GetByIdAsync(routeId);
|
||||||
if (route == null || !route.RequestMaps || route.MapsReady)
|
if (route == null)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId}: Route not found, skipping processing", routeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!route.RequestMaps)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: RequestMaps=false, skipping processing", routeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.MapsReady)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: MapsReady=true, skipping processing", routeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Starting processing check - RequestMaps={RequestMaps}, MapsReady={MapsReady}",
|
||||||
|
routeId, route.RequestMaps, route.MapsReady);
|
||||||
|
|
||||||
var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList();
|
var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList();
|
||||||
var regionIdsList = (await _routeRepository.GetRegionIdsByRouteAsync(routeId)).ToList();
|
var regionIdsList = (await _routeRepository.GetRegionIdsByRouteAsync(routeId)).ToList();
|
||||||
|
var geofenceRegionIdsList = (await _routeRepository.GetGeofenceRegionIdsByRouteAsync(routeId)).ToList();
|
||||||
|
|
||||||
if (regionIdsList.Count == 0)
|
var allRegionIds = regionIdsList.Union(geofenceRegionIdsList).ToList();
|
||||||
|
|
||||||
|
if (regionIdsList.Count == 0 && routePointsList.Count > 0)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: No route point regions linked yet. Will create regions for {PointCount} route points", routeId, routePointsList.Count);
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var regionService = scope.ServiceProvider.GetRequiredService<Common.Interfaces.IRegionService>();
|
var regionService = scope.ServiceProvider.GetRequiredService<Common.Interfaces.IRegionService>();
|
||||||
|
|
||||||
@@ -106,6 +133,9 @@ public class RouteProcessingService : BackgroundService
|
|||||||
{
|
{
|
||||||
var regionId = Guid.NewGuid();
|
var regionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_logger.LogInformation("RouteProcessingService - Creating region {RegionId} for route {RouteId} at point: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
regionId, routeId, point.Latitude, point.Longitude);
|
||||||
|
|
||||||
await regionService.RequestRegionAsync(
|
await regionService.RequestRegionAsync(
|
||||||
regionId,
|
regionId,
|
||||||
point.Latitude,
|
point.Latitude,
|
||||||
@@ -124,7 +154,7 @@ public class RouteProcessingService : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var regions = new List<DataAccess.Models.RegionEntity>();
|
var regions = new List<DataAccess.Models.RegionEntity>();
|
||||||
foreach (var regionId in regionIdsList)
|
foreach (var regionId in allRegionIds)
|
||||||
{
|
{
|
||||||
var region = await _regionRepository.GetByIdAsync(regionId);
|
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||||
if (region != null)
|
if (region != null)
|
||||||
@@ -137,16 +167,36 @@ public class RouteProcessingService : BackgroundService
|
|||||||
var failedRegions = regions.Where(r => r.Status == "failed").ToList();
|
var failedRegions = regions.Where(r => r.Status == "failed").ToList();
|
||||||
var processingRegions = regions.Where(r => r.Status == "queued" || r.Status == "processing").ToList();
|
var processingRegions = regions.Where(r => r.Status == "queued" || r.Status == "processing").ToList();
|
||||||
|
|
||||||
var hasEnoughCompleted = completedRegions.Count >= routePointsList.Count;
|
var completedRoutePointRegions = completedRegions.Where(r => !geofenceRegionIdsList.Contains(r.Id)).ToList();
|
||||||
|
var completedGeofenceRegions = completedRegions.Where(r => geofenceRegionIdsList.Contains(r.Id)).ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Region counts - Total allRegionIds={AllCount}, regionIdsList={RoutePointCount}, geofenceRegionIdsList={GeofenceCount}",
|
||||||
|
routeId, allRegionIds.Count, regionIdsList.Count, geofenceRegionIdsList.Count);
|
||||||
|
_logger.LogInformation("Route {RouteId}: Status breakdown - Completed={Completed} (RoutePoint={CompletedRP}, Geofence={CompletedGF}), Failed={Failed}, Processing={Processing}",
|
||||||
|
routeId, completedRegions.Count, completedRoutePointRegions.Count, completedGeofenceRegions.Count, failedRegions.Count, processingRegions.Count);
|
||||||
|
|
||||||
|
var hasRoutePointRegions = regionIdsList.Count > 0;
|
||||||
|
var hasEnoughRoutePointRegions = !hasRoutePointRegions || completedRoutePointRegions.Count >= routePointsList.Count;
|
||||||
|
var hasAllGeofenceRegions = geofenceRegionIdsList.Count == 0 || completedGeofenceRegions.Count >= geofenceRegionIdsList.Count;
|
||||||
|
var hasEnoughCompleted = hasEnoughRoutePointRegions && hasAllGeofenceRegions;
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Condition checks - hasRoutePointRegions={HasRP}, hasEnoughRoutePointRegions={HasEnoughRP} (need {NeedRP}), hasAllGeofenceRegions={HasAllGF} (need {NeedGF}), hasEnoughCompleted={HasEnough}",
|
||||||
|
routeId, hasRoutePointRegions, hasEnoughRoutePointRegions, routePointsList.Count, hasAllGeofenceRegions, geofenceRegionIdsList.Count, hasEnoughCompleted);
|
||||||
|
|
||||||
var activeRegions = completedRegions.Count + processingRegions.Count;
|
var activeRegions = completedRegions.Count + processingRegions.Count;
|
||||||
var shouldRetryFailed = failedRegions.Count > 0 && !hasEnoughCompleted && activeRegions < routePointsList.Count;
|
var shouldRetryFailed = failedRegions.Count > 0 && !hasEnoughCompleted && activeRegions < allRegionIds.Count;
|
||||||
|
|
||||||
if (hasEnoughCompleted)
|
if (hasEnoughCompleted)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Route {RouteId}: Have {Completed} completed regions (required: {Required}). Generating final maps. Ignoring {Processing} processing and {Failed} failed regions.",
|
_logger.LogInformation("Route {RouteId}: Have {RoutePointCompleted}/{RoutePointRequired} route point regions and {GeofenceCompleted}/{GeofenceRequired} geofence regions completed. Generating final maps. Ignoring {Processing} processing and {Failed} failed regions.",
|
||||||
routeId, completedRegions.Count, routePointsList.Count, processingRegions.Count, failedRegions.Count);
|
routeId, completedRoutePointRegions.Count, routePointsList.Count, completedGeofenceRegions.Count, geofenceRegionIdsList.Count, processingRegions.Count, failedRegions.Count);
|
||||||
|
|
||||||
await GenerateRouteMapsAsync(routeId, route, completedRegions.Take(routePointsList.Count).Select(r => r.Id), cancellationToken);
|
var orderedRouteRegions = MatchRegionsToRoutePoints(routePointsList, completedRoutePointRegions, routeId);
|
||||||
|
var routeRegionIds = orderedRouteRegions.Select(r => r.Id).ToList();
|
||||||
|
var allRegionIdsForStitching = routeRegionIds.Concat(completedGeofenceRegions.Select(r => r.Id)).Distinct();
|
||||||
|
var geofenceRegionIdsForBorders = completedGeofenceRegions.Select(r => r.Id).ToList();
|
||||||
|
|
||||||
|
await GenerateRouteMapsAsync(routeId, route, allRegionIdsForStitching, geofenceRegionIdsForBorders, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,16 +232,20 @@ public class RouteProcessingService : BackgroundService
|
|||||||
var anyProcessing = processingRegions.Count > 0;
|
var anyProcessing = processingRegions.Count > 0;
|
||||||
if (anyProcessing)
|
if (anyProcessing)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Route {RouteId}: Progress - {Completed}/{Required} regions completed, {Processing} still processing, {Failed} failed (will retry if needed)",
|
_logger.LogInformation("Route {RouteId}: Progress - {RoutePointCompleted}/{RoutePointRequired} route point regions, {GeofenceCompleted}/{GeofenceRequired} geofence regions completed, {Processing} still processing, {Failed} failed (will retry if needed)",
|
||||||
routeId, completedRegions.Count, routePointsList.Count, processingRegions.Count, failedRegions.Count);
|
routeId, completedRoutePointRegions.Count, routePointsList.Count, completedGeofenceRegions.Count, geofenceRegionIdsList.Count, processingRegions.Count, failedRegions.Count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogWarning("Route {RouteId}: No processing regions, but not enough completed. This is an unexpected state - hasEnoughCompleted={HasEnough}, shouldRetryFailed={ShouldRetry}, anyProcessing={AnyProcessing}",
|
||||||
|
routeId, hasEnoughCompleted, shouldRetryFailed, anyProcessing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GenerateRouteMapsAsync(
|
private async Task GenerateRouteMapsAsync(
|
||||||
Guid routeId,
|
Guid routeId,
|
||||||
DataAccess.Models.RouteEntity route,
|
DataAccess.Models.RouteEntity route,
|
||||||
IEnumerable<Guid> regionIds,
|
IEnumerable<Guid> regionIds,
|
||||||
|
List<Guid> geofenceRegionIds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -208,21 +262,42 @@ public class RouteProcessingService : BackgroundService
|
|||||||
var region = await _regionRepository.GetByIdAsync(regionId);
|
var region = await _regionRepository.GetByIdAsync(regionId);
|
||||||
if (region == null || string.IsNullOrEmpty(region.CsvFilePath) || !File.Exists(region.CsvFilePath))
|
if (region == null || string.IsNullOrEmpty(region.CsvFilePath) || !File.Exists(region.CsvFilePath))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Region {RegionId} CSV not found for route {RouteId}", regionId, routeId);
|
_logger.LogWarning("Route {RouteId}: Region {RegionId} CSV not found", routeId, regionId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isGeofence = geofenceRegionIds.Contains(regionId);
|
||||||
|
_logger.LogInformation("Route {RouteId}: Processing region {RegionId} ({Type}) - Lat={Lat}, Lon={Lon}, Size={Size}m, CSV={CsvPath}",
|
||||||
|
routeId, regionId, isGeofence ? "GEOFENCE" : "RoutePoint",
|
||||||
|
region.Latitude, region.Longitude, region.SizeMeters, region.CsvFilePath);
|
||||||
|
|
||||||
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
||||||
|
|
||||||
|
var lineNumber = 0;
|
||||||
foreach (var line in csvLines.Skip(1))
|
foreach (var line in csvLines.Skip(1))
|
||||||
{
|
{
|
||||||
|
lineNumber++;
|
||||||
var parts = line.Split(',');
|
var parts = line.Split(',');
|
||||||
if (parts.Length < 3) continue;
|
if (parts.Length < 3) continue;
|
||||||
|
|
||||||
if (!double.TryParse(parts[0], out var lat)) continue;
|
if (!double.TryParse(parts[0], out var lat))
|
||||||
if (!double.TryParse(parts[1], out var lon)) continue;
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId} - Failed to parse latitude from CSV line {LineNumber}: {Line}", routeId, lineNumber, line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!double.TryParse(parts[1], out var lon))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId} - Failed to parse longitude from CSV line {LineNumber}: {Line}", routeId, lineNumber, line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var filePath = parts[2];
|
var filePath = parts[2];
|
||||||
|
|
||||||
|
if (lineNumber <= 3)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId} - Reading tile from region {RegionId} CSV: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
routeId, regionId, lat, lon);
|
||||||
|
}
|
||||||
|
|
||||||
totalTilesFromRegions++;
|
totalTilesFromRegions++;
|
||||||
var key = $"{lat:F6}_{lon:F6}";
|
var key = $"{lat:F6}_{lon:F6}";
|
||||||
|
|
||||||
@@ -251,8 +326,54 @@ public class RouteProcessingService : BackgroundService
|
|||||||
string? stitchedImagePath = null;
|
string? stitchedImagePath = null;
|
||||||
if (route.RequestMaps)
|
if (route.RequestMaps)
|
||||||
{
|
{
|
||||||
|
var geofenceTileBounds = new List<(Guid RegionId, int MinX, int MinY, int MaxX, int MaxY)>();
|
||||||
|
|
||||||
|
foreach (var geofenceId in geofenceRegionIds)
|
||||||
|
{
|
||||||
|
var region = await _regionRepository.GetByIdAsync(geofenceId);
|
||||||
|
if (region != null && !string.IsNullOrEmpty(region.CsvFilePath) && File.Exists(region.CsvFilePath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: Loading geofence region {RegionId} tile bounds",
|
||||||
|
routeId, region.Id);
|
||||||
|
|
||||||
|
var csvLines = await File.ReadAllLinesAsync(region.CsvFilePath, cancellationToken);
|
||||||
|
int? minX = null, minY = null, maxX = null, maxY = null;
|
||||||
|
|
||||||
|
foreach (var line in csvLines.Skip(1))
|
||||||
|
{
|
||||||
|
var parts = line.Split(',');
|
||||||
|
if (parts.Length >= 3)
|
||||||
|
{
|
||||||
|
if (double.TryParse(parts[0], out var lat) && double.TryParse(parts[1], out var lon))
|
||||||
|
{
|
||||||
|
var tile = GeoUtils.WorldToTilePos(new Common.DTO.GeoPoint { Lat = lat, Lon = lon }, route.ZoomLevel);
|
||||||
|
minX = minX == null ? tile.x : Math.Min(minX.Value, tile.x);
|
||||||
|
minY = minY == null ? tile.y : Math.Min(minY.Value, tile.y);
|
||||||
|
maxX = maxX == null ? tile.x : Math.Max(maxX.Value, tile.x);
|
||||||
|
maxY = maxY == null ? tile.y : Math.Max(maxY.Value, tile.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minX.HasValue && minY.HasValue && maxX.HasValue && maxY.HasValue)
|
||||||
|
{
|
||||||
|
geofenceTileBounds.Add((region.Id, minX.Value, minY.Value, maxX.Value, maxY.Value));
|
||||||
|
_logger.LogInformation("Route {RouteId}: Geofence {RegionId} tile bounds: X=[{MinX}..{MaxX}], Y=[{MinY}..{MaxY}]",
|
||||||
|
routeId, region.Id, minX.Value, maxX.Value, minY.Value, maxY.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId}: Geofence region {RegionId} CSV not found",
|
||||||
|
routeId, geofenceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Starting stitching with {GeofenceCount} geofence regions",
|
||||||
|
routeId, geofenceTileBounds.Count);
|
||||||
|
|
||||||
stitchedImagePath = Path.Combine(readyDir, $"route_{routeId}_stitched.jpg");
|
stitchedImagePath = Path.Combine(readyDir, $"route_{routeId}_stitched.jpg");
|
||||||
await StitchRouteTilesAsync(allTiles.Values.ToList(), stitchedImagePath, route.ZoomLevel, cancellationToken);
|
await StitchRouteTilesAsync(allTiles.Values.ToList(), stitchedImagePath, route.ZoomLevel, geofenceTileBounds, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
var summaryPath = Path.Combine(readyDir, $"route_{routeId}_summary.txt");
|
var summaryPath = Path.Combine(readyDir, $"route_{routeId}_summary.txt");
|
||||||
@@ -339,6 +460,7 @@ public class RouteProcessingService : BackgroundService
|
|||||||
List<TileInfo> tiles,
|
List<TileInfo> tiles,
|
||||||
string outputPath,
|
string outputPath,
|
||||||
int zoomLevel,
|
int zoomLevel,
|
||||||
|
List<(Guid RegionId, int MinX, int MinY, int MaxX, int MaxY)> geofenceTileBounds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (tiles.Count == 0)
|
if (tiles.Count == 0)
|
||||||
@@ -437,6 +559,48 @@ public class RouteProcessingService : BackgroundService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (geofenceTileBounds.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Drawing {Count} geofence borders on image {Width}x{Height} (grid: minX={MinX}, minY={MinY})",
|
||||||
|
geofenceTileBounds.Count, imageWidth, imageHeight, minX, minY);
|
||||||
|
|
||||||
|
foreach (var (regionId, geoMinX, geoMinY, geoMaxX, geoMaxY) in geofenceTileBounds)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Geofence {RegionId}: Tile range - X=[{MinX}..{MaxX}], Y=[{MinY}..{MaxY}]",
|
||||||
|
regionId, geoMinX, geoMaxX, geoMinY, geoMaxY);
|
||||||
|
|
||||||
|
var x1 = (geoMinX - minX) * tileSizePixels;
|
||||||
|
var y1 = (geoMinY - minY) * tileSizePixels;
|
||||||
|
var x2 = (geoMaxX - minX + 1) * tileSizePixels - 1;
|
||||||
|
var y2 = (geoMaxY - minY + 1) * tileSizePixels - 1;
|
||||||
|
|
||||||
|
_logger.LogInformation("Geofence {RegionId}: Pixel coords before clipping - ({X1},{Y1}) to ({X2},{Y2})",
|
||||||
|
regionId, x1, y1, x2, y2);
|
||||||
|
|
||||||
|
x1 = Math.Max(0, Math.Min(x1, imageWidth - 1));
|
||||||
|
y1 = Math.Max(0, Math.Min(y1, imageHeight - 1));
|
||||||
|
x2 = Math.Max(0, Math.Min(x2, imageWidth - 1));
|
||||||
|
y2 = Math.Max(0, Math.Min(y2, imageHeight - 1));
|
||||||
|
|
||||||
|
if (x1 >= 0 && y1 >= 0 && x2 < imageWidth && y2 < imageHeight && x2 > x1 && y2 > y1)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Geofence {RegionId}: Drawing border at pixel coords ({X1},{Y1}) to ({X2},{Y2})",
|
||||||
|
regionId, x1, y1, x2, y2);
|
||||||
|
|
||||||
|
DrawRectangleBorder(stitchedImage, x1, y1, x2, y2, new Rgb24(255, 255, 0));
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully drew geofence border for region {RegionId}", regionId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Geofence {RegionId}: Border out of bounds or invalid - ({X1},{Y1}) to ({X2},{Y2}), image size: {Width}x{Height}",
|
||||||
|
regionId, x1, y1, x2, y2, imageWidth, imageHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Completed drawing all geofence borders, now saving image...");
|
||||||
|
}
|
||||||
|
|
||||||
await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
|
await stitchedImage.SaveAsJpegAsync(outputPath, cancellationToken);
|
||||||
|
|
||||||
var totalPossibleTiles = gridWidth * gridHeight;
|
var totalPossibleTiles = gridWidth * gridHeight;
|
||||||
@@ -450,6 +614,54 @@ public class RouteProcessingService : BackgroundService
|
|||||||
totalPossibleTiles, gridWidth, gridHeight);
|
totalPossibleTiles, gridWidth, gridHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<DataAccess.Models.RegionEntity> MatchRegionsToRoutePoints(
|
||||||
|
List<DataAccess.Models.RoutePointEntity> routePoints,
|
||||||
|
List<DataAccess.Models.RegionEntity> regions,
|
||||||
|
Guid routeId)
|
||||||
|
{
|
||||||
|
var orderedRegions = new List<DataAccess.Models.RegionEntity>();
|
||||||
|
var availableRegions = new List<DataAccess.Models.RegionEntity>(regions);
|
||||||
|
|
||||||
|
foreach (var point in routePoints)
|
||||||
|
{
|
||||||
|
var matchedRegion = availableRegions
|
||||||
|
.OrderBy(r => CalculateDistance(point.Latitude, point.Longitude, r.Latitude, r.Longitude))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (matchedRegion != null)
|
||||||
|
{
|
||||||
|
orderedRegions.Add(matchedRegion);
|
||||||
|
availableRegions.Remove(matchedRegion);
|
||||||
|
|
||||||
|
var distance = CalculateDistance(point.Latitude, point.Longitude, matchedRegion.Latitude, matchedRegion.Longitude);
|
||||||
|
_logger.LogInformation("Route {RouteId}: Matched route point Seq={Seq} ({Lat:F6},{Lon:F6}) to region {RegionId} ({RegLat:F6},{RegLon:F6}), distance={Distance:F2}m",
|
||||||
|
routeId, point.SequenceNumber, point.Latitude, point.Longitude,
|
||||||
|
matchedRegion.Id, matchedRegion.Latitude, matchedRegion.Longitude, distance);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Route {RouteId}: No region found for route point Seq={Seq} ({Lat:F6},{Lon:F6})",
|
||||||
|
routeId, point.SequenceNumber, point.Latitude, point.Longitude);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedRegions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double CalculateDistance(double lat1, double lon1, double lat2, double lon2)
|
||||||
|
{
|
||||||
|
const double earthRadiusMeters = 6371000;
|
||||||
|
var dLat = (lat2 - lat1) * Math.PI / 180.0;
|
||||||
|
var dLon = (lon2 - lon1) * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(lat1 * Math.PI / 180.0) * Math.Cos(lat2 * Math.PI / 180.0) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||||
|
|
||||||
|
return earthRadiusMeters * c;
|
||||||
|
}
|
||||||
|
|
||||||
private static (int TileX, int TileY) ExtractTileCoordinatesFromFilename(string filePath)
|
private static (int TileX, int TileY) ExtractTileCoordinatesFromFilename(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -471,6 +683,49 @@ public class RouteProcessingService : BackgroundService
|
|||||||
|
|
||||||
return (-1, -1);
|
return (-1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (double NorthLat, double SouthLat, double WestLon, double EastLon) CalculateGeofenceCorners(
|
||||||
|
double centerLat,
|
||||||
|
double centerLon,
|
||||||
|
double halfSizeMeters)
|
||||||
|
{
|
||||||
|
var center = new Common.DTO.GeoPoint { Lat = centerLat, Lon = centerLon };
|
||||||
|
|
||||||
|
var north = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 0 });
|
||||||
|
var south = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 180 });
|
||||||
|
var east = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 90 });
|
||||||
|
var west = GeoUtils.GoDirection(center, new Common.DTO.Direction { Distance = halfSizeMeters, Azimuth = 270 });
|
||||||
|
|
||||||
|
return (north.Lat, south.Lat, west.Lon, east.Lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawRectangleBorder(Image<Rgb24> image, int x1, int y1, int x2, int y2, Rgb24 color)
|
||||||
|
{
|
||||||
|
const int thickness = 5;
|
||||||
|
|
||||||
|
for (int t = 0; t < thickness; t++)
|
||||||
|
{
|
||||||
|
for (int x = x1; x <= x2; x++)
|
||||||
|
{
|
||||||
|
int topY = y1 + t;
|
||||||
|
int bottomY = y2 - t;
|
||||||
|
if (x >= 0 && x < image.Width && topY >= 0 && topY < image.Height)
|
||||||
|
image[x, topY] = color;
|
||||||
|
if (x >= 0 && x < image.Width && bottomY >= 0 && bottomY < image.Height)
|
||||||
|
image[x, bottomY] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = y1; y <= y2; y++)
|
||||||
|
{
|
||||||
|
int leftX = x1 + t;
|
||||||
|
int rightX = x2 - t;
|
||||||
|
if (leftX >= 0 && leftX < image.Width && y >= 0 && y < image.Height)
|
||||||
|
image[leftX, y] = color;
|
||||||
|
if (rightX >= 0 && rightX < image.Width && y >= 0 && y < image.Height)
|
||||||
|
image[rightX, y] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TileInfo
|
public class TileInfo
|
||||||
|
|||||||
@@ -41,8 +41,12 @@ public class RouteService : IRouteService
|
|||||||
throw new ArgumentException("Route name is required");
|
throw new ArgumentException("Route name is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Creating route {RouteId} with {PointCount} original points",
|
_logger.LogInformation("Creating route {RouteId} with {PointCount} original points and {GeofenceCount} geofences",
|
||||||
request.Id, request.Points.Count);
|
request.Id, request.Points.Count, request.Geofences?.Polygons?.Count ?? 0);
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId} - Input coordinates: First point ({Lat}, {Lon}), Last point ({LastLat}, {LastLon})",
|
||||||
|
request.Id, request.Points[0].Latitude, request.Points[0].Longitude,
|
||||||
|
request.Points[^1].Latitude, request.Points[^1].Longitude);
|
||||||
|
|
||||||
var allPoints = new List<RoutePointDto>();
|
var allPoints = new List<RoutePointDto>();
|
||||||
var totalDistance = 0.0;
|
var totalDistance = 0.0;
|
||||||
@@ -67,7 +71,7 @@ public class RouteService : IRouteService
|
|||||||
|
|
||||||
var pointType = isStart ? "start" : (isEnd ? "end" : "action");
|
var pointType = isStart ? "start" : (isEnd ? "end" : "action");
|
||||||
|
|
||||||
allPoints.Add(new RoutePointDto
|
var routePointDto = new RoutePointDto
|
||||||
{
|
{
|
||||||
Latitude = currentPoint.Latitude,
|
Latitude = currentPoint.Latitude,
|
||||||
Longitude = currentPoint.Longitude,
|
Longitude = currentPoint.Longitude,
|
||||||
@@ -75,7 +79,15 @@ public class RouteService : IRouteService
|
|||||||
SequenceNumber = sequenceNumber++,
|
SequenceNumber = sequenceNumber++,
|
||||||
SegmentIndex = segmentIndex,
|
SegmentIndex = segmentIndex,
|
||||||
DistanceFromPrevious = distanceFromPrevious
|
DistanceFromPrevious = distanceFromPrevious
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (segmentIndex == 0 || segmentIndex == request.Points.Count - 1)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId} - Creating {PointType} point: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
request.Id, pointType, routePointDto.Latitude, routePointDto.Longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
allPoints.Add(routePointDto);
|
||||||
|
|
||||||
if (!isEnd)
|
if (!isEnd)
|
||||||
{
|
{
|
||||||
@@ -143,8 +155,64 @@ public class RouteService : IRouteService
|
|||||||
CreatedAt = now
|
CreatedAt = now
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId} - Saving {Count} route points to DB. First: Lat={Lat:F12}, Lon={Lon:F12}, Last: Lat={LastLat:F12}, Lon={LastLon:F12}",
|
||||||
|
request.Id, pointEntities.Count, pointEntities[0].Latitude, pointEntities[0].Longitude,
|
||||||
|
pointEntities[^1].Latitude, pointEntities[^1].Longitude);
|
||||||
|
|
||||||
await _routeRepository.InsertRoutePointsAsync(pointEntities);
|
await _routeRepository.InsertRoutePointsAsync(pointEntities);
|
||||||
|
|
||||||
|
if (request.Geofences?.Polygons != null && request.Geofences.Polygons.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Route {RouteId}: Processing {GeofenceCount} geofence polygons",
|
||||||
|
request.Id, request.Geofences.Polygons.Count);
|
||||||
|
|
||||||
|
foreach (var polygon in request.Geofences.Polygons)
|
||||||
|
{
|
||||||
|
if (polygon.NorthWest is null || polygon.SouthEast is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Geofence polygon coordinates are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Math.Abs(polygon.NorthWest.Lat) < 0.0001 && Math.Abs(polygon.NorthWest.Lon) < 0.0001) ||
|
||||||
|
(Math.Abs(polygon.SouthEast.Lat) < 0.0001 && Math.Abs(polygon.SouthEast.Lon) < 0.0001))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Geofence polygon coordinates cannot be (0,0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygon.NorthWest.Lat < -90 || polygon.NorthWest.Lat > 90 ||
|
||||||
|
polygon.SouthEast.Lat < -90 || polygon.SouthEast.Lat > 90 ||
|
||||||
|
polygon.NorthWest.Lon < -180 || polygon.NorthWest.Lon > 180 ||
|
||||||
|
polygon.SouthEast.Lon < -180 || polygon.SouthEast.Lon > 180)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Geofence polygon coordinates must be valid (lat: -90 to 90, lon: -180 to 180)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygon.NorthWest.Lat <= polygon.SouthEast.Lat)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Geofence northWest latitude must be greater than southEast latitude");
|
||||||
|
}
|
||||||
|
|
||||||
|
var center = GeoUtils.CalculateCenter(polygon.NorthWest, polygon.SouthEast);
|
||||||
|
var diagonalDistance = GeoUtils.CalculatePolygonDiagonalDistance(polygon.NorthWest, polygon.SouthEast);
|
||||||
|
var geofenceRegionSize = Math.Max(diagonalDistance * 0.6, request.RegionSizeMeters);
|
||||||
|
|
||||||
|
var geofenceRegionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
_logger.LogInformation("Route {RouteId}: Requesting geofence region {RegionId} at center ({Lat}, {Lon}) with size {Size}m",
|
||||||
|
request.Id, geofenceRegionId, center.Lat, center.Lon, geofenceRegionSize);
|
||||||
|
|
||||||
|
await _regionService.RequestRegionAsync(
|
||||||
|
geofenceRegionId,
|
||||||
|
center.Lat,
|
||||||
|
center.Lon,
|
||||||
|
geofenceRegionSize,
|
||||||
|
request.ZoomLevel,
|
||||||
|
stitchTiles: false);
|
||||||
|
|
||||||
|
await _routeRepository.LinkRouteToRegionAsync(request.Id, geofenceRegionId, isGeofence: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.RequestMaps)
|
if (request.RequestMaps)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.",
|
_logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.",
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public class TileService : ITileService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var centerPoint = new GeoPoint(latitude, longitude);
|
var centerPoint = new GeoPoint(latitude, longitude);
|
||||||
|
_logger.LogInformation("TileService - Downloading tiles for center: Lat={Lat:F12}, Lon={Lon:F12}, Radius={Radius}m, Zoom={Zoom}",
|
||||||
|
latitude, longitude, sizeMeters / 2, zoomLevel);
|
||||||
|
|
||||||
var downloadedTiles = await _downloader.GetTilesWithMetadataAsync(
|
var downloadedTiles = await _downloader.GetTilesWithMetadataAsync(
|
||||||
centerPoint,
|
centerPoint,
|
||||||
sizeMeters / 2,
|
sizeMeters / 2,
|
||||||
@@ -67,6 +70,10 @@ public class TileService : ITileService
|
|||||||
foreach (var downloadedTile in downloadedTiles)
|
foreach (var downloadedTile in downloadedTiles)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_logger.LogInformation("TileService - Preparing to save tile: CenterLat={CenterLat:F12}, CenterLon={CenterLon:F12} from downloader",
|
||||||
|
downloadedTile.CenterLatitude, downloadedTile.CenterLongitude);
|
||||||
|
|
||||||
var tileEntity = new TileEntity
|
var tileEntity = new TileEntity
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -83,6 +90,9 @@ public class TileService : ITileService
|
|||||||
UpdatedAt = now
|
UpdatedAt = now
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("TileService - TileEntity before DB insert: Lat={Lat:F12}, Lon={Lon:F12}",
|
||||||
|
tileEntity.Latitude, tileEntity.Longitude);
|
||||||
|
|
||||||
await _tileRepository.InsertAsync(tileEntity);
|
await _tileRepository.InsertAsync(tileEntity);
|
||||||
_logger.LogInformation("Saved new tile {Id} at ({Lat:F12}, {Lon:F12}) version {Version}", tileEntity.Id, tileEntity.Latitude, tileEntity.Longitude, currentVersion);
|
_logger.LogInformation("Saved new tile {Id} at ({Lat:F12}, {Lon:F12}) version {Version}", tileEntity.Id, tileEntity.Latitude, tileEntity.Longitude, currentVersion);
|
||||||
result.Add(MapToMetadata(tileEntity));
|
result.Add(MapToMetadata(tileEntity));
|
||||||
|
|||||||
Reference in New Issue
Block a user