diff --git a/src/Filee.Engines/Geo/GeoCsvFormat.cs b/src/Filee.Engines/Geo/GeoCsvFormat.cs index c7c9e197c..b8070072f 100644 --- a/src/Filee.Engines/Geo/GeoCsvFormat.cs +++ b/src/Filee.Engines/Geo/GeoCsvFormat.cs @@ -142,20 +142,23 @@ public static void Write(GeoDataset dataset, string path, CancellationToken canc { var columns = dataset.Columns(); var geometries = dataset.Features.Select(f => f.Geometry).OfType().ToList(); - // An attribute that already is a longitude or latitude column would be mistaken for the geometry when the CSV is - // read again, so such a dataset gets its geometry as WKT. - var ambiguous = columns.Any(c => LonNames.Contains(Key(c)) || LatNames.Contains(Key(c))); + // An attribute that already is a longitude, latitude or WKT column would be mistaken for the geometry when the + // CSV is read again, so such a dataset gets its geometry as WKT. + var ambiguous = columns.Any(c => LonNames.Contains(Key(c)) || LatNames.Contains(Key(c)) || WktNames.Contains(Key(c))); var points = !ambiguous && geometries.Count > 0 && geometries.All(g => g is GeoPoint); var withHeight = points && geometries.Any(g => g.HasZ); - string[] geometryColumns = !points ? ["wkt"] : withHeight ? ["longitude", "latitude", "elevation"] : ["longitude", "latitude"]; - // An attribute that already has one of these names keeps it; the geometry column gets "_geo" after it. + // The generated WKT header uses the reader's first candidate when an attribute already does, so its leading column wins on read. + var wktColumn = columns.Any(c => Key(c) == WktNames[0]) ? "wkt_geo" : "wkt"; + string[] geometryColumns = !points ? [wktColumn] : withHeight ? ["longitude", "latitude", "elevation"] : ["longitude", "latitude"]; + // An attribute that already has one of these names keeps it; the geometry column gets a suffix after it. var taken = new HashSet(columns, StringComparer.OrdinalIgnoreCase); var geometryNames = geometryColumns.Select(name => { var unique = name; + var suffix = Key(name) == WktNames[0] ? "_" : "_geo"; while (!taken.Add(unique)) - unique += "_geo"; + unique += suffix; return unique; }).ToList(); diff --git a/tests/Filee.Engines.Tests/GeoFormatTests.cs b/tests/Filee.Engines.Tests/GeoFormatTests.cs index 2c77867a0..769e5bece 100644 --- a/tests/Filee.Engines.Tests/GeoFormatTests.cs +++ b/tests/Filee.Engines.Tests/GeoFormatTests.cs @@ -646,6 +646,44 @@ public async Task Csv_output_uses_wkt_when_an_attribute_is_already_called_longit Assert.Equal(111, Features(doc)[0].GetProperty("properties").GetProperty("LONGITUDE").GetInt64()); } + [Theory] + [InlineData("shape")] + [InlineData("wkt")] + [InlineData("geometry")] + [InlineData("wkt_geo")] + [InlineData("WKT-GEO")] + [InlineData("WKT GEO")] + public async Task Csv_output_uses_wkt_when_an_attribute_is_already_called_a_wkt_column_name_so_it_reads_back_unchanged(string attribute) + { + var text = $$$"""{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"{{{attribute}}}":"POINT (9 9)","n":1},"geometry":{"type":"Point","coordinates":[1,2]}}]}"""; + var csv = (await fx.ConvertOkAsync(GeoFixtures.WriteText(fx, attribute + ".geojson", text), "csv")).Single(); + using var doc = await ToGeoJsonAsync(csv); + Assert.Equal([1.0, 2.0], GeoFixtures.Position(Features(doc)[0].GetProperty("geometry"))); + Assert.Equal("POINT (9 9)", Features(doc)[0].GetProperty("properties").GetProperty(attribute).GetString()); + } + + [Fact] + public async Task Csv_output_keeps_geometry_and_attributes_when_wkt_and_wkt_geo_are_both_attribute_names() + { + var text = """{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"wkt":"POINT (9 9)","wkt_geo":"POINT (8 8)","n":1},"geometry":{"type":"Point","coordinates":[1,2]}}]}"""; + var csv = (await fx.ConvertOkAsync(GeoFixtures.WriteText(fx, "wkt-both.geojson", text), "csv")).Single(); + using var doc = await ToGeoJsonAsync(csv); + Assert.Equal([1.0, 2.0], GeoFixtures.Position(Features(doc)[0].GetProperty("geometry"))); + Assert.Equal("POINT (9 9)", Features(doc)[0].GetProperty("properties").GetProperty("wkt").GetString()); + Assert.Equal("POINT (8 8)", Features(doc)[0].GetProperty("properties").GetProperty("wkt_geo").GetString()); + } + + [Fact] + public async Task Csv_wkt_column_keeps_precedence_over_an_earlier_shape_column() + { + var (doc, _) = await CsvToGeoJsonAsync("shape,id,wkt\n\"POINT (9 9)\",1,\"POINT (1 2)\"\n"); + using (doc) + { + Assert.Equal([1.0, 2.0], GeoFixtures.Position(Features(doc)[0].GetProperty("geometry"))); + Assert.Equal("POINT (9 9)", Features(doc)[0].GetProperty("properties").GetProperty("shape").GetString()); + } + } + [Fact] public async Task Csv_output_quotes_commas_quotes_and_line_breaks() {