From 813f11d47213022e3cd882c7505771b0c1b09eb1 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 26 Jun 2026 11:36:40 +1000 Subject: [PATCH 01/10] . --- src/GeoConvert.App/Cli.cs | 467 +++++++++++++++++++++++ src/GeoConvert.App/ConversionService.cs | 222 +++++++++++ src/GeoConvert.App/DiffMode.cs | 12 + src/GeoConvert.App/FileAssociations.cs | 94 +++++ src/GeoConvert.App/FirstRun.cs | 55 +++ src/GeoConvert.App/FormatInfo.cs | 25 ++ src/GeoConvert.App/GeoConvert.App.csproj | 57 +++ src/GeoConvert.App/GeoParquetSettings.cs | 11 + src/GeoConvert.App/GlobalUsings.cs | 9 + src/GeoConvert.App/KmzSettings.cs | 7 + src/GeoConvert.App/MapDiff.cs | 251 ++++++++++++ src/GeoConvert.App/NativeConsole.cs | 44 +++ src/GeoConvert.App/OptionChoices.cs | 48 +++ src/GeoConvert.App/Program.cs | 79 ++++ src/GeoConvert.App/RenderSettings.cs | 61 +++ src/GeoConvert.App/RendererBackend.cs | 14 + src/GeoConvert.App/RgbaColors.cs | 17 + src/GeoConvert.App/Settings.cs | 15 + src/GeoConvert.App/SettingsManager.cs | 69 ++++ src/GeoConvert.App/SimplifySettings.cs | 28 ++ src/GeoConvert.App/Ui/Combos.cs | 39 ++ src/GeoConvert.App/Ui/DiffForm.cs | 335 ++++++++++++++++ src/GeoConvert.App/Ui/Images.cs | 15 + src/GeoConvert.App/Ui/MainForm.cs | 361 ++++++++++++++++++ src/GeoConvert.App/Ui/OptionsPanel.cs | 377 ++++++++++++++++++ src/GeoConvert.App/Ui/SplitLayout.cs | 28 ++ src/GeoConvert.App/nuget-readme.md | 46 +++ src/GeoConvert.slnx | 1 + 28 files changed, 2787 insertions(+) create mode 100644 src/GeoConvert.App/Cli.cs create mode 100644 src/GeoConvert.App/ConversionService.cs create mode 100644 src/GeoConvert.App/DiffMode.cs create mode 100644 src/GeoConvert.App/FileAssociations.cs create mode 100644 src/GeoConvert.App/FirstRun.cs create mode 100644 src/GeoConvert.App/FormatInfo.cs create mode 100644 src/GeoConvert.App/GeoConvert.App.csproj create mode 100644 src/GeoConvert.App/GeoParquetSettings.cs create mode 100644 src/GeoConvert.App/GlobalUsings.cs create mode 100644 src/GeoConvert.App/KmzSettings.cs create mode 100644 src/GeoConvert.App/MapDiff.cs create mode 100644 src/GeoConvert.App/NativeConsole.cs create mode 100644 src/GeoConvert.App/OptionChoices.cs create mode 100644 src/GeoConvert.App/Program.cs create mode 100644 src/GeoConvert.App/RenderSettings.cs create mode 100644 src/GeoConvert.App/RendererBackend.cs create mode 100644 src/GeoConvert.App/RgbaColors.cs create mode 100644 src/GeoConvert.App/Settings.cs create mode 100644 src/GeoConvert.App/SettingsManager.cs create mode 100644 src/GeoConvert.App/SimplifySettings.cs create mode 100644 src/GeoConvert.App/Ui/Combos.cs create mode 100644 src/GeoConvert.App/Ui/DiffForm.cs create mode 100644 src/GeoConvert.App/Ui/Images.cs create mode 100644 src/GeoConvert.App/Ui/MainForm.cs create mode 100644 src/GeoConvert.App/Ui/OptionsPanel.cs create mode 100644 src/GeoConvert.App/Ui/SplitLayout.cs create mode 100644 src/GeoConvert.App/nuget-readme.md diff --git a/src/GeoConvert.App/Cli.cs b/src/GeoConvert.App/Cli.cs new file mode 100644 index 0000000..55c4b68 --- /dev/null +++ b/src/GeoConvert.App/Cli.cs @@ -0,0 +1,467 @@ +namespace GeoConvert.App; + +/// +/// The command-line surface of the app, hand-rolled in the same spirit as the geoconvert CLI's +/// Runner (no third-party parser). It owns the headless commands — diff, the file +/// association management, and settings — and the usage text. Interactive conversion/rendering is +/// the GUI's job; the CLI deliberately mirrors only the diff feature plus app management. +/// +public static class Cli +{ + /// A fully-parsed diff invocation. null means "open the diff in + /// the GUI" rather than render headlessly. + public sealed record DiffRequest( + string PathA, + string PathB, + string? Output, + RenderSettings Settings, + DiffMode Mode, + Rgba ColorA, + Rgba ColorB); + + /// + /// Parses the arguments after diff. Returns 0 with set on success, + /// or 2 on a usage error (message already written to ). + /// + public static int ParseDiff(string[] args, out DiffRequest? request, TextWriter error) + { + request = null; + var settings = new RenderSettings(); + var mode = DiffMode.Overlay; + var colorA = MapDiff.DefaultColorA; + var colorB = MapDiff.DefaultColorB; + var positionals = new List(); + + for (var i = 0; i < args.Length; i++) + { + var argument = args[i]; + switch (argument) + { + case "--mode": + if (!TryNext(args, ref i, error, "--mode", out var modeText)) + { + return 2; + } + + if (!TryParseMode(modeText, out mode)) + { + error.WriteLine("--mode must be 'overlay' or 'side-by-side'."); + return 2; + } + + break; + case "--color-a": + if (!TryNext(args, ref i, error, "--color-a", out var colorAText) || + !RequireColor(colorAText, "--color-a", error, out colorA)) + { + return 2; + } + + break; + case "--color-b": + if (!TryNext(args, ref i, error, "--color-b", out var colorBText) || + !RequireColor(colorBText, "--color-b", error, out colorB)) + { + return 2; + } + + break; + case "--bbox": + if (!TryNext(args, ref i, error, "--bbox", out var bboxText)) + { + return 2; + } + + if (!TryParseBounds(bboxText, out var bounds)) + { + error.WriteLine("--bbox must be 'minX,minY,maxX,maxY'."); + return 2; + } + + settings.Bounds = bounds; + break; + case "--size": + if (!TryNext(args, ref i, error, "--size", out var sizeText)) + { + return 2; + } + + if (!TryParseSize(sizeText, out var width, out var height)) + { + error.WriteLine("--size must be 'WIDTH' or 'WIDTHxHEIGHT'."); + return 2; + } + + // An explicit pixel size overrides the default fit-to-box. + settings.MaxDimension = 0; + settings.Width = width; + settings.Height = height; + break; + case "--max-dimension": + if (!TryNext(args, ref i, error, "--max-dimension", out var maxText)) + { + return 2; + } + + if (!int.TryParse(maxText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var maxDimension) || maxDimension <= 0) + { + error.WriteLine("--max-dimension must be a positive integer (pixels)."); + return 2; + } + + settings.MaxDimension = maxDimension; + break; + case "--projection": + if (!TryNext(args, ref i, error, "--projection", out var projectionText)) + { + return 2; + } + + if (!TryParseProjection(projectionText, out var projection)) + { + error.WriteLine("--projection must be 'auto', 'plate-carree', 'web-mercator', 'lambert', or 'goode'."); + return 2; + } + + settings.Projection = projection; + break; + case "--renderer": + if (!TryNext(args, ref i, error, "--renderer", out var rendererText)) + { + return 2; + } + + if (!TryParseRenderer(rendererText, out var renderer)) + { + error.WriteLine("--renderer must be 'builtin' or 'imagesharp'."); + return 2; + } + + settings.Renderer = renderer; + break; + default: + if (argument.StartsWith('-')) + { + error.WriteLine($"Unknown option '{argument}'."); + return 2; + } + + positionals.Add(argument); + break; + } + } + + if (positionals.Count is < 2 or > 3) + { + error.WriteLine("Usage: geoconvert-app diff [output.png] [options]"); + return 2; + } + + var output = positionals.Count == 3 ? positionals[2] : null; + request = new(positionals[0], positionals[1], output, settings, mode, colorA, colorB); + return 0; + } + + /// Runs a headless diff: renders the diff image to and prints the summary. + public static int ExecuteDiff(DiffRequest request, TextWriter output, TextWriter error) + { + if (!File.Exists(request.PathA)) + { + error.WriteLine($"Input file not found: {request.PathA}"); + return 1; + } + + if (!File.Exists(request.PathB)) + { + error.WriteLine($"Input file not found: {request.PathB}"); + return 1; + } + + try + { + var a = GeoConverter.Read(request.PathA); + var b = GeoConverter.Read(request.PathB); + + output.Write(MapDiff.Summarize(Path.GetFileName(request.PathA), a, Path.GetFileName(request.PathB), b)); + + if (request.Output is { } destination) + { + var image = MapDiff.Render(a, b, request.Settings, request.Mode, request.ColorA, request.ColorB); + File.WriteAllBytes(destination, image); + output.WriteLine(); + output.WriteLine($"Diff image ({request.Mode}) written to {destination}."); + } + + return 0; + } + catch (GeoConvertException exception) + { + error.WriteLine($"Error: {exception.Message}"); + return 1; + } + } + + public static int Associate(TextWriter output) + { + FileAssociations.Associate(); + output.WriteLine("Bound the supported map formats to GeoConvert:"); + output.WriteLine($" {string.Join(" ", FileAssociations.Extensions)}"); + return 0; + } + + public static int Unassociate(TextWriter output) + { + FileAssociations.Unassociate(); + output.WriteLine("Removed GeoConvert's map file associations."); + return 0; + } + + public static int PrintSettings(TextWriter output, SettingsManager settingsManager) + { + output.WriteLine(settingsManager.SettingsPath); + if (File.Exists(settingsManager.SettingsPath)) + { + output.WriteLine(File.ReadAllText(settingsManager.SettingsPath)); + } + else + { + output.WriteLine("No settings file found."); + } + + output.WriteLine($"File associations bound: {(FileAssociations.IsAssociated() ? "yes" : "no")}"); + return 0; + } + + // --- parsing helpers (shared shapes with the geoconvert CLI's Runner) --- + + static bool TryNext(string[] args, ref int index, TextWriter error, string option, out string value) + { + if (index + 1 >= args.Length) + { + error.WriteLine($"Missing value for {option}."); + value = string.Empty; + return false; + } + + value = args[++index]; + return true; + } + + static bool RequireColor(string text, string option, TextWriter error, out Rgba color) + { + if (TryParseColor(text, out color)) + { + return true; + } + + error.WriteLine($"{option} must be '#RRGGBB' or '#RRGGBBAA'."); + return false; + } + + static bool TryParseMode(string text, out DiffMode mode) + { + switch (text.ToLowerInvariant()) + { + case "overlay": + mode = DiffMode.Overlay; + return true; + case "side-by-side": + case "sidebyside": + case "side": + mode = DiffMode.SideBySide; + return true; + default: + mode = default; + return false; + } + } + + static bool TryParseBounds(string text, out Envelope bounds) + { + bounds = default; + var parts = text.Split(','); + if (parts.Length != 4) + { + return false; + } + + var values = new double[4]; + for (var i = 0; i < 4; i++) + { + if (!double.TryParse(parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out values[i])) + { + return false; + } + } + + bounds = new(values[0], values[1], values[2], values[3]); + return true; + } + + static bool TryParseSize(string text, out int width, out int height) + { + width = 0; + height = 0; + var parts = text.Split('x', 'X'); + if (parts.Length is < 1 or > 2) + { + return false; + } + + if (!int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out width) || width <= 0) + { + return false; + } + + if (parts.Length == 2 && + (!int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out height) || height <= 0)) + { + return false; + } + + return true; + } + + static bool TryParseProjection(string text, out MapProjection projection) + { + switch (text.ToLowerInvariant()) + { + case "auto": + case "automatic": + projection = MapProjection.Auto; + return true; + case "plate-carree": + case "platecarree": + case "equirectangular": + projection = MapProjection.PlateCarree; + return true; + case "web-mercator": + case "webmercator": + case "mercator": + projection = MapProjection.WebMercator; + return true; + case "lambert": + case "lambert-conformal": + case "lambert-conformal-conic": + case "lcc": + projection = MapProjection.Lambert; + return true; + case "goode": + case "homolosine": + case "goode-homolosine": + projection = MapProjection.Goode; + return true; + default: + projection = default; + return false; + } + } + + static bool TryParseRenderer(string text, out RendererBackend renderer) + { + switch (text.ToLowerInvariant()) + { + case "builtin": + case "built-in": + case "default": + renderer = RendererBackend.BuiltIn; + return true; + case "imagesharp": + case "image-sharp": + case "sixlabors": + renderer = RendererBackend.ImageSharp; + return true; + default: + renderer = default; + return false; + } + } + + static bool TryParseColor(string text, out Rgba color) + { + color = default; + if (text.Length < 7 || text[0] != '#') + { + return false; + } + + var hex = text.AsSpan(1); + if (hex.Length != 6 && hex.Length != 8) + { + return false; + } + + if (!byte.TryParse(hex.Slice(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var r) || + !byte.TryParse(hex.Slice(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var g) || + !byte.TryParse(hex.Slice(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b)) + { + return false; + } + + byte a = 255; + if (hex.Length == 8 && + !byte.TryParse(hex.Slice(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out a)) + { + return false; + } + + color = new(r, g, b, a); + return true; + } + + public static void PrintUsage(TextWriter writer) => + writer.WriteLine( + """ + geoconvert-app - a desktop map converter, renderer and diff tool. + + Run with no arguments to open the window. Pass a map file to open it directly. + + Usage: + geoconvert-app Open the app. + geoconvert-app Open the app with a map loaded. + geoconvert-app diff [output.png] [options] + Compare two maps. With an output path the diff is + rendered headlessly and a summary is printed; without + one the diff opens in the window. + geoconvert-app associate Bind the supported map formats to this app. + geoconvert-app unassociate Remove those file associations. + geoconvert-app settings Show the settings file and association state. + geoconvert-app --list List supported formats. + geoconvert-app --help Show this help. + + diff options: + --mode overlay|side-by-side Overlay both maps on one canvas (default) or place them + side by side at a shared extent. + --color-a <#hex> Colour for the first map (default red). + --color-b <#hex> Colour for the second map (default blue). + --bbox minX,minY,maxX,maxY Extent to render (defaults to the union of both maps). + --size WIDTH[xHEIGHT] Image size in pixels. + --max-dimension Cap the longer edge at this many pixels (fit-to-box). + --projection auto | plate-carree | web-mercator | lambert | goode. + --renderer builtin (default) or imagesharp (PNG only). + + Examples: + geoconvert-app world.geojson + geoconvert-app diff before.geojson after.geojson changes.png + geoconvert-app diff a.kml b.kml diff.png --mode side-by-side --size 1600 + """); + + public static void PrintFormats(TextWriter writer) => + writer.WriteLine( + """ + Supported formats: + geojson .geojson .json (read/write) + topojson .topojson (read/write) + shapefile .shp (+ .shx .dbf .prj) (read/write) + flatgeobuf .fgb (read/write) + kml .kml (read/write) + kmz .kmz (read/write) + gpx .gpx (read/write) + wkt .wkt (read/write) + wkb .wkb (read/write) + csv .csv (read/write) + geoparquet .parquet .geoparquet (read/write) + png .png (render output) + svg .svg (render output) + """); +} diff --git a/src/GeoConvert.App/ConversionService.cs b/src/GeoConvert.App/ConversionService.cs new file mode 100644 index 0000000..a60f6e6 --- /dev/null +++ b/src/GeoConvert.App/ConversionService.cs @@ -0,0 +1,222 @@ +namespace GeoConvert.App; + +/// +/// Wraps and the renderers for the desktop app — the counterpart of the +/// Blazor app's conversion service, but path/stream based and filesystem-aware, so the path-only +/// Shapefile is a first-class format here (the browser version had to exclude it). Everything the GUI +/// and the CLI do — detect, read, simplify, write, render, choose a PNG backend — funnels through here. +/// +public static class ConversionService +{ + static IReadOnlyList AllFormats { get; } = + [ + new(GeoFormat.GeoJson, "GeoJSON", ".geojson", [".geojson", ".json"], CanRead: true, CanWrite: true), + new(GeoFormat.TopoJson, "TopoJSON", ".topojson", [".topojson"], CanRead: true, CanWrite: true), + new(GeoFormat.Shapefile, "Shapefile", ".shp", [".shp"], CanRead: true, CanWrite: true), + new(GeoFormat.FlatGeobuf, "FlatGeobuf", ".fgb", [".fgb"], CanRead: true, CanWrite: true), + new(GeoFormat.Kml, "KML", ".kml", [".kml"], CanRead: true, CanWrite: true), + new(GeoFormat.Kmz, "KMZ", ".kmz", [".kmz"], CanRead: true, CanWrite: true), + new(GeoFormat.Gpx, "GPX", ".gpx", [".gpx"], CanRead: true, CanWrite: true), + new(GeoFormat.Wkt, "WKT", ".wkt", [".wkt"], CanRead: true, CanWrite: true), + new(GeoFormat.Wkb, "WKB", ".wkb", [".wkb"], CanRead: true, CanWrite: true), + new(GeoFormat.Csv, "CSV", ".csv", [".csv"], CanRead: true, CanWrite: true), + new(GeoFormat.GeoParquet, "GeoParquet", ".parquet", [".parquet", ".geoparquet"], CanRead: true, CanWrite: true), + new(GeoFormat.Png, "PNG image", ".png", [".png"], CanRead: false, CanWrite: true), + new(GeoFormat.Svg, "SVG image", ".svg", [".svg"], CanRead: false, CanWrite: true), + ]; + + public static IReadOnlyList Formats => AllFormats; + + /// Formats that can be read into features (everything except the write-only images). + public static IReadOnlyList ReadableFormats { get; } = [.. AllFormats.Where(_ => _.CanRead)]; + + /// Formats that can be written (every format, including the render-only PNG/SVG). + public static IReadOnlyList WritableFormats { get; } = [.. AllFormats.Where(_ => _.CanWrite)]; + + /// Every distinct extension across the readable formats — used for the open dialog and file associations. + public static IReadOnlyList ReadableExtensions { get; } = + [.. ReadableFormats.SelectMany(_ => _.Extensions).Distinct(StringComparer.OrdinalIgnoreCase)]; + + /// The write-only image formats whose write is a render (projection + size apply), not a plain codec write. + public static bool IsRendered(GeoFormat format) => + format is GeoFormat.Png or GeoFormat.Svg; + + public static FormatInfo? Find(GeoFormat format) => + AllFormats.FirstOrDefault(_ => _.Format == format); + + /// Infers the format of a file name, or null when the extension is unknown. + public static FormatInfo? Detect(string fileName) => + GeoConverter.TryDetectFormat(fileName, out var format) ? Find(format) : null; + + public static FeatureCollection Read(string path, GeoFormat format, IProgress? progress = null) => + // GeoConverter.Read is path-based and already special-cases Shapefile's sibling .shp/.shx/.dbf. + GeoConverter.Read(path, format, progress); + + /// + /// Writes to in , applying + /// the relevant options: a render (honouring the chosen ) for + /// PNG/SVG, the option-carrying overloads for KMZ/GeoParquet, and the plain codec write (Shapefile + /// included) otherwise. + /// + public static void Save( + FeatureCollection features, + string path, + GeoFormat format, + RenderSettings render, + KmzSettings kmz, + GeoParquetSettings parquet, + IProgress? progress = null) + { + switch (format) + { + case GeoFormat.Png: + File.WriteAllBytes(path, RenderPng(features, render, progress)); + break; + case GeoFormat.Svg: + MapRenderer.RenderSvg(features, path, RenderOptionsFor(render, progress)); + break; + case GeoFormat.Kmz: + WriteKmz(features, path, kmz); + break; + case GeoFormat.GeoParquet: + WriteGeoParquet(features, path, parquet); + break; + default: + GeoConverter.Write(features, path, format, progress); + break; + } + } + + static void WriteKmz(FeatureCollection features, string path, KmzSettings settings) + { + using var stream = File.Create(path); + Kmz.Write(stream, features, settings.Compression); + } + + static void WriteGeoParquet(FeatureCollection features, string path, GeoParquetSettings settings) + { + using var stream = File.Create(path); + GeoParquet.Write(stream, features, settings.Codec, settings.GzipLevel); + } + + /// + /// Renders a PNG through the chosen backend (built-in software + /// rasterizer or ImageSharp). + /// + public static byte[] RenderPng(FeatureCollection features, RenderSettings render, IProgress? progress = null) + { + var options = RenderOptionsFor(render, progress); + return render.Renderer switch + { + RendererBackend.ImageSharp => ImageSharpRenderer.RenderPng(features, options), + _ => MapRenderer.RenderPng(features, options), + }; + } + + /// + /// Renders a quick preview PNG. Always uses the built-in renderer (no third-party warm-up, always + /// available) regardless of the selected export backend, and reports no progress — it's a best-effort + /// thumbnail for the window, not the final export. + /// + public static byte[] RenderPreview(FeatureCollection features, RenderSettings render) => + MapRenderer.RenderPng(features, RenderOptionsFor(render, null)); + + public static string RenderSvg(FeatureCollection features, RenderSettings render, IProgress? progress = null) => + MapRenderer.RenderSvg(features, RenderOptionsFor(render, progress)); + + // Common name-like property keys, tried in order, so the "Labels" toggle works without the user + // having to name a property — mirrors the Blazor app. Falls back to the feature id. + static readonly string[] labelKeys = + ["name", "NAME", "Name", "name_en", "NAME_EN", "admin", "ADMIN", "title", "label", "id"]; + + static string? AutoLabel(Feature feature) + { + foreach (var key in labelKeys) + { + if (feature.Properties.TryGetValue(key, out var value) && value is not null) + { + var text = value.ToString(); + if (!string.IsNullOrWhiteSpace(text)) + { + return text; + } + } + } + + return feature.Id?.ToString(); + } + + /// Maps a onto a . + public static RenderOptions RenderOptionsFor(RenderSettings settings, IProgress? progress) + { + var options = new RenderOptions + { + Projection = settings.Projection, + Bounds = settings.Bounds, + Padding = settings.Padding, + Background = settings.Background, + Stroke = settings.Stroke, + Fill = settings.Fill, + StrokeWidth = settings.StrokeWidth, + PointRadius = settings.PointRadius, + StrokeAutoScale = settings.StrokeAutoScale, + LabelSize = settings.LabelSize, + LabelColor = settings.LabelColor, + // Ocean paints the projection's world envelope under every feature (the lobes for Goode, the + // whole canvas otherwise) — see the Blazor service for the full rationale. On by default. + Ocean = settings.OceanEnabled ? settings.Ocean : null, + LabelHalo = settings.HaloEnabled ? settings.LabelHalo : null, + LabelKnockout = settings.KnockoutEnabled ? settings.LabelKnockout : null, + MinFeaturePixels = settings.MinFeaturePixels, + Png = new() { Compression = settings.PngCompression }, + Svg = new() { SimplifyTolerance = settings.SvgSimplifyTolerance }, + Progress = progress, + }; + + // MaxDimension (fit-to-box) wins when set; otherwise an explicit width with a derived or explicit height. + if (settings.MaxDimension > 0) + { + options.MaxDimension = settings.MaxDimension; + } + else + { + if (settings.Width > 0) + { + options.Width = settings.Width; + } + + options.Height = settings.Height; + } + + if (settings.Labels) + { + if (string.IsNullOrWhiteSpace(settings.LabelProperty)) + { + options.Label = AutoLabel; + } + else + { + var key = settings.LabelProperty; + options.Label = _ => + _.Properties.TryGetValue(key, out var value) && value is not null + ? value.ToString() + : null; + } + } + + return options; + } + + /// Builds an open/save dialog filter: an "All supported" clause, every format, then "All files". + public static string BuildDialogFilter(IReadOnlyList formats) + { + var clauses = new List(); + var allPatterns = string.Join( + ';', + formats.SelectMany(_ => _.Extensions).Distinct(StringComparer.OrdinalIgnoreCase).Select(_ => $"*{_}")); + clauses.Add($"All supported ({allPatterns})|{allPatterns}"); + clauses.AddRange(formats.Select(_ => _.DialogFilter)); + clauses.Add("All files (*.*)|*.*"); + return string.Join('|', clauses); + } +} diff --git a/src/GeoConvert.App/DiffMode.cs b/src/GeoConvert.App/DiffMode.cs new file mode 100644 index 0000000..5094d50 --- /dev/null +++ b/src/GeoConvert.App/DiffMode.cs @@ -0,0 +1,12 @@ +namespace GeoConvert.App; + +/// How a map diff is drawn. +public enum DiffMode +{ + /// Both maps drawn on one canvas in distinct colours, so shared geometry blends and + /// differences stand out in pure A- or B-colour. + Overlay, + + /// The two maps drawn separately at the same extent/scale and placed next to each other. + SideBySide, +} diff --git a/src/GeoConvert.App/FileAssociations.cs b/src/GeoConvert.App/FileAssociations.cs new file mode 100644 index 0000000..ed8036e --- /dev/null +++ b/src/GeoConvert.App/FileAssociations.cs @@ -0,0 +1,94 @@ +namespace GeoConvert.App; + +/// +/// Registers this app as the handler for the supported map file extensions, per-user (no admin needed) +/// under HKCU\Software\Classes. One ProgId is created and pointed at the running executable, and +/// every readable map extension is bound to it (set as the default and added to its OpenWith list). The +/// binding is fully reversible via . +/// +public static class FileAssociations +{ + const string ProgId = "GeoConvert.Map"; + const string ProgIdLabel = "GeoConvert Map"; + + const int ShcneAssocchanged = 0x08000000; + const uint ShcnfIdlist = 0; + + [DllImport("shell32.dll")] + static extern void SHChangeNotify(int eventId, uint flags, IntPtr item1, IntPtr item2); + + /// The extensions bound to the app — every format that can be read into the editor. + public static IReadOnlyList Extensions => ConversionService.ReadableExtensions; + + static string ExecutablePath => + Environment.ProcessPath ?? + throw new InvalidOperationException("Could not resolve the running executable path."); + + /// True when the ProgId is registered and bound to the first supported extension. + public static bool IsAssociated() + { + using var classes = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{Extensions[0]}"); + return classes?.GetValue(null) as string == ProgId; + } + + /// Binds every supported map extension to this app. + public static void Associate() + { + var executable = ExecutablePath; + + using (var progId = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}")) + { + progId.SetValue(null, ProgIdLabel); + using (var icon = progId.CreateSubKey("DefaultIcon")) + { + icon.SetValue(null, $"\"{executable}\",0"); + } + + using var command = progId.CreateSubKey(@"shell\open\command"); + command.SetValue(null, $"\"{executable}\" \"%1\""); + } + + foreach (var extension in Extensions) + { + using var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{extension}"); + // Set as the default handler (the "bind" the user asked for) and also advertise the ProgId in + // the extension's OpenWith list so the app shows up there and the binding is cleanly removable. + key.SetValue(null, ProgId); + using var openWith = key.CreateSubKey("OpenWithProgids"); + openWith.SetValue(ProgId, Array.Empty(), RegistryValueKind.None); + } + + NotifyShell(); + } + + /// Removes the bindings created by , leaving other handlers intact. + public static void Unassociate() + { + foreach (var extension in Extensions) + { + using var key = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{extension}", writable: true); + if (key == null) + { + continue; + } + + // Only clear the default if it still points at us — never stomp a handler the user has since + // chosen. + if (key.GetValue(null) as string == ProgId) + { + // "" is the name of a key's default value (DeleteValue, unlike GetValue, won't take null). + key.DeleteValue(string.Empty, throwOnMissingValue: false); + } + + using var openWith = key.OpenSubKey("OpenWithProgids", writable: true); + openWith?.DeleteValue(ProgId, throwOnMissingValue: false); + } + + Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{ProgId}", throwOnMissingSubKey: false); + NotifyShell(); + } + + static void NotifyShell() => + // Tell Explorer the associations changed so icons / "Open with" refresh without a sign-out. + SHChangeNotify(ShcneAssocchanged, ShcnfIdlist, IntPtr.Zero, IntPtr.Zero); +} diff --git a/src/GeoConvert.App/FirstRun.cs b/src/GeoConvert.App/FirstRun.cs new file mode 100644 index 0000000..f124404 --- /dev/null +++ b/src/GeoConvert.App/FirstRun.cs @@ -0,0 +1,55 @@ +namespace GeoConvert.App; + +/// +/// The one-time, first-launch prompt that offers to bind the supported map formats to this app. Gated by +/// so it appears exactly once, whatever the user answers. +/// +public static class FirstRun +{ + public static void PromptForAssociationsIfNeeded(SettingsManager settingsManager, IWin32Window owner) + { + var settings = settingsManager.Read(); + if (settings.AssociationsPrompted) + { + return; + } + + // Persist first, so a crash in the registry step never re-prompts on every launch. + settingsManager.Update(_ => _.AssociationsPrompted = true); + + var extensions = string.Join(" ", FileAssociations.Extensions); + var result = MessageBox.Show( + owner, + $""" + Bind the supported map formats to GeoConvert, so double-clicking one opens it here? + + This sets GeoConvert as the handler for: + {extensions} + + Note: this includes the shared .json and .csv extensions. The change is per-user (no admin + needed) and can be undone any time from Tools ▸ Remove file associations. + """, + "Associate map files with GeoConvert?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (result != DialogResult.Yes) + { + return; + } + + try + { + FileAssociations.Associate(); + } + catch (Exception exception) + { + MessageBox.Show( + owner, + $"Could not set file associations:\n{exception.Message}", + "GeoConvert", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + } +} diff --git a/src/GeoConvert.App/FormatInfo.cs b/src/GeoConvert.App/FormatInfo.cs new file mode 100644 index 0000000..9930828 --- /dev/null +++ b/src/GeoConvert.App/FormatInfo.cs @@ -0,0 +1,25 @@ +namespace GeoConvert.App; + +/// +/// Desktop-facing metadata for a : how to name it, which file extensions it +/// owns, and whether it can be read / written. The desktop has a real filesystem, so (unlike the +/// browser sample) the path-based Shapefile is a first-class format here. +/// +public record FormatInfo( + GeoFormat Format, + string DisplayName, + string Extension, + IReadOnlyList Extensions, + bool CanRead, + bool CanWrite) +{ + /// A single-format file-dialog filter clause, e.g. GeoJSON (*.geojson;*.json)|*.geojson;*.json. + public string DialogFilter + { + get + { + var patterns = string.Join(';', Extensions.Select(_ => $"*{_}")); + return $"{DisplayName} ({patterns})|{patterns}"; + } + } +} diff --git a/src/GeoConvert.App/GeoConvert.App.csproj b/src/GeoConvert.App/GeoConvert.App.csproj new file mode 100644 index 0000000..d15908c --- /dev/null +++ b/src/GeoConvert.App/GeoConvert.App.csproj @@ -0,0 +1,57 @@ + + + + + Exe + net10.0;net11.0 + true + enable + $(NoWarn);CA1416;NETSDK1137 + true + true + geoconvert-app + GeoConvert.App + LatestMajor + + false + true + A WinForms desktop app and .NET tool that converts maps between geospatial formats, renders them to PNG/SVG, and diffs two maps. Built on GeoConvert. + geojson;shapefile;flatgeobuf;topojson;kml;kmz;gpx;wkt;wkb;gis;geospatial;conversion;winforms;diff + readme.md + + + + + + + + + Windows + + + + + + + + + + + + + + + + diff --git a/src/GeoConvert.App/GeoParquetSettings.cs b/src/GeoConvert.App/GeoParquetSettings.cs new file mode 100644 index 0000000..7dd85e5 --- /dev/null +++ b/src/GeoConvert.App/GeoParquetSettings.cs @@ -0,0 +1,11 @@ +namespace GeoConvert.App; + +/// +/// Options for a GeoParquet write — the data-page codec, plus the +/// deflate level used only when the codec is . +/// +public sealed class GeoParquetSettings +{ + public ParquetCompression Codec { get; set; } = ParquetCompression.Snappy; + public CompressionLevel GzipLevel { get; set; } = CompressionLevel.Optimal; +} diff --git a/src/GeoConvert.App/GlobalUsings.cs b/src/GeoConvert.App/GlobalUsings.cs new file mode 100644 index 0000000..c11d14a --- /dev/null +++ b/src/GeoConvert.App/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System.Globalization; +global using System.IO.Compression; +global using System.Runtime.InteropServices; +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using GeoConvert; +global using GeoConvert.ImageSharp; +global using Microsoft.Win32; diff --git a/src/GeoConvert.App/KmzSettings.cs b/src/GeoConvert.App/KmzSettings.cs new file mode 100644 index 0000000..3857c8d --- /dev/null +++ b/src/GeoConvert.App/KmzSettings.cs @@ -0,0 +1,7 @@ +namespace GeoConvert.App; + +/// Options for a KMZ write — the zip deflate level for the archived doc.kml entry. +public sealed class KmzSettings +{ + public CompressionLevel Compression { get; set; } = CompressionLevel.Optimal; +} diff --git a/src/GeoConvert.App/MapDiff.cs b/src/GeoConvert.App/MapDiff.cs new file mode 100644 index 0000000..3446e76 --- /dev/null +++ b/src/GeoConvert.App/MapDiff.cs @@ -0,0 +1,251 @@ +using System.Drawing.Imaging; + +namespace GeoConvert.App; + +/// +/// Compares two maps. The visual diff reuses the existing renderer with zero new drawing code: an +/// stacks both collections on one canvas via the multi-collection +/// overload with a +/// per-layer colouring A and B differently (so shared geometry blends and +/// differences read as pure A- or B-colour), and renders each map to +/// the same union extent and composites them. The structural reports feature +/// counts, geometry-type histograms, bounds, property keys and their deltas — the headline of a +/// command-line diff. Inputs are flattened (layers collapsed) for the visual diff so per-layer styling +/// stays correct on layered sources; still reports the original layer counts. +/// +public static class MapDiff +{ + public static Rgba DefaultColorA { get; } = new(200, 30, 30); + + public static Rgba DefaultColorB { get; } = new(40, 90, 210); + + /// Renders the diff image for as PNG bytes. + public static byte[] Render( + FeatureCollection a, + FeatureCollection b, + RenderSettings settings, + DiffMode mode, + Rgba colorA, + Rgba colorB) => + mode == DiffMode.SideBySide + ? RenderSideBySide(a, b, settings, colorA, colorB) + : RenderOverlay(a, b, settings, colorA, colorB); + + static byte[] RenderOverlay(FeatureCollection a, FeatureCollection b, RenderSettings settings, Rgba colorA, Rgba colorB) + { + var flatA = Flatten(a); + var flatB = Flatten(b); + + var options = ConversionService.RenderOptionsFor(settings, null); + // The ocean fill would paint a solid layer over one of the maps; labels would clutter the + // comparison. Both off for the diff, regardless of the user's export settings. + options.Ocean = null; + options.Label = null; + options.LayerStyle = layer => + { + if (ReferenceEquals(layer, flatA)) + { + return StyleFor(colorA); + } + + if (ReferenceEquals(layer, flatB)) + { + return StyleFor(colorB); + } + + return null; + }; + + // Bounds left as the user set them (null => the renderer unions both inputs), so the two layers + // share one extent and line up. + return MapRenderer.RenderPng([flatA, flatB], options); + } + + static byte[] RenderSideBySide(FeatureCollection a, FeatureCollection b, RenderSettings settings, Rgba colorA, Rgba colorB) + { + var flatA = Flatten(a); + var flatB = Flatten(b); + + // Force both panels onto the same extent so they are spatially comparable: the user's bbox if + // set, otherwise the union of both maps. + var bounds = settings.Bounds ?? flatA.GetBounds().ExpandToInclude(flatB.GetBounds()); + if (bounds.IsEmpty) + { + throw new GeoConvertException("Cannot render a side-by-side diff: neither map has a spatial extent."); + } + + var bytesA = RenderPanel(flatA, settings, bounds, colorA); + var bytesB = RenderPanel(flatB, settings, bounds, colorB); + + using var imageA = LoadBitmap(bytesA); + using var imageB = LoadBitmap(bytesB); + + const int gap = 8; + var width = imageA.Width + gap + imageB.Width; + var height = Math.Max(imageA.Height, imageB.Height); + using var combined = new Bitmap(width, height); + using (var graphics = Graphics.FromImage(combined)) + { + graphics.Clear(settings.Background.ToColor()); + graphics.DrawImage(imageA, 0, 0); + graphics.DrawImage(imageB, imageA.Width + gap, 0); + } + + using var output = new MemoryStream(); + combined.Save(output, ImageFormat.Png); + return output.ToArray(); + } + + static byte[] RenderPanel(FeatureCollection collection, RenderSettings settings, Envelope bounds, Rgba color) + { + var options = ConversionService.RenderOptionsFor(settings, null); + options.Bounds = bounds; + options.Ocean = null; + options.Label = null; + options.Stroke = color; + options.Fill = color with { A = 70 }; + return MapRenderer.RenderPng(collection, options); + } + + static LayerStyle StyleFor(Rgba color) => + new() + { + Stroke = color, + Fill = color with { A = 70 }, + }; + + static Bitmap LoadBitmap(byte[] png) + { + using var stream = new MemoryStream(png); + return new(stream); + } + + // Collapse a (possibly layered) collection into a single flat layer of all its features. The visual + // diff styles per top-level layer, so flattening keeps A and B each a single styled layer even when + // the source had folders/sub-layers. + static FeatureCollection Flatten(FeatureCollection collection) => + new(collection) + { + Name = collection.Name, + }; + + /// Builds the human-readable structural comparison printed by the CLI and shown in the diff view. + public static string Summarize(string nameA, FeatureCollection a, string nameB, FeatureCollection b) + { + var statsA = MapStats.Analyze(a); + var statsB = MapStats.Analyze(b); + + var builder = new StringBuilder(); + AppendMap(builder, "Map A", nameA, statsA); + builder.AppendLine(); + AppendMap(builder, "Map B", nameB, statsB); + builder.AppendLine(); + AppendDifferences(builder, statsA, statsB); + return builder.ToString(); + } + + static void AppendMap(StringBuilder builder, string label, string name, MapStats stats) + { + builder.AppendLine(CultureInfo.InvariantCulture, $"{label}: {name}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Features: {stats.FeatureCount}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Layers: {stats.LayerCount}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Bounds: {FormatBounds(stats.Bounds)}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Geometry: {FormatKinds(stats.GeometryKinds)}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Properties: {FormatKeys(stats.PropertyKeys)}"); + } + + static void AppendDifferences(StringBuilder builder, MapStats a, MapStats b) + { + builder.AppendLine("Differences:"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Features: {FormatDelta(b.FeatureCount - a.FeatureCount)}"); + + var kindLines = new List(); + foreach (var kind in a.GeometryKinds.Keys.Union(b.GeometryKinds.Keys).OrderBy(_ => _, StringComparer.Ordinal)) + { + var delta = b.GeometryKinds.GetValueOrDefault(kind) - a.GeometryKinds.GetValueOrDefault(kind); + if (delta != 0) + { + kindLines.Add($"{kind} {FormatDelta(delta)}"); + } + } + + builder.AppendLine(CultureInfo.InvariantCulture, $" Geometry: {(kindLines.Count == 0 ? "(no change)" : string.Join(", ", kindLines))}"); + + var onlyInA = a.PropertyKeys.Except(b.PropertyKeys, StringComparer.Ordinal).OrderBy(_ => _, StringComparer.Ordinal).ToList(); + var onlyInB = b.PropertyKeys.Except(a.PropertyKeys, StringComparer.Ordinal).OrderBy(_ => _, StringComparer.Ordinal).ToList(); + builder.AppendLine(CultureInfo.InvariantCulture, $" Properties only in A: {(onlyInA.Count == 0 ? "(none)" : string.Join(", ", onlyInA))}"); + builder.AppendLine(CultureInfo.InvariantCulture, $" Properties only in B: {(onlyInB.Count == 0 ? "(none)" : string.Join(", ", onlyInB))}"); + } + + static string FormatDelta(int value) => + value > 0 ? $"+{value}" : value.ToString(CultureInfo.InvariantCulture); + + static string FormatBounds(Envelope bounds) => + bounds.IsEmpty + ? "(empty)" + : string.Format( + CultureInfo.InvariantCulture, + "{0:0.###}, {1:0.###} .. {2:0.###}, {3:0.###}", + bounds.MinX, + bounds.MinY, + bounds.MaxX, + bounds.MaxY); + + static string FormatKinds(IReadOnlyDictionary kinds) => + kinds.Count == 0 + ? "(none)" + : string.Join(", ", kinds.OrderByDescending(_ => _.Value).Select(_ => $"{_.Key} {_.Value}")); + + static string FormatKeys(IReadOnlyCollection keys) => + keys.Count == 0 ? "(none)" : string.Join(", ", keys); + + sealed record MapStats( + int FeatureCount, + int LayerCount, + Envelope Bounds, + IReadOnlyDictionary GeometryKinds, + IReadOnlyCollection PropertyKeys) + { + public static MapStats Analyze(FeatureCollection collection) + { + var kinds = new Dictionary(StringComparer.Ordinal); + var keys = new SortedSet(StringComparer.Ordinal); + foreach (var feature in collection) + { + var kind = KindOf(feature.Geometry); + kinds[kind] = kinds.GetValueOrDefault(kind) + 1; + foreach (var key in feature.Properties.Keys) + { + keys.Add(key); + } + } + + return new(collection.Count, CountLayers(collection), collection.GetBounds(), kinds, keys); + } + + static int CountLayers(FeatureCollection collection) + { + var total = 1; + foreach (var child in collection.Children) + { + total += CountLayers(child); + } + + return total; + } + + static string KindOf(Geometry? geometry) => + geometry switch + { + null => "(none)", + Point => "Point", + LineString => "LineString", + Polygon => "Polygon", + MultiPoint => "MultiPoint", + MultiLineString => "MultiLineString", + MultiPolygon => "MultiPolygon", + GeometryCollection => "GeometryCollection", + _ => geometry.GetType().Name, + }; + } +} diff --git a/src/GeoConvert.App/NativeConsole.cs b/src/GeoConvert.App/NativeConsole.cs new file mode 100644 index 0000000..900c76a --- /dev/null +++ b/src/GeoConvert.App/NativeConsole.cs @@ -0,0 +1,44 @@ +namespace GeoConvert.App; + +/// +/// Console-window management for the hybrid GUI/CLI tool. The app is a console-subsystem exe (so CLI +/// subcommands print and return exit codes normally when run from a terminal). The cost is that an +/// Explorer double-click — a file association launch — allocates a fresh console window. When we detect +/// that case (we own the console alone) and we're about to show the GUI, we hide it so the windowed app +/// looks like a windowed app. Launched from a real terminal we leave the console alone. +/// +static class NativeConsole +{ + const int SwHide = 0; + + [DllImport("kernel32.dll")] + static extern IntPtr GetConsoleWindow(); + + [DllImport("kernel32.dll")] + static extern uint GetConsoleProcessList(uint[] processList, uint processCount); + + [DllImport("user32.dll")] + static extern bool ShowWindow(IntPtr handle, int command); + + /// + /// True when this process is the only one attached to its console — the signature of a console + /// freshly allocated for us by Explorer, rather than a terminal we were launched from (which has at + /// least the shell attached too). + /// + public static bool OwnsConsoleAlone() + { + var processes = new uint[4]; + var count = GetConsoleProcessList(processes, (uint) processes.Length); + return count == 1; + } + + /// Hides this process's console window, if it has one. + public static void HideConsole() + { + var handle = GetConsoleWindow(); + if (handle != IntPtr.Zero) + { + ShowWindow(handle, SwHide); + } + } +} diff --git a/src/GeoConvert.App/OptionChoices.cs b/src/GeoConvert.App/OptionChoices.cs new file mode 100644 index 0000000..0c2348e --- /dev/null +++ b/src/GeoConvert.App/OptionChoices.cs @@ -0,0 +1,48 @@ +namespace GeoConvert.App; + +/// +/// The dropdown choice lists shared by the GUI option editors and the CLI help — the desktop +/// counterpart of the Blazor app's ExportOptionChoices, kept in one place so labels stay +/// consistent across the window, the diff view and the command line. +/// +public static class OptionChoices +{ + public static readonly (MapProjection Value, string Label)[] Projections = + [ + (MapProjection.Auto, "Automatic"), + (MapProjection.PlateCarree, "Plate Carrée"), + (MapProjection.WebMercator, "Web Mercator"), + (MapProjection.Lambert, "Lambert Conformal Conic"), + (MapProjection.Goode, "Goode Homolosine"), + ]; + + public static readonly (RendererBackend Value, string Label)[] Renderers = + [ + (RendererBackend.BuiltIn, "Built-in (dependency-free)"), + (RendererBackend.ImageSharp, "ImageSharp"), + ]; + + public static readonly (int Value, string Label)[] Dimensions = + [ + (512, "512 px"), + (1024, "1024 px"), + (2048, "2048 px"), + (4096, "4096 px"), + (8192, "8192 px"), + ]; + + public static readonly (CompressionLevel Value, string Label)[] CompressionLevels = + [ + (CompressionLevel.Optimal, "Optimal"), + (CompressionLevel.SmallestSize, "Smallest size"), + (CompressionLevel.Fastest, "Fastest"), + (CompressionLevel.NoCompression, "None"), + ]; + + public static readonly (ParquetCompression Value, string Label)[] ParquetCodecs = + [ + (ParquetCompression.Snappy, "Snappy"), + (ParquetCompression.Gzip, "GZIP"), + (ParquetCompression.Uncompressed, "Uncompressed"), + ]; +} diff --git a/src/GeoConvert.App/Program.cs b/src/GeoConvert.App/Program.cs new file mode 100644 index 0000000..3faf54a --- /dev/null +++ b/src/GeoConvert.App/Program.cs @@ -0,0 +1,79 @@ +namespace GeoConvert.App; + +static class Program +{ + static readonly SettingsManager settingsManager = new(SettingsManager.DefaultSettingsPath); + + [STAThread] + static int Main(string[] args) + { + if (args.Length == 0) + { + return LaunchGui(() => new MainForm(settingsManager, null)); + } + + switch (args[0].ToLowerInvariant()) + { + case "-h": + case "--help": + case "help": + Cli.PrintUsage(Console.Out); + return 0; + case "--list": + case "list": + Cli.PrintFormats(Console.Out); + return 0; + case "associate": + return Cli.Associate(Console.Out); + case "unassociate": + return Cli.Unassociate(Console.Out); + case "settings": + return Cli.PrintSettings(Console.Out, settingsManager); + case "diff": + return Diff(args[1..]); + } + + // Not a subcommand. A lone existing file is the file-association double-click case: open it in the + // window. Anything else is a usage error. + if (args.Length == 1 && File.Exists(args[0])) + { + var file = args[0]; + return LaunchGui(() => new MainForm(settingsManager, file)); + } + + Cli.PrintUsage(Console.Error); + return 2; + } + + static int Diff(string[] diffArgs) + { + var code = Cli.ParseDiff(diffArgs, out var request, Console.Error); + if (code != 0 || request == null) + { + return code; + } + + // No output path => show the comparison in the window; otherwise render it headlessly. + if (request.Output == null) + { + return LaunchGui(() => new DiffForm(request)); + } + + return Cli.ExecuteDiff(request, Console.Out, Console.Error); + } + + static int LaunchGui(Func
createForm) + { + // Launched from Explorer (a file-association double-click), a console-subsystem exe is handed its + // own console window. Hide it so the windowed app presents cleanly. Launched from a terminal we + // share the user's console and leave it be. + if (NativeConsole.OwnsConsoleAlone()) + { + NativeConsole.HideConsole(); + } + + ApplicationConfiguration.Initialize(); + Application.Run(createForm()); + return 0; + } +} diff --git a/src/GeoConvert.App/RenderSettings.cs b/src/GeoConvert.App/RenderSettings.cs new file mode 100644 index 0000000..e55041f --- /dev/null +++ b/src/GeoConvert.App/RenderSettings.cs @@ -0,0 +1,61 @@ +namespace GeoConvert.App; + +/// +/// User-tunable knobs for the PNG/SVG image export — a desktop superset of the Blazor app's render +/// settings. It carries every knob the GUI and CLI surface (the web app's +/// projection / size / colours / strokes / labels) plus the extras a desktop with a filesystem can +/// afford: an explicit render extent (), an explicit pixel size, the PNG +/// backend, and the label knockout. Defaults mirror 's +/// own, with the ocean fill and sub-pixel feature culling pre-enabled, so an untouched instance renders +/// the same map the Blazor preview did. +/// +public sealed class RenderSettings +{ + // Size & layout. + public MapProjection Projection { get; set; } = MapProjection.Auto; + public RendererBackend Renderer { get; set; } = RendererBackend.BuiltIn; + + /// When > 0, caps the longer edge at this many pixels (fit-to-box) and ignores + /// /. The default matches the Blazor preview. + public int MaxDimension { get; set; } = 2048; + public int Width { get; set; } = 2048; + public int Height { get; set; } + + /// Render extent in lon/lat. Null renders the data bounds (the common case). + public Envelope? Bounds { get; set; } + public int Padding { get; set; } = 8; + + // Strokes & features. + public int StrokeWidth { get; set; } = 2; + public int PointRadius { get; set; } = 4; + public bool StrokeAutoScale { get; set; } = true; + public double MinFeaturePixels { get; set; } = 1; + + // Labels. + public bool Labels { get; set; } + + /// The property whose value labels each feature. Null/blank falls back to the common + /// name-like keys (name, NAME, admin, …) then the feature id — the Blazor app's behaviour. + public string? LabelProperty { get; set; } + public double LabelSize { get; set; } = 14; + + // Colors. + public Rgba Background { get; set; } = Rgba.White; + public bool OceanEnabled { get; set; } = true; + public Rgba Ocean { get; set; } = new(200, 220, 240); + public Rgba Stroke { get; set; } = new(30, 30, 30); + public Rgba Fill { get; set; } = new(70, 130, 180, 120); + public Rgba LabelColor { get; set; } = new(20, 20, 20); + public bool HaloEnabled { get; set; } = true; + public Rgba LabelHalo { get; set; } = new(255, 255, 255, 200); + + /// The "knockout" backdrop painted under each label (off by default, like + /// ). Erases the geometry under the text instead of + /// overlaying it. + public bool KnockoutEnabled { get; set; } + public Rgba LabelKnockout { get; set; } = Rgba.White; + + // Format-specific. PngCompression only affects a PNG write; SvgSimplifyTolerance only an SVG one. + public CompressionLevel PngCompression { get; set; } = CompressionLevel.Optimal; + public double SvgSimplifyTolerance { get; set; } +} diff --git a/src/GeoConvert.App/RendererBackend.cs b/src/GeoConvert.App/RendererBackend.cs new file mode 100644 index 0000000..8bd9a3e --- /dev/null +++ b/src/GeoConvert.App/RendererBackend.cs @@ -0,0 +1,14 @@ +namespace GeoConvert.App; + +/// +/// Which PNG rasterizer to use, mirroring the geoconvert CLI's renderer flag. SVG always uses the +/// built-in vector writer regardless of this choice. +/// +public enum RendererBackend +{ + /// GeoConvert's dependency-free software rasterizer (). + BuiltIn, + + /// SixLabors.ImageSharp-backed rasterizer (); labels use a system font. + ImageSharp, +} diff --git a/src/GeoConvert.App/RgbaColors.cs b/src/GeoConvert.App/RgbaColors.cs new file mode 100644 index 0000000..fca9d25 --- /dev/null +++ b/src/GeoConvert.App/RgbaColors.cs @@ -0,0 +1,17 @@ +namespace GeoConvert.App; + +/// Bridges GeoConvert's and WinForms' . +public static class RgbaColors +{ + public static Color ToColor(this Rgba color) => + Color.FromArgb(color.A, color.R, color.G, color.B); + + public static Rgba ToRgba(this Color color) => + new(color.R, color.G, color.B, color.A); + + /// Replaces only the RGB channels, preserving the existing alpha — what a WinForms + /// (which has no alpha channel) should do when paired with a separate + /// opacity slider. + public static Rgba WithRgbOf(this Rgba color, Color picked) => + new(picked.R, picked.G, picked.B, color.A); +} diff --git a/src/GeoConvert.App/Settings.cs b/src/GeoConvert.App/Settings.cs new file mode 100644 index 0000000..78d4386 --- /dev/null +++ b/src/GeoConvert.App/Settings.cs @@ -0,0 +1,15 @@ +namespace GeoConvert.App; + +/// +/// Persisted user preferences, stored as JSON under . +/// Kept deliberately small — the only thing that has to survive between runs is whether the first-run +/// file-association prompt has already been shown. +/// +public class Settings +{ + /// + /// True once the user has been asked (on first launch) whether to bind the supported map formats to + /// this app. Gates the one-time prompt so it never reappears, whatever the user answered. + /// + public bool AssociationsPrompted { get; set; } +} diff --git a/src/GeoConvert.App/SettingsManager.cs b/src/GeoConvert.App/SettingsManager.cs new file mode 100644 index 0000000..8b77ae7 --- /dev/null +++ b/src/GeoConvert.App/SettingsManager.cs @@ -0,0 +1,69 @@ +namespace GeoConvert.App; + +/// +/// Reads and writes as indented JSON. Modelled on MsOfficeDiff's settings +/// manager, but synchronous and dependency-free (System.Text.Json, no logging package): a read failure +/// degrades to defaults rather than throwing, so a corrupt file never blocks startup. +/// +public class SettingsManager +{ + readonly string settingsPath; + + public SettingsManager(string settingsPath) + { + var directory = Path.GetDirectoryName(settingsPath); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + + this.settingsPath = settingsPath; + } + + static readonly JsonSerializerOptions jsonOptions = new() + { + WriteIndented = true, + }; + + public static string DefaultSettingsPath { get; } = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", + "GeoConvert", + "settings.json"); + + public string SettingsPath => settingsPath; + + public Settings Read() + { + if (!File.Exists(settingsPath)) + { + return new(); + } + + try + { + using var stream = File.OpenRead(settingsPath); + return JsonSerializer.Deserialize(stream) ?? new(); + } + catch + { + // A malformed settings file is not worth failing startup over — fall back to defaults. The + // next Write rewrites it cleanly. + return new(); + } + } + + public void Write(Settings settings) + { + using var stream = File.Create(settingsPath); + JsonSerializer.Serialize(stream, settings, jsonOptions); + } + + /// Reads the settings, applies , and writes them back. + public void Update(Action mutate) + { + var settings = Read(); + mutate(settings); + Write(settings); + } +} diff --git a/src/GeoConvert.App/SimplifySettings.cs b/src/GeoConvert.App/SimplifySettings.cs new file mode 100644 index 0000000..ede1243 --- /dev/null +++ b/src/GeoConvert.App/SimplifySettings.cs @@ -0,0 +1,28 @@ +namespace GeoConvert.App; + +/// +/// Optional lossy vertex reduction applied before writing or rendering — the GUI/CLI surface of +/// . Off by default; when on, the loaded features are thinned (a new graph, the +/// original is untouched) using the chosen and tolerance, with +/// switching to the shared-boundary variant so adjacent polygons stay joined. +/// +public sealed class SimplifySettings +{ + public bool Enabled { get; set; } + public double Tolerance { get; set; } = 0.01; + public SimplifyMethod Method { get; set; } = SimplifyMethod.DouglasPeucker; + public bool Topology { get; set; } + + /// Returns thinned per these settings, or unchanged when off. + public FeatureCollection Apply(FeatureCollection collection) + { + if (!Enabled || Tolerance <= 0) + { + return collection; + } + + return Topology + ? Simplifier.SimplifyTopology(collection, Tolerance, Method) + : Simplifier.Simplify(collection, Tolerance, Method); + } +} diff --git a/src/GeoConvert.App/Ui/Combos.cs b/src/GeoConvert.App/Ui/Combos.cs new file mode 100644 index 0000000..2fa9798 --- /dev/null +++ b/src/GeoConvert.App/Ui/Combos.cs @@ -0,0 +1,39 @@ +namespace GeoConvert.App; + +/// Builds value-carrying es (label shown, typed value behind it). +static class Combos +{ + public static ComboBox Build(IReadOnlyList<(T Value, string Label)> choices, T current, Action onChange) + where T : notnull + { + var combo = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + Width = 170, + Margin = new(3), + }; + foreach (var (value, label) in choices) + { + combo.Items.Add(new Choice(value, label)); + } + + for (var index = 0; index < combo.Items.Count; index++) + { + if (EqualityComparer.Default.Equals(((Choice) combo.Items[index]!).Value, current)) + { + combo.SelectedIndex = index; + break; + } + } + + combo.SelectedIndexChanged += (_, _) => onChange(((Choice) combo.SelectedItem!).Value); + return combo; + } + + sealed class Choice(T value, string label) + { + public T Value { get; } = value; + + public override string ToString() => label; + } +} diff --git a/src/GeoConvert.App/Ui/DiffForm.cs b/src/GeoConvert.App/Ui/DiffForm.cs new file mode 100644 index 0000000..3a4e210 --- /dev/null +++ b/src/GeoConvert.App/Ui/DiffForm.cs @@ -0,0 +1,335 @@ +namespace GeoConvert.App; + +/// +/// The map comparison window. Pick two maps (or arrive preloaded from the diff command line), +/// see the visual diff — an overlay of both in distinct colours, or a side-by-side at a shared extent — +/// alongside a structural summary (feature counts, geometry histograms, bounds, property deltas), and +/// save the diff image. +/// +sealed class DiffForm : Form +{ + readonly RenderSettings settings; + DiffMode mode; + Rgba colorA; + Rgba colorB; + string? pathA; + string? pathB; + + FeatureCollection? mapA; + FeatureCollection? mapB; + byte[]? currentImage; + int renderToken; + bool initialLoadDone; + + TextBox pathBoxA = null!; + TextBox pathBoxB = null!; + PictureBox preview = null!; + TextBox summary = null!; + Button saveButton = null!; + Button swatchA = null!; + Button swatchB = null!; + SplitContainer split = null!; + + public DiffForm() + : this(new(), DiffMode.Overlay, MapDiff.DefaultColorA, MapDiff.DefaultColorB, null, null) + { + } + + public DiffForm(Cli.DiffRequest request) + : this(request.Settings, request.Mode, request.ColorA, request.ColorB, request.PathA, request.PathB) + { + } + + DiffForm(RenderSettings settings, DiffMode mode, Rgba colorA, Rgba colorB, string? pathA, string? pathB) + { + this.settings = settings; + this.mode = mode; + this.colorA = colorA; + this.colorB = colorB; + this.pathA = pathA; + this.pathB = pathB; + BuildUi(); + } + + protected override void OnShown(EventArgs args) + { + base.OnShown(args); + if (initialLoadDone) + { + return; + } + + initialLoadDone = true; + if (pathA != null && pathB != null) + { + BeginInvoke(() => _ = LoadBothAsync()); + } + } + + void BuildUi() + { + Text = "Compare maps — GeoConvert"; + StartPosition = FormStartPosition.CenterScreen; + Size = new(1100, 720); + MinimumSize = new(820, 520); + + split = new SplitContainer + { + Dock = DockStyle.Fill, + FixedPanel = FixedPanel.Panel2, + }; + preview = new PictureBox + { + Dock = DockStyle.Fill, + SizeMode = PictureBoxSizeMode.Zoom, + BackColor = Color.FromArgb(245, 245, 245), + }; + summary = new TextBox + { + Dock = DockStyle.Fill, + Multiline = true, + ReadOnly = true, + WordWrap = false, + ScrollBars = ScrollBars.Both, + Font = new("Consolas", 9F), + BackColor = Color.White, + }; + split.Panel1.Controls.Add(preview); + split.Panel2.Controls.Add(summary); + + saveButton = new Button + { + Dock = DockStyle.Bottom, + Height = 38, + Text = "Save diff image…", + Enabled = false, + }; + saveButton.Click += (_, _) => SaveImage(); + + Controls.Add(split); + Controls.Add(saveButton); + Controls.Add(BuildToolbar()); + Controls.Add(BuildInputs()); + } + + protected override void OnLoad(EventArgs args) + { + base.OnLoad(args); + SplitLayout.ConfigureSplit(split, 360); + } + + TableLayoutPanel BuildInputs() + { + var table = new TableLayoutPanel + { + Dock = DockStyle.Top, + ColumnCount = 3, + RowCount = 2, + Height = 84, + Padding = new(6, 6, 6, 2), + }; + table.ColumnStyles.Add(new(SizeType.Absolute, 60)); + table.ColumnStyles.Add(new(SizeType.Percent, 100)); + table.ColumnStyles.Add(new(SizeType.Absolute, 90)); + table.RowStyles.Add(new(SizeType.Absolute, 38)); + table.RowStyles.Add(new(SizeType.Absolute, 38)); + + pathBoxA = AddInputRow(table, "Map A:", pathA, _ => LoadAInto(_)); + pathBoxB = AddInputRow(table, "Map B:", pathB, _ => LoadBInto(_)); + return table; + } + + TextBox AddInputRow(TableLayoutPanel table, string label, string? value, Action onPicked) + { + // Anchor (not Dock) so each control keeps its natural height and the TableLayoutPanel centres it + // vertically in the row — Dock=Fill here stretched the controls and overlapped the toolbar below. + table.Controls.Add(new Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(3, 0, 3, 0) }); + var box = new TextBox { Anchor = AnchorStyles.Left | AnchorStyles.Right, ReadOnly = true, Text = value ?? string.Empty, Margin = new(3, 0, 3, 0) }; + table.Controls.Add(box); + var browse = new Button { Text = "Browse…", Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new(3, 0, 3, 0) }; + browse.Click += (_, _) => + { + using var dialog = new OpenFileDialog + { + Title = label, + Filter = ConversionService.BuildDialogFilter(ConversionService.ReadableFormats), + }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + box.Text = dialog.FileName; + onPicked(dialog.FileName); + } + }; + table.Controls.Add(browse); + return box; + } + + FlowLayoutPanel BuildToolbar() + { + var bar = new FlowLayoutPanel + { + Dock = DockStyle.Top, + AutoSize = true, + WrapContents = false, + Padding = new(6, 2, 6, 2), + }; + + bar.Controls.Add(new Label { Text = "Mode", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(3, 8, 3, 3) }); + bar.Controls.Add(Combos.Build( + [(DiffMode.Overlay, "Overlay"), (DiffMode.SideBySide, "Side by side")], + mode, + value => + { + mode = value; + _ = RenderAsync(); + })); + + bar.Controls.Add(new Label { Text = "Projection", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 3, 3) }); + bar.Controls.Add(Combos.Build( + OptionChoices.Projections, + settings.Projection, + value => + { + settings.Projection = value; + _ = RenderAsync(); + })); + + bar.Controls.Add(new Label { Text = "Resolution", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 3, 3) }); + bar.Controls.Add(Combos.Build( + OptionChoices.Dimensions, + settings.MaxDimension > 0 ? settings.MaxDimension : 2048, + value => + { + settings.MaxDimension = value; + _ = RenderAsync(); + })); + + bar.Controls.Add(new Label { Text = "A", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 1, 3) }); + swatchA = ColorSwatch(() => colorA, _ => colorA = _); + bar.Controls.Add(swatchA); + bar.Controls.Add(new Label { Text = "B", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 1, 3) }); + swatchB = ColorSwatch(() => colorB, _ => colorB = _); + bar.Controls.Add(swatchB); + + return bar; + } + + Button ColorSwatch(Func get, Action set) + { + var current = get(); + var swatch = new Button + { + Width = 40, + Height = 24, + FlatStyle = FlatStyle.Flat, + BackColor = Color.FromArgb(255, current.R, current.G, current.B), + Margin = new(1, 4, 1, 3), + }; + swatch.Click += (_, _) => + { + using var dialog = new ColorDialog { Color = swatch.BackColor, FullOpen = true }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + set(dialog.Color.ToRgba()); + swatch.BackColor = Color.FromArgb(255, dialog.Color.R, dialog.Color.G, dialog.Color.B); + _ = RenderAsync(); + } + }; + return swatch; + } + + void LoadAInto(string path) => _ = LoadAsync(path, isFirst: true); + + void LoadBInto(string path) => _ = LoadAsync(path, isFirst: false); + + async Task LoadBothAsync() + { + await LoadAsync(pathA!, isFirst: true, render: false); + await LoadAsync(pathB!, isFirst: false, render: false); + await RenderAsync(); + } + + async Task LoadAsync(string path, bool isFirst, bool render = true) + { + try + { + var collection = await Task.Run(() => GeoConverter.Read(path)); + if (isFirst) + { + mapA = collection; + pathA = path; + } + else + { + mapB = collection; + pathB = path; + } + + if (render) + { + await RenderAsync(); + } + } + catch (Exception exception) + { + MessageBox.Show(this, $"Could not read '{Path.GetFileName(path)}':\n{exception.Message}", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + async Task RenderAsync() + { + if (mapA is not { } a || mapB is not { } b) + { + return; + } + + var token = ++renderToken; + var localMode = mode; + var localColorA = colorA; + var localColorB = colorB; + try + { + var result = await Task.Run(() => + { + var image = MapDiff.Render(a, b, settings, localMode, localColorA, localColorB); + var text = MapDiff.Summarize(Path.GetFileName(pathA!), a, Path.GetFileName(pathB!), b); + return (image, text); + }); + + if (token != renderToken) + { + return; + } + + currentImage = result.image; + preview.Image?.Dispose(); + preview.Image = Images.DecodePng(result.image); + summary.Text = result.text.ReplaceLineEndings(); + saveButton.Enabled = true; + } + catch (Exception exception) + { + MessageBox.Show(this, $"Could not render the diff:\n{exception.Message}", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + void SaveImage() + { + if (currentImage is not { } image) + { + return; + } + + using var dialog = new SaveFileDialog + { + Title = "Save diff image", + Filter = "PNG image (*.png)|*.png", + FileName = "diff.png", + DefaultExt = "png", + }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + File.WriteAllBytes(dialog.FileName, image); + } + } +} diff --git a/src/GeoConvert.App/Ui/Images.cs b/src/GeoConvert.App/Ui/Images.cs new file mode 100644 index 0000000..1d2bf47 --- /dev/null +++ b/src/GeoConvert.App/Ui/Images.cs @@ -0,0 +1,15 @@ +namespace GeoConvert.App; + +static class Images +{ + /// + /// Fully materialises PNG bytes into a so the backing stream can be disposed + /// immediately — a Bitmap built straight from a stream keeps a lazy reference to it. + /// + public static Bitmap DecodePng(byte[] png) + { + using var stream = new MemoryStream(png); + using var decoded = new Bitmap(stream); + return new(decoded); + } +} diff --git a/src/GeoConvert.App/Ui/MainForm.cs b/src/GeoConvert.App/Ui/MainForm.cs new file mode 100644 index 0000000..aff31e4 --- /dev/null +++ b/src/GeoConvert.App/Ui/MainForm.cs @@ -0,0 +1,361 @@ +namespace GeoConvert.App; + +/// +/// The main window: open a map, see a live preview, tune the full set of render/convert options, and +/// save to any supported format — the desktop counterpart of the Blazor converter page. Reads, writes +/// and previews run off the UI thread with progress reported back through . +/// +sealed class MainForm : Form +{ + readonly SettingsManager settingsManager; + readonly RenderSettings render = new(); + readonly SimplifySettings simplify = new(); + readonly KmzSettings kmz = new(); + readonly GeoParquetSettings parquet = new(); + readonly IProgress progress; + + OptionsPanel optionsPanel = null!; + PictureBox preview = null!; + Button saveButton = null!; + Label fileLabel = null!; + SplitContainer split = null!; + ToolStripStatusLabel statusLabel = null!; + ToolStripProgressBar progressBar = null!; + + FeatureCollection? features; + string? sourcePath; + FormatInfo? sourceFormat; + string? initialFile; + int previewToken; + bool busy; + + public MainForm(SettingsManager settingsManager, string? initialFile) + { + this.settingsManager = settingsManager; + this.initialFile = initialFile; + progress = new Progress(OnProgress); + + BuildUi(); + UpdateState(); + } + + protected override void OnShown(EventArgs args) + { + base.OnShown(args); + FirstRun.PromptForAssociationsIfNeeded(settingsManager, this); + + // Load any file passed on the command line (the file-association open) now that the window — and + // its handle — exists, so the read runs on the live message loop rather than from the constructor. + if (initialFile is { } file) + { + initialFile = null; + _ = LoadAsync(file); + } + } + + void BuildUi() + { + Text = "GeoConvert"; + StartPosition = FormStartPosition.CenterScreen; + Size = new(1100, 720); + MinimumSize = new(820, 520); + + split = new SplitContainer + { + Dock = DockStyle.Fill, + FixedPanel = FixedPanel.Panel2, + }; + + preview = new PictureBox + { + Dock = DockStyle.Fill, + SizeMode = PictureBoxSizeMode.Zoom, + BackColor = Color.FromArgb(245, 245, 245), + }; + fileLabel = new Label + { + Dock = DockStyle.Top, + AutoSize = false, + Height = 28, + TextAlign = ContentAlignment.MiddleLeft, + Padding = new(8, 0, 0, 0), + Text = "No map loaded", + }; + split.Panel1.Controls.Add(preview); + split.Panel1.Controls.Add(fileLabel); + + optionsPanel = new(render, simplify, kmz, parquet) { Dock = DockStyle.Fill }; + optionsPanel.Changed += (_, _) => _ = RefreshPreviewAsync(); + optionsPanel.TargetChanged += (_, _) => UpdateSaveLabel(); + + saveButton = new Button + { + Dock = DockStyle.Bottom, + Height = 40, + Text = "Save As…", + Enabled = false, + }; + saveButton.Click += (_, _) => _ = SaveAsync(); + split.Panel2.Controls.Add(optionsPanel); + split.Panel2.Controls.Add(saveButton); + + var status = new StatusStrip(); + statusLabel = new ToolStripStatusLabel { Spring = true, TextAlign = ContentAlignment.MiddleLeft }; + progressBar = new ToolStripProgressBar { Visible = false, Width = 200 }; + status.Items.Add(statusLabel); + status.Items.Add(progressBar); + + Controls.Add(split); + Controls.Add(status); + Controls.Add(BuildMenu()); + } + + protected override void OnLoad(EventArgs args) + { + base.OnLoad(args); + SplitLayout.ConfigureSplit(split, 400); + } + + MenuStrip BuildMenu() + { + var menu = new MenuStrip(); + + var file = new ToolStripMenuItem("&File"); + file.DropDownItems.Add("&Open…", null, (_, _) => OpenFile()); + file.DropDownItems.Add("&Save As…", null, (_, _) => _ = SaveAsync()); + file.DropDownItems.Add(new ToolStripSeparator()); + file.DropDownItems.Add("E&xit", null, (_, _) => Close()); + + var tools = new ToolStripMenuItem("&Tools"); + tools.DropDownItems.Add("&Compare maps…", null, (_, _) => new DiffForm().Show(this)); + tools.DropDownItems.Add(new ToolStripSeparator()); + tools.DropDownItems.Add("&Associate map file types", null, (_, _) => AssociateFromMenu()); + tools.DropDownItems.Add("&Remove file associations", null, (_, _) => UnassociateFromMenu()); + + var help = new ToolStripMenuItem("&Help"); + help.DropDownItems.Add("&About", null, (_, _) => ShowAbout()); + + menu.Items.Add(file); + menu.Items.Add(tools); + menu.Items.Add(help); + return menu; + } + + void OpenFile() + { + using var dialog = new OpenFileDialog + { + Title = "Open a map", + Filter = ConversionService.BuildDialogFilter(ConversionService.ReadableFormats), + }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + _ = LoadAsync(dialog.FileName); + } + } + + async Task LoadAsync(string path) + { + var detected = ConversionService.Detect(path); + if (detected is not { CanRead: true }) + { + MessageBox.Show(this, $"Can't read '{Path.GetFileName(path)}': unsupported map format.", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + SetBusy(true, $"Reading {detected.DisplayName}…"); + try + { + var collection = await Task.Run(() => ConversionService.Read(path, detected.Format, progress)); + features = collection; + sourcePath = path; + sourceFormat = detected; + fileLabel.Text = $"{Path.GetFileName(path)} · {detected.DisplayName} · {collection.Count} feature{(collection.Count == 1 ? "" : "s")}"; + await RefreshPreviewAsync(); + } + catch (Exception exception) + { + features = null; + ShowError("Could not read the map", exception); + } + finally + { + SetBusy(false, null); + UpdateState(); + } + } + + async Task RefreshPreviewAsync() + { + if (features is not { Count: > 0 } collection) + { + preview.Image?.Dispose(); + preview.Image = null; + return; + } + + var token = ++previewToken; + try + { + var image = await Task.Run(() => + { + var prepared = simplify.Apply(collection); + var png = ConversionService.RenderPreview(prepared, render); + return Images.DecodePng(png); + }); + + // A newer refresh started while this one ran — discard this stale image. + if (token != previewToken) + { + image.Dispose(); + return; + } + + preview.Image?.Dispose(); + preview.Image = image; + } + catch + { + // Preview is best-effort (e.g. a map with no spatial extent can't be rendered) — just leave + // the previous image. Saving still surfaces real errors. + } + } + + async Task SaveAsync() + { + if (features is not { } collection || sourceFormat == null || busy) + { + return; + } + + var format = optionsPanel.SelectedFormat; + var info = ConversionService.Find(format)!; + + using var dialog = new SaveFileDialog + { + Title = "Save map", + Filter = info.DialogFilter + "|All files (*.*)|*.*", + FileName = Path.GetFileNameWithoutExtension(sourcePath) + info.Extension, + DefaultExt = info.Extension.TrimStart('.'), + }; + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + var destination = dialog.FileName; + SetBusy(true, ConversionService.IsRendered(format) ? "Rendering…" : $"Writing {info.DisplayName}…"); + try + { + await Task.Run(() => + { + var prepared = simplify.Apply(collection); + ConversionService.Save(prepared, destination, format, render, kmz, parquet, progress); + }); + statusLabel.Text = $"Saved {Path.GetFileName(destination)}"; + } + catch (Exception exception) + { + ShowError($"Could not save as {info.DisplayName}", exception); + } + finally + { + SetBusy(false, null); + } + } + + void AssociateFromMenu() + { + try + { + FileAssociations.Associate(); + MessageBox.Show(this, "GeoConvert is now the handler for the supported map formats.", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception exception) + { + ShowError("Could not set file associations", exception); + } + } + + void UnassociateFromMenu() + { + try + { + FileAssociations.Unassociate(); + MessageBox.Show(this, "Removed GeoConvert's map file associations.", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception exception) + { + ShowError("Could not remove file associations", exception); + } + } + + void ShowAbout() => + MessageBox.Show( + this, + """ + GeoConvert + + Convert maps between GeoJSON, TopoJSON, Shapefile, FlatGeobuf, KML/KMZ, GPX, WKT, WKB, CSV and + GeoParquet; render to PNG/SVG; and compare two maps. + + Tools ▸ Compare maps… diffs two files. From a terminal: geoconvert-app --help. + """, + "About GeoConvert", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + void UpdateState() + { + saveButton.Enabled = features is { Count: > 0 } && !busy; + UpdateSaveLabel(); + } + + void UpdateSaveLabel() + { + var info = ConversionService.Find(optionsPanel.SelectedFormat); + saveButton.Text = info == null ? "Save As…" : $"Save As {info.DisplayName}…"; + } + + void SetBusy(bool value, string? message) + { + busy = value; + progressBar.Visible = value; + if (!value) + { + progressBar.Style = ProgressBarStyle.Blocks; + progressBar.Value = 0; + } + + if (message != null) + { + statusLabel.Text = message; + } + + saveButton.Enabled = features is { Count: > 0 } && !busy; + Cursor = value ? Cursors.AppStarting : Cursors.Default; + } + + void OnProgress(ConvertProgress report) + { + if (!busy) + { + return; + } + + if (report.Fraction is { } fraction) + { + progressBar.Style = ProgressBarStyle.Blocks; + progressBar.Value = (int) Math.Clamp(fraction * 100, 0, 100); + } + else + { + // No derivable fraction (e.g. the read phase, where the total isn't known yet) — show motion + // rather than a frozen bar. + progressBar.Style = ProgressBarStyle.Marquee; + } + } + + void ShowError(string action, Exception exception) => + MessageBox.Show(this, $"{action}:\n{exception.Message}", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Error); +} diff --git a/src/GeoConvert.App/Ui/OptionsPanel.cs b/src/GeoConvert.App/Ui/OptionsPanel.cs new file mode 100644 index 0000000..f563136 --- /dev/null +++ b/src/GeoConvert.App/Ui/OptionsPanel.cs @@ -0,0 +1,377 @@ +namespace GeoConvert.App; + +/// +/// The export options editor — the desktop equivalent of the Blazor app's ExportOptions component. +/// Surfaces every knob plus the per-format options (KMZ deflate level, +/// GeoParquet codec) and the optional pre-pass, showing only the +/// sections relevant to the chosen output format. Raises when a preview-affecting +/// knob moves and when the output format changes. +/// +sealed class OptionsPanel : FlowLayoutPanel +{ + readonly RenderSettings render; + readonly SimplifySettings simplify; + readonly KmzSettings kmz; + readonly GeoParquetSettings parquet; + + GroupBox imageSection = null!; + GroupBox pngSection = null!; + GroupBox svgSection = null!; + GroupBox kmzSection = null!; + GroupBox parquetSection = null!; + GroupBox noteSection = null!; + + TableLayoutPanel currentTable = null!; + + public OptionsPanel(RenderSettings render, SimplifySettings simplify, KmzSettings kmz, GeoParquetSettings parquet) + { + this.render = render; + this.simplify = simplify; + this.kmz = kmz; + this.parquet = parquet; + + FlowDirection = FlowDirection.TopDown; + WrapContents = false; + AutoScroll = true; + Padding = new(4); + + BuildOutputSection(); + BuildImageSection(); + BuildPngSection(); + BuildSvgSection(); + BuildKmzSection(); + BuildParquetSection(); + BuildSimplifySection(); + BuildNoteSection(); + + UpdateVisibility(); + } + + /// The currently selected output format. + public GeoFormat SelectedFormat { get; private set; } = GeoFormat.Kml; + + /// Raised when a preview-affecting option changes (so the host can re-render the preview). + public event EventHandler? Changed; + + /// Raised when the output format changes. + public event EventHandler? TargetChanged; + + void RaiseChanged() => Changed?.Invoke(this, EventArgs.Empty); + + // --- sections --- + + void BuildOutputSection() + { + BeginSection("Output"); + AddCombo( + "Format", + [.. ConversionService.WritableFormats.Select(_ => (_.Format, _.DisplayName))], + SelectedFormat, + value => + { + SelectedFormat = value; + UpdateVisibility(); + TargetChanged?.Invoke(this, EventArgs.Empty); + }); + } + + void BuildImageSection() + { + imageSection = BeginSection("Image (PNG / SVG)"); + AddCombo("Projection", OptionChoices.Projections, render.Projection, _ => render.Projection = _); + AddCombo("Resolution", OptionChoices.Dimensions, render.MaxDimension, _ => render.MaxDimension = _); + AddInt("Padding (px)", 0, 500, render.Padding, _ => render.Padding = _); + AddInt("Stroke width (px)", 0, 50, render.StrokeWidth, _ => render.StrokeWidth = _); + AddInt("Point radius (px)", 0, 50, render.PointRadius, _ => render.PointRadius = _); + AddCheck("Auto-scale strokes to zoom", render.StrokeAutoScale, _ => render.StrokeAutoScale = _); + AddDouble("Min feature size (px)", 0, 64, 1, render.MinFeaturePixels, _ => render.MinFeaturePixels = _); + AddCheck("Show labels", render.Labels, _ => render.Labels = _); + AddText("Label property (blank = auto)", render.LabelProperty ?? string.Empty, _ => render.LabelProperty = _); + AddDouble("Label size (px)", 1, 200, 1, render.LabelSize, _ => render.LabelSize = _); + AddColor("Background", () => render.Background, _ => render.Background = _, withAlpha: false); + AddCheck("Ocean fill", render.OceanEnabled, _ => render.OceanEnabled = _); + AddColor("Ocean colour", () => render.Ocean, _ => render.Ocean = _, withAlpha: true); + AddColor("Stroke", () => render.Stroke, _ => render.Stroke = _, withAlpha: false); + AddColor("Polygon fill", () => render.Fill, _ => render.Fill = _, withAlpha: true); + AddColor("Label text", () => render.LabelColor, _ => render.LabelColor = _, withAlpha: false); + AddCheck("Label halo", render.HaloEnabled, _ => render.HaloEnabled = _); + AddColor("Halo colour", () => render.LabelHalo, _ => render.LabelHalo = _, withAlpha: true); + AddCheck("Label knockout", render.KnockoutEnabled, _ => render.KnockoutEnabled = _); + AddColor("Knockout colour", () => render.LabelKnockout, _ => render.LabelKnockout = _, withAlpha: true); + } + + void BuildPngSection() + { + pngSection = BeginSection("PNG"); + AddCombo("Renderer", OptionChoices.Renderers, render.Renderer, _ => render.Renderer = _); + AddCombo("Compression", OptionChoices.CompressionLevels, render.PngCompression, _ => render.PngCompression = _, affectsPreview: false); + } + + void BuildSvgSection() + { + svgSection = BeginSection("SVG"); + AddDouble("Simplify tolerance (px)", 0, 20, 1, render.SvgSimplifyTolerance, _ => render.SvgSimplifyTolerance = _, affectsPreview: false); + } + + void BuildKmzSection() + { + kmzSection = BeginSection("KMZ"); + AddCombo("Compression", OptionChoices.CompressionLevels, kmz.Compression, _ => kmz.Compression = _, affectsPreview: false); + } + + void BuildParquetSection() + { + parquetSection = BeginSection("GeoParquet"); + AddCombo("Codec", OptionChoices.ParquetCodecs, parquet.Codec, _ => parquet.Codec = _, affectsPreview: false); + AddCombo("GZIP level", OptionChoices.CompressionLevels, parquet.GzipLevel, _ => parquet.GzipLevel = _, affectsPreview: false); + } + + void BuildSimplifySection() + { + BeginSection("Simplify (optional pre-pass)"); + AddCheck("Simplify geometry", simplify.Enabled, _ => simplify.Enabled = _); + AddDouble("Tolerance", 0, 1000, 4, simplify.Tolerance, _ => simplify.Tolerance = _); + AddCombo( + "Method", + [(SimplifyMethod.DouglasPeucker, "Douglas–Peucker"), (SimplifyMethod.Visvalingam, "Visvalingam")], + simplify.Method, + _ => simplify.Method = _); + AddCheck("Preserve shared boundaries", simplify.Topology, _ => simplify.Topology = _); + } + + void BuildNoteSection() + { + noteSection = BeginSection("Format"); + var note = new Label + { + AutoSize = true, + MaximumSize = new(330, 0), + Margin = new(3), + Text = "This format writes geometry and properties directly. Use the Simplify section above to thin vertices before writing.", + }; + currentTable.Controls.Add(note); + currentTable.SetColumnSpan(note, 2); + } + + void UpdateVisibility() + { + var format = SelectedFormat; + imageSection.Visible = ConversionService.IsRendered(format); + pngSection.Visible = format == GeoFormat.Png; + svgSection.Visible = format == GeoFormat.Svg; + kmzSection.Visible = format == GeoFormat.Kmz; + parquetSection.Visible = format == GeoFormat.GeoParquet; + noteSection.Visible = format is not (GeoFormat.Png or GeoFormat.Svg or GeoFormat.Kmz or GeoFormat.GeoParquet); + } + + // --- row/control builders --- + + GroupBox BeginSection(string title) + { + var table = new TableLayoutPanel + { + ColumnCount = 2, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + Dock = DockStyle.Top, + Padding = new(4), + }; + table.ColumnStyles.Add(new(SizeType.Absolute, 150)); + table.ColumnStyles.Add(new(SizeType.Absolute, 200)); + + var box = new GroupBox + { + Text = title, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + // Pin the width (Min == Max), leaving only the height to AutoSize. Without this an AutoSize + // GroupBox wrapping a Dock=Top table can't resolve its width (each defers to the other) and + // collapses to a sliver. + MinimumSize = new(372, 0), + MaximumSize = new(372, 0), + Margin = new(3), + Padding = new(6, 3, 6, 6), + }; + box.Controls.Add(table); + Controls.Add(box); + currentTable = table; + return box; + } + + void Row(string label, Control control) + { + var caption = new Label + { + Text = label, + AutoSize = true, + Anchor = AnchorStyles.Left, + Margin = new(3, 7, 3, 3), + MaximumSize = new(145, 0), + }; + currentTable.Controls.Add(caption); + currentTable.Controls.Add(control); + } + + void AddCombo(string label, IReadOnlyList<(T Value, string Label)> choices, T current, Action set, bool affectsPreview = true) + where T : notnull + { + var combo = Combos.Build( + choices, + current, + value => + { + set(value); + if (affectsPreview) + { + RaiseChanged(); + } + }); + combo.Width = 190; + Row(label, combo); + } + + void AddInt(string label, int min, int max, int current, Action set, bool affectsPreview = true) + { + var numeric = new NumericUpDown + { + Minimum = min, + Maximum = max, + Value = Math.Clamp(current, min, max), + Width = 190, + Margin = new(3), + }; + numeric.ValueChanged += (_, _) => + { + set((int) numeric.Value); + if (affectsPreview) + { + RaiseChanged(); + } + }; + Row(label, numeric); + } + + void AddDouble(string label, double min, double max, int decimals, double current, Action set, bool affectsPreview = true) + { + var numeric = new NumericUpDown + { + Minimum = (decimal) min, + Maximum = (decimal) max, + DecimalPlaces = decimals, + Increment = (decimal) Math.Pow(10, -decimals), + Value = (decimal) Math.Clamp(current, min, max), + Width = 190, + Margin = new(3), + }; + numeric.ValueChanged += (_, _) => + { + set((double) numeric.Value); + if (affectsPreview) + { + RaiseChanged(); + } + }; + Row(label, numeric); + } + + void AddCheck(string label, bool current, Action set, bool affectsPreview = true) + { + var check = new CheckBox + { + Checked = current, + AutoSize = true, + Margin = new(3), + }; + check.CheckedChanged += (_, _) => + { + set(check.Checked); + if (affectsPreview) + { + RaiseChanged(); + } + }; + Row(label, check); + } + + void AddText(string label, string current, Action set, bool affectsPreview = true) + { + var text = new TextBox + { + Text = current, + Width = 190, + Margin = new(3), + }; + text.TextChanged += (_, _) => + { + set(text.Text.Length == 0 ? null : text.Text); + if (affectsPreview) + { + RaiseChanged(); + } + }; + Row(label, text); + } + + void AddColor(string label, Func get, Action set, bool withAlpha, bool affectsPreview = true) + { + var holder = new FlowLayoutPanel + { + AutoSize = true, + FlowDirection = FlowDirection.LeftToRight, + WrapContents = false, + Margin = new(3), + }; + + var swatch = new Button + { + Width = 44, + Height = 24, + BackColor = Opaque(get()), + FlatStyle = FlatStyle.Flat, + Margin = new(0, 0, 6, 0), + }; + swatch.Click += (_, _) => + { + using var dialog = new ColorDialog { Color = Opaque(get()), FullOpen = true }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + var updated = get().WithRgbOf(dialog.Color); + set(updated); + swatch.BackColor = Opaque(updated); + if (affectsPreview) + { + RaiseChanged(); + } + } + }; + holder.Controls.Add(swatch); + + if (withAlpha) + { + var alpha = new TrackBar + { + Minimum = 0, + Maximum = 255, + Value = get().A, + Width = 120, + Height = 26, + TickStyle = TickStyle.None, + }; + alpha.ValueChanged += (_, _) => + { + set(get() with { A = (byte) alpha.Value }); + if (affectsPreview) + { + RaiseChanged(); + } + }; + holder.Controls.Add(alpha); + } + + Row(label, holder); + } + + // A WinForms button can't render alpha, so the swatch shows the opaque RGB; the alpha slider conveys + // transparency for the colours that carry it. + static Color Opaque(Rgba color) => Color.FromArgb(255, color.R, color.G, color.B); +} diff --git a/src/GeoConvert.App/Ui/SplitLayout.cs b/src/GeoConvert.App/Ui/SplitLayout.cs new file mode 100644 index 0000000..f56b908 --- /dev/null +++ b/src/GeoConvert.App/Ui/SplitLayout.cs @@ -0,0 +1,28 @@ +namespace GeoConvert.App; + +static class SplitLayout +{ + /// + /// Sizes a dock-filled so Panel2 ends up about + /// pixels wide, clamping the splitter into its valid range. Call this once the container has its real + /// size (e.g. from ) — setting + /// while the container is still at its tiny construction-time default throws, because the default + /// splitter distance then sits outside [Panel1MinSize, Width - Panel2MinSize]. + /// + public static void ConfigureSplit(SplitContainer split, int panel2Width) + { + if (split.Width <= 0) + { + return; + } + + split.Panel2MinSize = Math.Min(panel2Width, Math.Max(80, split.Width / 3)); + var max = split.Width - split.Panel2MinSize; + if (max < split.Panel1MinSize) + { + return; + } + + split.SplitterDistance = Math.Clamp(split.Width - panel2Width, split.Panel1MinSize, max); + } +} diff --git a/src/GeoConvert.App/nuget-readme.md b/src/GeoConvert.App/nuget-readme.md new file mode 100644 index 0000000..848d328 --- /dev/null +++ b/src/GeoConvert.App/nuget-readme.md @@ -0,0 +1,46 @@ +# GeoConvert.App + +A Windows desktop app — and .NET tool — that converts maps between geospatial formats, renders them to +PNG/SVG, and diffs two maps. It puts a GUI on top of [GeoConvert](https://www.nuget.org/packages/GeoConvert/) +and grows out of the same feature set as the GeoConvert Blazor sample. + +## Install + +``` +dotnet tool install -g GeoConvert.App +``` + +This installs the `geoconvert-app` command (Windows, .NET 10 or later). + +## The app + +Run `geoconvert-app` (or open a map file with it) to launch the window: + +- **Open** a GeoJSON, TopoJSON, Shapefile, FlatGeobuf, KML, KMZ, GPX, WKT, WKB, CSV or GeoParquet file + and see a live preview. +- **Convert** to any supported format. For PNG/SVG output the full render options are exposed — + projection, resolution, padding, strokes, point radius, stroke auto-scale, min-feature culling, + labels (with halo / knockout), colours and ocean fill — plus the PNG renderer backend, PNG + compression, SVG simplify tolerance, KMZ deflate level and GeoParquet codec. +- **Simplify** geometry as an optional pre-pass (Douglas–Peucker or Visvalingam, with a + topology-preserving mode for shared borders). +- **Compare maps** (Tools ▸ Compare maps…) — an overlay of the two maps in distinct colours, or a + side-by-side at a shared extent, alongside a structural summary. + +### First run + +On first launch the app offers to bind the supported map file types to itself (per-user, no admin), so +double-clicking a map opens it here. This can be changed any time from Tools ▸ Associate / Remove file +associations, or the `associate` / `unassociate` commands. + +## Command line + +The diff is scriptable headlessly: + +``` +geoconvert-app diff before.geojson after.geojson changes.png +geoconvert-app diff a.kml b.kml diff.png --mode side-by-side --size 1600 --projection lambert +``` + +With an output path the diff image is written and a summary printed; without one the comparison opens in +the window. Other commands: `associate`, `unassociate`, `settings`, `--list`, `--help`. diff --git a/src/GeoConvert.slnx b/src/GeoConvert.slnx index 005cef3..b3c8c9b 100644 --- a/src/GeoConvert.slnx +++ b/src/GeoConvert.slnx @@ -1,5 +1,6 @@ + From fc5349aa36629babf5d7123eb2699f87d33479ac Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 26 Jun 2026 17:59:39 +1000 Subject: [PATCH 02/10] . --- readme.md | 479 ++++++++++++++++++++++- src/GeoConvert.App/Cli.cs | 8 +- src/GeoConvert.App/ConversionService.cs | 1 + src/GeoConvert.App/GeoConvert.App.csproj | 25 +- src/GeoConvert.App/GlobalUsings.cs | 1 + src/GeoConvert.App/OptionChoices.cs | 1 + src/GeoConvert.App/RendererBackend.cs | 3 + src/GeoConvert.Skia/SkiaSurface.cs | 41 +- 8 files changed, 520 insertions(+), 39 deletions(-) diff --git a/readme.md b/readme.md index 9085b14..a823cd6 100644 --- a/readme.md +++ b/readme.md @@ -52,7 +52,14 @@ Convert a file to another format (both formats inferred from their extensions): GeoConverter.Convert("cities.geojson", "cities.kml"); GeoConverter.Convert("roads.shp", "roads.fgb"); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Formats are inferred from the file extensions. +GeoConverter.Convert("cities.geojson", "cities.kml"); +GeoConverter.Convert("roads.shp", "roads.fgb"); +``` +snippet source | anchor Read into the common feature model, then write a different format: @@ -74,7 +81,24 @@ foreach (var feature in collection) // Write it back out as a different format. GeoConverter.Write(collection, "roads.fgb"); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Read any supported format into the common feature model. +var collection = GeoConverter.Read("roads.shp"); + +foreach (var feature in collection) +{ + if (feature.Properties.TryGetValue("name", out var name)) + { + Console.WriteLine(name); + } +} + +// Write it back out as a different format. +GeoConverter.Write(collection, "roads.fgb"); +``` +snippet source | anchor Build a collection in memory and serialize it: @@ -94,7 +118,22 @@ var collection = new FeatureCollection var geoJson = GeoJson.WriteString(collection); ``` -snippet source | anchor +snippet source | anchor + +```cs +var collection = new FeatureCollection +{ + new Feature( + new Point(new(151.21, -33.87)), + new Dictionary + { + ["name"] = "Sydney" + }), +}; + +var geoJson = GeoJson.WriteString(collection); +``` +snippet source | anchor @@ -135,7 +174,36 @@ var countries = GeoConverter.Read("countries.geojson"); var topo = Simplifier.SimplifyTopology(countries, 0.05); GeoConverter.Write(topo, "countries-thin.fgb"); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Reduce vertex count before writing — the highest-leverage way to shrink dense vector data. +// Simplifier.Simplify returns a NEW collection (the input is left untouched), preserving layer +// names, properties, feature ids and structure; only line and polygon-ring vertices are thinned. +var collection = GeoConverter.Read("coastline.geojson"); + +// Douglas–Peucker (the default): tolerance is a perpendicular distance in coordinate units +// (degrees for WGS84). Vertices within that distance of the retained line are dropped. +var coarse = Simplifier.Simplify(collection, 0.01); +GeoConverter.Write(coarse, "coastline.topojson"); + +// Visvalingam–Whyatt: tolerance is an effective triangle area (degrees²) — tends to give a +// smoother, more evenly generalised outline. Points pass through untouched; polygon rings stay +// closed and never collapse below a triangle, so the result is always valid. +var smooth = Simplifier.Simplify(collection, 0.0001, SimplifyMethod.Visvalingam); +GeoConverter.Write(smooth, "coastline-vw.geojson"); + +// SimplifyTopology: same algorithms, but adjacent polygons that share a border get that +// border simplified once — so the two sides stay seamlessly joined. The plain overload thins +// each ring independently; two countries' shared edges then get different chord choices and +// no longer line up, leaving hairline gaps (visible as white stripes between countries) or +// alpha-stacked overlaps when the fill is translucent. Pick this for topologically +// consistent datasets like Natural Earth admin layers where shared boundaries matter. +var countries = GeoConverter.Read("countries.geojson"); +var topo = Simplifier.SimplifyTopology(countries, 0.05); +GeoConverter.Write(topo, "countries-thin.fgb"); +``` +snippet source | anchor @@ -163,7 +231,27 @@ GeoConverter.Convert("countries.geojson", "countries.fgb", progress); var features = GeoConverter.Read("countries.geojson"); MapRenderer.RenderPng(features, "world.png", new() { Progress = progress }); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Read, Write and Convert each take an optional IProgress. Convert reports the +// read half under ProgressPhase.Reading and the write half under ProgressPhase.Writing. Every +// report carries a feature count and a byte count; ConvertProgress.Fraction picks whichever +// total is known (features when writing, bytes when reading a seekable source) and returns null +// when neither is — an honest "indeterminate" rather than a fabricated percentage. +var progress = new Progress(report => +{ + var percent = report.Fraction is { } fraction ? $"{fraction:P0}" : "?"; + Console.WriteLine($"{report.Phase}: {report.Features} features ({percent})"); +}); + +GeoConverter.Convert("countries.geojson", "countries.fgb", progress); + +// PNG rendering reports through RenderOptions.Progress (one report per feature rasterised). +var features = GeoConverter.Read("countries.geojson"); +MapRenderer.RenderPng(features, "world.png", new() { Progress = progress }); +``` +snippet source | anchor @@ -186,7 +274,22 @@ var options = new RenderOptions MapRenderer.RenderPng(features, "europe.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var features = GeoConverter.Read("countries.geojson"); + +// Render a specific bounding box (min lon, min lat, max lon, max lat) to a PNG. +var options = new RenderOptions +{ + Bounds = new Envelope(-10, 35, 30, 60), + Width = 1200, + Height = 900, +}; + +MapRenderer.RenderPng(features, "europe.png", options); +``` +snippet source | anchor `RenderOptions` controls the extent (`Bounds`), pixel `Width`/`Height` (height is derived from the aspect ratio when left at 0), `Padding`, and the `Background`/`Stroke`/`Fill` colors. From the command line, output a `.png` and pass `--bbox` and `--size`: @@ -244,7 +347,29 @@ File.WriteAllText("europe.svg", markup); // Or write straight to a file / stream. MapRenderer.RenderSvg(features, "europe.svg", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var features = GeoConverter.Read("countries.geojson"); + +// SVG is a vector export: same RenderOptions as PNG (bounds, size, projection, colours, +// labels), but geometry becomes // and labels become native +// , so the output scales crisply at any zoom. RenderSvg returns the markup as a +// string; the path/stream overloads write it out directly. +var options = new RenderOptions +{ + Bounds = new Envelope(-10, 35, 30, 60), + Width = 1200, + Height = 900, +}; + +var markup = MapRenderer.RenderSvg(features, options); +File.WriteAllText("europe.svg", markup); + +// Or write straight to a file / stream. +MapRenderer.RenderSvg(features, "europe.svg", options); +``` +snippet source | anchor The same `RenderOptions` knobs apply (the format-specific `RenderOptions.Png` and `RenderOptions.Svg` sub-options are the exception — `Png` is ignored for SVG output and `Svg` for PNG). Because labels are emitted as native ``, their glyph shapes depend on the fonts available to the viewer; placement and collision are identical to the PNG renderer (which reserves boxes from the hand-rolled stroke font's metrics). From the command line, output a `.svg` and pass `--bbox`/`--size` exactly as for PNG: @@ -272,7 +397,22 @@ var options = new RenderOptions MapRenderer.RenderSvg(features, "world.svg", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var options = new RenderOptions +{ + Bounds = MapRenderer.WebMercatorWorldBounds, + Width = 1024, + // Half a pixel: invisible at this render size, but collapses the dense sub-pixel + // detail that otherwise bloats the file. A world borders layer drops from ~109 MB + // to ~16 MB. The matching PNG render is unaffected (Svg options are SVG-only). + Svg = new() { SimplifyTolerance = 0.5 }, +}; + +MapRenderer.RenderSvg(features, "world.svg", options); +``` +snippet source | anchor Because the pass runs after projection and the `MinFeaturePixels` selection, it only thins geometry that is actually being drawn; raising the tolerance further yields diminishing returns once the per-vertex spacing drops below a pixel. @@ -309,7 +449,23 @@ var options = new RenderOptions MapRenderer.RenderPng(features, "world.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var features = GeoConverter.Read("countries.geojson"); + +// Web Mercator matches the layout of standard web tile maps. Pair it with +// MapRenderer.WebMercatorWorldBounds for the canonical 1:1 square world view; latitude is +// clamped to ±85.0511° (the cutoff every tile provider uses). +var options = new RenderOptions +{ + Bounds = MapRenderer.WebMercatorWorldBounds, + Projection = MapProjection.WebMercator, +}; + +MapRenderer.RenderPng(features, "world.png", options); +``` +snippet source | anchor From the command line, pass `--projection`: @@ -335,7 +491,22 @@ var options = new RenderOptions MapRenderer.RenderPng(features, "states.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var features = GeoConverter.Read("states.geojson"); + +// Lambert Conformal Conic with standard parallels picked from the data bounds — the textbook +// choice for state/country-scale maps. Conformal and low-distortion across a regional extent, +// so this avoids both plate-carrée's high-latitude squish and Web Mercator's pole stretch. +var options = new RenderOptions +{ + Projection = MapProjection.Lambert, +}; + +MapRenderer.RenderPng(features, "states.png", options); +``` +snippet source | anchor ``` @@ -364,7 +535,27 @@ var options = new RenderOptions MapRenderer.RenderPng(features, "world.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +var features = GeoConverter.Read("countries.geojson"); + +// Goode's Homolosine (interrupted into 2 northern and 4 southern lobes along ocean +// meridians, the conventional layout): equal-area, so areas at high latitudes don't blow +// up like they do under Web Mercator or compress like they do under plate carrée, and the +// lobe interrupts keep distortion low on every continent. This is what MapProjection.Auto +// picks for a world map, so the explicit Projection assignment is only needed when you +// want the specific extent — leaving it off and letting Auto pick produces the same result. +// Ocean fills each lobe under the continents so the projection's lobed shape (and the +// inter-lobe gaps) reads clearly. +var options = new RenderOptions +{ + Projection = MapProjection.Goode, +}; + +MapRenderer.RenderPng(features, "world.png", options); +``` +snippet source | anchor ``` @@ -423,7 +614,52 @@ var options = new RenderOptions MapRenderer.RenderPng(basemap, "europe.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +// A FeatureCollection with named sub-layers — the renderer walks the tree depth-first, so a +// parent layer paints under its children. RenderOptions.LayerStyle picks per-layer colors; +// any property left null falls back to the defaults on RenderOptions. +var basemap = new FeatureCollection +{ + Name = "basemap" +}; +basemap.Add( + new Feature( + new Polygon( + [ + [new(-10, 35), new(30, 35), new(30, 60), new(-10, 60), new(-10, 35)], + ]))); + +var roads = new FeatureCollection +{ + Name = "roads" +}; +roads.Add(new Feature(new LineString([new(0, 40), new(20, 55)]))); +basemap.Children.Add(roads); + +var options = new RenderOptions +{ + Bounds = new Envelope(-10, 35, 30, 60), + LayerStyle = layer => layer.Name switch + { + "basemap" => new() + { + Fill = new(230, 230, 230), + Stroke = new(180, 180, 180), + }, + "roads" => new() + { + Stroke = new(200, 60, 60), + StrokeWidth = 3, + }, + _ => null, + }, +}; + +MapRenderer.RenderPng(basemap, "europe.png", options); +``` +snippet source | anchor When the layers come from independent sources (typically a basemap file plus an overlay file), pass the collections as a list — they render in order, first under, last on top. Each `FeatureCollection` is a top-level layer for `RenderOptions.LayerStyle`, and the rendered extent defaults to the union of every input's bounds: @@ -461,7 +697,40 @@ var options = new RenderOptions MapRenderer.RenderPng([basemap, roads], "stacked.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +// When the layers come from independent sources (a basemap file plus an overlay file, say), +// pass them as a list — they render in order, first under, last on top. Each FeatureCollection +// is a top-level layer for RenderOptions.LayerStyle, so giving each one a Name is enough to +// style them distinctly. When Bounds is null the rendered extent is the union of every input. +var basemap = GeoConverter.Read("countries.geojson"); +basemap.Name = "basemap"; + +var roads = GeoConverter.Read("roads.shp"); +roads.Name = "roads"; + +var options = new RenderOptions +{ + LayerStyle = layer => layer.Name switch + { + "basemap" => new() + { + Fill = new(230, 230, 230), + Stroke = new(180, 180, 180), + }, + "roads" => new() + { + Stroke = new(200, 60, 60), + StrokeWidth = 3, + }, + _ => null, + }, +}; + +MapRenderer.RenderPng([basemap, roads], "stacked.png", options); +``` +snippet source | anchor @@ -507,7 +776,34 @@ var options = new RenderOptions }; MapRenderer.RenderPng(features, "europe-halo.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Halo treatment: every glyph stroke is first drawn in the halo colour at a slightly +// wider stroke, so the foreground text reads against busy fills as if outlined. The +// halo extends 2 px past the foreground stroke on every side; that's enough to lift +// text off most country-fill colours but a thin border line can still bleed through +// the ring on dense political maps. Default halo is a semi-transparent white, which +// works for dark text on light backgrounds out of the box; pass null to disable. +var features = GeoConverter.Read("countries.geojson"); +var options = new RenderOptions +{ + Bounds = new(-12, 35, 32, 60), + Width = 800, + Projection = MapProjection.Lambert, + Background = new(245, 245, 245), + Fill = new(220, 220, 210), + Stroke = new(120, 120, 120), + StrokeWidth = 1, + Label = feature => + feature.Properties.TryGetValue("NAME", out var value) ? value as string : null, + LabelSize = 14, + LabelColor = new(30, 30, 30), + LabelHalo = new(255, 255, 255, 220), +}; +MapRenderer.RenderPng(features, "europe-halo.png", options); +``` +snippet source | anchor @@ -544,7 +840,35 @@ var options = new RenderOptions }; MapRenderer.RenderPng(features, "europe-knockout.png", options); ``` -snippet source | anchor +snippet source | anchor + +```cs +// Knockout treatment: before the halo and text strokes, a solid rect of the knockout +// colour is painted over the label's bounding box. The geometry underneath is fully +// erased (opaque colour) or dimmed (semi-transparent), so country borders don't bleed +// through the way they can with a halo ring. Typically set to match Background for a +// clean masked look; pair with LabelHalo = null for a flat rectangle, or leave the +// halo on for a knockout-rect with an outline around the text. +var features = GeoConverter.Read("countries.geojson"); +var options = new RenderOptions +{ + Bounds = new(-12, 35, 32, 60), + Width = 800, + Projection = MapProjection.Lambert, + Background = new(245, 245, 245), + Fill = new(220, 220, 210), + Stroke = new(120, 120, 120), + StrokeWidth = 1, + Label = feature => + feature.Properties.TryGetValue("NAME", out var value) ? value as string : null, + LabelSize = 14, + LabelColor = new(30, 30, 30), + LabelHalo = null, + LabelKnockout = new(245, 245, 245), +}; +MapRenderer.RenderPng(features, "europe-knockout.png", options); +``` +snippet source | anchor @@ -610,7 +934,66 @@ options.LabelPriority = feature => return 0; }; ``` -snippet source | anchor +snippet source | anchor + +```cs +// Label every feature with its "name" property. Polygon/line labels sit on the +// centroid / arclength midpoint; point labels walk Imhof's 8-position candidate ring +// around the dot (NE → NW → SE → SW → E → W → N → S) so the label doesn't paint on +// top of the point marker. Collision and off-canvas rejection drop labels silently. +// The single-stroke vector font handles printable ASCII plus the Latin diacritics that +// decompose to an ASCII base + combining mark (grave, acute, circumflex, tilde, +// diaeresis, ring, caron, cedilla); ligatures like ß, æ, ø and the non-Latin blocks +// render as '?'. LabelSize is the cap height in pixels — the font scales continuously, +// so any positive value works (12–16 for 2k canvases, 20+ for high-res). +var features = GeoConverter.Read("cities.geojson"); + +var options = new RenderOptions +{ + Label = feature => + feature.Properties.TryGetValue("name", out var value) ? value as string : null, + LabelSize = 18, + LabelColor = new(20, 20, 20), + LabelHalo = new(255, 255, 255, 220), +}; + +MapRenderer.RenderPng(features, "cities.png", options); + +// Per-layer override: a child layer can carry its own label callback (or scale/color/halo) +// independent of the options-wide default. Setting Label = _ => null on a LayerStyle +// suppresses labelling for that layer. +options.LayerStyle = layer => layer.Name == "annotations" + ? new LayerStyle { Label = feature => feature.Properties["text"] as string } + : null; + +// By default, labels are placed largest-feature-first so when two collide the bigger +// polygon's name wins. Override LabelPriority to drive collision order from anything +// else — a feature property like population, or an external lookup captured in the +// closure. Without this, Natural Earth's "Ireland" would beat "United Kingdom" on file +// order; with population priority, UK (67M) outranks Ireland (5M) and gets the spot. +options.LabelPriority = feature => + feature.Properties.TryGetValue("POP_EST", out var p) ? Convert.ToDouble(p) : 0; + +// Or look priorities up in a separate table — useful when the data and the importance +// ranking live in different files. +var populations = new Dictionary +{ + ["United Kingdom"] = 67_000_000, + ["Ireland"] = 5_000_000, +}; +options.LabelPriority = feature => +{ + if (feature.Properties.TryGetValue("NAME", out var name) && + name is string n && + populations.TryGetValue(n, out var pop)) + { + return pop; + } + + return 0; +}; +``` +snippet source | anchor @@ -652,7 +1035,33 @@ using (var parquet = File.Create("world.parquet")) GeoParquet.Write(parquet, features, ParquetCompression.Gzip, CompressionLevel.SmallestSize); } ``` -snippet source | anchor +snippet source | anchor + +```cs +// PNG: the deflate level for the IDAT chunk is exposed on RenderOptions.Png. +MapRenderer.RenderPng( + features, + "world.png", + new() + { + Bounds = MapRenderer.WebMercatorWorldBounds, + Projection = MapProjection.WebMercator, + Png = new() { Compression = CompressionLevel.Fastest }, + }); + +// KMZ: the doc.kml zip entry's compression level is an optional Write argument. +using (var kmz = File.Create("world.kmz")) +{ + Kmz.Write(kmz, features, CompressionLevel.SmallestSize); +} + +// GeoParquet: pick the codec (default Snappy); CompressionLevel only applies to Gzip. +using (var parquet = File.Create("world.parquet")) +{ + GeoParquet.Write(parquet, features, ParquetCompression.Gzip, CompressionLevel.SmallestSize); +} +``` +snippet source | anchor @@ -741,7 +1150,41 @@ foreach (var feature in root) Console.WriteLine(feature.Geometry); } ``` -snippet source | anchor +snippet source | anchor + +```cs +// A FeatureCollection can hold nested child layers, each with its own Name. Formats with a +// native layer concept (KML folders, TopoJSON objects, KMZ documents, GPX wpt/rte/trk, +// Shapefile bundle directories) round-trip this structure; everything else flattens via the +// recursive enumerator. +var cities = new FeatureCollection +{ + Name = "cities" +}; +cities.Add(new Feature(new Point(new(151.21, -33.87)))); + +var roads = new FeatureCollection +{ + Name = "roads" +}; +roads.Add(new Feature(new LineString([new(151.20, -33.86), new(151.22, -33.88)]))); + +var root = new FeatureCollection +{ + Name = "sydney" +}; +root.Children.Add(cities); +root.Children.Add(roads); + +GeoConverter.Write(root, "sydney.kml"); // emits … + +// Single-layer formats just flatten — iterating any collection always yields every feature. +foreach (var feature in root) +{ + Console.WriteLine(feature.Geometry); +} +``` +snippet source | anchor diff --git a/src/GeoConvert.App/Cli.cs b/src/GeoConvert.App/Cli.cs index 55c4b68..920ead6 100644 --- a/src/GeoConvert.App/Cli.cs +++ b/src/GeoConvert.App/Cli.cs @@ -133,7 +133,7 @@ public static int ParseDiff(string[] args, out DiffRequest? request, TextWriter if (!TryParseRenderer(rendererText, out var renderer)) { - error.WriteLine("--renderer must be 'builtin' or 'imagesharp'."); + error.WriteLine("--renderer must be 'builtin', 'skia', or 'imagesharp'."); return 2; } @@ -366,6 +366,10 @@ static bool TryParseRenderer(string text, out RendererBackend renderer) case "default": renderer = RendererBackend.BuiltIn; return true; + case "skia": + case "skiasharp": + renderer = RendererBackend.Skia; + return true; case "imagesharp": case "image-sharp": case "sixlabors": @@ -438,7 +442,7 @@ side by side at a shared extent. --size WIDTH[xHEIGHT] Image size in pixels. --max-dimension Cap the longer edge at this many pixels (fit-to-box). --projection auto | plate-carree | web-mercator | lambert | goode. - --renderer builtin (default) or imagesharp (PNG only). + --renderer builtin (default), skia or imagesharp (PNG only). Examples: geoconvert-app world.geojson diff --git a/src/GeoConvert.App/ConversionService.cs b/src/GeoConvert.App/ConversionService.cs index a60f6e6..c0ff5a6 100644 --- a/src/GeoConvert.App/ConversionService.cs +++ b/src/GeoConvert.App/ConversionService.cs @@ -108,6 +108,7 @@ public static byte[] RenderPng(FeatureCollection features, RenderSettings render var options = RenderOptionsFor(render, progress); return render.Renderer switch { + RendererBackend.Skia => SkiaRenderer.RenderPng(features, options), RendererBackend.ImageSharp => ImageSharpRenderer.RenderPng(features, options), _ => MapRenderer.RenderPng(features, options), }; diff --git a/src/GeoConvert.App/GeoConvert.App.csproj b/src/GeoConvert.App/GeoConvert.App.csproj index d15908c..9c1f986 100644 --- a/src/GeoConvert.App/GeoConvert.App.csproj +++ b/src/GeoConvert.App/GeoConvert.App.csproj @@ -6,11 +6,9 @@ Target hacks below let the project keep a *plain* TFM (net10.0 / net11.0, no '-windows' suffix) so it packs as an ordinary framework-dependent dotnet tool. Unlike MsOfficeDiff this stays inside GeoConvert's no-third-party-dependency discipline: argument parsing is hand-rolled (see Cli.cs) and - settings use System.Text.Json, so no CliFx / Serilog / DI packages are pulled in. The ImageSharp - render backend is referenced for parity with the geoconvert CLI's renderer flag; it (and only it) - carries a third-party dependency into the tool, exactly as the CLI already does. The Skia backend is - deliberately not referenced: it currently fails to build on main (obsolete SkiaSharp APIs vs - TreatWarningsAsErrors), so wiring it in would break this app's build through no fault of its own. + settings use System.Text.Json, so no CliFx / Serilog / DI packages are pulled in. The Skia and + ImageSharp render backends are referenced for parity with the geoconvert CLI's renderer flag; those + (and only those) carry third-party dependencies into the tool, exactly as the CLI already does. --> Exe @@ -47,8 +45,25 @@ + + + + + + + + + diff --git a/src/GeoConvert.App/GlobalUsings.cs b/src/GeoConvert.App/GlobalUsings.cs index c11d14a..6b0ba69 100644 --- a/src/GeoConvert.App/GlobalUsings.cs +++ b/src/GeoConvert.App/GlobalUsings.cs @@ -6,4 +6,5 @@ global using System.Text.Json.Serialization; global using GeoConvert; global using GeoConvert.ImageSharp; +global using GeoConvert.Skia; global using Microsoft.Win32; diff --git a/src/GeoConvert.App/OptionChoices.cs b/src/GeoConvert.App/OptionChoices.cs index 0c2348e..056ce01 100644 --- a/src/GeoConvert.App/OptionChoices.cs +++ b/src/GeoConvert.App/OptionChoices.cs @@ -19,6 +19,7 @@ public static readonly (MapProjection Value, string Label)[] Projections = public static readonly (RendererBackend Value, string Label)[] Renderers = [ (RendererBackend.BuiltIn, "Built-in (dependency-free)"), + (RendererBackend.Skia, "SkiaSharp"), (RendererBackend.ImageSharp, "ImageSharp"), ]; diff --git a/src/GeoConvert.App/RendererBackend.cs b/src/GeoConvert.App/RendererBackend.cs index 8bd9a3e..ebbc661 100644 --- a/src/GeoConvert.App/RendererBackend.cs +++ b/src/GeoConvert.App/RendererBackend.cs @@ -9,6 +9,9 @@ public enum RendererBackend /// GeoConvert's dependency-free software rasterizer (). BuiltIn, + /// SkiaSharp-backed rasterizer (); labels use Skia's default typeface. + Skia, + /// SixLabors.ImageSharp-backed rasterizer (); labels use a system font. ImageSharp, } diff --git a/src/GeoConvert.Skia/SkiaSurface.cs b/src/GeoConvert.Skia/SkiaSurface.cs index fd080df..e574a4a 100644 --- a/src/GeoConvert.Skia/SkiaSurface.cs +++ b/src/GeoConvert.Skia/SkiaSurface.cs @@ -49,13 +49,8 @@ public void FillPolygon((double X, double Y)[][] rings, Rgba color) continue; } - path.MoveTo((float)ring[0].X, (float)ring[0].Y); - for (var i = 1; i < ring.Length; i++) - { - path.LineTo((float)ring[i].X, (float)ring[i].Y); - } - - path.Close(); + // Each ring is a closed sub-path; even-odd fill makes interior rings cut holes. + AppendChain(path, ring, close: true); } using var paint = Fill(color); @@ -71,11 +66,8 @@ public void StrokePath(IReadOnlyList<(double X, double Y)> points, double width, } using var path = new SKPath(); - path.MoveTo((float)points[0].X, (float)points[0].Y); - for (var i = 1; i < points.Count; i++) - { - path.LineTo((float)points[i].X, (float)points[i].Y); - } + // Open polyline — no closing segment back to the start. + AppendChain(path, points, close: false); using var paint = new SKPaint { @@ -120,11 +112,11 @@ public void DrawText(string text, double leftX, double baselineY, double size, R StrokeWidth = Math.Max(1f, emSize / 6f), StrokeJoin = SKStrokeJoin.Round, }; - canvas.DrawText(text, (float)leftX, (float)baselineY, font, haloPaint); + canvas.DrawText(text, (float)leftX, (float)baselineY, SKTextAlign.Left, font, haloPaint); } using var paint = Fill(color); - canvas.DrawText(text, (float)leftX, (float)baselineY, font, paint); + canvas.DrawText(text, (float)leftX, (float)baselineY, SKTextAlign.Left, font, paint); } /// Encodes the painted bitmap as a PNG to . Skia chooses its own @@ -137,6 +129,27 @@ public void Encode(Stream stream, CompressionLevel compression) data.SaveTo(stream); } + // SkiaSharp 4.148 marks the imperative SKPath build methods (MoveTo/LineTo/Close) obsolete in + // favour of SKPathBuilder, but that type is not shipped in this package version — the imperative + // surface is the only available way to build a path. Keep the obsolete calls confined here and + // suppress CS0618 (warnings are errors) at the single point that touches them. Callers guard + // against empty input, so points[0] is always present. +#pragma warning disable CS0618 // SKPathBuilder (the suggested replacement) is absent from SkiaSharp 4.148.0. + static void AppendChain(SKPath path, IReadOnlyList<(double X, double Y)> points, bool close) + { + path.MoveTo((float)points[0].X, (float)points[0].Y); + for (var i = 1; i < points.Count; i++) + { + path.LineTo((float)points[i].X, (float)points[i].Y); + } + + if (close) + { + path.Close(); + } + } +#pragma warning restore CS0618 + static SKPaint Fill(Rgba color) => new() { From 738dfc02ac872d36b580c21fdc66c7f9b09aede8 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 09:03:18 +1000 Subject: [PATCH 03/10] . --- src/Directory.Build.props | 14 ++++++++++++++ src/GeoConvert.App/GeoConvert.App.csproj | 13 +++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fdb2abc..da3e265 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -16,4 +16,18 @@ + + + + + + diff --git a/src/GeoConvert.App/GeoConvert.App.csproj b/src/GeoConvert.App/GeoConvert.App.csproj index 9c1f986..6804242 100644 --- a/src/GeoConvert.App/GeoConvert.App.csproj +++ b/src/GeoConvert.App/GeoConvert.App.csproj @@ -46,16 +46,13 @@ - + - From c64097743fbd07d241c5771cb4d5f406f445b70f Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 10:32:55 +1000 Subject: [PATCH 04/10] . --- src/Directory.Packages.props | 1 + .../DiffRenderTests.Overlay.verified.png | Bin 0 -> 12779 bytes .../DiffRenderTests.SideBySide.verified.png | Bin 0 -> 14045 bytes src/GeoConvert.App.Tests/DiffRenderTests.cs | 22 +++++ .../DiffSummaryTests.Summarize.verified.txt | 19 ++++ src/GeoConvert.App.Tests/DiffSummaryTests.cs | 8 ++ .../FormsTests.DiffWindow.verified.png | Bin 0 -> 15627 bytes .../FormsTests.MainWindow.verified.png | Bin 0 -> 19627 bytes src/GeoConvert.App.Tests/FormsTests.cs | 26 ++++++ .../GeoConvert.App.Tests.csproj | 26 ++++++ src/GeoConvert.App.Tests/GlobalUsings.cs | 5 + src/GeoConvert.App.Tests/ModuleInitializer.cs | 22 +++++ .../OptionsPanelTests.Kml.verified.png | Bin 0 -> 8890 bytes .../OptionsPanelTests.Png.verified.png | Bin 0 -> 26572 bytes src/GeoConvert.App.Tests/OptionsPanelTests.cs | 24 +++++ src/GeoConvert.App.Tests/SampleMaps.cs | 41 +++++++++ src/GeoConvert.App.Tests/WinFormsSnapshot.cs | 85 +++++++++++++++++ src/GeoConvert.App/FileAssociations.cs | 26 +++--- src/GeoConvert.App/GeoConvert.App.csproj | 15 +++ src/GeoConvert.App/SampleMap.cs | 17 ++++ src/GeoConvert.App/Ui/Combos.cs | 14 +++ src/GeoConvert.App/Ui/DiffForm.cs | 10 +- src/GeoConvert.App/Ui/MainForm.cs | 86 ++++++++++++------ src/GeoConvert.App/Ui/OptionsPanel.cs | 9 +- .../Components/ExportOptionsTests.cs | 2 +- src/GeoConvert.Web/Pages/Index.razor.cs | 2 +- src/GeoConvert.Web/wwwroot/favicon.png | Bin 0 -> 13061 bytes src/GeoConvert.Web/wwwroot/index.html | 1 + src/GeoConvert.slnx | 1 + src/Tests/RenderBackendTests.cs | 2 +- src/Tests/SvgTests.cs | 4 +- 31 files changed, 416 insertions(+), 52 deletions(-) create mode 100644 src/GeoConvert.App.Tests/DiffRenderTests.Overlay.verified.png create mode 100644 src/GeoConvert.App.Tests/DiffRenderTests.SideBySide.verified.png create mode 100644 src/GeoConvert.App.Tests/DiffRenderTests.cs create mode 100644 src/GeoConvert.App.Tests/DiffSummaryTests.Summarize.verified.txt create mode 100644 src/GeoConvert.App.Tests/DiffSummaryTests.cs create mode 100644 src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png create mode 100644 src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png create mode 100644 src/GeoConvert.App.Tests/FormsTests.cs create mode 100644 src/GeoConvert.App.Tests/GeoConvert.App.Tests.csproj create mode 100644 src/GeoConvert.App.Tests/GlobalUsings.cs create mode 100644 src/GeoConvert.App.Tests/ModuleInitializer.cs create mode 100644 src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png create mode 100644 src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png create mode 100644 src/GeoConvert.App.Tests/OptionsPanelTests.cs create mode 100644 src/GeoConvert.App.Tests/SampleMaps.cs create mode 100644 src/GeoConvert.App.Tests/WinFormsSnapshot.cs create mode 100644 src/GeoConvert.App/SampleMap.cs create mode 100644 src/GeoConvert.Web/wwwroot/favicon.png diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b665296..7d4a828 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,6 +19,7 @@ + diff --git a/src/GeoConvert.App.Tests/DiffRenderTests.Overlay.verified.png b/src/GeoConvert.App.Tests/DiffRenderTests.Overlay.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbc493dca7248094b279c33c03843098f9811eb GIT binary patch literal 12779 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 f|L-3wfoRtA1nhI49EQ9|)H0kHC literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/DiffRenderTests.SideBySide.verified.png b/src/GeoConvert.App.Tests/DiffRenderTests.SideBySide.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..9340e4c92aa219294d6fe15b3f118b9e88e5dd71 GIT binary patch literal 14045 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 literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/DiffRenderTests.cs b/src/GeoConvert.App.Tests/DiffRenderTests.cs new file mode 100644 index 0000000..fced73b --- /dev/null +++ b/src/GeoConvert.App.Tests/DiffRenderTests.cs @@ -0,0 +1,22 @@ +namespace GeoConvert.App.Tests; + +public class DiffRenderTests +{ + // A small fixed pixel size keeps the snapshots compact and deterministic. + static RenderSettings Settings() => + new() { MaxDimension = 0, Width = 400, Height = 0 }; + + [Test] + public Task Overlay() => + VerifyDiff(DiffMode.Overlay); + + [Test] + public Task SideBySide() => + VerifyDiff(DiffMode.SideBySide); + + static Task VerifyDiff(DiffMode mode) + { + var png = MapDiff.Render(SampleMaps.A(), SampleMaps.B(), Settings(), mode, MapDiff.DefaultColorA, MapDiff.DefaultColorB); + return Verify(Images.DecodePng(png)).UseParameters(mode); + } +} diff --git a/src/GeoConvert.App.Tests/DiffSummaryTests.Summarize.verified.txt b/src/GeoConvert.App.Tests/DiffSummaryTests.Summarize.verified.txt new file mode 100644 index 0000000..3c08392 --- /dev/null +++ b/src/GeoConvert.App.Tests/DiffSummaryTests.Summarize.verified.txt @@ -0,0 +1,19 @@ +Map A: a.geojson + Features: 3 + Layers: 1 + Bounds: 0, 0 .. 20, 12 + Geometry: Polygon 2, LineString 1 + Properties: name, pop + +Map B: b.geojson + Features: 2 + Layers: 1 + Bounds: 1, 1 .. 16, 11 + Geometry: Polygon 1, Point 1 + Properties: iso, name, pop + +Differences: + Features: -1 + Geometry: LineString -1, Point +1, Polygon -1 + Properties only in A: (none) + Properties only in B: iso diff --git a/src/GeoConvert.App.Tests/DiffSummaryTests.cs b/src/GeoConvert.App.Tests/DiffSummaryTests.cs new file mode 100644 index 0000000..6da7db6 --- /dev/null +++ b/src/GeoConvert.App.Tests/DiffSummaryTests.cs @@ -0,0 +1,8 @@ +namespace GeoConvert.App.Tests; + +public class DiffSummaryTests +{ + [Test] + public Task Summarize() => + Verify(MapDiff.Summarize("a.geojson", SampleMaps.A(), "b.geojson", SampleMaps.B())); +} diff --git a/src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..afa6a7fcbf70d5ff808770ab848b4992d4ebb77d GIT binary patch literal 15627 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 literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..582b5ae0157b923b1a6455a9594673775e22fbcc GIT binary patch literal 19627 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 + // The whole main window: menu, the "no map loaded" bar, the (empty) preview and the fixed-width + // options column on the right. + Verify(WinFormsSnapshot.Render(() => new MainForm(SeededSettings(), null), 1000, 680)); + + [Test] + public Task DiffWindow() => + // The empty compare window: the two file pickers, the mode/projection/colour toolbar, and the + // (empty) preview / summary panes. + Verify(WinFormsSnapshot.Render(() => new DiffForm(), 1000, 680)); + + static SettingsManager SeededSettings() + { + // Pre-mark the first-run prompt as shown so the briefly-shown MainForm doesn't pop the (blocking) + // association MessageBox. A throwaway temp path keeps it away from the real user settings. + var manager = new SettingsManager(Path.Combine(Path.GetTempPath(), "GeoConvert.App.Tests", "settings.json")); + manager.Write(new() { AssociationsPrompted = true }); + return manager; + } +} diff --git a/src/GeoConvert.App.Tests/GeoConvert.App.Tests.csproj b/src/GeoConvert.App.Tests/GeoConvert.App.Tests.csproj new file mode 100644 index 0000000..92c9d55 --- /dev/null +++ b/src/GeoConvert.App.Tests/GeoConvert.App.Tests.csproj @@ -0,0 +1,26 @@ + + + + + net11.0-windows + Exe + true + false + true + + $(NoWarn);CA1416 + + + + + + + + + + + + diff --git a/src/GeoConvert.App.Tests/GlobalUsings.cs b/src/GeoConvert.App.Tests/GlobalUsings.cs new file mode 100644 index 0000000..66083f3 --- /dev/null +++ b/src/GeoConvert.App.Tests/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using System.Drawing.Imaging; +global using GeoConvert; +global using GeoConvert.App; +global using TUnit.Core; +global using static VerifyTUnit.Verifier; diff --git a/src/GeoConvert.App.Tests/ModuleInitializer.cs b/src/GeoConvert.App.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..5b6022d --- /dev/null +++ b/src/GeoConvert.App.Tests/ModuleInitializer.cs @@ -0,0 +1,22 @@ +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() + { + // Verify.WinForms snapshots WinForms UI as images. Its Control converter renders via a live + // message loop, which deadlocks headless, so the tests render each control to a Bitmap on an STA + // thread (see WinFormsSnapshot) and snapshot that. This converter writes a Bitmap out as a PNG so + // Verify treats it as an image; SSIM comparison then tolerates the small machine-dependent pixel + // differences inherent in rendering WinForms to a bitmap. + VerifyWinForms.Initialize(); + VerifierSettings.UseSsimForPng(); + VerifierSettings.RegisterFileConverter( + (bitmap, _) => + { + var stream = new MemoryStream(); + bitmap.Save(stream, ImageFormat.Png); + stream.Position = 0; + return new(null, "png", stream, null); + }); + } +} diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..04854772650ca385e6fd144b1683f9c7c3459f66 GIT binary patch literal 8890 zcmeHtXH-*L*X|Z+f(TYbx?rU!AfQM`Bpw6=6r@H)L`n!nf|P(!!A8kJ1*GL5O{7E# zB-DU%sG*3INJ0^TASFa82}wwDgZh5=+%evJhhx0oxcATfk)5oWwdY=$d(AnY=gEF_ z!PZJbY@Zka021dwXB+@vQ$GI{Eh@~n+)?fg=f7&oX_nT%i$E6w|H==h+C_rlS# zf(`|hYIT)n>m~Z2swYjp61#hBGyaZ4T#-I2)*LS>dgWWox0B0V2p_*2YrDU_bIos` z*lKCiX!(bA}{JK;x{iK19tDgZodsakJI+M;T? zu9yxl-FBu53DDh*8%S(eEOq>@1l2$TY@d^k*;6;>; z1~}PN;7(UVVJcess?sJP>DeC|hme3$Xz@S@KfKy$|LG8UKb$tE3mwNp@Jx!VQq~i2HJ1b9afU#BD6%eT+o0cupX9uQ}wT#Jj;! z{kZevXYFk7Me2m zrJPZk2ozOSer~1|`_rR?jNp>hzcVuERPr#cL!jnDTde_QdihGpMZZ&s`>OR(jB@5& z2|7a`ItC0o{_{)zPPHyb-3bDaH5+sP^I@;>4nPTBfp0s!7X$$Aai#*m8N_cMCelq` z6DZ$ruXGkVE$3WvG<;kbX^=Wz8rYd@GtB+);H6*AP-IN+>zfMRknZvG^!W>+3SlWuUD#B*k0A<)oitma=y*($xZn^YmON~=DHU{{Gxe0Wi1 zJ%zQ_QF8%>4empBcJpL{D`OU7Tt~H|4%l3Jh&kYM(|~IZ?WGrD3-zMm+PKl(&fE8e zLY(@iZQ+WqtzB=_g{&KZd4=pw2dY0OWbOkV2J_!iH9wz6LWk6sM_6U&3beOuLgb$p zYCBr(b^9iowt{9Pw3KBlh~-@#x@~h7JIfm765Mv%vO{uQt&X*h!V<5|&Nqgk6n0dA zbs*a~C+LWUBB&cnJC{4Wseq=&PJVQN+BwZ)mSYr%^}$BO^i3o7$B|PE->w+kSzcPW z^Bs%06DKri{hM2GS?*THZiP7-@WiBBF5vf2Mi)omq?OltEtTKTZXZ8sG#`&u>G{-( zk)3gIeC1)M$c7~#L%aQleTtV(ZVuYITq9jHY?`u6IZt!V6yG{tS&u%=u6R;HV-zdC z23X)j4)XyLs$kULe3$U~SxdL_$;;v8u}Q{EA<`=St&V%rVDEWq#1}0f=&?rC`fj?2 zgNKWdetYw% zUE*=*c#jPyG`p-?kncD63(Ko1ooIZv9ODdFxn3lwyfHztUV6K8WoFo7zBg;x4N==; z7Q>DZX;0~v;WPfy@+DX3X{lX=>!I+Eo+G%Ptuy3XcK%R-W}}{G~EdTwWfh%hND;dth(8>Mq0rlZ+*G9EDUk_M?C<&_d&sjqFJ*=-Hh?r z@%5E{JI`@A!(+MQ5UO(G02x?3`rOr4NqNg$msMQiRwe1(u6_~2!a>eIn%lVxX_ME# z-G4RhE!*Vrv*qw>0QRS60fb<{kKTtA6xi}f^SNtGjMAX@^6gjXUCzG}UyeKYiA01D z=nsr{U0SXjvJewhy;5i4#}w+3$k*G;lA0?jA-x?@t~9Nnm}D$fQaxMr@li_1J5(5P z=}DF>Xs&Cj0<#1Bj^1r&qdEC0o_}$&2jN&q;6^%IuHNcWeC+=}6Pn_(d$IuU3y*S{ zH*V3^rlx4VC(k+7S#>jFdsZrAHjcQF#5Vpd<6(PpS$WIB+sRdrtX7}`Hogo2Oo9C^Y%+< zlLjADO_ye#h|QHB)Ax)F1eb_>jODSxDha}V#)tuBB=Wv8vh;#fZf@W*AssR0Hl~k~ z#U0QO8|l=^KOm}_+;DGiV~E?~*F{we*J}9e#+ikjMUNZR zk4%>|Xqml<50Jx>JA*${xei^R0u^Ml@sheuh9#FtYnSJT0x>G5}F-?s-g z<7{f=-8&eup#CihqFZOQn{5OgP7^ToP-Fc-9sbI9fadEDB1wRDui=1ZD) z9JCBNBp8yP6c*M+n0e>)tN++tSUm-9Tn?1z%rF*kEA^%*Mg*)Ns7elgRZ7<}rrygS0w zyfcPP#Zk!6#wv>B0NhUouJD84IIlc;e`$^VCPP9B#-oNx2S6V!Uh>K=B`ar z=FyGVMaSglq3^x#)u8{lhHkv@S41fK7-F{J5X0C1=_fApG-t$6z0^=ncUmXAP`_2-mzj#R!iEq1idq#R}k%) z`#xIyJL2^JgiYPQ9?hIi>$Xya57hIxY|lAvuL*C&Rk|AUleIP%)%v9*5Cw zXTq6U9StAXMMW>2ll82S_+0K#JT15*<*%OTi=Wzo5c}MM)HDbX{)_#Gow60Sd+6$k z-86D(BdN@HY(S`=o~1xdu%CId0h#!24vD0&B7>=F9Zz|kLCn=zpV|<&?<)N7=)@&| zcPWsgJ>1E1`S5FsuSVKqHH$BukQTnSfhB_p=wM%^;r`;93Z8SIT8!Y1H=|tFm_%t| z2S$|3p6*F|y{cNCi*C#92sDb(%x7M+6<`+z;6Tw2Fz|Me%D+WZ zoT~MG(t;_lp)0$f_UVZjVXAr_3Mv>6bs4^Kx-8r1RBzIhOk$0M&MS9n=7e^YZ_XCY zeMR3{86$fJd0`*DlbhZS*wTAt{qX_mMI7K^f zdIsjrW>wM;KGXx?h+UtsSevl{;Pc^)5ZZ221)55JqtN~+!|PL~A@$Qt2U#q1U2;2+ z59I?~;SIT9V-J`qZ8D|&_{BRxIcR?*>hf{;rTbfVz-xI8A58|EMhn*~?|FkIm9dZ& zdtRx5bK@o@;~%$CRFyivueA5}({bn&yINibv@{7@Nv69jMFhd37SRu+IHEBJgn_?( zH}JF&;=1trwd^xVb`A6xu>E1xmDw7*F3+V^sR|?N)<8qd+zeR}DF%EW-q@GHN?~cW zo;yITbOEsBgh$(r7+s?*A@Z*Yx285 z{Ju54UefYW5@^~Z$QNVC0ThOzF*3H6z2v%L^9$l+_*wDQ#*R)hL-phkGim zrcHEN!z_^PVNT~uFnJRnbHZdk3`zpkIPa?URa^cA3We>ON^_iiEa91a#b8nYX zp8!DK#)NI6s(@+Nk0J_O=hINzja~eIb66z&aZRG!+}=ojT&zS3pAo%jwTeR3Z>NJH zITcl2ZMKNX<4#3#Dx{c`1)O&|9I7i(7mC&)MVB>Htsmu|{r4(RM}g#$B2IW~%z&p~ z1y^Sc(@R>kv6orfLo5}0N$~;Bw&;nC(=>+TvuwvV;PDH=kK3sMVHA3A0DJJ7jGH<~ zwB}{955xkiRzwUco6o?d23Xh@(W+fz%84QlWtx&-)0U=|_;P+B0Zw^=bhY&qlhZs^ zJv2+wAiRt)>@0+>m}#&}2RIdWUv^w~aU;_xHXKI+0g}u%T@4Os?kg%Pf_Me^F>B!Y z5U8A^H>1VRu_rvidm;uy4o|chm$Y=;bxm%rqI5E)l#_mwQ^K;YL2xg#uU${JS3vc9 z9c=8VUmXL~}yvZzna6K&95tz4Xmz7zHKTkLFXR*4U8A*Jp1{#Ho$Q!*{>J3ajJ zcz@faWS5Z*st_5hCv<`XQ~<%@4>!tgSU9* z1IJ9@7;gq@zQhhCNa)+rW(^LcM(PhGNkOB=D!i(AO~ zdb7>7^^{SzrLE7?mx(ROkG>{D!Gi1}@U@Is_jUFk{%vn_qR0qesBE@P+kFP3i5|T? z&AJvz*V6l%{5H0(HC>)Teh+uYGcL*lHB$gh@kttrhI%DL0JY04EgQOUB7?(mk* zrDCctI;XOH@$M#-inVWt*`@)#wc{g)=+iRJnO4-cpX$OMZ^zF4CoZq2f8qqw#IwIL z7S=4-M+bUR;avHeLp&wRV^+_@>bdA~L3 zN$*C^d>2UxKG(XM_9(#T_D9M2HSAlMYt*}ohKN?_A$hy4mVaqk|P1TJj|6yg+$QqENp}d;mD9Zwy-}Uy~CH z7GyjxN%UyDKZYNbo}stCwOxJ?EX-bvPM~@pq{vDq=$oyzE>O{5@8Ocg>A1n(YB^^@ zZJ+MLEA!4*4}Ze z4Ln#Y6#PPkg~`ipM5|9(33b?0rw+!qtr`Ds3m{*ms~LOj+px+MB~?Xk$nnnj+eg{A zGM<4kqAQ#Vj&zWpqJWp=qlr&Le0}t*an>BtH3kv7`5eAs%|&<67fM?(%B|xiR&yZ* zcy1PTH+i(yrY0vVrb>;C@peNSVADw22eYn*Yc#gkAo6*KIJ40rjZp6JV&!NV`PqEO ztaDw3i5s@cJ6o+~tdafcm!KE>ZdehMtN4mTdxGz5@*JwQgE~gXS=bMXn=Fk*XI3)* zJYJ5oB}{iy4R~y~bsTQ>2!Kc1en{YX7zbLojn^bclLt+T%6#DJ$^IkFbU98?pif}K zg>m_0XpkaA#*w)A1r2|rz=*+PAiWi>+;54agN24zws!;DLoqXE)}0=DjobWsa&3^_ zQGvE|`M%c=z8sq&oJ!ijAt6df`giuhgBT)<7Y?)hzf8Uxww)L9>ab$6V`$`lcgLE?Aag25Jh?B1w%x`GBEk^N_lj|t`hXqN+(=&BS$Vd*@ z5N0MSUm4;$HlwDEAAjIWZNl{=72;XkYdYLH|C!T|b3^Mmm0t3UlkWW)#`UN1-~AxH zfyB4q=*ew~y4BfDk?(xRY306fZiZF|OrSqW%hoHrl4?*`ijP;&x5^BWw?fsoJ;&K- zrN*G-msXEh)B3Ugqdp5d>R~rEO!~qQC)NwRjQ*T@wQo&E7P0o!_^+WF~Ox?JIS|7morRVPahn%y+R_!L-9kX@9AQJr`M}b z5rfrdjLPK*r1;6Xzxc6_R7BjW&huxw@P*>qikvI-0^x`^afO4lrN|n(^c#%L@K{h2 zA-O($8Jw|Kz5bTh>N&MP#)7L0qlV^{tr>qp>sT>;;?JQY2E%PHsDi|08+J^%jlWak zIqc4iZAOdb%$*EH^X25sDEVQN@tE*q{y1MhcH~!Lk$OY#y2X)OLcsDbNt1){2Jb+M zplD6c`bTMt$xFC2Of0GrDvC@yEZkaf-2P3&tIxqVu7eX11sl*jOAcgpdhyCFu(^PgJWWO~^)WiNZd3viX?!UkD69d$AYgV@~v*y_-OyK+J7rdGM43I z)|C#HLf#er|KmZsfAgV(-*_(J|4;e7$2WMAhfXsE_a|ZY^Pe{X&RN=?DL21z_kRFr CzuBn( literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..aee83149be8e33a2e5f7e302ebaa32bd85658455 GIT binary patch literal 26572 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 PVE_V8S3j3^P6 + // A plain vector format: only the Output, Simplify and format-note sections show. + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 420, 380)); + + [Test] + public Task Png() => + // The image formats reveal the full render-options section (projection, strokes, labels, colours) + // plus the PNG sub-section, so this covers most of the options UI and its show/hide logic. + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 420, 1320)); + + static OptionsPanel Panel(GeoFormat format) + { + var panel = new OptionsPanel(new(), new(), new(), new()); + panel.SelectFormat(format); + return panel; + } +} diff --git a/src/GeoConvert.App.Tests/SampleMaps.cs b/src/GeoConvert.App.Tests/SampleMaps.cs new file mode 100644 index 0000000..7bb049c --- /dev/null +++ b/src/GeoConvert.App.Tests/SampleMaps.cs @@ -0,0 +1,41 @@ +namespace GeoConvert.App.Tests; + +/// Small, deterministic in-memory maps used across the snapshot tests. +static class SampleMaps +{ + // Two overlapping squares, a triangle and a line — enough geometry to exercise the renderer, the + // diff overlay and the structural summary without depending on any external data file. + public static FeatureCollection A() => + [ + new Feature( + new Polygon([[new(0, 0), new(10, 0), new(10, 10), new(0, 10), new(0, 0)]]), + Props(("name", "Square"), ("pop", 100L))), + new Feature( + new Polygon([[new(12, 0), new(20, 0), new(16, 8), new(12, 0)]]), + Props(("name", "Tri"))), + new Feature( + new LineString([new(0, 12), new(20, 12)]), + Props(("name", "Road"))), + ]; + + public static FeatureCollection B() => + [ + new Feature( + new Polygon([[new(1, 1), new(11, 1), new(11, 11), new(1, 11), new(1, 1)]]), + Props(("name", "Square"), ("pop", 100L), ("iso", "SQ"))), + new Feature( + new Point(new(16, 4)), + Props(("name", "Dot"), ("iso", "DT"))), + ]; + + static IDictionary Props(params (string Key, object? Value)[] pairs) + { + var properties = new Dictionary(); + foreach (var (key, value) in pairs) + { + properties[key] = value; + } + + return properties; + } +} diff --git a/src/GeoConvert.App.Tests/WinFormsSnapshot.cs b/src/GeoConvert.App.Tests/WinFormsSnapshot.cs new file mode 100644 index 0000000..f710323 --- /dev/null +++ b/src/GeoConvert.App.Tests/WinFormsSnapshot.cs @@ -0,0 +1,85 @@ +namespace GeoConvert.App.Tests; + +/// +/// Renders a WinForms control to a for Verify to snapshot. WinForms requires an STA +/// thread, so the control is created, laid out and drawn on a dedicated one; the resulting bitmap is +/// thread-agnostic and handed back. Forms are briefly shown off-screen so their OnLoad (e.g. the +/// diff window's splitter layout) runs before the draw; plain controls just get a handle and a layout +/// pass. Nothing is verified here — the caller passes the bitmap to Verify. +/// +static class WinFormsSnapshot +{ + static bool stylesEnabled; + + public static Bitmap Render(Func factory, int width, int height) + { + Bitmap? result = null; + Exception? failure = null; + + var thread = new Thread(() => + { + try + { + EnsureStyles(); + using var control = factory(); + if (control is Form form) + { + // Off-screen + off-taskbar so the brief show is invisible; Show() raises OnLoad/OnShown + // so docked/split layouts settle before the draw. + form.StartPosition = FormStartPosition.Manual; + form.ShowInTaskbar = false; + form.Location = new(-5000, -5000); + form.Size = new(width, height); + form.Show(); + Application.DoEvents(); + result = Draw(form); + form.Close(); + } + else + { + control.Size = new(width, height); + _ = control.Handle; + control.PerformLayout(); + Application.DoEvents(); + result = Draw(control); + } + } + catch (Exception exception) + { + failure = exception; + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + thread.Join(); + + if (failure != null) + { + throw failure; + } + + return result!; + } + + static Bitmap Draw(Control control) + { + var bounds = control.ClientRectangle; + var bitmap = new Bitmap(Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)); + control.DrawToBitmap(bitmap, bounds); + return bitmap; + } + + static void EnsureStyles() + { + if (stylesEnabled) + { + return; + } + + // Match the real app's themed rendering (ApplicationConfiguration.Initialize does this). + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + stylesEnabled = true; + } +} diff --git a/src/GeoConvert.App/FileAssociations.cs b/src/GeoConvert.App/FileAssociations.cs index ed8036e..4bb3a66 100644 --- a/src/GeoConvert.App/FileAssociations.cs +++ b/src/GeoConvert.App/FileAssociations.cs @@ -8,11 +8,11 @@ namespace GeoConvert.App; /// public static class FileAssociations { - const string ProgId = "GeoConvert.Map"; - const string ProgIdLabel = "GeoConvert Map"; + const string progId = "GeoConvert.Map"; + const string progIdLabel = "GeoConvert Map"; - const int ShcneAssocchanged = 0x08000000; - const uint ShcnfIdlist = 0; + const int shcneAssocchanged = 0x08000000; + const uint shcnfIdlist = 0; [DllImport("shell32.dll")] static extern void SHChangeNotify(int eventId, uint flags, IntPtr item1, IntPtr item2); @@ -28,7 +28,7 @@ public static class FileAssociations public static bool IsAssociated() { using var classes = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{Extensions[0]}"); - return classes?.GetValue(null) as string == ProgId; + return classes?.GetValue(null) as string == progId; } /// Binds every supported map extension to this app. @@ -36,9 +36,9 @@ public static void Associate() { var executable = ExecutablePath; - using (var progId = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{ProgId}")) + using (var progId = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{FileAssociations.progId}")) { - progId.SetValue(null, ProgIdLabel); + progId.SetValue(null, progIdLabel); using (var icon = progId.CreateSubKey("DefaultIcon")) { icon.SetValue(null, $"\"{executable}\",0"); @@ -53,9 +53,9 @@ public static void Associate() using var key = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{extension}"); // Set as the default handler (the "bind" the user asked for) and also advertise the ProgId in // the extension's OpenWith list so the app shows up there and the binding is cleanly removable. - key.SetValue(null, ProgId); + key.SetValue(null, progId); using var openWith = key.CreateSubKey("OpenWithProgids"); - openWith.SetValue(ProgId, Array.Empty(), RegistryValueKind.None); + openWith.SetValue(progId, Array.Empty(), RegistryValueKind.None); } NotifyShell(); @@ -74,21 +74,21 @@ public static void Unassociate() // Only clear the default if it still points at us — never stomp a handler the user has since // chosen. - if (key.GetValue(null) as string == ProgId) + if (key.GetValue(null) as string == progId) { // "" is the name of a key's default value (DeleteValue, unlike GetValue, won't take null). key.DeleteValue(string.Empty, throwOnMissingValue: false); } using var openWith = key.OpenSubKey("OpenWithProgids", writable: true); - openWith?.DeleteValue(ProgId, throwOnMissingValue: false); + openWith?.DeleteValue(progId, throwOnMissingValue: false); } - Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{ProgId}", throwOnMissingSubKey: false); + Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{progId}", throwOnMissingSubKey: false); NotifyShell(); } static void NotifyShell() => // Tell Explorer the associations changed so icons / "Open with" refresh without a sign-out. - SHChangeNotify(ShcneAssocchanged, ShcnfIdlist, IntPtr.Zero, IntPtr.Zero); + SHChangeNotify(shcneAssocchanged, shcnfIdlist, IntPtr.Zero, IntPtr.Zero); } diff --git a/src/GeoConvert.App/GeoConvert.App.csproj b/src/GeoConvert.App/GeoConvert.App.csproj index 6804242..70cdb18 100644 --- a/src/GeoConvert.App/GeoConvert.App.csproj +++ b/src/GeoConvert.App/GeoConvert.App.csproj @@ -58,12 +58,27 @@ + + + Borders + 0.1 + + + + + diff --git a/src/GeoConvert.App/SampleMap.cs b/src/GeoConvert.App/SampleMap.cs new file mode 100644 index 0000000..3ccf7bd --- /dev/null +++ b/src/GeoConvert.App/SampleMap.cs @@ -0,0 +1,17 @@ +namespace GeoConvert.App; + +/// +/// The bundled sample world map — country borders, topology-simplified — the same map the Blazor app +/// ships. MapBundle stages it next to the app at build time as maps/World/borders.fgb (see the +/// csproj); this resolves that path at runtime, whether running from the build output or an installed +/// dotnet tool. +/// +static class SampleMap +{ + /// The bundled map's path, or null when it isn't present beside the app. + public static string? Locate() + { + var path = Path.Combine(AppContext.BaseDirectory, "maps", "World", "borders.fgb"); + return File.Exists(path) ? path : null; + } +} diff --git a/src/GeoConvert.App/Ui/Combos.cs b/src/GeoConvert.App/Ui/Combos.cs index 2fa9798..f8efc43 100644 --- a/src/GeoConvert.App/Ui/Combos.cs +++ b/src/GeoConvert.App/Ui/Combos.cs @@ -30,6 +30,20 @@ public static ComboBox Build(IReadOnlyList<(T Value, string Label)> choices, return combo; } + /// Selects the item carrying , raising the change handler as a user pick would. + public static void Select(ComboBox combo, T value) + where T : notnull + { + for (var index = 0; index < combo.Items.Count; index++) + { + if (combo.Items[index] is Choice choice && EqualityComparer.Default.Equals(choice.Value, value)) + { + combo.SelectedIndex = index; + return; + } + } + } + sealed class Choice(T value, string label) { public T Value { get; } = value; diff --git a/src/GeoConvert.App/Ui/DiffForm.cs b/src/GeoConvert.App/Ui/DiffForm.cs index 3a4e210..f37f551 100644 --- a/src/GeoConvert.App/Ui/DiffForm.cs +++ b/src/GeoConvert.App/Ui/DiffForm.cs @@ -73,18 +73,18 @@ void BuildUi() Size = new(1100, 720); MinimumSize = new(820, 520); - split = new SplitContainer + split = new() { Dock = DockStyle.Fill, FixedPanel = FixedPanel.Panel2, }; - preview = new PictureBox + preview = new() { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom, BackColor = Color.FromArgb(245, 245, 245), }; - summary = new TextBox + summary = new() { Dock = DockStyle.Fill, Multiline = true, @@ -97,7 +97,7 @@ void BuildUi() split.Panel1.Controls.Add(preview); split.Panel2.Controls.Add(summary); - saveButton = new Button + saveButton = new() { Dock = DockStyle.Bottom, Height = 38, @@ -258,11 +258,13 @@ async Task LoadAsync(string path, bool isFirst, bool render = true) { mapA = collection; pathA = path; + pathBoxA.Text = path; } else { mapB = collection; pathB = path; + pathBoxB.Text = path; } if (render) diff --git a/src/GeoConvert.App/Ui/MainForm.cs b/src/GeoConvert.App/Ui/MainForm.cs index aff31e4..b05a8d1 100644 --- a/src/GeoConvert.App/Ui/MainForm.cs +++ b/src/GeoConvert.App/Ui/MainForm.cs @@ -18,7 +18,6 @@ sealed class MainForm : Form PictureBox preview = null!; Button saveButton = null!; Label fileLabel = null!; - SplitContainer split = null!; ToolStripStatusLabel statusLabel = null!; ToolStripProgressBar progressBar = null!; @@ -29,6 +28,10 @@ sealed class MainForm : Form int previewToken; bool busy; + // Fixed width, in pixels, of the right-hand options column (wide enough for the 372px option groups + // plus the scrollbar, so no horizontal scroll appears). + const int optionsWidth = 420; + public MainForm(SettingsManager settingsManager, string? initialFile) { this.settingsManager = settingsManager; @@ -60,19 +63,13 @@ void BuildUi() Size = new(1100, 720); MinimumSize = new(820, 520); - split = new SplitContainer - { - Dock = DockStyle.Fill, - FixedPanel = FixedPanel.Panel2, - }; - - preview = new PictureBox + preview = new() { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom, BackColor = Color.FromArgb(245, 245, 245), }; - fileLabel = new Label + fileLabel = new() { Dock = DockStyle.Top, AutoSize = false, @@ -81,14 +78,21 @@ void BuildUi() Padding = new(8, 0, 0, 0), Text = "No map loaded", }; - split.Panel1.Controls.Add(preview); - split.Panel1.Controls.Add(fileLabel); + var previewHost = new Panel + { + Dock = DockStyle.Fill + }; + previewHost.Controls.Add(preview); + previewHost.Controls.Add(fileLabel); - optionsPanel = new(render, simplify, kmz, parquet) { Dock = DockStyle.Fill }; + optionsPanel = new(render, simplify, kmz, parquet) + { + Dock = DockStyle.Fill + }; optionsPanel.Changed += (_, _) => _ = RefreshPreviewAsync(); optionsPanel.TargetChanged += (_, _) => UpdateSaveLabel(); - saveButton = new Button + saveButton = new() { Dock = DockStyle.Bottom, Height = 40, @@ -96,32 +100,47 @@ void BuildUi() Enabled = false, }; saveButton.Click += (_, _) => _ = SaveAsync(); - split.Panel2.Controls.Add(optionsPanel); - split.Panel2.Controls.Add(saveButton); + + // The options live in a fixed-width column pinned to the right edge — a plain docked panel, not a + // SplitContainer, so there is no draggable splitter and the column never grows or shrinks with the + // window. The preview takes all the remaining width. + var optionsHost = new Panel + { + Dock = DockStyle.Right, + Width = optionsWidth + }; + optionsHost.Controls.Add(optionsPanel); + optionsHost.Controls.Add(saveButton); var status = new StatusStrip(); - statusLabel = new ToolStripStatusLabel { Spring = true, TextAlign = ContentAlignment.MiddleLeft }; - progressBar = new ToolStripProgressBar { Visible = false, Width = 200 }; + statusLabel = new() + { + Spring = true, + TextAlign = ContentAlignment.MiddleLeft + }; + progressBar = new() + { + Visible = false, + Width = 200 + }; status.Items.Add(statusLabel); status.Items.Add(progressBar); - Controls.Add(split); + // Add the fill host first so it claims the leftover area; the menu and status (added last) span the + // full width top and bottom, with the fixed options column between them on the right. + Controls.Add(previewHost); + Controls.Add(optionsHost); Controls.Add(status); Controls.Add(BuildMenu()); } - protected override void OnLoad(EventArgs args) - { - base.OnLoad(args); - SplitLayout.ConfigureSplit(split, 400); - } - MenuStrip BuildMenu() { var menu = new MenuStrip(); var file = new ToolStripMenuItem("&File"); file.DropDownItems.Add("&Open…", null, (_, _) => OpenFile()); + file.DropDownItems.Add("&Load sample world map", null, (_, _) => OpenSample()); file.DropDownItems.Add("&Save As…", null, (_, _) => _ = SaveAsync()); file.DropDownItems.Add(new ToolStripSeparator()); file.DropDownItems.Add("E&xit", null, (_, _) => Close()); @@ -154,10 +173,21 @@ void OpenFile() } } + void OpenSample() + { + if (SampleMap.Locate() is { } path) + { + _ = LoadAsync(path); + return; + } + + MessageBox.Show(this, "The bundled sample world map isn't available next to the app.", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + async Task LoadAsync(string path) { var detected = ConversionService.Detect(path); - if (detected is not { CanRead: true }) + if (detected is not {CanRead: true}) { MessageBox.Show(this, $"Can't read '{Path.GetFileName(path)}': unsupported map format.", "GeoConvert", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; @@ -187,7 +217,7 @@ async Task LoadAsync(string path) async Task RefreshPreviewAsync() { - if (features is not { Count: > 0 } collection) + if (features is not {Count: > 0} collection) { preview.Image?.Dispose(); preview.Image = null; @@ -307,7 +337,7 @@ void ShowAbout() => void UpdateState() { - saveButton.Enabled = features is { Count: > 0 } && !busy; + saveButton.Enabled = features is {Count: > 0} && !busy; UpdateSaveLabel(); } @@ -332,7 +362,7 @@ void SetBusy(bool value, string? message) statusLabel.Text = message; } - saveButton.Enabled = features is { Count: > 0 } && !busy; + saveButton.Enabled = features is {Count: > 0} && !busy; Cursor = value ? Cursors.AppStarting : Cursors.Default; } diff --git a/src/GeoConvert.App/Ui/OptionsPanel.cs b/src/GeoConvert.App/Ui/OptionsPanel.cs index f563136..01f2917 100644 --- a/src/GeoConvert.App/Ui/OptionsPanel.cs +++ b/src/GeoConvert.App/Ui/OptionsPanel.cs @@ -20,6 +20,7 @@ sealed class OptionsPanel : FlowLayoutPanel GroupBox kmzSection = null!; GroupBox parquetSection = null!; GroupBox noteSection = null!; + ComboBox outputCombo = null!; TableLayoutPanel currentTable = null!; @@ -63,7 +64,7 @@ public OptionsPanel(RenderSettings render, SimplifySettings simplify, KmzSetting void BuildOutputSection() { BeginSection("Output"); - AddCombo( + outputCombo = AddCombo( "Format", [.. ConversionService.WritableFormats.Select(_ => (_.Format, _.DisplayName))], SelectedFormat, @@ -75,6 +76,9 @@ [.. ConversionService.WritableFormats.Select(_ => (_.Format, _.DisplayName))], }); } + /// Selects the output format programmatically, exactly as choosing it in the combo would. + internal void SelectFormat(GeoFormat format) => Combos.Select(outputCombo, format); + void BuildImageSection() { imageSection = BeginSection("Image (PNG / SVG)"); @@ -212,7 +216,7 @@ void Row(string label, Control control) currentTable.Controls.Add(control); } - void AddCombo(string label, IReadOnlyList<(T Value, string Label)> choices, T current, Action set, bool affectsPreview = true) + ComboBox AddCombo(string label, IReadOnlyList<(T Value, string Label)> choices, T current, Action set, bool affectsPreview = true) where T : notnull { var combo = Combos.Build( @@ -228,6 +232,7 @@ void AddCombo(string label, IReadOnlyList<(T Value, string Label)> choices, T }); combo.Width = 190; Row(label, combo); + return combo; } void AddInt(string label, int min, int max, int current, Action set, bool affectsPreview = true) diff --git a/src/GeoConvert.Web.Tests/Components/ExportOptionsTests.cs b/src/GeoConvert.Web.Tests/Components/ExportOptionsTests.cs index 6dea8cf..083d05b 100644 --- a/src/GeoConvert.Web.Tests/Components/ExportOptionsTests.cs +++ b/src/GeoConvert.Web.Tests/Components/ExportOptionsTests.cs @@ -139,7 +139,7 @@ public async Task GeoParquet_GzipLevelShownOnlyForGzipCodec() { var cut = Render(_ => _ .Add(component => component.Target, GeoFormat.GeoParquet) - .Add(component => component.Parquet, new GeoParquetSettings())); + .Add(component => component.Parquet, new())); // Snappy (the default) ignores the deflate level, so the GZIP-level control is hidden. await Assert.That(cut.FindAll("#parquet-gzip").Count).IsEqualTo(0); diff --git a/src/GeoConvert.Web/Pages/Index.razor.cs b/src/GeoConvert.Web/Pages/Index.razor.cs index 2082c0d..b0d3399 100644 --- a/src/GeoConvert.Web/Pages/Index.razor.cs +++ b/src/GeoConvert.Web/Pages/Index.razor.cs @@ -171,7 +171,7 @@ async Task DownloadSampleAsync(string url) { buffer.Write(chunk, 0, count); read += count; - progress?.Report(new ConvertProgress(ProgressPhase.Reading, 0, null, read, null)); + progress?.Report(new(ProgressPhase.Reading, 0, null, read, null)); } return buffer.ToArray(); diff --git a/src/GeoConvert.Web/wwwroot/favicon.png b/src/GeoConvert.Web/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..229f79bb5de986fcd4135ff5166d4197912edff0 GIT binary patch literal 13061 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

` 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 literal 0 HcmV?d00001 diff --git a/src/GeoConvert.Web/wwwroot/index.html b/src/GeoConvert.Web/wwwroot/index.html index 465eaea..23f35cf 100644 --- a/src/GeoConvert.Web/wwwroot/index.html +++ b/src/GeoConvert.Web/wwwroot/index.html @@ -5,6 +5,7 @@ GeoConvert — Map Format Converter + diff --git a/src/GeoConvert.slnx b/src/GeoConvert.slnx index b3c8c9b..e4eecfa 100644 --- a/src/GeoConvert.slnx +++ b/src/GeoConvert.slnx @@ -1,6 +1,7 @@ + diff --git a/src/Tests/RenderBackendTests.cs b/src/Tests/RenderBackendTests.cs index 631d07a..d2cf8fa 100644 --- a/src/Tests/RenderBackendTests.cs +++ b/src/Tests/RenderBackendTests.cs @@ -50,7 +50,7 @@ public async Task PaintSurface_validates_before_invoking_the_factory() try { MapRenderer.PaintSurface( - [new FeatureCollection()], + [new()], new(), (width, height) => { diff --git a/src/Tests/SvgTests.cs b/src/Tests/SvgTests.cs index 74ea33e..352f81a 100644 --- a/src/Tests/SvgTests.cs +++ b/src/Tests/SvgTests.cs @@ -410,8 +410,8 @@ public async Task Simplify_tolerance_thins_rings_and_polylines() { var x = i / 10.0; // A barely-perceptible wobble (well under a pixel once projected) on an otherwise straight edge. - ring.Add(new(x, 5 + (i % 2) * 0.001)); - line.Add(new(x, 2 + (i % 2) * 0.001)); + ring.Add(new(x, 5 + i % 2 * 0.001)); + line.Add(new(x, 2 + i % 2 * 0.001)); } ring.Add(new(10, 0)); From 93cbe7a06b954e5398fd1b4caa2a6150fbf51328 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 10:44:40 +1000 Subject: [PATCH 05/10] Update DiffForm.cs --- src/GeoConvert.App/Ui/DiffForm.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/GeoConvert.App/Ui/DiffForm.cs b/src/GeoConvert.App/Ui/DiffForm.cs index f37f551..49c4510 100644 --- a/src/GeoConvert.App/Ui/DiffForm.cs +++ b/src/GeoConvert.App/Ui/DiffForm.cs @@ -141,9 +141,10 @@ TableLayoutPanel BuildInputs() TextBox AddInputRow(TableLayoutPanel table, string label, string? value, Action onPicked) { - // Anchor (not Dock) so each control keeps its natural height and the TableLayoutPanel centres it - // vertically in the row — Dock=Fill here stretched the controls and overlapped the toolbar below. - table.Controls.Add(new Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(3, 0, 3, 0) }); + // The label fills its fixed-height cell and centres its text vertically (MiddleLeft), so it lines + // up with the text box. The box and button anchor left+right at their natural height and the + // TableLayoutPanel centres them in the row. + table.Controls.Add(new Label { Text = label, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Margin = new(3, 0, 3, 0) }); var box = new TextBox { Anchor = AnchorStyles.Left | AnchorStyles.Right, ReadOnly = true, Text = value ?? string.Empty, Margin = new(3, 0, 3, 0) }; table.Controls.Add(box); var browse = new Button { Text = "Browse…", Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new(3, 0, 3, 0) }; From 28b60962789fe654350e6c1bac993be94b251132 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 10:47:30 +1000 Subject: [PATCH 06/10] Update Tests.csproj --- src/Tests/Tests.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 0da842a..595f7b7 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -19,9 +19,6 @@ - - - PreserveNewest From 283996b0fab89d0abd6d2444d2eed80e29c9fc94 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 11:33:32 +1000 Subject: [PATCH 07/10] . --- .../FormsTests.MainWindow.verified.png | Bin 19627 -> 23765 bytes .../OptionsPanelTests.Kml.verified.png | Bin 8890 -> 13523 bytes .../OptionsPanelTests.Png.verified.png | Bin 26572 -> 29700 bytes src/GeoConvert.App.Tests/OptionsPanelTests.cs | 7 +- src/GeoConvert.App/Program.cs | 8 ++- src/GeoConvert.App/Ui/MainForm.cs | 4 +- src/GeoConvert.App/Ui/OptionsPanel.cs | 62 ++++++++++++++++-- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png index 582b5ae0157b923b1a6455a9594673775e22fbcc..31baa0b62d362863d7cd834a4667a1038216dbf1 100644 GIT binary patch literal 23765 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 G;{O1`%iuTw literal 19627 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 CzuBn( diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png index aee83149be8e33a2e5f7e302ebaa32bd85658455..8f0ed21c3530c0df56a18f0bd0e2987fd8fe1b2c 100644 GIT binary patch literal 29700 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 OUSHWeZ)0rniv9;R*5H%? literal 26572 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 PVE_V8S3j3^P6 - // A plain vector format: only the Output, Simplify and format-note sections show. - Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 420, 380)); + // A plain vector format: the always-on Projection radios plus the Output, Simplify and + // format-note sections (no image options). + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 480, 560)); [Test] public Task Png() => // The image formats reveal the full render-options section (projection, strokes, labels, colours) // plus the PNG sub-section, so this covers most of the options UI and its show/hide logic. - Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 420, 1320)); + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 480, 1320)); static OptionsPanel Panel(GeoFormat format) { diff --git a/src/GeoConvert.App/Program.cs b/src/GeoConvert.App/Program.cs index 3faf54a..93a6950 100644 --- a/src/GeoConvert.App/Program.cs +++ b/src/GeoConvert.App/Program.cs @@ -2,7 +2,13 @@ namespace GeoConvert.App; static class Program { - static readonly SettingsManager settingsManager = new(SettingsManager.DefaultSettingsPath); + // The GEOCONVERT_SETTINGS environment variable overrides where settings live — used by tests and + // screenshot tooling so they never touch the real per-user settings file (which gates the one-time + // association prompt). + static readonly SettingsManager settingsManager = new( + Environment.GetEnvironmentVariable("GEOCONVERT_SETTINGS") is { Length: > 0 } path + ? path + : SettingsManager.DefaultSettingsPath); [STAThread] static int Main(string[] args) diff --git a/src/GeoConvert.App/Ui/MainForm.cs b/src/GeoConvert.App/Ui/MainForm.cs index b05a8d1..6f3b382 100644 --- a/src/GeoConvert.App/Ui/MainForm.cs +++ b/src/GeoConvert.App/Ui/MainForm.cs @@ -28,9 +28,9 @@ sealed class MainForm : Form int previewToken; bool busy; - // Fixed width, in pixels, of the right-hand options column (wide enough for the 372px option groups + // Fixed width, in pixels, of the right-hand options column (wide enough for the 440px option groups // plus the scrollbar, so no horizontal scroll appears). - const int optionsWidth = 420; + const int optionsWidth = 480; public MainForm(SettingsManager settingsManager, string? initialFile) { diff --git a/src/GeoConvert.App/Ui/OptionsPanel.cs b/src/GeoConvert.App/Ui/OptionsPanel.cs index 01f2917..2f1b15e 100644 --- a/src/GeoConvert.App/Ui/OptionsPanel.cs +++ b/src/GeoConvert.App/Ui/OptionsPanel.cs @@ -36,6 +36,7 @@ public OptionsPanel(RenderSettings render, SimplifySettings simplify, KmzSetting AutoScroll = true; Padding = new(4); + BuildProjectionSection(); BuildOutputSection(); BuildImageSection(); BuildPngSection(); @@ -61,6 +62,14 @@ public OptionsPanel(RenderSettings render, SimplifySettings simplify, KmzSetting // --- sections --- + void BuildProjectionSection() + { + // Above Output and always shown: the projection drives the live preview too, not just the + // PNG/SVG export, so it stays available whatever the chosen output format. + BeginSection("Projection"); + AddRadioGroup(OptionChoices.Projections, render.Projection, _ => render.Projection = _); + } + void BuildOutputSection() { BeginSection("Output"); @@ -82,7 +91,6 @@ [.. ConversionService.WritableFormats.Select(_ => (_.Format, _.DisplayName))], void BuildImageSection() { imageSection = BeginSection("Image (PNG / SVG)"); - AddCombo("Projection", OptionChoices.Projections, render.Projection, _ => render.Projection = _); AddCombo("Resolution", OptionChoices.Dimensions, render.MaxDimension, _ => render.MaxDimension = _); AddInt("Padding (px)", 0, 500, render.Padding, _ => render.Padding = _); AddInt("Stroke width (px)", 0, 50, render.StrokeWidth, _ => render.StrokeWidth = _); @@ -149,7 +157,7 @@ void BuildNoteSection() var note = new Label { AutoSize = true, - MaximumSize = new(330, 0), + MaximumSize = new(400, 0), Margin = new(3), Text = "This format writes geometry and properties directly. Use the Simplify section above to thin vertices before writing.", }; @@ -180,7 +188,7 @@ GroupBox BeginSection(string title) Dock = DockStyle.Top, Padding = new(4), }; - table.ColumnStyles.Add(new(SizeType.Absolute, 150)); + table.ColumnStyles.Add(new(SizeType.Absolute, 215)); table.ColumnStyles.Add(new(SizeType.Absolute, 200)); var box = new GroupBox @@ -191,8 +199,8 @@ GroupBox BeginSection(string title) // Pin the width (Min == Max), leaving only the height to AutoSize. Without this an AutoSize // GroupBox wrapping a Dock=Top table can't resolve its width (each defers to the other) and // collapses to a sliver. - MinimumSize = new(372, 0), - MaximumSize = new(372, 0), + MinimumSize = new(440, 0), + MaximumSize = new(440, 0), Margin = new(3), Padding = new(6, 3, 6, 6), }; @@ -210,12 +218,54 @@ void Row(string label, Control control) AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(3, 7, 3, 3), - MaximumSize = new(145, 0), + MaximumSize = new(210, 0), }; currentTable.Controls.Add(caption); currentTable.Controls.Add(control); } + // A full-width column of mutually-exclusive radio buttons (one container => one radio group). Used + // where a choice reads better laid out than hidden in a dropdown — e.g. the projection. + void AddRadioGroup(IReadOnlyList<(T Value, string Label)> choices, T current, Action set, bool affectsPreview = true) + where T : notnull + { + var group = new FlowLayoutPanel + { + FlowDirection = FlowDirection.TopDown, + WrapContents = false, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + Margin = new(3), + }; + foreach (var (value, text) in choices) + { + var radio = new RadioButton + { + Text = text, + AutoSize = true, + Checked = EqualityComparer.Default.Equals(value, current), + Margin = new(0, 1, 0, 1), + }; + radio.CheckedChanged += (_, _) => + { + if (!radio.Checked) + { + return; + } + + set(value); + if (affectsPreview) + { + RaiseChanged(); + } + }; + group.Controls.Add(radio); + } + + currentTable.Controls.Add(group); + currentTable.SetColumnSpan(group, 2); + } + ComboBox AddCombo(string label, IReadOnlyList<(T Value, string Label)> choices, T current, Action set, bool affectsPreview = true) where T : notnull { From 54a7894c672d4e0fe9bc453d8bc1210b0a0e9719 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 12:03:24 +1000 Subject: [PATCH 08/10] . --- .../FormsTests.MainWindow.verified.png | Bin 23765 -> 20759 bytes .../OptionsPanelTests.Kml.verified.png | Bin 13523 -> 11360 bytes .../OptionsPanelTests.Png.verified.png | Bin 29700 -> 27592 bytes src/GeoConvert.App/Ui/OptionsPanel.cs | 38 +++++++++++++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png index 31baa0b62d362863d7cd834a4667a1038216dbf1..dc0af647c823dc0c58040f9bbfb76baaaaeb3f1f 100644 GIT binary patch literal 20759 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 G;{O1`%iuTw diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png index fe51a8996beb2594632c1f18a3d35ebec998f1af..5aaadaaac086f86ac3ac36e4639c05875b4ae039 100644 GIT binary patch literal 11360 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 O^!JY6m7TnN*9huduQ+U?7i1o_kFMZ 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

LROW9?=9JOM%|KpddtMRZ1U+8 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 O5RYB^cNTx*cm5yi{k4n$ literal 29700 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 OUSHWeZ)0rniv9;R*5H%? diff --git a/src/GeoConvert.App/Ui/OptionsPanel.cs b/src/GeoConvert.App/Ui/OptionsPanel.cs index 2f1b15e..518b906 100644 --- a/src/GeoConvert.App/Ui/OptionsPanel.cs +++ b/src/GeoConvert.App/Ui/OptionsPanel.cs @@ -141,7 +141,12 @@ void BuildParquetSection() void BuildSimplifySection() { BeginSection("Simplify (optional pre-pass)"); - AddCheck("Simplify geometry", simplify.Enabled, _ => simplify.Enabled = _); + var enabled = AddCheck("Simplify geometry", simplify.Enabled, _ => simplify.Enabled = _); + + // The tolerance / method / topology options only apply when simplification is on, so collapse + // them when "Simplify geometry" is unchecked. Capture the controls added below so the toggle can + // hide both the labels and the inputs (an AutoSize TableLayoutPanel row collapses when empty). + var dependentStart = currentTable.Controls.Count; AddDouble("Tolerance", 0, 1000, 4, simplify.Tolerance, _ => simplify.Tolerance = _); AddCombo( "Method", @@ -149,6 +154,23 @@ void BuildSimplifySection() simplify.Method, _ => simplify.Method = _); AddCheck("Preserve shared boundaries", simplify.Topology, _ => simplify.Topology = _); + + var dependents = new List(); + for (var index = dependentStart; index < currentTable.Controls.Count; index++) + { + dependents.Add(currentTable.Controls[index]); + } + + void Sync() + { + foreach (var dependent in dependents) + { + dependent.Visible = enabled.Checked; + } + } + + enabled.CheckedChanged += (_, _) => Sync(); + Sync(); } void BuildNoteSection() @@ -212,15 +234,18 @@ GroupBox BeginSection(string title) void Row(string label, Control control) { + // Fill the label cell and centre its text vertically so it lines up with the input regardless of + // the input's height; centre the input in the row too (Anchor without Top/Bottom). The column is + // wide enough (215px) that no label wraps. var caption = new Label { Text = label, - AutoSize = true, - Anchor = AnchorStyles.Left, - Margin = new(3, 7, 3, 3), - MaximumSize = new(210, 0), + Dock = DockStyle.Fill, + TextAlign = ContentAlignment.MiddleLeft, + Margin = new(3, 0, 3, 0), }; currentTable.Controls.Add(caption); + control.Anchor = AnchorStyles.Left; currentTable.Controls.Add(control); } @@ -329,7 +354,7 @@ void AddDouble(string label, double min, double max, int decimals, double curren Row(label, numeric); } - void AddCheck(string label, bool current, Action set, bool affectsPreview = true) + CheckBox AddCheck(string label, bool current, Action set, bool affectsPreview = true) { var check = new CheckBox { @@ -346,6 +371,7 @@ void AddCheck(string label, bool current, Action set, bool affectsPreview } }; Row(label, check); + return check; } void AddText(string label, string current, Action set, bool affectsPreview = true) From b6d60268d58a8708fb51846dcab595da8143fe30 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 12:18:24 +1000 Subject: [PATCH 09/10] . --- .../FormsTests.DiffWindow.verified.png | Bin 15627 -> 15700 bytes src/GeoConvert.App/Ui/DiffForm.cs | 16 +++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png index afa6a7fcbf70d5ff808770ab848b4992d4ebb77d..55176d4c1f7a8d3b078e3e9ef6c7cf6a2b1a40a3 100644 GIT binary patch delta 1071 zcmY+DeNd7I9LFD{gR3E|W0#=#(%st2wUn)5sJz^&&1>DHo4ef3SMta;XJTeDd3j-Exh*+U69ttQ8518-)I9}a!FMK=2a004BKkxLl3fznU=w!#~RCGmQnO0g$QB5@T646uuP%R`D=XWrswe48ANNWWDhK97#B6Y^%_#R9~Q;oCpwgUi=GXucRp|%jR@%wi@;zC1yue?=&!(P6n=jmZf^2e z-^mg=>oF^44gK-9#pRtAuYS1VGbYcS)LpsBcg>G;dWeq#6jlmRnXCI@#_JC{W;2Zf zam*dm)*O;J^B9c`wG0m;1FdDjs}C@U=f_;qLWcGTGl%RbmV2>u(D2)!tP&cXqv%Il zS6BAU`9yE!b@uBMdvmj%w~`cRWUzh~Uy&%JYa_gB>A(HFTaimH%Z=Bk=Jz=WOJ#MP zG!;9&g&oG0BP$_}^#EjAm5Cwko8d(pRG%-nMtfrZNikEqkhx<6Y@-iuve}#L7!Wgx z52WK`@&v@-z((Sx0A-0at210e@*u(c`<`&^CWlp@Uj-5ZE7!o_Cn;ePOo1c61OVGE z&9p?ZbeGm*n0=#nrP|y2U9B+4rbL+=IUKQfc$j-sfv#Q&HVD zl1Ko6Q&56bEC_%2EGCs?uJBq(%newr8F_HlXqqmds4D87D(`=UMs$L8DFz?cw!QV$ zWxK)d_tQi#zz|+G;0qzM^VG!}-LJl~BjNlrA?wXX!&4??F8%FoRGV|WZ4yQ_#__Q3 zU0GBvpTmz22i%kT>q)&dYgcIjH@gZ8mk~kv4gjFz91V7dyo8XhRuLqx$xkJS)NiL4 bcf4|LjD98Pya{as4V-vC{kZaI{@4Ej6iqok delta 1102 zcmY*Ye^Al~9Q`_WdN}#gPVY)VsIA#;wy@awgY;`_m3h0uyS8xC5N|r>B*jQ&2r{ON ziYYeEyqf}RUb#Cr#Cb`ZqHv@vyvZd?`Jp%?@yM1 z)pkDW`+ibZtf)8^G0+yvd|BiHlOl-WN;S3_2@C+32k+--#tzH%rQtJg<$t?Bf5@5w z3u%zKXwTeG#_X>Q>OD><9Yug33CFXdzs=)-6TD<85&-0>>4`?iF*JWIfod+J9~ z(DN`OJ*`oOf!LX$*@f3FDkuhq0sv!eXWWu6!QX_03Q=veC+a3{%z)wfg46K7hg*|U zj^AXam*E}Gs_ZdxBZOFDq{*XzFE1z-Rju7O^6-sdhm;5lq2p)(K$%H2T58m=5ZyR$ zLHYg217!aL`crdzX|-2JcTWc=Fmv|7BO$)8){Ng_QSvzNpqnq$(cefY=_x7NVgF)}5m^(BMp)1<0O-zrUovuXogk_@4k zw%(EO5CE|B+xsYEUW~@7eyE!OK^fREan5am{}a?a5P~cGo5Zf`daKh%-*5%F7TJj2 zEs<1LQ1n_x5dYAjQBa!{mvvpaTwlS;!ZAtC>dlnXD=@bdb&dv7T@Sgk_5fIP7uEymqA=6}M!6dbj+)FzuzY&(68>_82Qp zV-YbGh1j?}(=vJF59SYI%xjR33n?lDu+Zb?;A4FO6{p9JD=!7JxqsKQI_>jdqYggE zzq;)@l4y^ZLWE9)%g}mEEC!R*NKe~n^RCK$clo!7!``XU4EY={=jex%+L~+|8Eq>u zVf`AoKag<)Ck*ruZ#$%FP-9g^1AwH!l5AW*BxoXpxh?ioTV&8|9@g|=VI zza~V!ifeIcD=CUydBLi~FWR+HhL@)%8Ua8h!^;&Km8@*rW|fG?aZZYD1A3?k)k6C; zJev~@Yf$VwTFNN%L-p0n<-gQh)kk6>bgNKkc(IM5o%JAd!}PlAIpE0yrcP8Y$$?B6 z*UtsAinljX2-3EuYhIX4GIrPOwGMy5VA{UkeZ?u|N)W>VyFk-|$~Zs1_PYw=`!e2) z$aC!VBpzxN5#tG>>iDDx5ChlZFQ*K!2;(m$oT&Y{;{&)M2t+Rb LoadAInto(_)); pathBoxB = AddInputRow(table, "Map B:", pathB, _ => LoadBInto(_)); @@ -144,10 +146,10 @@ TextBox AddInputRow(TableLayoutPanel table, string label, string? value, Action< // The label fills its fixed-height cell and centres its text vertically (MiddleLeft), so it lines // up with the text box. The box and button anchor left+right at their natural height and the // TableLayoutPanel centres them in the row. - table.Controls.Add(new Label { Text = label, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Margin = new(3, 0, 3, 0) }); - var box = new TextBox { Anchor = AnchorStyles.Left | AnchorStyles.Right, ReadOnly = true, Text = value ?? string.Empty, Margin = new(3, 0, 3, 0) }; + table.Controls.Add(new Label { Text = label, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, Margin = new(3, 5, 3, 5) }); + var box = new TextBox { Anchor = AnchorStyles.Left | AnchorStyles.Right, ReadOnly = true, Text = value ?? string.Empty, Margin = new(3, 5, 3, 5) }; table.Controls.Add(box); - var browse = new Button { Text = "Browse…", Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new(3, 0, 3, 0) }; + var browse = new Button { Text = "Browse…", Anchor = AnchorStyles.Left | AnchorStyles.Right, Margin = new(3, 5, 3, 5) }; browse.Click += (_, _) => { using var dialog = new OpenFileDialog From 7653541999ae7e8da4c060a8762351a45c51a732 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 27 Jun 2026 13:07:24 +1000 Subject: [PATCH 10/10] . --- ...rmsTests.About_dpiPercent=100.verified.png | Bin 0 -> 7459 bytes ...rmsTests.About_dpiPercent=150.verified.png | Bin 0 -> 7941 bytes .../FormsTests.DiffWindow.verified.png | Bin 15700 -> 0 bytes ...sts.DiffWindow_dpiPercent=100.verified.png | Bin 0 -> 15727 bytes ...sts.DiffWindow_dpiPercent=150.verified.png | Bin 0 -> 22708 bytes ...ts.MainWindow_dpiPercent=100.verified.png} | Bin ...sts.MainWindow_dpiPercent=150.verified.png | Bin 0 -> 26790 bytes src/GeoConvert.App.Tests/FormsTests.cs | 24 ++++- ...anelTests.Kml_dpiPercent=100.verified.png} | Bin ...PanelTests.Kml_dpiPercent=150.verified.png | Bin 0 -> 13810 bytes ...anelTests.Png_dpiPercent=100.verified.png} | Bin ...PanelTests.Png_dpiPercent=150.verified.png | Bin 0 -> 34842 bytes src/GeoConvert.App.Tests/OptionsPanelTests.cs | 16 ++- src/GeoConvert.App.Tests/WinFormsSnapshot.cs | 25 ++++- src/GeoConvert.App/Ui/AboutForm.cs | 91 +++++++++++++++++ src/GeoConvert.App/Ui/DiffForm.cs | 94 +++++++++++------- src/GeoConvert.App/Ui/MainForm.cs | 20 ++-- 17 files changed, 210 insertions(+), 60 deletions(-) create mode 100644 src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=100.verified.png create mode 100644 src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=150.verified.png delete mode 100644 src/GeoConvert.App.Tests/FormsTests.DiffWindow.verified.png create mode 100644 src/GeoConvert.App.Tests/FormsTests.DiffWindow_dpiPercent=100.verified.png create mode 100644 src/GeoConvert.App.Tests/FormsTests.DiffWindow_dpiPercent=150.verified.png rename src/GeoConvert.App.Tests/{FormsTests.MainWindow.verified.png => FormsTests.MainWindow_dpiPercent=100.verified.png} (100%) create mode 100644 src/GeoConvert.App.Tests/FormsTests.MainWindow_dpiPercent=150.verified.png rename src/GeoConvert.App.Tests/{OptionsPanelTests.Kml.verified.png => OptionsPanelTests.Kml_dpiPercent=100.verified.png} (100%) create mode 100644 src/GeoConvert.App.Tests/OptionsPanelTests.Kml_dpiPercent=150.verified.png rename src/GeoConvert.App.Tests/{OptionsPanelTests.Png.verified.png => OptionsPanelTests.Png_dpiPercent=100.verified.png} (100%) create mode 100644 src/GeoConvert.App.Tests/OptionsPanelTests.Png_dpiPercent=150.verified.png create mode 100644 src/GeoConvert.App/Ui/AboutForm.cs diff --git a/src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=100.verified.png b/src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=100.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..53526967eda98fc1a8d2e1f57eaddee9a88a3688 GIT binary patch literal 7459 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 YcU|>5(&($;aSd?S)bdu{ji)dE1E^*AZU6uP literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=150.verified.png b/src/GeoConvert.App.Tests/FormsTests.About_dpiPercent=150.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..d556968f3e5197306fe965019d9b09fe9ca441f9 GIT binary patch literal 7941 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 z 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 z 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 G#s2^f21j!M literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png b/src/GeoConvert.App.Tests/FormsTests.MainWindow_dpiPercent=100.verified.png similarity index 100% rename from src/GeoConvert.App.Tests/FormsTests.MainWindow.verified.png rename to src/GeoConvert.App.Tests/FormsTests.MainWindow_dpiPercent=100.verified.png diff --git a/src/GeoConvert.App.Tests/FormsTests.MainWindow_dpiPercent=150.verified.png b/src/GeoConvert.App.Tests/FormsTests.MainWindow_dpiPercent=150.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..19531d83e7efe7a1b27bead01cb1465ce908646a GIT binary patch literal 26790 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 literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/FormsTests.cs b/src/GeoConvert.App.Tests/FormsTests.cs index 9642c08..6813f99 100644 --- a/src/GeoConvert.App.Tests/FormsTests.cs +++ b/src/GeoConvert.App.Tests/FormsTests.cs @@ -3,17 +3,33 @@ namespace GeoConvert.App.Tests; [NotInParallel] public class FormsTests { + // Each window is snapshotted at 100% and 150% scale so DPI-only layout breaks (fixed-pixel sizes that + // don't scale with the font, as the diff window's input rows once did) are caught. [Test] - public Task MainWindow() => + [Arguments(100)] + [Arguments(150)] + public Task MainWindow(int dpiPercent) => // The whole main window: menu, the "no map loaded" bar, the (empty) preview and the fixed-width // options column on the right. - Verify(WinFormsSnapshot.Render(() => new MainForm(SeededSettings(), null), 1000, 680)); + Verify(WinFormsSnapshot.Render(() => new MainForm(SeededSettings(), null), 1000, 680, dpiPercent / 100f)) + .UseParameters(dpiPercent); [Test] - public Task DiffWindow() => + [Arguments(100)] + [Arguments(150)] + public Task DiffWindow(int dpiPercent) => // The empty compare window: the two file pickers, the mode/projection/colour toolbar, and the // (empty) preview / summary panes. - Verify(WinFormsSnapshot.Render(() => new DiffForm(), 1000, 680)); + Verify(WinFormsSnapshot.Render(() => new DiffForm(), 1000, 680, dpiPercent / 100f)) + .UseParameters(dpiPercent); + + [Test] + [Arguments(100)] + [Arguments(150)] + public Task About(int dpiPercent) => + // The (auto-sizing) About dialog: title, description, the clickable project link and OK button. + Verify(WinFormsSnapshot.Render(() => new AboutForm(), 420, 220, dpiPercent / 100f)) + .UseParameters(dpiPercent); static SettingsManager SeededSettings() { diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Kml_dpiPercent=100.verified.png similarity index 100% rename from src/GeoConvert.App.Tests/OptionsPanelTests.Kml.verified.png rename to src/GeoConvert.App.Tests/OptionsPanelTests.Kml_dpiPercent=100.verified.png diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Kml_dpiPercent=150.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Kml_dpiPercent=150.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..09852539dd58035fffd6f601d2fab329b0edea83 GIT binary patch literal 13810 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 XM8meTEl^Jb*tH{gd&TER{_=kSAU|mz literal 0 HcmV?d00001 diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Png_dpiPercent=100.verified.png similarity index 100% rename from src/GeoConvert.App.Tests/OptionsPanelTests.Png.verified.png rename to src/GeoConvert.App.Tests/OptionsPanelTests.Png_dpiPercent=100.verified.png diff --git a/src/GeoConvert.App.Tests/OptionsPanelTests.Png_dpiPercent=150.verified.png b/src/GeoConvert.App.Tests/OptionsPanelTests.Png_dpiPercent=150.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..60b7020fc4199af7635eba4b5d3bbb81049695d6 GIT binary patch literal 34842 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 Lv9I9WBd7liASe + [Arguments(100)] + [Arguments(150)] + public Task Kml(int dpiPercent) => // A plain vector format: the always-on Projection radios plus the Output, Simplify and // format-note sections (no image options). - Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 480, 560)); + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Kml), 480, 560, dpiPercent / 100f)) + .UseParameters(dpiPercent); [Test] - public Task Png() => + [Arguments(100)] + [Arguments(150)] + public Task Png(int dpiPercent) => // The image formats reveal the full render-options section (projection, strokes, labels, colours) // plus the PNG sub-section, so this covers most of the options UI and its show/hide logic. - Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 480, 1320)); + Verify(WinFormsSnapshot.Render(() => Panel(GeoFormat.Png), 480, 1320, dpiPercent / 100f)) + .UseParameters(dpiPercent); static OptionsPanel Panel(GeoFormat format) { diff --git a/src/GeoConvert.App.Tests/WinFormsSnapshot.cs b/src/GeoConvert.App.Tests/WinFormsSnapshot.cs index f710323..51242fe 100644 --- a/src/GeoConvert.App.Tests/WinFormsSnapshot.cs +++ b/src/GeoConvert.App.Tests/WinFormsSnapshot.cs @@ -6,12 +6,18 @@ namespace GeoConvert.App.Tests; /// thread-agnostic and handed back. Forms are briefly shown off-screen so their OnLoad (e.g. the /// diff window's splitter layout) runs before the draw; plain controls just get a handle and a layout /// pass. Nothing is verified here — the caller passes the bitmap to Verify. +/// +/// A scale > 1 simulates a higher display DPI: WinForms applies DPI scaling through +/// , so calling it directly reproduces the same scaling behaviour +/// (including the class of bug where fixed-pixel sizes don't scale while font-sized controls do) without +/// needing an actual high-DPI monitor. scale 1.25 == 120 DPI / 125%, scale 1.5 == 144 DPI / 150%. +/// /// static class WinFormsSnapshot { static bool stylesEnabled; - public static Bitmap Render(Func factory, int width, int height) + public static Bitmap Render(Func factory, int width, int height, float scale = 1f) { Bitmap? result = null; Exception? failure = null; @@ -29,7 +35,13 @@ public static Bitmap Render(Func factory, int width, int height) form.StartPosition = FormStartPosition.Manual; form.ShowInTaskbar = false; form.Location = new(-5000, -5000); - form.Size = new(width, height); + // An AutoSize form sizes itself to its content — forcing a size would clip it. + if (!form.AutoSize) + { + form.Size = new(width, height); + } + + Rescale(form, scale); form.Show(); Application.DoEvents(); result = Draw(form); @@ -39,6 +51,7 @@ public static Bitmap Render(Func factory, int width, int height) { control.Size = new(width, height); _ = control.Handle; + Rescale(control, scale); control.PerformLayout(); Application.DoEvents(); result = Draw(control); @@ -62,6 +75,14 @@ public static Bitmap Render(Func factory, int width, int height) return result!; } + static void Rescale(Control control, float scale) + { + if (scale != 1f) + { + control.Scale(new SizeF(scale, scale)); + } + } + static Bitmap Draw(Control control) { var bounds = control.ClientRectangle; diff --git a/src/GeoConvert.App/Ui/AboutForm.cs b/src/GeoConvert.App/Ui/AboutForm.cs new file mode 100644 index 0000000..13d533e --- /dev/null +++ b/src/GeoConvert.App/Ui/AboutForm.cs @@ -0,0 +1,91 @@ +using System.Diagnostics; + +namespace GeoConvert.App; + +///

+/// The Help ▸ About dialog. A plain form rather than a MessageBox, so it stays silent (the information +/// MessageBox plays a system sound) and can host a clickable link to the project. Auto-sizes to its +/// content so it lays out correctly at any display DPI. +/// +sealed class AboutForm : Form +{ + const string projectUrl = "https://github.com/SimonCropp/GeoConvert"; + + public AboutForm() + { + Text = "About GeoConvert"; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterParent; + MinimizeBox = false; + MaximizeBox = false; + ShowInTaskbar = false; + AutoSize = true; + AutoSizeMode = AutoSizeMode.GrowAndShrink; + + // Not docked: a Dock=Fill panel inside an AutoSize form is circular (each defers to the other) + // and resolves a few pixels short. Undocked + AutoSize, the form simply sizes to the panel. + var layout = new TableLayoutPanel + { + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + ColumnCount = 1, + Padding = new(16), + }; + + var title = new Label + { + Text = "GeoConvert", + AutoSize = true, + Font = new(Font.FontFamily, Font.Size + 3, FontStyle.Bold), + Margin = new(3, 3, 3, 8), + }; + + var description = new Label + { + Text = + "Convert maps between GeoJSON, TopoJSON, Shapefile, FlatGeobuf, KML/KMZ, GPX, WKT, WKB,\n" + + "CSV and GeoParquet; render to PNG/SVG; and compare two maps.", + AutoSize = true, + Margin = new(3, 3, 3, 10), + }; + + var link = new LinkLabel + { + Text = projectUrl, + AutoSize = true, + Margin = new(3, 3, 3, 14), + }; + link.LinkClicked += (_, _) => OpenProject(); + + var ok = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + AutoSize = true, + Anchor = AnchorStyles.Right, + Margin = new(3), + }; + + layout.Controls.Add(title); + layout.Controls.Add(description); + layout.Controls.Add(link); + layout.Controls.Add(ok); + Controls.Add(layout); + + AcceptButton = ok; + CancelButton = ok; + } + + static void OpenProject() + { + try + { + // UseShellExecute lets the OS open the URL in the default browser. + Process.Start(new ProcessStartInfo(projectUrl) { UseShellExecute = true }); + } + catch + { + // Opening a link is best-effort; if no handler is registered, do nothing rather than crash. + } + } +} diff --git a/src/GeoConvert.App/Ui/DiffForm.cs b/src/GeoConvert.App/Ui/DiffForm.cs index 8d6fc29..a2e07d3 100644 --- a/src/GeoConvert.App/Ui/DiffForm.cs +++ b/src/GeoConvert.App/Ui/DiffForm.cs @@ -167,52 +167,74 @@ TextBox AddInputRow(TableLayoutPanel table, string label, string? value, Action< return box; } - FlowLayoutPanel BuildToolbar() + TableLayoutPanel BuildToolbar() { - var bar = new FlowLayoutPanel + // A single-row TableLayoutPanel (not a FlowLayoutPanel) so each item anchors left and the cell + // centres it vertically — labels line up with the combos at any DPI. A FlowLayoutPanel top-aligns + // its children, which needed hand-tuned top margins that drifted once the controls scaled. + var bar = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, - WrapContents = false, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + ColumnCount = 10, + RowCount = 1, Padding = new(6, 2, 6, 2), }; + for (var column = 0; column < bar.ColumnCount; column++) + { + bar.ColumnStyles.Add(new(SizeType.AutoSize)); + } - bar.Controls.Add(new Label { Text = "Mode", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(3, 8, 3, 3) }); - bar.Controls.Add(Combos.Build( - [(DiffMode.Overlay, "Overlay"), (DiffMode.SideBySide, "Side by side")], - mode, - value => - { - mode = value; - _ = RenderAsync(); - })); - - bar.Controls.Add(new Label { Text = "Projection", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 3, 3) }); - bar.Controls.Add(Combos.Build( - OptionChoices.Projections, - settings.Projection, - value => - { - settings.Projection = value; - _ = RenderAsync(); - })); - - bar.Controls.Add(new Label { Text = "Resolution", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 3, 3) }); - bar.Controls.Add(Combos.Build( - OptionChoices.Dimensions, - settings.MaxDimension > 0 ? settings.MaxDimension : 2048, - value => - { - settings.MaxDimension = value; - _ = RenderAsync(); - })); + void Add(Control control, int gapLeft) + { + control.Anchor = AnchorStyles.Left; + control.Margin = new(gapLeft, 3, 3, 3); + bar.Controls.Add(control); + } - bar.Controls.Add(new Label { Text = "A", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 1, 3) }); + Add(new Label { Text = "Mode", AutoSize = true }, 3); + Add( + Combos.Build( + [(DiffMode.Overlay, "Overlay"), (DiffMode.SideBySide, "Side by side")], + mode, + value => + { + mode = value; + _ = RenderAsync(); + }), + 0); + + Add(new Label { Text = "Projection", AutoSize = true }, 10); + Add( + Combos.Build( + OptionChoices.Projections, + settings.Projection, + value => + { + settings.Projection = value; + _ = RenderAsync(); + }), + 0); + + Add(new Label { Text = "Resolution", AutoSize = true }, 10); + Add( + Combos.Build( + OptionChoices.Dimensions, + settings.MaxDimension > 0 ? settings.MaxDimension : 2048, + value => + { + settings.MaxDimension = value; + _ = RenderAsync(); + }), + 0); + + Add(new Label { Text = "A", AutoSize = true }, 10); swatchA = ColorSwatch(() => colorA, _ => colorA = _); - bar.Controls.Add(swatchA); - bar.Controls.Add(new Label { Text = "B", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new(8, 8, 1, 3) }); + Add(swatchA, 1); + Add(new Label { Text = "B", AutoSize = true }, 8); swatchB = ColorSwatch(() => colorB, _ => colorB = _); - bar.Controls.Add(swatchB); + Add(swatchB, 1); return bar; } diff --git a/src/GeoConvert.App/Ui/MainForm.cs b/src/GeoConvert.App/Ui/MainForm.cs index 6f3b382..3816b80 100644 --- a/src/GeoConvert.App/Ui/MainForm.cs +++ b/src/GeoConvert.App/Ui/MainForm.cs @@ -320,20 +320,12 @@ void UnassociateFromMenu() } } - void ShowAbout() => - MessageBox.Show( - this, - """ - GeoConvert - - Convert maps between GeoJSON, TopoJSON, Shapefile, FlatGeobuf, KML/KMZ, GPX, WKT, WKB, CSV and - GeoParquet; render to PNG/SVG; and compare two maps. - - Tools ▸ Compare maps… diffs two files. From a terminal: geoconvert-app --help. - """, - "About GeoConvert", - MessageBoxButtons.OK, - MessageBoxIcon.Information); + void ShowAbout() + { + // A custom form (not a MessageBox) so the dialog is silent and can show a clickable project link. + using var about = new AboutForm(); + about.ShowDialog(this); + } void UpdateState() {