Files
LMEMappeCatastaliProApp/Services/MapExportService.cs
T

476 lines
22 KiB
C#

using System.Globalization;
using System.IO.Compression;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
namespace LMEMappeCatastaliApp.Services;
public sealed class MapExportService
{
private static readonly XNamespace Kml = "http://www.opengis.net/kml/2.2";
public async Task<MapExportFile> CreateAsync(
string requestedName,
string requestedFormat,
string geoJson,
CancellationToken cancellationToken = default)
{
using var document = JsonDocument.Parse(geoJson);
var root = document.RootElement;
if (!root.TryGetProperty("type", out var type) || type.GetString() != "FeatureCollection"
|| !root.TryGetProperty("features", out var features)
|| features.ValueKind != JsonValueKind.Array)
{
throw new InvalidDataException("I dati raccolti dalla mappa non sono validi.");
}
var featureCount = features.GetArrayLength();
if (featureCount == 0)
{
throw new InvalidOperationException("Nessun oggetto selezionato e disponibile da esportare.");
}
var format = requestedFormat.Trim().ToLowerInvariant() switch
{
"kml" => "kml",
"kmz" => "kmz",
"gpx" => "gpx",
"pdf" => "pdf",
_ => "geojson"
};
var baseName = NormalizeFileName(requestedName);
var exportDirectory = Path.Combine(FileSystem.CacheDirectory, "Exports");
Directory.CreateDirectory(exportDirectory);
var path = Path.Combine(exportDirectory, $"{baseName}.{format}");
if (format == "geojson")
{
using var output = File.Create(path);
await JsonSerializer.SerializeAsync(output, root, new JsonSerializerOptions { WriteIndented = true }, cancellationToken);
return new(path, "application/geo+json", featureCount, Path.GetFileName(path));
}
if (format == "gpx")
{
await File.WriteAllTextAsync(path, BuildGpx(features, baseName), new UTF8Encoding(false), cancellationToken);
return new(path, "application/gpx+xml", featureCount, Path.GetFileName(path));
}
if (format == "pdf")
{
await File.WriteAllBytesAsync(path, BuildPdfReport(features, baseName), cancellationToken);
return new(path, "application/pdf", featureCount, Path.GetFileName(path));
}
var kml = BuildKml(features, baseName);
if (format == "kml")
{
await File.WriteAllTextAsync(path, kml, new UTF8Encoding(false), cancellationToken);
return new(path, "application/vnd.google-earth.kml+xml", featureCount, Path.GetFileName(path));
}
await using (var output = File.Create(path))
using (var archive = new ZipArchive(output, ZipArchiveMode.Create))
{
var entry = archive.CreateEntry("doc.kml", CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await using var writer = new StreamWriter(entryStream, new UTF8Encoding(false));
await writer.WriteAsync(kml.AsMemory(), cancellationToken);
}
return new(path, "application/vnd.google-earth.kmz", featureCount, Path.GetFileName(path));
}
private static string BuildGpx(JsonElement features, string name)
{
var root = new XElement("gpx",
new XAttribute("version", "1.1"),
new XAttribute("creator", "LME Mappe Catastali 0.4.1"),
new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"),
new XElement("metadata", new XElement("name", name)));
foreach (var feature in features.EnumerateArray())
{
if (!feature.TryGetProperty("geometry", out var geometry) || geometry.ValueKind != JsonValueKind.Object) continue;
var properties = feature.TryGetProperty("properties", out var value) ? value : default;
var featureName = FeatureName(properties, GetString(properties, "_lmeCategory") ?? "drawing");
var type = geometry.GetProperty("type").GetString();
var coordinates = geometry.GetProperty("coordinates");
if (type == "Point")
{
root.Add(GpxPoint("wpt", coordinates, featureName));
}
else
{
foreach (var line in EnumerateGpxLines(type, coordinates))
{
root.Add(new XElement("trk", new XElement("name", featureName),
new XElement("trkseg", line.Select(point => GpxPoint("trkpt", point, null)))));
}
}
}
return new XDocument(new XDeclaration("1.0", "utf-8", null), root).ToString();
}
private static XElement GpxPoint(string element, JsonElement coordinate, string? name)
{
var values = coordinate.EnumerateArray().ToArray();
var point = new XElement(element,
new XAttribute("lat", values[1].GetDouble().ToString("G17", CultureInfo.InvariantCulture)),
new XAttribute("lon", values[0].GetDouble().ToString("G17", CultureInfo.InvariantCulture)));
if (!string.IsNullOrWhiteSpace(name)) point.Add(new XElement("name", name));
return point;
}
private static IEnumerable<JsonElement[]> EnumerateGpxLines(string? type, JsonElement coordinates)
{
if (type == "LineString") yield return coordinates.EnumerateArray().ToArray();
else if (type == "Polygon")
foreach (var ring in coordinates.EnumerateArray()) yield return ring.EnumerateArray().ToArray();
else if (type == "MultiLineString")
foreach (var line in coordinates.EnumerateArray()) yield return line.EnumerateArray().ToArray();
else if (type == "MultiPolygon")
foreach (var polygon in coordinates.EnumerateArray())
foreach (var ring in polygon.EnumerateArray()) yield return ring.EnumerateArray().ToArray();
}
private static byte[] BuildPdfReport(JsonElement features, string title)
{
var paths = features.EnumerateArray()
.Where(feature => feature.TryGetProperty("geometry", out _))
.SelectMany(feature => PdfCoordinatePaths(feature.GetProperty("geometry")))
.Where(path => path.Count > 0)
.ToArray();
var points = paths.SelectMany(path => path).ToArray();
var minLon = points.Length == 0 ? 0 : points.Min(point => point.Lon);
var maxLon = points.Length == 0 ? 1 : points.Max(point => point.Lon);
var minLat = points.Length == 0 ? 0 : points.Min(point => point.Lat);
var maxLat = points.Length == 0 ? 1 : points.Max(point => point.Lat);
var lonSpan = Math.Max(0.0000001, maxLon - minLon);
var latSpan = Math.Max(0.0000001, maxLat - minLat);
const double mapLeft = 48, mapBottom = 438, mapWidth = 499, mapHeight = 330;
var scale = Math.Min(mapWidth / lonSpan, mapHeight / latSpan);
var drawing = new StringBuilder("0.08 0.44 0.37 RG 0.86 0.94 0.91 rg 1.4 w ");
foreach (var path in paths.Take(400))
{
for (var pointIndex = 0; pointIndex < path.Count; pointIndex++)
{
var x = mapLeft + (path[pointIndex].Lon - minLon) * scale;
var y = mapBottom + (path[pointIndex].Lat - minLat) * scale;
drawing.AppendFormat(CultureInfo.InvariantCulture, "{0:0.##} {1:0.##} {2} ", x, y, pointIndex == 0 ? "m" : "l");
}
drawing.Append("S ");
}
drawing.Append("0 0 0 RG 1 w 497 730 m 497 758 l S 497 758 m 491 746 l 503 746 l h f ");
var middleLatitude = (minLat + maxLat) / 2;
var widthMeters = lonSpan * 111_320 * Math.Cos(middleLatitude * Math.PI / 180);
var scaleMeters = Math.Max(1, NiceScaleDistance(widthMeters / 4));
var scalePixels = widthMeters <= 0 ? 80 : Math.Clamp(scaleMeters / widthMeters * mapWidth, 35, 150);
drawing.AppendFormat(CultureInfo.InvariantCulture, "48 422 m {0:0.##} 422 l S 48 418 m 48 426 l S {0:0.##} 418 m {0:0.##} 426 l S ", 48 + scalePixels);
var lines = new List<string>
{
title,
$"Rapporto LME Mappe Catastali - {DateTime.Now:dd/MM/yyyy HH:mm}",
$"Oggetti: {features.GetArrayLength()}",
$"Scala grafica: {scaleMeters:0} m | Nord: alto pagina",
""
};
var index = 1;
foreach (var feature in features.EnumerateArray())
{
var properties = feature.TryGetProperty("properties", out var value) ? value : default;
var category = GetString(properties, "_lmeCategory") ?? "drawing";
var geometry = feature.TryGetProperty("geometry", out var geo) ? geo.GetProperty("type").GetString() : "-";
var sheet = GetString(properties, "foglio") ?? GetString(properties, "FOGLIO");
var parcel = GetString(properties, "particella") ?? GetString(properties, "PARTICELLA") ?? GetString(properties, "LABEL");
var reference = string.Join(" ", new[] { sheet is null ? null : $"F.{sheet}", parcel is null ? null : $"P.{parcel}" }.Where(item => item is not null));
lines.Add($"{index++}. {FeatureName(properties, category)} [{category}, {geometry}] {reference}".TrimEnd());
}
var content = drawing.Append("BT /F1 12 Tf 48 810 Td ");
foreach (var line in lines.Take(21))
{
var safe = line.Replace("\\", "\\\\").Replace("(", "\\(").Replace(")", "\\)")
.Select(character => character <= 255 ? character : '?').ToArray();
content.Append('(').Append(safe).Append(") Tj 0 -16 Td ");
}
content.Append("ET");
var stream = Encoding.Latin1.GetBytes(content.ToString());
var objects = new[]
{
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
$"<< /Length {stream.Length} >>\nstream\n{Encoding.Latin1.GetString(stream)}\nendstream",
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"
};
using var output = new MemoryStream();
using var writer = new StreamWriter(output, Encoding.Latin1, leaveOpen: true) { NewLine = "\n" };
writer.Write("%PDF-1.4\n"); writer.Flush();
var offsets = new List<long> { 0 };
for (var i = 0; i < objects.Length; i++)
{
offsets.Add(output.Position); writer.Write($"{i + 1} 0 obj\n{objects[i]}\nendobj\n"); writer.Flush();
}
var xref = output.Position;
writer.Write($"xref\n0 {objects.Length + 1}\n0000000000 65535 f \n");
foreach (var offset in offsets.Skip(1)) writer.Write($"{offset:0000000000} 00000 n \n");
writer.Write($"trailer << /Size {objects.Length + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF");
writer.Flush();
return output.ToArray();
}
private static double NiceScaleDistance(double value)
{
if (value <= 0) return 1;
var power = Math.Pow(10, Math.Floor(Math.Log10(value)));
var normalized = value / power;
var nice = normalized >= 5 ? 5 : normalized >= 2 ? 2 : 1;
return nice * power;
}
private static IEnumerable<List<(double Lon, double Lat)>> PdfCoordinatePaths(JsonElement geometry)
{
if (!geometry.TryGetProperty("type", out var typeElement) || !geometry.TryGetProperty("coordinates", out var coordinates)) yield break;
var type = typeElement.GetString();
if (type == "Point") { yield return [PdfPoint(coordinates)]; yield break; }
if (type == "LineString") { yield return coordinates.EnumerateArray().Select(PdfPoint).ToList(); yield break; }
if (type == "Polygon" || type == "MultiLineString")
{
foreach (var path in coordinates.EnumerateArray()) yield return path.EnumerateArray().Select(PdfPoint).ToList();
yield break;
}
if (type == "MultiPolygon")
{
foreach (var polygon in coordinates.EnumerateArray())
foreach (var path in polygon.EnumerateArray()) yield return path.EnumerateArray().Select(PdfPoint).ToList();
}
}
private static (double Lon, double Lat) PdfPoint(JsonElement coordinate) =>
(coordinate[0].GetDouble(), coordinate[1].GetDouble());
private static string BuildKml(JsonElement features, string documentName)
{
var document = new XElement(Kml + "Document",
new XElement(Kml + "name", documentName),
BuildStyle("imported", "ff2ecc71", "402ecc71"),
BuildStyle("imported-sheet", "ff22803d", "1022803d"),
BuildStyle("cadastral", "ff4444ef", "4224bffb"),
BuildStyle("cadastral-gps", "ffff7a00", "337aff00"),
BuildStyle("drawing", "ff1679f9", "333c92fb"),
BuildStyle("poi", "ffff7a00", "33ff7a00"),
BuildStyle("gps-trail", "ff00a5ff", "2200a5ff"));
var categoryFolders = new Dictionary<string, XElement>(StringComparer.Ordinal);
var groupFolders = new Dictionary<(string Category, string Group), XElement>();
foreach (var feature in features.EnumerateArray())
{
if (!feature.TryGetProperty("geometry", out var geometry)
|| geometry.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
continue;
}
var properties = feature.TryGetProperty("properties", out var propertyElement)
&& propertyElement.ValueKind == JsonValueKind.Object
? propertyElement
: default;
var category = GetString(properties, "_lmeCategory") ?? "drawing";
var placemark = new XElement(Kml + "Placemark",
new XElement(Kml + "name", FeatureName(properties, category)),
new XElement(Kml + "styleUrl", $"#{NormalizeStyleId(category)}"));
var featureStyle = BuildFeatureStyle(properties);
if (featureStyle is not null)
{
placemark.Add(featureStyle);
}
var extendedData = BuildExtendedData(properties);
if (extendedData is not null)
{
placemark.Add(extendedData);
}
placemark.Add(BuildGeometry(geometry));
var categoryName = GetString(properties, "_lmeCategoryName")?.Trim();
var groupName = GetString(properties, "_lmeGroupName")?.Trim();
var parent = document;
if (!string.IsNullOrWhiteSpace(categoryName))
{
if (!categoryFolders.TryGetValue(categoryName, out var categoryFolder))
{
categoryFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", categoryName));
categoryFolders[categoryName] = categoryFolder;
document.Add(categoryFolder);
}
parent = categoryFolder;
}
if (!string.IsNullOrWhiteSpace(groupName))
{
var key = (categoryName ?? string.Empty, groupName);
if (!groupFolders.TryGetValue(key, out var groupFolder))
{
groupFolder = new XElement(Kml + "Folder", new XElement(Kml + "name", groupName));
groupFolders[key] = groupFolder;
parent.Add(groupFolder);
}
parent = groupFolder;
}
parent.Add(placemark);
}
return new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement(Kml + "kml", document)).ToString();
}
private static XElement BuildStyle(string id, string lineColor, string fillColor) =>
new(Kml + "Style",
new XAttribute("id", id),
new XElement(Kml + "LineStyle",
new XElement(Kml + "color", lineColor),
new XElement(Kml + "width", 3)),
new XElement(Kml + "PolyStyle", new XElement(Kml + "color", fillColor)));
private static XElement? BuildFeatureStyle(JsonElement properties)
{
var color = GetString(properties, "_lmeColor");
var fillColor = GetString(properties, "_lmeFillColor") ?? color;
if (string.IsNullOrWhiteSpace(color) && string.IsNullOrWhiteSpace(fillColor)) return null;
var weight = GetNumber(properties, "_lmeWeight", 3);
var opacity = GetNumber(properties, "_lmeOpacity", 1);
var fillOpacity = GetNumber(properties, "_lmeFillOpacity", 0.2);
return new XElement(Kml + "Style",
new XElement(Kml + "LineStyle",
new XElement(Kml + "color", ToKmlColor(color ?? "#f97316", opacity)),
new XElement(Kml + "width", weight.ToString("0.##", CultureInfo.InvariantCulture))),
new XElement(Kml + "PolyStyle",
new XElement(Kml + "color", ToKmlColor(fillColor ?? "#fb923c", fillOpacity))));
}
private static double GetNumber(JsonElement properties, string name, double fallback) =>
properties.ValueKind == JsonValueKind.Object
&& properties.TryGetProperty(name, out var value)
&& value.ValueKind == JsonValueKind.Number
? value.GetDouble()
: fallback;
private static string ToKmlColor(string html, double opacity)
{
var value = html.TrimStart('#');
if (value.Length != 6) value = "f97316";
var alpha = (int)Math.Round(Math.Clamp(opacity, 0, 1) * 255);
return $"{alpha:x2}{value[4..6]}{value[2..4]}{value[0..2]}";
}
private static XElement? BuildExtendedData(JsonElement properties)
{
if (properties.ValueKind != JsonValueKind.Object)
{
return null;
}
var data = properties.EnumerateObject()
.Where(item => item.Name != "_lmeCategory"
&& item.Value.ValueKind is JsonValueKind.String or JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
.Select(item => new XElement(Kml + "Data",
new XAttribute("name", item.Name),
new XElement(Kml + "value", ScalarText(item.Value))))
.ToArray();
return data.Length == 0 ? null : new XElement(Kml + "ExtendedData", data);
}
private static XElement BuildGeometry(JsonElement geometry)
{
var type = geometry.GetProperty("type").GetString();
if (type == "GeometryCollection")
{
return new XElement(Kml + "MultiGeometry",
geometry.GetProperty("geometries").EnumerateArray().Select(BuildGeometry));
}
var coordinates = geometry.GetProperty("coordinates");
return type switch
{
"Point" => new XElement(Kml + "Point", CoordinatesElement([coordinates])),
"LineString" => new XElement(Kml + "LineString", CoordinatesElement(coordinates.EnumerateArray())),
"Polygon" => BuildPolygon(coordinates),
"MultiPoint" => new XElement(Kml + "MultiGeometry",
coordinates.EnumerateArray().Select(point => new XElement(Kml + "Point", CoordinatesElement([point])))),
"MultiLineString" => new XElement(Kml + "MultiGeometry",
coordinates.EnumerateArray().Select(line => new XElement(Kml + "LineString", CoordinatesElement(line.EnumerateArray())))),
"MultiPolygon" => new XElement(Kml + "MultiGeometry", coordinates.EnumerateArray().Select(BuildPolygon)),
_ => throw new InvalidDataException($"Geometria GeoJSON non supportata: {type}.")
};
}
private static XElement BuildPolygon(JsonElement rings)
{
var ringList = rings.EnumerateArray().ToArray();
var polygon = new XElement(Kml + "Polygon");
if (ringList.Length > 0)
{
polygon.Add(new XElement(Kml + "outerBoundaryIs",
new XElement(Kml + "LinearRing", CoordinatesElement(ringList[0].EnumerateArray()))));
}
foreach (var ring in ringList.Skip(1))
{
polygon.Add(new XElement(Kml + "innerBoundaryIs",
new XElement(Kml + "LinearRing", CoordinatesElement(ring.EnumerateArray()))));
}
return polygon;
}
private static XElement CoordinatesElement(IEnumerable<JsonElement> points) =>
new(Kml + "coordinates", string.Join(" ", points.Select(FormatCoordinate)));
private static string FormatCoordinate(JsonElement coordinate)
{
var values = coordinate.EnumerateArray().Take(3)
.Select(value => value.GetDouble().ToString("G17", CultureInfo.InvariantCulture));
return string.Join(",", values);
}
private static string FeatureName(JsonElement properties, string category) =>
GetString(properties, "_lmeDisplayName")
?? GetString(properties, "name")
?? GetString(properties, "Name")
?? GetString(properties, "LABEL")
?? GetString(properties, "NATIONALCADASTRALREFERENCE")
?? category switch
{
"imported-sheet" => "Cornice o foglio importato",
"cadastral" => "Particella catastale",
"cadastral-gps" => "Particella GPS",
"poi" => "Punto di interesse",
"gps-trail" => "Traccia GPS",
_ => "Oggetto mappa"
};
private static string? GetString(JsonElement element, string name) =>
element.ValueKind == JsonValueKind.Object
&& element.TryGetProperty(name, out var value)
&& value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
private static string ScalarText(JsonElement value) => value.ValueKind == JsonValueKind.String
? value.GetString() ?? string.Empty
: value.GetRawText();
private static string NormalizeStyleId(string category) => category switch
{
"imported" or "imported-sheet" or "cadastral" or "cadastral-gps" or "drawing" or "poi" or "gps-trail" => category,
_ => "drawing"
};
private static string NormalizeFileName(string requestedName)
{
var name = string.IsNullOrWhiteSpace(requestedName) ? "Mappa" : requestedName.Trim();
foreach (var invalid in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalid, '_');
}
name = Path.GetFileNameWithoutExtension(name).Trim().TrimEnd('.');
return string.IsNullOrWhiteSpace(name) ? "Mappa" : name[..Math.Min(name.Length, 80)];
}
}
public sealed record MapExportFile(string Path, string MimeType, int FeatureCount, string FileName);