[AZ-367] Refactor C14: extract shared TileGridStitcher

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 02:55:25 +03:00
parent 23d513b24c
commit 10d31b4c1c
6 changed files with 850 additions and 177 deletions
@@ -4,11 +4,11 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SatelliteProvider.Common.Configs;
using SatelliteProvider.Common.Imaging;
using SatelliteProvider.Common.Utils;
using SatelliteProvider.DataAccess.Repositories;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SatelliteProvider.Services.RouteManagement;
@@ -458,103 +458,82 @@ public class RouteProcessingService : BackgroundService
const int tileSizePixels = 256;
var tileCoords = tiles.Select(t =>
{
var (tileX, tileY) = ExtractTileCoordinatesFromFilename(t.FilePath);
return new
var placements = tiles
.Select(t =>
{
t.Latitude,
t.Longitude,
t.FilePath,
TileX = tileX,
TileY = tileY
};
}).Where(t => t.TileX >= 0 && t.TileY >= 0).ToList();
var minX = tileCoords.Min(t => t.TileX);
var maxX = tileCoords.Max(t => t.TileX);
var minY = tileCoords.Min(t => t.TileY);
var maxY = tileCoords.Max(t => t.TileY);
var gridWidth = maxX - minX + 1;
var gridHeight = maxY - minY + 1;
var imageWidth = gridWidth * tileSizePixels;
var imageHeight = gridHeight * tileSizePixels;
using var stitchedImage = new Image<Rgb24>(imageWidth, imageHeight);
stitchedImage.Mutate(ctx => ctx.BackgroundColor(Color.Black));
var uniqueTileCoords = tileCoords
.GroupBy(t => $"{t.TileX}_{t.TileY}")
.Select(g => g.First())
.OrderBy(t => t.TileY)
.ThenBy(t => t.TileX)
var (tileX, tileY) = ExtractTileCoordinatesFromFilename(t.FilePath);
return new TilePlacement(tileX, tileY, t.FilePath);
})
.Where(p => p.TileX >= 0 && p.TileY >= 0)
.ToList();
int placedTiles = 0;
int missingTiles = 0;
foreach (var tile in uniqueTileCoords)
if (placements.Count == 0)
{
var destX = (tile.TileX - minX) * tileSizePixels;
var destY = (tile.TileY - minY) * tileSizePixels;
_logger.LogWarning("No tiles with extractable coordinates to stitch for route map");
return;
}
if (File.Exists(tile.FilePath))
var stitcher = new TileGridStitcher();
var result = await stitcher.StitchAsync(
placements,
tileSizePixels,
deduplicateByTileCoords: true,
swallowTileLoadErrors: true,
cancellationToken);
using var stitchedImage = result.Image;
foreach (var missing in result.MissingTiles)
{
if (missing.Reason == MissingTileReason.LoadFailed)
{
try
{
using var tileImage = await Image.LoadAsync<Rgb24>(tile.FilePath, cancellationToken);
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
placedTiles++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load tile at {FilePath}, leaving black", tile.FilePath);
missingTiles++;
}
_logger.LogWarning(missing.Error, "Failed to load tile at {FilePath}, leaving black", missing.Tile.FilePath);
}
else
{
_logger.LogWarning("Tile file not found: {FilePath}, leaving black", tile.FilePath);
missingTiles++;
_logger.LogWarning("Tile file not found: {FilePath}, leaving black", missing.Tile.FilePath);
}
}
if (geofencePolygonBounds.Count > 0)
var minX = result.MinX;
var minY = result.MinY;
var imageWidth = result.ImageWidth;
var imageHeight = result.ImageHeight;
var yellow = new Rgb24(255, 255, 0);
var red = new Rgb24(255, 0, 0);
foreach (var (geoMinX, geoMinY, geoMaxX, geoMaxY) in geofencePolygonBounds)
{
for (int i = 0; i < geofencePolygonBounds.Count; i++)
var x1 = (geoMinX - minX) * tileSizePixels;
var y1 = (geoMinY - minY + 1) * tileSizePixels;
var x2 = (geoMaxX - minX + 2) * tileSizePixels - 1;
var y2 = (geoMaxY - minY + 1) * tileSizePixels - 1;
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)
{
var (geoMinX, geoMinY, geoMaxX, geoMaxY) = geofencePolygonBounds[i];
var x1 = (geoMinX - minX) * tileSizePixels;
var y1 = (geoMinY - minY + 1) * tileSizePixels;
var x2 = (geoMaxX - minX + 2) * tileSizePixels - 1;
var y2 = (geoMaxY - minY + 1) * tileSizePixels - 1;
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)
{
DrawRectangleBorder(stitchedImage, x1, y1, x2, y2, new Rgb24(255, 255, 0));
}
stitcher.DrawRectangleBorder(
stitchedImage,
new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1),
yellow);
}
}
foreach (var point in routePoints)
{
var geoPoint = new Common.DTO.GeoPoint { Lat = point.Latitude, Lon = point.Longitude };
var (tileX, tileY) = GeoUtils.WorldToTilePos(geoPoint, zoomLevel);
var pixelX = (tileX - minX) * tileSizePixels + tileSizePixels / 2;
var pixelY = (tileY - minY) * tileSizePixels + tileSizePixels / 2;
if (pixelX >= 0 && pixelX < imageWidth && pixelY >= 0 && pixelY < imageHeight)
{
DrawCross(stitchedImage, pixelX, pixelY, new Rgb24(255, 0, 0), 50);
stitcher.DrawCross(stitchedImage, new Point(pixelX, pixelY), red, 50);
}
}
@@ -599,62 +578,6 @@ public class RouteProcessingService : BackgroundService
return (-1, -1);
}
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;
}
}
}
private static void DrawCross(Image<Rgb24> image, int centerX, int centerY, Rgb24 color, int armLength)
{
const int thickness = 10;
int halfThickness = thickness / 2;
for (int dx = -armLength; dx <= armLength; dx++)
{
for (int t = -halfThickness; t <= halfThickness; t++)
{
int x = centerX + dx;
int y = centerY + t;
if (x >= 0 && x < image.Width && y >= 0 && y < image.Height)
image[x, y] = color;
}
}
for (int dy = -armLength; dy <= armLength; dy++)
{
for (int t = -halfThickness; t <= halfThickness; t++)
{
int x = centerX + t;
int y = centerY + dy;
if (x >= 0 && x < image.Width && y >= 0 && y < image.Height)
image[x, y] = color;
}
}
}
private Task CreateTilesZipAsync(
string zipFilePath,
IEnumerable<TileInfo> tiles,