From 397a905657af15f680cf9da9fa70f93122b2453e Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 20 May 2024 14:14:01 +0200 Subject: [PATCH 1/2] Topographic map render toolkit --- .../ColorPalette.cs | 29 ++ .../ITopoMapData.cs | 32 ++ .../ITopoMapPdfRenderOptions.cs | 11 + MapToolkit.Drawing.Topographic/IconsRender.cs | 98 +++++ .../LegendRender.cs | 239 ++++++++++++ .../MapToolkit.Drawing.Topographic.csproj | 13 + MapToolkit.Drawing.Topographic/TopoIcon.cs | 15 + .../TopoIconType.cs | 11 + .../TopoLocation.cs | 18 + .../TopoLocationType.cs | 9 + MapToolkit.Drawing.Topographic/TopoMapData.cs | 34 ++ .../TopoMapDataExtensions.cs | 24 ++ .../TopoMapPathType.cs | 11 + .../TopoMapPdfRender.cs | 275 ++++++++++++++ .../TopoMapPdfTile.cs | 16 + .../TopoMapRender.cs | 344 ++++++++++++++++++ .../TopoMapRenderData.cs | 62 ++++ .../TopoMapStyle.cs | 209 +++++++++++ MapToolkit.sln | 10 +- 19 files changed, 1458 insertions(+), 2 deletions(-) create mode 100644 MapToolkit.Drawing.Topographic/ColorPalette.cs create mode 100644 MapToolkit.Drawing.Topographic/ITopoMapData.cs create mode 100644 MapToolkit.Drawing.Topographic/ITopoMapPdfRenderOptions.cs create mode 100644 MapToolkit.Drawing.Topographic/IconsRender.cs create mode 100644 MapToolkit.Drawing.Topographic/LegendRender.cs create mode 100644 MapToolkit.Drawing.Topographic/MapToolkit.Drawing.Topographic.csproj create mode 100644 MapToolkit.Drawing.Topographic/TopoIcon.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoIconType.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoLocation.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoLocationType.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapData.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapDataExtensions.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapPathType.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapPdfRender.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapPdfTile.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapRender.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapRenderData.cs create mode 100644 MapToolkit.Drawing.Topographic/TopoMapStyle.cs diff --git a/MapToolkit.Drawing.Topographic/ColorPalette.cs b/MapToolkit.Drawing.Topographic/ColorPalette.cs new file mode 100644 index 0000000..9b1541d --- /dev/null +++ b/MapToolkit.Drawing.Topographic/ColorPalette.cs @@ -0,0 +1,29 @@ +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + internal class ColorPalette + { + internal static readonly ColorPalette Default = new ColorPalette(); + + public Color ContourMinor { get; set; } = Color.ParseHex("D4C5BF"); + public Color ContourMajor { get; set; } = Color.ParseHex("B29A94"); + public Color ForestFill { get; set; } = Color.ParseHex("D1FEB9"); + public Color ForestBorder { get; set; } = Color.ParseHex("8AE854"); + public Color WaterFill { get; set; } = Color.ParseHex("B3D9FE"); + public Color WaterBorder { get; set; } = Color.ParseHex("77A5E1"); + public Color BuildingFill { get; set; } = Color.ParseHex("808080"); + public Color BuildingBorder { get; set; } = Color.ParseHex("000000"); + public Color RocksSymbol { get; set; } = Color.ParseHex("C0C0C0"); + public Color RocksFill { get; set; } = Color.ParseHex("C0C0C080"); + public Color RocksBorder { get; set; } = Color.ParseHex("808080"); + public Color MainRoad { get; set; } = Color.ParseHex("FE002C"); + public Color SecondaryRoad { get; set; } = Color.ParseHex("FF9643"); + public Color Road { get; set; } = Color.White; + public Color RoadBorder { get; set; } = Color.Black; + public Color Trail { get; set; } = Color.ParseHex("C0C0C0"); + public Color Graticule { get; set; } = Color.ParseHex("0071D6"); + public Color TextBorder { get; set; } = Color.ParseHex("FFFFFFCC"); + + } +} diff --git a/MapToolkit.Drawing.Topographic/ITopoMapData.cs b/MapToolkit.Drawing.Topographic/ITopoMapData.cs new file mode 100644 index 0000000..e9600b9 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/ITopoMapData.cs @@ -0,0 +1,32 @@ +using GeoJSON.Text.Geometry; +using MapToolkit.DataCells; + +namespace MapToolkit.Drawing.Topographic +{ + public interface ITopoMapData + { + string Title { get; } + + Dictionary? Roads { get; } + + Dictionary? Bridges { get; } + + MultiPolygon? ForestPolygons { get; } + + MultiPolygon? RockPolygons { get; } + + MultiPolygon? BuildingPolygons { get; } + + MultiPolygon? WaterPolygons { get; } + + IDemDataView DemDataCell { get; } + + List? Names { get; } + + List? Icons { get; } + + MultiLineString? Powerlines { get; } + + MultiPolygon? FortPolygons { get; } + } +} diff --git a/MapToolkit.Drawing.Topographic/ITopoMapPdfRenderOptions.cs b/MapToolkit.Drawing.Topographic/ITopoMapPdfRenderOptions.cs new file mode 100644 index 0000000..4b9153b --- /dev/null +++ b/MapToolkit.Drawing.Topographic/ITopoMapPdfRenderOptions.cs @@ -0,0 +1,11 @@ +namespace MapToolkit.Drawing.Topographic +{ + public interface ITopoMapPdfRenderOptions + { + public string FileName { get; } + + public string TargetDirectory { get; } + + public string Attribution { get; } + } +} diff --git a/MapToolkit.Drawing.Topographic/IconsRender.cs b/MapToolkit.Drawing.Topographic/IconsRender.cs new file mode 100644 index 0000000..f8cdb5e --- /dev/null +++ b/MapToolkit.Drawing.Topographic/IconsRender.cs @@ -0,0 +1,98 @@ +using MapToolkit.Drawing; +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + internal class IconsRender + { + internal static IDrawIcon Dot(IDrawSurface w) + { + var style = w.AllocateBrushStyle(Color.Black); + return w.AllocateIcon(new Vector(5, 5), (target) => + { + target.DrawCircle(new Vector(2.5, 2.5), 2, style); + }); + } + + internal static IDrawIcon Hospital(IDrawSurface w) + { + var border = w.AllocateStyle("FFFFFF", "FF0033"); + var cross = w.AllocatePenStyle("FF0033", 2); + return w.AllocateIcon(new Vector(12, 12), (target) => + { + target.DrawRectangle(new Vector(0, 0), new Vector(11, 11), border); + target.DrawPolyline(new[] { new Vector(0, 5.5), new Vector(11, 5.5) }, cross); + target.DrawPolyline(new[] { new Vector(5.5, 0), new Vector(5.5, 11) }, cross); + }); + } + + internal static IDrawIcon WaterTower(IDrawSurface w) + { + var style = w.AllocateBrushStyle("0080FF"); + return w.AllocateIcon(new Vector(13, 13), (target) => target.DrawCircle(new Vector(6, 6), 6, style)); + } + + internal static IDrawIcon TechnicalTower(IDrawSurface w) + { + var style = w.AllocateStyle(Color.White, Color.Black, 1); + var stylea = w.AllocatePenStyle(Color.Black, 1); + return w.AllocateIcon(new Vector(13, 13), (target) => + { + target.DrawCircle(new Vector(6, 6), 6, style); + target.DrawPolyline(new[] { new Vector(1.76, 1.76), new Vector(10.24, 10.24) }, stylea); + target.DrawPolyline(new[] { new Vector(10.24, 1.76), new Vector(1.76, 10.24) }, stylea); + }); + } + internal static IDrawIcon Transmitter(IDrawSurface w) + { + var full = w.AllocateBrushStyle(Color.White); + var center = w.AllocateStyle(Color.Black, Color.Black, 1); + var line = w.AllocatePenStyle(Color.Black, 2); + + return w.AllocateIcon(new Vector(13, 13), (target) => + { + var c = new Vector(6, 6); + target.DrawCircle(c, 6, full); + target.DrawCircle(c, 3, center); + target.DrawArc(c, 6, 50, 70, line); + target.DrawArc(c, 6, 140, 70, line); + target.DrawArc(c, 6, 230, 70, line); + target.DrawArc(c, 6, 320, 70, line); + }); + } + + internal static IDrawIcon WindTurbine(IDrawSurface w) + { + var style = w.AllocatePenStyle(Color.Black, 2); + var stylea = w.AllocatePenStyle(Color.Black, 1); + var stylec = w.AllocateBrushStyle(Color.Black); + return w.AllocateIcon(new Vector(32, 32), (target) => WindTurbine(target, style, stylea, stylec)); + } + + internal static void WindTurbine(IDrawSurface w, IDrawStyle style, IDrawStyle stylea, IDrawStyle stylec) + { + var center = new Vector(16, 16); + w.DrawPolyline( + new[] + { + center + new Vector( 0.9961946 * 2.5, -0.0871557 * 2.5), + center + new Vector( 0.6427876 * 15, 0.7660444 * 15), + center + new Vector( 0.5000000 * 15, 0.8660254 * 15), + center + new Vector(-0.4226182 * 2.5, 0.9063077 * 2.5), + center + new Vector(-0.9848077 * 15, 0.1736481 * 15), + center + new Vector(-1.0 * 15, 0 * 15), + center + new Vector(-0.5735764 * 2.5, -0.8191520 * 2.5), + center + new Vector( 0.3420201 * 15, -0.9396926 * 15), + center + new Vector( 0.5000000 * 15, -0.8660254 * 15), + center + new Vector( 0.9961946 * 2.5, -0.0871557 * 2.5) + }, style); + w.DrawArc(center, 10, 75, 80, stylea); + w.DrawArc(center, 15, 70, 90, stylea); + w.DrawArc(center, 10, 195, 80, stylea); + w.DrawArc(center, 15, 190, 90, stylea); + w.DrawArc(center, 10, -45, 80, stylea); + w.DrawArc(center, 15, -50, 90, stylea); + w.DrawCircle(center, 1, stylec); + } + } +} diff --git a/MapToolkit.Drawing.Topographic/LegendRender.cs b/MapToolkit.Drawing.Topographic/LegendRender.cs new file mode 100644 index 0000000..3bf28e0 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/LegendRender.cs @@ -0,0 +1,239 @@ +using MapToolkit.Drawing; +using MapToolkit.Drawing.PdfRender; +using MapToolkit.Projections; +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + internal class LegendRender + { + public const double LegendWidthPoints = LegendWidth * PaperSize.OnePixelAt300Dpi; + public const double LegendHeightPoints = LegendHeight * PaperSize.OnePixelAt300Dpi; + + // Pixels + + public const double LegendWidth = 1300; + public const double LegendHeight = 2000; + private const double InsideMargin = 40; + + private const double LegendHalfWidth = LegendWidth / 2; + + private const double LegendContentStart = InsideMargin; + private const double LegendContentCenter = LegendHalfWidth; + private const double LegendContentEnd = LegendWidth - InsideMargin; + + private const double LegendContentP1 = LegendContentCenter; + private const double LegendContentW = (LegendContentEnd - LegendContentCenter - InsideMargin * 2) / 3; + private const double LegendContentP2 = LegendContentP1 + LegendContentW; + private const double LegendContentP3 = LegendContentP2 + InsideMargin; + private const double LegendContentP4 = LegendContentP3 + LegendContentW; + private const double LegendContentP5 = LegendContentP4 + InsideMargin; + private const double LegendContentP6 = LegendContentEnd; + + public static void RenderLegend(IDrawSurface w, ITopoMapData data, ITopoMapPdfRenderOptions opts, int scale) + { + RenderLegend(w, data.Title, opts.Attribution, scale); + } + + public static void RenderLegend(IDrawSurface w, string title, string credits, int scale, string? fullCopyrightNotice = null, string? licenseNotice = null) + { + var style = TopoMapStyle.CreateFull(w, ColorPalette.Default, true); + + var titleText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 160, new SolidColorBrush(Color.Black), null, false, TextAnchor.TopCenter); + var subTitleText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 60, new SolidColorBrush(Color.Black), null, false, TextAnchor.TopCenter); + var normalText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 32, new SolidColorBrush(Color.Black), null, false, TextAnchor.TopCenter); + var smallText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 25, new SolidColorBrush(Color.Black), null, false, TextAnchor.TopCenter); + var normalTextLeft = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 32, new SolidColorBrush(Color.Black), null, false, TextAnchor.CenterLeft); + + w.DrawRectangle(new Vector(0, 0), new Vector(LegendWidth, LegendHeight), w.AllocateStyle(Color.White, Color.Black, 2)); + + w.DrawText(new Vector(LegendContentCenter, 20), "MapToolkit Topographic Map", subTitleText); + w.DrawText(new Vector(LegendContentCenter, 90), title, titleText); + if (scale == -1) + { + w.DrawText(new Vector(LegendContentCenter, 300), $"Dynamic scale : see bottom left of map", normalText); + } + else + { + w.DrawText(new Vector(LegendContentCenter, 300), $"Scale 1 : {scale} 000 - 1 cm = {scale * 10} m", normalText); + } + w.DrawText(new Vector(LegendContentCenter, 345), $"Version {DateTime.Now:yyyy-MM-dd}", smallText); + + RoadLegend(w, style, normalTextLeft, 500, TopoMapPathType.Main, "Main road"); + RoadLegend(w, style, normalTextLeft, 560, TopoMapPathType.Secondary, "Secondary road"); + RoadLegend(w, style, normalTextLeft, 620, TopoMapPathType.Road, "City road, Other road"); + RoadLegend(w, style, normalTextLeft, 680, TopoMapPathType.Track, "Vehicle track"); + RoadLegend(w, style, normalTextLeft, 740, TopoMapPathType.Trail, "Foot trail"); + + w.DrawText(new Vector(LegendContentStart, 800), $"Bridges Main, Secondary, Road", normalTextLeft); + RenderBridge(w, style, TopoMapPathType.Main, new Vector(LegendContentP1, 800), new Vector(LegendContentP2, 800)); + RenderBridge(w, style, TopoMapPathType.Secondary, new Vector(LegendContentP3, 800), new Vector(LegendContentP4, 800)); + RenderBridge(w, style, TopoMapPathType.Road, new Vector(LegendContentP5, 800), new Vector(LegendContentP6, 800)); + + w.DrawText(new Vector(LegendContentStart, 860), $"Forest, Rocks, Water", normalTextLeft); + w.DrawRectangle(new Vector(LegendContentP1, 840), new Vector(LegendContentP2, 880), style.forest); + w.DrawRectangle(new Vector(LegendContentP3, 840), new Vector(LegendContentP4, 880), style.rocks); + w.DrawRectangle(new Vector(LegendContentP5, 840), new Vector(LegendContentP6, 880), style.water); + + if (style.WindPowerPlant != null && style.WaterTower != null && style.Transmitter != null) + { + w.DrawText(new Vector(LegendContentStart, 920), $"Wind turbine, Water tower, Transmitter", normalTextLeft); + w.DrawIcon(new Vector((LegendContentP1 + LegendContentP2) / 2, 920), style.WindPowerPlant); + w.DrawIcon(new Vector((LegendContentP3 + LegendContentP4) / 2, 920), style.WaterTower); + w.DrawIcon(new Vector((LegendContentP5 + LegendContentP6) / 2, 920), style.Transmitter); + } + + w.DrawText(new Vector(LegendContentCenter, 1810), $"Original Map {credits}", smallText); + + if (!string.IsNullOrEmpty(fullCopyrightNotice)) + { + w.DrawText(new Vector(LegendContentCenter, 1850), fullCopyrightNotice, smallText); + } + if (scale == -1) + { + w.DrawText(new Vector(LegendContentCenter, 1890), "Web Map created by GrueArbre", smallText); + } + else + { + w.DrawText(new Vector(LegendContentCenter, 1890), "Print Map created by GrueArbre", smallText); + } + if (!string.IsNullOrEmpty(fullCopyrightNotice)) + { + w.DrawText(new Vector(LegendContentCenter, 1930), "ONLY FOR ARMA USE - NO COMMERCIAL USE - Licensed under APL-SA", smallText); + } + } + + private static void RenderBridge(IDrawSurface w, TopoMapStyle style, TopoMapPathType type, Vector begin, Vector end) + { + var bg = style.roadBackground[(int)type]; + var fg = style.roadForeground[(int)type]; + var bbg = style.bridgeBackground[(int)type]; + var bfg = style.bridgeForeground[(int)type]; + if (bg != null) + { + w.DrawPolyline(new[] { begin, end }, bg); + } + if (fg != null) + { + w.DrawPolyline(new[] { begin, end }, fg); + } + + var bbegin = (begin * 2 + end) / 3; + var bend = (begin + end * 2) / 3; + + if (bbg != null) + { + w.DrawPolyline(new[] { bbegin, bend }, bbg); + + TopoMapRender.BridgeLimit(w, bbegin, bend, style.BridgeLimit); + TopoMapRender.BridgeLimit(w, bend, bbegin, style.BridgeLimit); + } + if (bfg != null) + { + w.DrawPolyline(new[] { bbegin, bend }, bfg); + } + } + + private static void RoadLegend(IDrawSurface w, TopoMapStyle style, IDrawTextStyle textStyle, double top, TopoMapPathType type, string label) + { + w.DrawText(new Vector(LegendContentStart, top), label, textStyle); + + var bg = style.roadBackground[(int)type]; + var fg = style.roadForeground[(int)type]; + if (bg != null) + { + w.DrawPolyline(new[] { new Vector(LegendContentP1, top), new Vector(LegendContentP6, top) }, bg); + } + if (fg != null) + { + w.DrawPolyline(new[] { new Vector(LegendContentP1, top), new Vector(LegendContentP6, top) }, fg); + } + } + + public static void DrawMiniMap(IDrawSurface w, ITopoMapData data, List? tiles, TopoMapPdfTile? current, double size = LegendWidth) + { + var projection = new NoProjectionArea(data.DemDataCell.Start, data.DemDataCell.End, new Vector(size, size)); + + var tileNameText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 12 / 0.24, new SolidColorBrush(Color.Black), null, false, TextAnchor.BottomCenter); + var cityNameText = w.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Regular, 8 / 0.24, new SolidColorBrush(Color.Black), null, false, TextAnchor.CenterLeft); + + var cityCircle = w.AllocatePenStyle(Color.Black, 1); + var tileLimit = w.AllocatePenStyle(Color.Gray, 2); + var currentTile = w.AllocateStyle(new SolidColorBrush(Color.ParseHex("FFFF0080")), new Pen(Color.Gray, 4)); + + if (data.ForestPolygons != null) + { + TopoMapRender.DrawPolygonsSimplified(w, projection, data.ForestPolygons, w.AllocateBrushStyle("D1FEB9")); + } + if (data.RockPolygons != null) + { + TopoMapRender.DrawPolygonsSimplified(w, projection, data.RockPolygons, w.AllocateBrushStyle("C0C0C080")); + } + if (data.WaterPolygons != null) + { + TopoMapRender.DrawPolygonsSimplified(w, projection, data.WaterPolygons, w.AllocateBrushStyle("B3D9FE")); + } + + var mains = data.Roads?[TopoMapPathType.Main]?.Coordinates; + if (mains != null) + { + var mr = w.AllocatePenStyle("FE002C", 2); + foreach (var road in mains) + { + var simpler = LevelOfDetailHelper.SimplifyAnglesAndDistances(road.Coordinates.Select(projection.Project).ToList(), 1); + if (simpler.Count > 0) + { + w.DrawPolyline(simpler, mr); + } + } + } + if (tiles != null) + { + foreach (var tile in tiles) + { + if (tile != current) + { + w.DrawPolyline(GetTile(projection, tile), tileLimit); + } + } + } + if (current != null) + { + w.DrawPolygon(GetTile(projection, current), currentTile); + } + + if (data.Names != null) + { + foreach (var city in data.Names) + { + if (city.Type == TopoLocationType.City || city.Type == TopoLocationType.Local || city.Type == TopoLocationType.Village) + { + var pc = projection.Project(city.Position); + w.DrawCircle(pc, 1.5f, cityCircle); + w.DrawText(pc + new Vector(15, 0), city.Name, cityNameText); + } + } + } + + if (tiles != null) + { + foreach (var tile in tiles) + { + w.DrawText((projection.Project(tile.Min) + projection.Project(tile.Max)) / 2, tile.Name, tileNameText); + } + } + + w.DrawRectangle(new Vector(0, 0), new Vector(size, size), w.AllocatePenStyle(Color.Black, 2)); + } + + private static Vector[] GetTile(NoProjectionArea projection, TopoMapPdfTile tile) + { + return new[]{ + projection.Project(tile.Min), + projection.Project(new Coordinates(tile.Min.Latitude, tile.Max.Longitude)), + projection.Project(tile.Max), + projection.Project(new Coordinates(tile.Max.Latitude, tile.Min.Longitude)), + projection.Project(tile.Min)}; + } + } +} diff --git a/MapToolkit.Drawing.Topographic/MapToolkit.Drawing.Topographic.csproj b/MapToolkit.Drawing.Topographic/MapToolkit.Drawing.Topographic.csproj new file mode 100644 index 0000000..10085b4 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/MapToolkit.Drawing.Topographic.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/MapToolkit.Drawing.Topographic/TopoIcon.cs b/MapToolkit.Drawing.Topographic/TopoIcon.cs new file mode 100644 index 0000000..9552d0c --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoIcon.cs @@ -0,0 +1,15 @@ +namespace MapToolkit.Drawing.Topographic +{ + public sealed class TopoIcon + { + public TopoIcon(TopoIconType mapType, Coordinates coordinates) + { + MapType = mapType; + Coordinates = coordinates; + } + + public TopoIconType MapType { get; } + + public Coordinates Coordinates { get; } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoIconType.cs b/MapToolkit.Drawing.Topographic/TopoIconType.cs new file mode 100644 index 0000000..c24b168 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoIconType.cs @@ -0,0 +1,11 @@ +namespace MapToolkit.Drawing.Topographic +{ + public enum TopoIconType + { + WindPowerPlant, + WaterTower, + Transmitter, + ElectricityPylon, + Hospital + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoLocation.cs b/MapToolkit.Drawing.Topographic/TopoLocation.cs new file mode 100644 index 0000000..2abda38 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoLocation.cs @@ -0,0 +1,18 @@ +namespace MapToolkit.Drawing.Topographic +{ + public sealed class TopoLocation + { + public TopoLocation(string name, TopoLocationType type, Coordinates position) + { + Name = name; + Type = type; + Position = position; + } + + public string Name { get; } + + public TopoLocationType Type { get; } + + public Coordinates Position { get; } + } +} \ No newline at end of file diff --git a/MapToolkit.Drawing.Topographic/TopoLocationType.cs b/MapToolkit.Drawing.Topographic/TopoLocationType.cs new file mode 100644 index 0000000..70b7e22 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoLocationType.cs @@ -0,0 +1,9 @@ +namespace MapToolkit.Drawing.Topographic +{ + public enum TopoLocationType + { + City, + Local, + Village + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapData.cs b/MapToolkit.Drawing.Topographic/TopoMapData.cs new file mode 100644 index 0000000..96aa336 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapData.cs @@ -0,0 +1,34 @@ +using GeoJSON.Text.Geometry; +using MapToolkit.DataCells; + +namespace MapToolkit.Drawing.Topographic +{ + internal class TopoMapData : ITopoMapData + { + public string Title { get; set; } = "(no name)"; + + public Dictionary? Roads { get; set; } + + public Dictionary? Bridges { get; set; } + + public MultiPolygon? ForestPolygons { get; set; } + + public MultiPolygon? RockPolygons { get; set; } + + public MultiPolygon? BuildingPolygons { get; set; } + + public MultiPolygon? WaterPolygons { get; set; } + + public required IDemDataView DemDataCell { get; set; } + + public List? Names { get; set; } + + public List? PlottedPoints { get; set; } + + public List? Icons { get; set; } + + public MultiLineString? Powerlines { get; set; } + + public MultiPolygon? FortPolygons { get; internal set; } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapDataExtensions.cs b/MapToolkit.Drawing.Topographic/TopoMapDataExtensions.cs new file mode 100644 index 0000000..be63652 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapDataExtensions.cs @@ -0,0 +1,24 @@ +namespace MapToolkit.Drawing.Topographic +{ + public static class TopoMapDataExtensions + { + public static ITopoMapData Crop(this ITopoMapData other, Coordinates min, Coordinates max, string? title) + { + return new TopoMapData() + { + Title = title ?? other.Title, + DemDataCell = other.DemDataCell.CreateView(min, max), + ForestPolygons = other.ForestPolygons?.Crop(min, max), + RockPolygons = other.RockPolygons?.Crop(min, max), + FortPolygons = other.FortPolygons?.Crop(min, max), + BuildingPolygons = other.BuildingPolygons?.Crop(min, max), + WaterPolygons = other.WaterPolygons?.Crop(min, max), + Bridges = other.Bridges?.ToDictionary(k => k.Key, k => k.Value.Crop(min, max)), + Roads = other.Roads?.ToDictionary(k => k.Key, k => k.Value.Crop(min, max)), + Powerlines = other.Powerlines?.Crop(min, max), + Names = other.Names?.Where(n => n.Position?.IsInSquare(min, max) ?? false)?.ToList(), + Icons = other.Icons?.Where(n => n.Coordinates?.IsInSquare(min, max) ?? false)?.ToList() + }; + } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapPathType.cs b/MapToolkit.Drawing.Topographic/TopoMapPathType.cs new file mode 100644 index 0000000..3999560 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapPathType.cs @@ -0,0 +1,11 @@ +namespace MapToolkit.Drawing.Topographic +{ + public enum TopoMapPathType + { + Main, + Secondary, + Road, + Track, + Trail + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapPdfRender.cs b/MapToolkit.Drawing.Topographic/TopoMapPdfRender.cs new file mode 100644 index 0000000..5eb1e3e --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapPdfRender.cs @@ -0,0 +1,275 @@ +using DemUtility; +using MapToolkit.Drawing; +using MapToolkit.Drawing.PdfRender; +using MapToolkit.Projections; +using PdfSharpCore.Drawing; +using PdfSharpCore.Pdf; +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + public static class TopoMapPdfRender + { + private const double LegendWidth = LegendRender.LegendWidthPoints; + private const double LegendHeight = LegendRender.LegendHeightPoints; + private const double LegendHalfWidth = LegendWidth / 2; + private const double Margin = 20; + + private const double LegendWidthWithBothMargin = LegendWidth + 2 * Margin; + private const double LegendWidthWithAllsMargins = LegendWidth + 3 * Margin; + private const double DoubleLegendWidthWithBothMargin = LegendWidthWithBothMargin * 2; + + public static void RenderPDF(ITopoMapPdfRenderOptions opts, ITopoMapData data, int scale = 25) + { + var sizeInMeters = new Vector( + data.DemDataCell.End.Longitude - data.DemDataCell.Start.Longitude, + data.DemDataCell.End.Latitude - data.DemDataCell.Start.Latitude); + + var sizeInPoints = new Vector( + sizeInMeters.X / scale * PaperSize.OneMilimeter, + sizeInMeters.Y / scale * PaperSize.OneMilimeter); + + if (sizeInPoints.X > PaperSize.ArchEHeight || sizeInPoints.Y > PaperSize.ArchEWidth) + { + var paperSize = new Vector(PaperSize.ArchEHeight, PaperSize.ArchEWidth); + var paperSurface = new Vector(PaperSize.ArchEHeight - (LegendWidth + Margin * 3) - 1, PaperSize.ArchEWidth - Margin * 2); + + var tiles = GetTiles(data.DemDataCell.Start, scale, sizeInMeters, sizeInPoints, paperSurface); + + foreach (var tile in tiles) + { + var subdata = data.Crop(tile.Min, tile.Max, data.Title + " - " + tile.Name); + var file = Path.Combine(opts.TargetDirectory, $"{opts.FileName}_{tile.Name}.pdf"); + RenderSinglePdf(subdata, scale, paperSize, file, opts, data, tiles, tile); + } + } + else + { + var paperSize = GetPaperSize(sizeInPoints.X, sizeInPoints.Y); + var file = Path.Combine(opts.TargetDirectory, opts.FileName + ".pdf"); + RenderSinglePdf(data, scale, paperSize, file, opts); + } + } + + public static void RenderPDFBook(ITopoMapPdfRenderOptions opts, ITopoMapData data, int scale = 25) + { + var sizeInMeters = new Vector( + data.DemDataCell.End.Longitude - data.DemDataCell.Start.Longitude, + data.DemDataCell.End.Latitude - data.DemDataCell.Start.Latitude); + + var sizeInPoints = new Vector( + sizeInMeters.X / scale * PaperSize.OneMilimeter, + sizeInMeters.Y / scale * PaperSize.OneMilimeter); + + var paperSize = new Vector(PaperSize.A3Height, PaperSize.A3Width); + + var paperSurface = new Vector(paperSize.X - Margin * 2, paperSize.Y - Margin * 2); + + var file = Path.Combine(opts.TargetDirectory, opts.FileName + "-book.pdf"); + + var tiles = GetTiles(data.DemDataCell.Start, scale, sizeInMeters, sizeInPoints, paperSurface); + + var document = new PdfDocument(); + document.Info.Title = data.Title; + document.Info.Creator = "MapToolkit Topo Map - Print Map created by GrueArbre"; + document.Info.Author = $"Original Map {opts.Attribution}"; + + Console.WriteLine($"{tiles.Count} tiles"); + + var page = document.AddPage(); + page.Width = paperSize.X; + page.Height = paperSize.Y; + + var legendTopCenter = new Vector((paperSize.X - LegendWidth - LegendHeight - Margin) / 2, (paperSize.Y - LegendHeight) / 2); + + ToPdfPage(page, legendTopCenter, w => LegendRender.RenderLegend(w, data, opts, scale)); + ToPdfPage(page, legendTopCenter + new Vector(LegendWidth + Margin, 0), w => LegendRender.DrawMiniMap(w, data, tiles, null, LegendRender.LegendHeight)); + + foreach (var tile in tiles.OrderBy(t => t.Name)) + { + var subdata = data.Crop(tile.Min, tile.Max, tile.Name); + + var rdata = TopoMapRenderData.Create(subdata); + + RenderPage(rdata, scale, paperSize, opts, data, tiles, tile, document); + } + document.Save(file); + } + + private static List GetTiles(Coordinates start, int scale, Vector sizeInMeters, Vector sizeInPoints, Vector paperSurface) + { + var w = Math.Ceiling(sizeInPoints.X / paperSurface.X); + var h = Math.Ceiling(sizeInPoints.Y / paperSurface.Y); + + var overlapInMeters = new Vector( + (w * paperSurface.X - sizeInPoints.X) / w / PaperSize.OneMilimeter * scale, + (h * paperSurface.Y - sizeInPoints.Y) / h / PaperSize.OneMilimeter * scale); + + var baseSize = new Vector(sizeInMeters.X / w, sizeInMeters.Y / h); + + return GetTiles(start, w, h, overlapInMeters, baseSize); + } + + private static List GetTiles(Coordinates start, double w, double h, Vector overlapInMeters, Vector baseSize) + { + var tiles = new List(); + for (var x = 0; x < w; ++x) + { + for (var y = 0; y < h; ++y) + { + var name = $"{(char)('A' + x)}{h - y}"; + var min = start + new Vector(baseSize.X * x, baseSize.Y * y); + var max = start + new Vector(baseSize.X * (x + 1), baseSize.Y * (y + 1)); + if (x == 0) + { + max = max + new Vector(overlapInMeters.X, 0); + } + else if (x == w - 1) + { + min = min - new Vector(overlapInMeters.X, 0); + } + else + { + min = min - new Vector(overlapInMeters.X / 2, 0); + max = max + new Vector(overlapInMeters.X / 2, 0); + } + if (y == 0) + { + max = max + new Vector(0, overlapInMeters.Y); + } + else if (y == h - 1) + { + min = min - new Vector(0, overlapInMeters.Y); + } + else + { + min = min - new Vector(0, overlapInMeters.Y / 2); + max = max + new Vector(0, overlapInMeters.Y / 2); + } + tiles.Add(new TopoMapPdfTile(name, min, max)); + } + } + + return tiles; + } + + private static void RenderSinglePdf(ITopoMapData data, int scale, Vector paperSize, string file, ITopoMapPdfRenderOptions opts, ITopoMapData? fulldata = null, List? tiles = null, TopoMapPdfTile? current = null) + { + var document = new PdfDocument(); + document.Info.Title = data.Title; + document.Info.Creator = "MapToolkit Topo Map - Print Map created by GrueArbre"; + document.Info.Author = $"Original Map {opts.Attribution}"; + + var rdata = TopoMapRenderData.Create(data); + + RenderPage(rdata, scale, paperSize, opts, fulldata, tiles, current, document); + document.Save(file); + } + + private static void RenderPage(TopoMapRenderData rdata, int scale, Vector paperSizeInPoints, ITopoMapPdfRenderOptions opts, ITopoMapData? fulldata, List? tiles, TopoMapPdfTile? current, PdfDocument document) + { + var page = document.AddPage(); + page.Width = paperSizeInPoints.X; + page.Height = paperSizeInPoints.Y; + + var projSizeInPoints = new Vector(rdata.WidthInMeters / scale * PaperSize.OneMilimeter, rdata.HeightInMeters / scale * PaperSize.OneMilimeter); + + var proj = new NoProjectionArea(rdata.Start, rdata.End, projSizeInPoints / 0.24); + + var dX = paperSizeInPoints.X - projSizeInPoints.X; + var dY = paperSizeInPoints.Y - projSizeInPoints.Y; + + Vector mapTopLeft; + Vector legendTopCenter; + bool miniMap = false; + bool drawLegend = true; + + if (dX >= LegendWidthWithAllsMargins && dX < DoubleLegendWidthWithBothMargin) + { + var delta = (dX - LegendWidthWithAllsMargins) / 2; + // | Margin | ... Legend ... | Margin | ... Map ... | Margin | + mapTopLeft = new Vector(delta + Margin * 2 + LegendWidth, dY / 2); + legendTopCenter = new Vector(delta + LegendHalfWidth + Margin, dY / 2); + miniMap = true; + } + else + { + mapTopLeft = new Vector(dX / 2, dY / 2); + if (dX < DoubleLegendWidthWithBothMargin) + { + // | ... Map ... | + // | | ... Legend ... | Margin | + legendTopCenter = new Vector(paperSizeInPoints.X - dX / 2 - LegendHalfWidth - Margin, paperSizeInPoints.Y - dY / 2 - LegendHeight - Margin); + drawLegend = paperSizeInPoints.X > PaperSize.A3Height; + } + else + { + // | Margin | ... Legend ... | Margin | ... Map ... | Margin | ... Space for legend ...| Margin | + legendTopCenter = new Vector(dX / 4, dY / 2); + miniMap = true; + } + } + + ToPdfPage(page, mapTopLeft, w => TopoMapRender.RenderWithExternGraticule(w, rdata, proj)); + if (drawLegend) + { + ToPdfPage(page, legendTopCenter - new Vector(LegendHalfWidth, 0), w => LegendRender.RenderLegend(w, rdata.Data, opts, scale)); + if (miniMap) + { + ToPdfPage(page, legendTopCenter + new Vector(-LegendHalfWidth, LegendHeight + Margin), w => LegendRender.DrawMiniMap(w, fulldata ?? rdata.Data, tiles, current)); + } + } + else + { + ToPdfPage(page, mapTopLeft, w => + { + var style = w.AllocateTextStyle(new[] { "Calibri" }, SixLabors.Fonts.FontStyle.Regular, 30, new SolidColorBrush(Color.Black), null, false, TextAnchor.CenterLeft); + w.DrawText(new Vector(0, -30), rdata.Data.Title, style); + w.DrawText(new Vector(0, proj.Size.Y + 30), rdata.Data.Title, style); + + style = w.AllocateTextStyle(new[] { "Calibri" }, SixLabors.Fonts.FontStyle.Regular, 30, new SolidColorBrush(Color.Black), null, false, TextAnchor.CenterRight); + w.DrawText(new Vector(proj.Size.X, -30), rdata.Data.Title, style); + w.DrawText(new Vector(proj.Size.X, proj.Size.Y + 30), rdata.Data.Title, style); + }); + } + } + + + + private static Vector GetPaperSize(double widthInPoints, double heightInPoints) + { + if (heightInPoints > PaperSize.A0Width) + { + // Arch E / Maxmimum size + return new Vector(PaperSize.ArchEHeight, PaperSize.ArchEWidth); + } + if (widthInPoints > PaperSize.A1Height || heightInPoints > PaperSize.A1Width) + { + // A0 + return new Vector(PaperSize.A0Height, PaperSize.A0Width); + } + if (widthInPoints > PaperSize.A2Height || heightInPoints > PaperSize.A2Width) + { + // A1 + return new Vector(PaperSize.A1Height, PaperSize.A1Width); + } + if (widthInPoints > PaperSize.A3Height || heightInPoints > PaperSize.A3Width) + { + // A2 + return new Vector(PaperSize.A2Height, PaperSize.A2Width); + } + // A3 + return new Vector(PaperSize.A3Height, PaperSize.A3Width); + } + + + public static void ToPdfPage(PdfPage page, Vector shiftInPoints, Action draw) + { + using (var xgfx = XGraphics.FromPdfPage(page)) + { + xgfx.TranslateTransform(shiftInPoints.X, shiftInPoints.Y); + Drawing.Render.ToPdfGraphics(xgfx, PaperSize.OnePixelAt300Dpi, draw); + } + } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapPdfTile.cs b/MapToolkit.Drawing.Topographic/TopoMapPdfTile.cs new file mode 100644 index 0000000..12dfb57 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapPdfTile.cs @@ -0,0 +1,16 @@ +namespace MapToolkit.Drawing.Topographic +{ + internal class TopoMapPdfTile + { + public TopoMapPdfTile(string name, Coordinates min, Coordinates max) + { + Name = name; + Min = min; + Max = max; + } + + public string Name { get; } + public Coordinates Min { get; } + public Coordinates Max { get; } + } +} \ No newline at end of file diff --git a/MapToolkit.Drawing.Topographic/TopoMapRender.cs b/MapToolkit.Drawing.Topographic/TopoMapRender.cs new file mode 100644 index 0000000..95e48fb --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapRender.cs @@ -0,0 +1,344 @@ +using System.Globalization; +using GeoJSON.Text.Geometry; +using MapToolkit.Drawing; +using MapToolkit.Drawing.Contours; +using MapToolkit.Projections; +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + public static class TopoMapRender + { + public static void Render(IDrawSurface writer, TopoMapRenderData renderData, NoProjectionArea proj) + { + var style = TopoMapStyle.CreateFull(writer, ColorPalette.Default); + + RenderAny(writer, renderData, proj, style); + + NaiveGraticule(writer, proj, style); + } + + public static void RenderWithExternGraticule(IDrawSurface writer, TopoMapRenderData renderData, NoProjectionArea proj) + { + var style = TopoMapStyle.CreateFull(writer, ColorPalette.Default, true); + + RenderAny(writer, renderData, proj, style); + + NaiveGraticule(writer, proj, style); + } + + public static void RenderLod2(IDrawSurface writer, TopoMapRenderData renderData, NoProjectionArea proj) + { + var style = TopoMapStyle.CreateLod2(writer, ColorPalette.Default); + + RenderAny(writer, renderData, proj, style, true); + + NaiveGraticule(writer, proj, style); + } + + public static void RenderLod3(IDrawSurface writer, TopoMapRenderData renderData, NoProjectionArea proj) + { + var style = TopoMapStyle.CreateLod3(writer, ColorPalette.Default); + var data = renderData.Data; + + if (data.ForestPolygons != null) + { + DrawPolygons(writer, proj, data.ForestPolygons, style.forest); + } + + if (data.RockPolygons != null) + { + DrawPolygons(writer, proj, data.RockPolygons, style.rocks); + } + + if (renderData.Img != null) + { + writer.DrawImage(renderData.Img, Vector.Zero, proj.Size, 0.5); + } + + if (data.WaterPolygons != null) + { + DrawPolygons(writer, proj, data.WaterPolygons, style.water); + } + + if (data.Roads != null) + { + RenderRoads(writer, data.Roads, proj, style.roadForeground, style.roadBackground); + } + + if (data.Bridges != null) + { + RenderBridgesRoads(writer, data.Bridges, proj, style.bridgeForeground, style.bridgeBackground, style.BridgeLimit); + } + + RenderNames(writer, data, proj, style); + + NaiveGraticule(writer, proj, style, 10000); + } + + private static void RenderAny(IDrawSurface writer, TopoMapRenderData renderData, NoProjectionArea proj, TopoMapStyle style, bool simpler = false) + { + var data = renderData.Data; + + if (data.ForestPolygons != null) + { + DrawPolygons(writer, proj, data.ForestPolygons, style.forest); + } + if (data.RockPolygons != null) + { + DrawPolygons(writer, proj, data.RockPolygons, style.rocks); + } + + var render = new ContourRender(writer, style.contourStyle); + if (simpler) + { + render.RenderSimpler(renderData.Contour, proj, renderData.Img); + } + else + { + render.Render(renderData.Contour, proj, renderData.Img); + } + + if (data.WaterPolygons != null) + { + DrawPolygons(writer, proj, data.WaterPolygons, style.water); + } + + if (data.BuildingPolygons != null && style.buildings != null) + { + DrawPolygons(writer, proj, data.BuildingPolygons, style.buildings); + } + + if (data.Roads != null) + { + RenderRoads(writer, data.Roads, proj, style.roadForeground, style.roadBackground); + } + + if (data.FortPolygons != null && style.Forts != null) + { + DrawPolygons(writer, proj, data.FortPolygons, style.Forts); + } + + if (data.Bridges != null) + { + RenderBridgesRoads(writer, data.Bridges, proj, style.bridgeForeground, style.bridgeBackground, style.BridgeLimit); + } + + if (data.Powerlines != null && style.Powerline != null) + { + foreach (var pl in data.Powerlines.Coordinates) + { + writer.DrawPolyline(pl.Coordinates.Select(p => proj.Project(p)), style.Powerline); + } + } + + if (renderData.PlottedPoints != null && style.plotted != null && style.plottedCircle != null) + { + RenderPlotted(writer, proj, renderData.PlottedPoints, style.plotted, style.plottedCircle); + } + + if (data.Icons != null) + { + foreach (var icon in data.Icons) + { + var i = GetIcon(style, icon.MapType); + if (i != null) + { + writer.DrawIcon(proj.Project(icon.Coordinates), i); + } + } + } + + RenderNames(writer, data, proj, style); + } + + private static void RenderNames(IDrawSurface writer, ITopoMapData data, NoProjectionArea proj, TopoMapStyle style) + { + if (data.Names != null) + { + foreach (var entry in data.Names) + { + var pos = proj.Project(entry.Position); + var nstyle = style.name; + if (entry.Type == TopoLocationType.City) + { + nstyle = style.city; + } + else if (entry.Type == TopoLocationType.Local) + { + nstyle = style.local; + } + if (nstyle != null) + { + writer.DrawText(pos, entry.Name, nstyle); + } + } + } + } + + private static IDrawIcon? GetIcon(TopoMapStyle style, TopoIconType mapType) + { + switch (mapType) + { + case TopoIconType.WindPowerPlant: + return style.WindPowerPlant; + + case TopoIconType.WaterTower: + return style.WaterTower; + + case TopoIconType.Transmitter: + return style.Transmitter; + + case TopoIconType.ElectricityPylon: + return style.Dot; + + case TopoIconType.Hospital: + return style.Hospital; + } + return null; + } + + private static void RenderPlotted(IDrawSurface writer, NoProjectionArea proj, List plottedPoints, IDrawTextStyle plotted, IDrawStyle plottedCircle) + { + foreach (var point in plottedPoints) + { + var projected = proj.Project(point.Coordinates); + var text = Math.Round(point.Elevation).ToString(CultureInfo.InvariantCulture); + + writer.DrawCircle(projected, 1.5f, plottedCircle); + writer.DrawText(projected + new Vector(3, 0), text, plotted); + + } + } + + private static void NaiveGraticule(IDrawSurface writer, NoProjectionArea proj, TopoMapStyle style, int step = 1000) + { + var format = "00"; + if (step == 10000) + { + format = "0"; + } + for (int x = 1; x < 100; ++x) + { + var projected = proj.Project(new Coordinates(0, x * step)); + if (projected.X >= 0 && projected.X <= proj.Size.X) + { + writer.DrawPolyline(new[] { + new Vector(projected.X, 0), + new Vector(projected.X, proj.Size.Y), + }, x % 10 == 0 ? style.gl2 : style.gl1); + + writer.DrawText(new Vector(projected.X, 0), x.ToString(format), style.gltT); + writer.DrawText(new Vector(projected.X, proj.Size.Y), x.ToString(format), style.gltB); + } + } + for (int y = 1; y < 100; ++y) + { + var projected = proj.Project(new Coordinates(y * step, 0)); + if (projected.Y >= 0 && projected.Y <= proj.Size.Y) + { + writer.DrawPolyline(new[] { + new Vector(0, projected.Y), + new Vector(proj.Size.X, projected.Y), + }, y % 10 == 0 ? style.gl2 : style.gl1); + + writer.DrawText(new Vector(0, projected.Y), y.ToString(format), style.gltL); + writer.DrawText(new Vector(proj.Size.X, projected.Y), y.ToString(format), style.gltR); + } + } + } + + private static void RenderRoads(IDrawSurface writer, Dictionary data, NoProjectionArea proj, IDrawStyle?[] foreground, IDrawStyle?[] background) + { + foreach (var roads in data) + { + var style = background[(int)roads.Key]; + if (style != null) + { + foreach (var road in roads.Value.Coordinates) + { + writer.DrawPolyline(road.Coordinates.Select(p => proj.Project(p)), style); + } + } + } + foreach (var roads in data.OrderByDescending(t => t.Key)) + { + var style = foreground[(int)roads.Key]; + if (style != null) + { + foreach (var road in roads.Value.Coordinates) + { + writer.DrawPolyline(road.Coordinates.Select(p => proj.Project(p)), style); + } + } + } + } + + private static void RenderBridgesRoads(IDrawSurface writer, Dictionary data, NoProjectionArea proj, IDrawStyle?[] foreground, IDrawStyle?[] background, IDrawStyle limitLine) + { + foreach (var roads in data) + { + var style = background[(int)roads.Key]; + if (style != null) + { + foreach (var road in roads.Value.Coordinates) + { + writer.DrawPolyline(road.Coordinates.Select(p => proj.Project(p)), style); + + BridgeLimit(writer, proj.Project(road.Coordinates[0]), proj.Project(road.Coordinates[1]), limitLine); + BridgeLimit(writer, proj.Project(road.Coordinates[road.Coordinates.Count - 1]), proj.Project(road.Coordinates[road.Coordinates.Count - 2]), limitLine); + } + } + } + foreach (var roads in data.OrderByDescending(t => t.Key)) + { + var style = foreground[(int)roads.Key]; + if (style != null) + { + foreach (var road in roads.Value.Coordinates) + { + writer.DrawPolyline(road.Coordinates.Select(p => proj.Project(p)), style); + } + } + } + } + + internal static void BridgeLimit(IDrawSurface writer, Vector begin, Vector inside, IDrawStyle limitLine) + { + var delta = System.Numerics.Vector2.Normalize((inside - begin).ToFloat()); + var d8 = delta * 10; + var d4 = delta * 4.5f; + var x1 = System.Numerics.Vector2.Transform(d8, System.Numerics.Matrix3x2.CreateRotation((float)(3 * Math.PI / 4))); + var x2 = System.Numerics.Vector2.Transform(d8, System.Numerics.Matrix3x2.CreateRotation((float)(-3 * Math.PI / 4))); + writer.DrawPolyline(new[] { begin + new Vector(d4), begin + new Vector(x1 + d4) }, limitLine); + writer.DrawPolyline(new[] { begin + new Vector(d4), begin + new Vector(x2 + d4) }, limitLine); + } + + private static void DrawPolygons(IDrawSurface writer, NoProjectionArea proj, MultiPolygon surface, IDrawStyle styleToUse) + { + foreach (var poly in surface.Coordinates) + { + writer.DrawPolygon( + poly.Coordinates[0].Coordinates.Cast().Select(proj.Project), + poly.Coordinates.Skip(1).Select(l => l.Coordinates.Cast().Select(proj.Project)), + styleToUse); + } + } + + internal static void DrawPolygonsSimplified(IDrawSurface writer, NoProjectionArea proj, MultiPolygon surface, IDrawStyle styleToUse) + { + foreach (var poly in surface.Coordinates) + { + var contour = LevelOfDetailHelper.SimplifyAnglesAndDistancesClosed(poly.Coordinates[0].Coordinates.Cast().Select(proj.Project), 1); + + if (contour.Count > 0) + { + writer.DrawPolygon( + contour, + LevelOfDetailHelper.SimplifyAnglesAndDistancesClosed(poly.Coordinates.Skip(1).Select(l => l.Coordinates.Cast().Select(proj.Project)), 1) ?? Enumerable.Empty>(), + styleToUse); + } + } + } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapRenderData.cs b/MapToolkit.Drawing.Topographic/TopoMapRenderData.cs new file mode 100644 index 0000000..2fa71f7 --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapRenderData.cs @@ -0,0 +1,62 @@ +using MapToolkit.Contours; +using MapToolkit.DataCells; +using MapToolkit.GeodeticSystems; +using MapToolkit.Hillshading; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace MapToolkit.Drawing.Topographic +{ + public sealed class TopoMapRenderData + { + private TopoMapRenderData(ITopoMapData data, Image img, ContourGraph contour, List plotted) + { + Data = data; + Img = img; + Contour = contour; + PlottedPoints = plotted; + } + + public ITopoMapData Data { get; } + + public Image Img { get; } + + public ContourGraph Contour { get; } + + public List PlottedPoints { get; } + + public double WidthInMeters => End.Longitude - Start.Longitude; + + public double HeightInMeters => End.Latitude - Start.Latitude; + + public Coordinates End => Data.DemDataCell.End; + + public Coordinates Start => Data.DemDataCell.Start; + + public static TopoMapRenderData Create(ITopoMapData data) + { + var img = new HillshaderFast(new Vector(10, 10)).GetPixelsAlphaBelowFlat(data.DemDataCell); + + var contour = new ContourGraph(); + contour.Add(data.DemDataCell, new ContourLevelGenerator(10, 10), false); + + var plotted = ComputePlottedPoints(data.DemDataCell, contour); + + return new TopoMapRenderData(data, img, contour, plotted); + } + + + internal static List ComputePlottedPoints(IDemDataView demView, ContourGraph contour) + { + var lines = contour.Lines.Where(l => l.IsClosed && LengthInMeters(l.Points) > 200); + var plotted = ContourMaximaMinima.FindMaxima(demView, lines); + plotted.AddRange(ContourMaximaMinima.FindMinima(demView, lines)); + return plotted; + } + + private static double LengthInMeters(List points) + { + return points.Take(points.Count - 1).Zip(points.Skip(1), (a, b) => MeterProjectedDistance.Instance.DistanceInMeters(a, b)).Sum(); + } + } +} diff --git a/MapToolkit.Drawing.Topographic/TopoMapStyle.cs b/MapToolkit.Drawing.Topographic/TopoMapStyle.cs new file mode 100644 index 0000000..88f16be --- /dev/null +++ b/MapToolkit.Drawing.Topographic/TopoMapStyle.cs @@ -0,0 +1,209 @@ +using MapToolkit.Drawing; +using MapToolkit.Drawing.Contours; +using SixLabors.ImageSharp; + +namespace MapToolkit.Drawing.Topographic +{ + internal class TopoMapStyle + { + public IDrawIcon? WindPowerPlant { get; private set; } + public required IDrawStyle rocks { get; internal set; } + public required IDrawStyle forest { get; internal set; } + public required IDrawStyle water { get; internal set; } + public IDrawStyle? buildings { get; private set; } + public required ContourRenderStyle contourStyle { get; internal set; } + public required IDrawStyle?[] roadForeground { get; internal set; } + public required IDrawStyle?[] roadBackground { get; internal set; } + public required IDrawStyle?[] bridgeForeground { get; internal set; } + public required IDrawStyle?[] bridgeBackground { get; internal set; } + public required IDrawStyle gl1 { get; internal set; } + public required IDrawStyle gl2 { get; internal set; } + public required IDrawTextStyle gltT { get; internal set; } + public required IDrawTextStyle gltB { get; internal set; } + public required IDrawTextStyle gltL { get; internal set; } + public required IDrawTextStyle gltR { get; internal set; } + public required IDrawTextStyle city { get; internal set; } + public IDrawTextStyle? local { get; private set; } + public IDrawTextStyle? name { get; private set; } + public IDrawTextStyle? plotted { get; private set; } + public IDrawStyle? plottedCircle { get; private set; } + public IDrawIcon? WaterTower { get; private set; } + public IDrawIcon? TechnicalTower { get; internal set; } + public IDrawIcon? Transmitter { get; private set; } + public IDrawStyle? Powerline { get; private set; } + public IDrawIcon? Dot { get; private set; } + public IDrawIcon? Hospital { get; private set; } + public required IDrawStyle BridgeLimit { get; internal set; } + public IDrawStyle? Forts { get; internal set; } + + public static TopoMapStyle CreateFull(IDrawSurface writer, ColorPalette palette, bool externGraticule = false) + { + var rocksFill = writer.AllocateBrushStyle(palette.RocksSymbol); + var rocks = writer.AllocateStyle(new VectorBrush(writer, 20, 20, d => + { + d.DrawCircle(new Vector(5, 5), 3, rocksFill); + d.DrawCircle(new Vector(15, 15), 3, rocksFill); + }), new Pen(new SolidColorBrush(palette.RocksBorder))); + + var forest = writer.AllocateStyle(palette.ForestFill, palette.ForestBorder); + var water = writer.AllocateStyle(palette.WaterFill, palette.WaterBorder); + var buildings = writer.AllocateStyle(palette.BuildingFill, palette.BuildingBorder); + + var contourStyle = new ContourRenderStyle(writer); + + var r1 = writer.AllocatePenStyle(palette.MainRoad, 5); + var b1 = writer.AllocatePenStyle(palette.RoadBorder, 6); + var bx1 = writer.AllocatePenStyle(palette.RoadBorder, 10); + var r2 = writer.AllocatePenStyle(palette.SecondaryRoad, 4); + var b2 = writer.AllocatePenStyle(palette.RoadBorder, 5); + var bx2 = writer.AllocatePenStyle(palette.RoadBorder, 9); + var r3 = writer.AllocatePenStyle(palette.Road, 4); + var b4 = writer.AllocatePenStyle(palette.RoadBorder, 5, new[] { 10.0, 6.0 }); + var r7 = writer.AllocatePenStyle(palette.Trail, 5); + + var gl1 = writer.AllocatePenStyle(palette.Graticule, 1); + var gl2 = writer.AllocatePenStyle(palette.Graticule, 2); + var gltT = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.TopCenter); + var gltB = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.BottomCenter); + var gltL = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterLeft); + var gltR = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterRight); + + var city = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Italic, 32, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + var local = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.BoldItalic, 26, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + var name = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 22, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + + return new TopoMapStyle() + { + rocks = rocks, + forest = forest, + buildings = buildings, + water = water, + contourStyle = contourStyle, + roadForeground = new[] { r1, r2, r3, r3, r7 }, + roadBackground = new[] { b1, b2, b2, b4, null }, + bridgeForeground = new[] { r1, r2, r3, r3, r7 }, + bridgeBackground = new[] { bx1, bx2, bx2, bx2, null }, + gl1 = gl1, + gl2 = gl2, + gltT = externGraticule ? gltB : gltT, + gltB = externGraticule ? gltT : gltB, + gltL = externGraticule ? gltR : gltL, + gltR = externGraticule ? gltL : gltR, + city = city, + local = local, + name = name, + plotted = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Italic, 18, new SolidColorBrush(Color.Black), null, false, TextAnchor.CenterLeft), + plottedCircle = writer.AllocateBrushStyle(Color.Black), + WindPowerPlant = IconsRender.WindTurbine(writer), + WaterTower = IconsRender.WaterTower(writer), + //TechnicalTower = IconsRender.TechnicalTower(writer), + Transmitter = IconsRender.Transmitter(writer), + Powerline = writer.AllocatePenStyle(Color.Black), + Dot = IconsRender.Dot(writer), + Hospital = IconsRender.Hospital(writer), + BridgeLimit = writer.AllocatePenStyle(palette.RoadBorder, 3), + Forts = writer.AllocateStyle(Color.White, Color.Black) + }; + } + + + public static TopoMapStyle CreateLod2(IDrawSurface writer, ColorPalette palette) + { + var rocks = writer.AllocateBrushStyle(palette.RocksFill); + var forest = writer.AllocateBrushStyle(palette.ForestFill); + var water = writer.AllocateBrushStyle(palette.WaterFill); + + var contourStyle = new ContourRenderStyle(writer); + + var r1 = writer.AllocatePenStyle(palette.MainRoad, 5); + var b1 = writer.AllocatePenStyle(Color.Black, 6); + var bx1 = writer.AllocatePenStyle(Color.Black, 10); + var r2 = writer.AllocatePenStyle(palette.SecondaryRoad, 4); + var b2 = writer.AllocatePenStyle(Color.Black, 5); + var bx2 = writer.AllocatePenStyle(Color.Black, 9); + var r3 = writer.AllocatePenStyle(Color.White, 4); + + var gl1 = writer.AllocatePenStyle(palette.Graticule, 1); + var gl2 = writer.AllocatePenStyle(palette.Graticule, 2); + var gltT = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 4, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.TopCenter); + var gltB = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 4, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.BottomCenter); + var gltL = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 4, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterLeft); + var gltR = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 4, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterRight); + + var city = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Italic, 32 * 4, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + var local = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.BoldItalic, 26 * 4, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + + + return new TopoMapStyle() + { + rocks = rocks, + forest = forest, + buildings = null, + water = water, + contourStyle = contourStyle, + roadForeground = new[] { r1, r2, r3, null, null }, + roadBackground = new[] { b1, b2, b2, null, null }, + bridgeForeground = new[] { r1, r2, r3, null, null }, + bridgeBackground = new[] { bx1, bx2, bx2, null, null }, + gl1 = gl1, + gl2 = gl2, + gltT = gltT, + gltB = gltB, + gltL = gltL, + gltR = gltR, + city = city, + local = local, + name = null, + BridgeLimit = writer.AllocatePenStyle(palette.RoadBorder, 3) + }; + } + + public static TopoMapStyle CreateLod3(IDrawSurface writer, ColorPalette palette) + { + var rocks = writer.AllocateBrushStyle(palette.RocksFill); + var forest = writer.AllocateBrushStyle(palette.ForestFill); + var water = writer.AllocateBrushStyle(palette.WaterFill); + + var contourStyle = new ContourRenderStyle(writer); + + var r1 = writer.AllocatePenStyle(palette.MainRoad, 5); + var b1 = writer.AllocatePenStyle(Color.Black, 6); + var bx1 = writer.AllocatePenStyle(Color.Black, 10); + var r2 = writer.AllocatePenStyle(palette.SecondaryRoad, 4); + var b2 = writer.AllocatePenStyle(Color.Black, 5); + var bx2 = writer.AllocatePenStyle(Color.Black, 9); + + var gl1 = writer.AllocatePenStyle(palette.Graticule, 1); + var gl2 = writer.AllocatePenStyle(palette.Graticule, 2); + var gltT = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 16, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.TopCenter); + var gltB = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 16, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.BottomCenter); + var gltL = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 16, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterLeft); + var gltR = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Bold, 28 * 16, new SolidColorBrush(palette.Graticule), new Pen(new SolidColorBrush(palette.TextBorder), 3), true, TextAnchor.CenterRight); + + var city = writer.AllocateTextStyle(new[] { "Calibri", "sans-serif" }, SixLabors.Fonts.FontStyle.Italic, 32 * 16, new SolidColorBrush(Color.Black), new Pen(new SolidColorBrush(palette.TextBorder), 3), true); + + return new TopoMapStyle() + { + rocks = rocks, + forest = forest, + buildings = null, + water = water, + contourStyle = contourStyle, + roadForeground = new[] { r1, r2, null, null, null }, + roadBackground = new[] { b1, b2, null, null, null }, + bridgeForeground = new[] { r1, r2, null, null, null }, + bridgeBackground = new[] { bx1, bx2, null, null, null }, + gl1 = gl1, + gl2 = gl2, + gltT = gltT, + gltB = gltB, + gltL = gltL, + gltR = gltR, + city = city, + local = null, + name = null, + BridgeLimit = writer.AllocatePenStyle(palette.RoadBorder, 3) + }; + } + } +} diff --git a/MapToolkit.sln b/MapToolkit.sln index 873cef1..9ed4cb5 100644 --- a/MapToolkit.sln +++ b/MapToolkit.sln @@ -9,9 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemUtility", "DemUtility\De EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapToolkit.Test", "MapToolkit.Test\MapToolkit.Test.csproj", "{F797D011-4BE7-435F-9F5F-0E52140B787A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToolkit.Drawing", "MapToolkit.Drawing\MapToolkit.Drawing.csproj", "{CD89FBC4-5213-4908-9B77-421C73CFFB6D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapToolkit.Drawing", "MapToolkit.Drawing\MapToolkit.Drawing.csproj", "{CD89FBC4-5213-4908-9B77-421C73CFFB6D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToolkit.Drawing.Test", "MapToolkit.Drawing.Test\MapToolkit.Drawing.Test.csproj", "{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapToolkit.Drawing.Test", "MapToolkit.Drawing.Test\MapToolkit.Drawing.Test.csproj", "{789FFCD4-375D-4201-BCEA-776B3A8D2EAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToolkit.Drawing.Topographic", "MapToolkit.Drawing.Topographic\MapToolkit.Drawing.Topographic.csproj", "{A4A7588A-0141-4D37-A614-4E889006CF1F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Debug|Any CPU.Build.0 = Debug|Any CPU {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|Any CPU.ActiveCfg = Release|Any CPU {789FFCD4-375D-4201-BCEA-776B3A8D2EAB}.Release|Any CPU.Build.0 = Release|Any CPU + {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4A7588A-0141-4D37-A614-4E889006CF1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4A7588A-0141-4D37-A614-4E889006CF1F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a701c3302d52d49f87df71a9dffb488a63992ccd Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 20 May 2024 14:18:11 +0200 Subject: [PATCH 2/2] Upgrade everyone to .net 8 --- .github/workflows/dotnet.yml | 2 +- DemUtility/DemUtility.csproj | 2 +- MapToolkit.Drawing.Test/MapToolkit.Drawing.Test.csproj | 2 +- MapToolkit.Drawing/MapToolkit.Drawing.csproj | 2 +- MapToolkit.Test/MapToolkit.Test.csproj | 2 +- MapToolkit/MapToolkit.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b1552f3..fb6ed96 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore MapToolkit.sln - name: Build diff --git a/DemUtility/DemUtility.csproj b/DemUtility/DemUtility.csproj index d9f4449..9bcc381 100644 --- a/DemUtility/DemUtility.csproj +++ b/DemUtility/DemUtility.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable dem diff --git a/MapToolkit.Drawing.Test/MapToolkit.Drawing.Test.csproj b/MapToolkit.Drawing.Test/MapToolkit.Drawing.Test.csproj index b20bdef..2021a3d 100644 --- a/MapToolkit.Drawing.Test/MapToolkit.Drawing.Test.csproj +++ b/MapToolkit.Drawing.Test/MapToolkit.Drawing.Test.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/MapToolkit.Drawing/MapToolkit.Drawing.csproj b/MapToolkit.Drawing/MapToolkit.Drawing.csproj index 09615e2..1038651 100644 --- a/MapToolkit.Drawing/MapToolkit.Drawing.csproj +++ b/MapToolkit.Drawing/MapToolkit.Drawing.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable diff --git a/MapToolkit.Test/MapToolkit.Test.csproj b/MapToolkit.Test/MapToolkit.Test.csproj index 8ad48b6..6d1db94 100644 --- a/MapToolkit.Test/MapToolkit.Test.csproj +++ b/MapToolkit.Test/MapToolkit.Test.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable false diff --git a/MapToolkit/MapToolkit.csproj b/MapToolkit/MapToolkit.csproj index 550bd3e..ff7ab34 100644 --- a/MapToolkit/MapToolkit.csproj +++ b/MapToolkit/MapToolkit.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable True https://github.com/jetelain/mapkit