Skip to content

Added support for numeric Feature.Id values (Issue #35) #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
build:

name: 'Build and Test'
runs-on: windows-2019
runs-on: windows-2025

env:
VSTEST_CONNECTION_TIMEOUT: 900
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/continous-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
benchmark:
name: Continuous benchmarking
runs-on: windows-2019
runs-on: windows-2025

defaults:
run:
Expand Down Expand Up @@ -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
# Run `github-action-benchmark` action
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 47 additions & 1 deletion src/GeoJSON.Text.Test.Unit/Feature/FeatureTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using GeoJSON.Text.Feature;
using GeoJSON.Text.Geometry;
using NUnit.Framework;
using System;
Expand All @@ -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<Text.Feature.Feature>(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);
}
Expand Down Expand Up @@ -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<Text.Feature.Feature>(leftJson);

var rightFeature = new Text.Feature.Feature(geometry, null, 42);
var rightJson = JsonSerializer.Serialize(rightFeature);
var right = JsonSerializer.Deserialize<Text.Feature.Feature>(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<Text.Feature.Feature>(leftJson);

rightFeature = new Text.Feature.Feature(geometry, GetPropertiesInRandomOrder(), uint.MaxValue);
rightJson = JsonSerializer.Serialize(rightFeature);
right = JsonSerializer.Deserialize<Text.Feature.Feature>(rightJson);

Assert_Are_Equal(left, right); // assert id's + properties doesn't influence comparison and hashcode
}

[Test]
public void Feature_Equals_Null_Issue94()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "Feature",
"id" : 17,
"geometry": {
"type": "Point",
"coordinates": [125.6, 10.1]
},
"properties": {
"name": "Dinagat Islands"
}
}
6 changes: 3 additions & 3 deletions src/GeoJSON.Text.Test.Unit/Feature/GenericFeatureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/GeoJSON.Text.Test.Unit/GeoJSON.Text.Test.Unit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<ItemGroup>
<EmbeddedResource Include="Feature\FeatureCollectionTests_Can_Deserialize.json" />
<EmbeddedResource Include="Feature\FeatureCollectionTests_Can_DeserializeGeneric.json" />
<EmbeddedResource Include="Feature\FeatureTests_Can_Deserialize_Point_Feature_With_Numeric_Id.json" />
<EmbeddedResource Include="Feature\FeatureTests_Can_Deserialize_Point_Feature.json" />
<EmbeddedResource Include="Feature\FeatureTests_Can_Serialize_Dictionary_Subclass.json" />
<EmbeddedResource Include="Feature\FeatureTests_Can_Serialize_LineString_Feature.json" />
Expand Down
25 changes: 25 additions & 0 deletions src/GeoJSON.Text/Converters/FeatureIdConverter.cs
Original file line number Diff line number Diff line change
@@ -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<FeatureId>
{
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);
}
}
}
46 changes: 37 additions & 9 deletions src/GeoJSON.Text/Feature/Feature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace GeoJSON.Text.Feature
public class Feature<TGeometry, TProps> : GeoJSONObject, IEquatable<Feature<TGeometry, TProps>>
where TGeometry : IGeometryObject
{
private string _id;
private FeatureId _id;
private bool _idHasValue = false;
private TGeometry _geometry;
private bool _geometryHasValue = false;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -195,12 +196,12 @@ public Feature()

}

public Feature(IGeometryObject geometry, IDictionary<string, object> properties = null, string id = null)
public Feature(IGeometryObject geometry, IDictionary<string, object> 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)
{
}
Expand All @@ -225,7 +226,7 @@ public Feature()
/// <param name="geometry">The Geometry Object.</param>
/// <param name="properties">The properties.</param>
/// <param name="id">The (optional) identifier.</param>
public Feature(TGeometry geometry, IDictionary<string, object> properties = null, string id = null)
public Feature(TGeometry geometry, IDictionary<string, object> properties = null, FeatureId id = null)
: base(geometry, properties ?? new Dictionary<string, object>(), id)
{
}
Expand All @@ -236,7 +237,7 @@ public Feature(TGeometry geometry, IDictionary<string, object> properties = null
/// <param name="geometry">The Geometry Object.</param>
/// <param name="properties">The properties.</param>
/// <param name="id">The (optional) identifier.</param>
public Feature(IGeometryObject geometry, IDictionary<string, object> properties = null, string id = null)
public Feature(IGeometryObject geometry, IDictionary<string, object> properties = null, FeatureId id = null)
: base((TGeometry)geometry, properties ?? new Dictionary<string, object>(), id)
{
}
Expand All @@ -250,7 +251,7 @@ public Feature(IGeometryObject geometry, IDictionary<string, object> properties
/// properties
/// </param>
/// <param name="id">The (optional) identifier.</param>
public Feature(TGeometry geometry, object properties, string id = null)
public Feature(TGeometry geometry, object properties, FeatureId id = null)
: this(geometry, GetDictionaryOfPublicProperties(properties), id)
{
}
Expand Down Expand Up @@ -312,5 +313,32 @@ public override int GetHashCode()
{
return !(left?.Equals(right) ?? right is null);
}
}

public sealed class FeatureId : IEquatable<FeatureId>
{
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);
}
}
Loading