diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 06fc27e..43c38ef 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -19,7 +19,7 @@ jobs: build: name: 'Build and Test' - runs-on: windows-2019 + runs-on: windows-2025 env: VSTEST_CONNECTION_TIMEOUT: 900 diff --git a/.github/workflows/continous-benchmark.yml b/.github/workflows/continous-benchmark.yml index 9b84a0e..3b455a8 100644 --- a/.github/workflows/continous-benchmark.yml +++ b/.github/workflows/continous-benchmark.yml @@ -6,7 +6,7 @@ on: jobs: benchmark: name: Continuous benchmarking - runs-on: windows-2019 + runs-on: windows-2025 defaults: run: @@ -43,4 +43,4 @@ jobs: # GitHub API token to make a commit comment github-token: ${{ secrets.GITHUB_TOKEN }} # Upload the updated cache file for the next job by actions/cache - # Run `github-action-benchmark` action \ No newline at end of file + # Run `github-action-benchmark` action diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0be7637..2778ea3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: deploy: name: 'Deploy to Nuget' if: github.event_name == 'release' - runs-on: windows-2019 + runs-on: windows-2025 steps: - name: Validate release version diff --git a/src/GeoJSON.Text.Test.Unit/Feature/FeatureCollectionTests.cs b/src/GeoJSON.Text.Test.Unit/Feature/FeatureCollectionTests.cs index c3116ab..e3e0c00 100644 --- a/src/GeoJSON.Text.Test.Unit/Feature/FeatureCollectionTests.cs +++ b/src/GeoJSON.Text.Test.Unit/Feature/FeatureCollectionTests.cs @@ -134,7 +134,7 @@ public void FeatureCollection_Test_IndexOf() var actualId = actualFeature.Id; var actualIndex = model.Features.IndexOf(actualFeature); - var expectedId = expectedIds[i]; + FeatureId expectedId = expectedIds[i]; var expectedIndex = expectedIndexes[i]; Assert.AreEqual(expectedId, actualId); diff --git a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs index 3df4536..cd3e03b 100644 --- a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs +++ b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs @@ -1,3 +1,4 @@ +using GeoJSON.Text.Feature; using GeoJSON.Text.Geometry; using NUnit.Framework; using System; @@ -24,7 +25,26 @@ public void Can_Deserialize_Point_Feature() Assert.IsTrue(feature.Properties.ContainsKey("name")); Assert.AreEqual(feature.Properties["name"].ToString(), "Dinagat Islands"); - Assert.AreEqual("test-id", feature.Id); + Assert.AreEqual((FeatureId)"test-id", feature.Id); + + Assert.AreEqual(GeoJSONObjectType.Point, feature.Geometry.Type); + } + + [Test] + public void Can_Deserialize_Point_Feature_With_Numeric_Id() + { + var json = GetExpectedJson(); + + var feature = JsonSerializer.Deserialize(json); + + Assert.IsNotNull(feature); + Assert.IsNotNull(feature.Properties); + Assert.IsTrue(feature.Properties.Any()); + + Assert.IsTrue(feature.Properties.ContainsKey("name")); + Assert.AreEqual(feature.Properties["name"].ToString(), "Dinagat Islands"); + + Assert.AreEqual((FeatureId)17, feature.Id); Assert.AreEqual(GeoJSONObjectType.Point, feature.Geometry.Type); } @@ -354,6 +374,32 @@ public void Serialized_And_Deserialized_Feature_Equals_And_Share_HashCode() Assert_Are_Equal(left, right); // assert id's + properties doesn't influence comparison and hashcode } + [Test] + public void Serialized_And_Deserialized_Feature_With_Numeric_Id_Equals_And_Share_HashCode() + { + var geometry = GetGeometry(); + + var leftFeature = new Text.Feature.Feature(geometry, null, 42); + var leftJson = JsonSerializer.Serialize(leftFeature); + var left = JsonSerializer.Deserialize(leftJson); + + var rightFeature = new Text.Feature.Feature(geometry, null, 42); + var rightJson = JsonSerializer.Serialize(rightFeature); + var right = JsonSerializer.Deserialize(rightJson); + + Assert_Are_Equal(left, right); // assert id's doesn't influence comparison and hashcode + + leftFeature = new Text.Feature.Feature(geometry, GetPropertiesInRandomOrder(), uint.MaxValue); + leftJson = JsonSerializer.Serialize(leftFeature); + left = JsonSerializer.Deserialize(leftJson); + + rightFeature = new Text.Feature.Feature(geometry, GetPropertiesInRandomOrder(), uint.MaxValue); + rightJson = JsonSerializer.Serialize(rightFeature); + right = JsonSerializer.Deserialize(rightJson); + + Assert_Are_Equal(left, right); // assert id's + properties doesn't influence comparison and hashcode + } + [Test] public void Feature_Equals_Null_Issue94() { diff --git a/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json new file mode 100644 index 0000000..4d89a4b --- /dev/null +++ b/src/GeoJSON.Text.Test.Unit/Feature/FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json @@ -0,0 +1,11 @@ +{ + "type": "Feature", + "id" : 17, + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands" + } +} \ No newline at end of file diff --git a/src/GeoJSON.Text.Test.Unit/Feature/GenericFeatureTests.cs b/src/GeoJSON.Text.Test.Unit/Feature/GenericFeatureTests.cs index d19d051..cfe9cb2 100644 --- a/src/GeoJSON.Text.Test.Unit/Feature/GenericFeatureTests.cs +++ b/src/GeoJSON.Text.Test.Unit/Feature/GenericFeatureTests.cs @@ -26,7 +26,7 @@ public void Can_Deserialize_Point_Feature() string name = feature.Properties["name"].ToString(); Assert.AreEqual("Dinagat Islands", name); - Assert.AreEqual("test-id", feature.Id); + Assert.AreEqual((FeatureId)"test-id", feature.Id); Assert.AreEqual(GeoJSONObjectType.Point, feature.Geometry.Type); Assert.AreEqual(125.6, feature.Geometry.Coordinates.Longitude); @@ -47,7 +47,7 @@ public void Can_Deserialize_LineString_Feature() Assert.IsTrue(feature.Properties.ContainsKey("name")); Assert.AreEqual("Dinagat Islands", feature.Properties["name"].ToString()); - Assert.AreEqual("test-id", feature.Id); + Assert.AreEqual((FeatureId)"test-id", feature.Id); Assert.AreEqual(GeoJSONObjectType.LineString, feature.Geometry.Type); @@ -103,7 +103,7 @@ public void Can_Deserialize_Typed_Point_Feature() Assert.AreEqual(feature.Properties.Name, "Dinagat Islands"); Assert.AreEqual(feature.Properties.Value, 4.2); - Assert.AreEqual(feature.Id, "test-id"); + Assert.AreEqual(feature.Id, (FeatureId)"test-id"); Assert.AreEqual(feature.Geometry.Type, GeoJSONObjectType.Point); } diff --git a/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj b/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj index af09af1..f4c65f8 100644 --- a/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj +++ b/src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj @@ -39,6 +39,7 @@ + diff --git a/src/GeoJSON.Text/Converters/FeatureIdConverter.cs b/src/GeoJSON.Text/Converters/FeatureIdConverter.cs new file mode 100644 index 0000000..effb8df --- /dev/null +++ b/src/GeoJSON.Text/Converters/FeatureIdConverter.cs @@ -0,0 +1,25 @@ +// Copyright © Joerg Battermann 2014, Matt Hunt 2017 + +using GeoJSON.Text.Feature; +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GeoJSON.Text.Converters +{ + internal class FeatureIdConverter : JsonConverter + { + public override FeatureId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.TryGetUInt64(out var value) ? value : throw new JsonException(), + _ => throw new JsonException(), + }; + + public override void Write(Utf8JsonWriter writer, FeatureId value, JsonSerializerOptions options) + { + if (value.IsNumeric) writer.WriteNumberValue(value); + else writer.WriteStringValue(value); + } + } +} \ No newline at end of file diff --git a/src/GeoJSON.Text/Feature/Feature.cs b/src/GeoJSON.Text/Feature/Feature.cs index ba4d2bd..ba45a07 100644 --- a/src/GeoJSON.Text/Feature/Feature.cs +++ b/src/GeoJSON.Text/Feature/Feature.cs @@ -21,7 +21,7 @@ namespace GeoJSON.Text.Feature public class Feature : GeoJSONObject, IEquatable> where TGeometry : IGeometryObject { - private string _id; + private FeatureId _id; private bool _idHasValue = false; private TGeometry _geometry; private bool _geometryHasValue = false; @@ -33,14 +33,14 @@ public Feature() } - public Feature(TGeometry geometry, TProps properties, string id = null) + public Feature(TGeometry geometry, TProps properties, FeatureId id = null) { Geometry = geometry; Properties = properties; Id = id; } - public Feature(IGeometryObject geometry, TProps properties, string id = null) + public Feature(IGeometryObject geometry, TProps properties, FeatureId id = null) { Geometry = (TGeometry)geometry; Properties = properties; @@ -53,7 +53,8 @@ public Feature(IGeometryObject geometry, TProps properties, string id = null) [JsonPropertyName( "id")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string Id { + [JsonConverter(typeof(FeatureIdConverter))] + public FeatureId Id { get { return _id; @@ -195,12 +196,12 @@ public Feature() } - public Feature(IGeometryObject geometry, IDictionary properties = null, string id = null) + public Feature(IGeometryObject geometry, IDictionary properties = null, FeatureId id = null) : base(geometry, properties, id) { } - public Feature(IGeometryObject geometry, object properties, string id = null) + public Feature(IGeometryObject geometry, object properties, FeatureId id = null) : base(geometry, properties, id) { } @@ -225,7 +226,7 @@ public Feature() /// The Geometry Object. /// The properties. /// The (optional) identifier. - public Feature(TGeometry geometry, IDictionary properties = null, string id = null) + public Feature(TGeometry geometry, IDictionary properties = null, FeatureId id = null) : base(geometry, properties ?? new Dictionary(), id) { } @@ -236,7 +237,7 @@ public Feature(TGeometry geometry, IDictionary properties = null /// The Geometry Object. /// The properties. /// The (optional) identifier. - public Feature(IGeometryObject geometry, IDictionary properties = null, string id = null) + public Feature(IGeometryObject geometry, IDictionary properties = null, FeatureId id = null) : base((TGeometry)geometry, properties ?? new Dictionary(), id) { } @@ -250,7 +251,7 @@ public Feature(IGeometryObject geometry, IDictionary properties /// properties /// /// The (optional) identifier. - public Feature(TGeometry geometry, object properties, string id = null) + public Feature(TGeometry geometry, object properties, FeatureId id = null) : this(geometry, GetDictionaryOfPublicProperties(properties), id) { } @@ -312,5 +313,32 @@ public override int GetHashCode() { return !(left?.Equals(right) ?? right is null); } + } + + public sealed class FeatureId : IEquatable + { + private readonly string _strId; + private readonly ulong? _numId; + + private FeatureId(string str, ulong? num) => (_strId, _numId) = (str, num); + + public static implicit operator FeatureId(string str) => new(str, null); + public static implicit operator FeatureId(ulong num) => new(null, num); + + public bool IsNumeric => _numId.HasValue; + public bool IsString => !IsNumeric; + + public static implicit operator string(FeatureId id) => id.IsString ? id._strId : throw new InvalidCastException(); + public static implicit operator ulong(FeatureId id) => id.IsNumeric ? id._numId.Value : throw new InvalidCastException(); + + public override int GetHashCode() => (_strId.GetHashCode() * 397) ^ _numId.GetHashCode(); + + public bool Equals(FeatureId other) => _strId == other._strId && _numId == other._numId; + + public override bool Equals(object obj) => obj is FeatureId id && Equals(id); + + public static bool operator ==(FeatureId left, FeatureId right) => left.Equals(right); + + public static bool operator !=(FeatureId left, FeatureId right) => !(left == right); } }