Skip to content
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

Add support for Detour Road Events and upgrade library to NET 9 #18

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# WZDx .NET Library

This repository contains the source code for IBI Group's [.NET 6.0](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-6) [WZDx (Work Zone Data Exchange)](https://github.com/usdot-jpo-ode/wzdx) class library, `IBI.WZDx`.
This repository contains the source code for IBI Group's [.NET 9.0](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview) [WZDx (Work Zone Data Exchange)](https://github.com/usdot-jpo-ode/wzdx) class library, `IBI.WZDx`.

## About

The `IBI.WZDx` class library provides models and utitlies for producing and consuming [Work Zone Data Exchange (WZDx)](https://github.com/usdot-jpo-ode/wzdx) data feeds.
The `IBI.WZDx` class library provides models and utilities for producing and consuming [Work Zone Data Exchange (WZDx)](https://github.com/usdot-jpo-ode/wzdx) data feeds.

The library provides the following functionality:

Expand All @@ -16,8 +16,6 @@ The library provides the following functionality:

WZDx versions 4.0, 4.1, and 4.2 are supported; the [WzdxSerializer](./src/IBI.WZDx/Serialization/WzdxSerializer.cs) defaults to outputting v4.2 (latest WZDx).

[Detour road events](https://github.com/usdot-jpo-ode/wzdx/blob/main/spec-content/objects/DetourRoadEvent.md) are not supported. When provided with a Work Zone Feed that includes detour road events, the WzdxSerializer.DeserializeFeed method will deserialize the detour events into a [RoadEventFeature](./src/IBI.WZDx/Models/RoadEvents/RoadEventFeature.cs) with `Properties` as `null`.

## Usage

### NuGet Package
Expand Down Expand Up @@ -71,7 +69,7 @@ The PATCH version number is incremented as changes are made to the library that

## Tests

This solution includes a [IBI.WZDx.UnitTests](/tests/IBI.WZDx.UnitTests/) Xunit test project. Run all unit tests with the following command:
This solution includes a [IBI.WZDx.UnitTests](tests/IBI.WZDx.UnitTests) Xunit test project. Run all unit tests with the following command:

```
dotnet test
Expand Down
29 changes: 29 additions & 0 deletions src/IBI.WZDx/Equality/DoubleExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace IBI.WZDx.Equality;

/// <summary>
/// Extensions for <see cref="double"/> related to equality.
/// </summary>
public static class DoubleExtensions
{
/// <summary>
/// Determines whether two nullable double values are approximately equal.
/// </summary>
/// <param name="value">The first nullable double value to compare.</param>
/// <param name="other">The second nullable double value to compare.</param>
/// <returns>
/// True if both values are null, or their absolute difference is less than or equal
/// to <see cref="double.Epsilon"/>; otherwise, false.
/// </returns>
public static bool NullEqualsApproximation(this double? value, double? other)
{
if (value is null && other is null)
return true;

if (value is null || other is null)
return false;

return Math.Abs(value.Value - other.Value) <= double.Epsilon;
}
}
8 changes: 4 additions & 4 deletions src/IBI.WZDx/IBI.WZDx.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<PackageId>IBI.WZDx</PackageId>
<Description>Models and utitlies for producing and consuming Work Zone Data Exchange (WZDx) data feeds.</Description>
<PackageTags>WZDx;Work Zone Data Exchange;Work Zone Feed;Road Event;Device Feed;Field Device;GeoJSON</PackageTags>
<Authors>IBI Group</Authors>
<VersionPrefix>4.2.0</VersionPrefix>
<VersionPrefix>4.2.1</VersionPrefix>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryType>git</RepositoryType>
Expand All @@ -21,8 +21,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Macross.Json.Extensions" Version="2.2.0" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />
<PackageReference Include="Macross.Json.Extensions" Version="3.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.2" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions src/IBI.WZDx/Models/RoadEvents/RoadEventCoreDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using IBI.WZDx.Equality;
using IBI.WZDx.Models.RoadEvents.WorkZones;

#pragma warning disable CS0618 // Type or member is obsolete

namespace IBI.WZDx.Models.RoadEvents;

/// <summary>
Expand Down
194 changes: 194 additions & 0 deletions src/IBI.WZDx/Models/RoadEvents/WorkZones/DetourRoadEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using IBI.WZDx.Equality;

namespace IBI.WZDx.Models.RoadEvents.WorkZones;

/// <summary>
/// Describes a detour on a roadway. It can be either a segment of a detour
/// (each segment represented by its own <see cref="DetourRoadEvent"/>) or the entire detour.
/// </summary>
/// <param name="CoreDetails">
/// The core details of the road event that apply to all types of road events, not specific to detour road events.
/// </param>
/// <param name="StartDate">
/// The UTC time and date when the event begins.
/// </param>
/// <param name="EndDate">
/// The UTC time and date when the event ends.
/// </param>
/// <param name="LocationMethod">
/// The typical method used to locate the beginning and end of a detour area.
/// </param>
/// <param name="VehicleImpact">
/// The impact to vehicular lanes along a single road in a single direction.
/// </param>
/// <param name="IsStartDateVerified">
/// Indicates if work has been confirmed to have started, such as from a person or field device.
/// </param>
/// <param name="IsEndDateVerified">
/// Indicates if work has been confirmed to have ended, such as from a person or field device.
/// </param>
/// <param name="IsStartPositionVerified">
/// Indicates if the start position (first geometric coordinate pair, see
/// <see cref="RoadEventFeature.Geometry"/>) is based on actual reported data from a GPS-equipped
/// device that measured the location of the start of the detour.
/// </param>
/// <param name="IsEndPositionVerified">
/// Indicates if the end position (last geometric coordinate pair, see
/// <see cref="RoadEventFeature.Geometry"/>) is based on actual reported data from a GPS-equipped
/// device that measured the location of the end of the detour.
/// </param>
/// <param name="Lanes">
/// A list of individual lanes within a road event (roadway segment).
/// </param>
/// <param name="BeginningCrossStreet">
/// Name or number of the nearest cross street along the roadway where the event begins.
/// </param>
/// <param name="EndingCrossStreet">
/// Name or number of the nearest cross street along the roadway where the event ends.
/// </param>
/// <param name="BeginningMilepost">
/// The linear distance measured against a milepost marker along a roadway where the event begins.
/// </param>
/// <param name="EndingMilepost">
/// The linear distance measured against a milepost marker along a roadway where the event ends.
/// </param>
/// <param name="TypesOfWork">
/// A list of the types of work being done in a road event and an indication of if each type
/// results in an architectural change to the roadway.
/// </param>
/// <param name="WorkerPresence">
/// Information about whether workers are present in the road event area.
/// </param>
/// <param name="ReducedSpeedLimitKph">
/// The reduced speed limit posted within the road event, in kilometers per hour. This property only
/// needs to be supplied if the speed limit within the road event is lower than the posted speed
/// limit of the roadway.
/// </param>
/// <param name="Restrictions">
/// A list of zero or more road restrictions that apply to the roadway segment described by this
/// road event.
/// </param>
/// <param name="ImpactedCdsCurbZones">
/// A list of references to external
/// <see href="https://github.com/openmobilityfoundation/curb-data-specification/tree/main/curbs#curb-zone">
/// CDS Curb Zones
/// </see>
/// impacted by the detour.
/// </param>
/// <param name="WorkZoneType">
/// The type of detour road event, such as if the road event is static or actively moving as part
/// of a moving operation.
/// </param>
public record DetourRoadEvent(
RoadEventCoreDetails CoreDetails,
DateTimeOffset StartDate,
DateTimeOffset EndDate,
LocationMethod LocationMethod,
VehicleImpact VehicleImpact,
bool? IsStartDateVerified = null,
bool? IsEndDateVerified = null,
bool? IsStartPositionVerified = null,
bool? IsEndPositionVerified = null,
WorkZoneType? WorkZoneType = null,
List<CdsCurbZonesReference>? ImpactedCdsCurbZones = null,
List<Lane>? Lanes = null,
string? BeginningCrossStreet = null,
string? EndingCrossStreet = null,
double? BeginningMilepost = null,
double? EndingMilepost = null,
List<TypeOfWork>? TypesOfWork = null,
WorkerPresence? WorkerPresence = null,
double? ReducedSpeedLimitKph = null,
List<Restriction>? Restrictions = null
) : IRoadEvent
{
/// <summary>
/// Determine if another <see cref="DetourRoadEvent"/> is equal to this <see cref="DetourRoadEvent"/>.
/// </summary>
public virtual bool Equals(DetourRoadEvent? other)
{
return other != null
&& CoreDetails == other.CoreDetails
&& StartDate == other.StartDate
&& EndDate == other.EndDate
&& LocationMethod == other.LocationMethod
&& VehicleImpact == other.VehicleImpact
&& IsStartDateVerified == other.IsStartDateVerified
&& IsEndDateVerified == other.IsEndDateVerified
&& IsStartPositionVerified == other.IsStartPositionVerified
&& IsEndPositionVerified == other.IsEndPositionVerified
&& Lanes.NullHandlingSequenceEqual(other.Lanes)
&& BeginningCrossStreet == other.BeginningCrossStreet
&& EndingCrossStreet == other.EndingCrossStreet
&& BeginningMilepost.NullEqualsApproximation(other.BeginningMilepost)
&& EndingMilepost.NullEqualsApproximation(other.EndingMilepost)
&& TypesOfWork.NullHandlingSequenceEqual(other.TypesOfWork)
&& WorkerPresence == other.WorkerPresence
&& ReducedSpeedLimitKph.NullEqualsApproximation(other.ReducedSpeedLimitKph)
&& Restrictions.NullHandlingSequenceEqual(other.Restrictions)
&& ImpactedCdsCurbZones.NullHandlingSequenceEqual(other.ImpactedCdsCurbZones)
&& WorkZoneType == other.WorkZoneType;
}

/// <inheritdoc/>
public override int GetHashCode()
{
var hash = new HashCode();

hash.Add(CoreDetails);
hash.Add(StartDate);
hash.Add(EndDate);
hash.Add(LocationMethod);
hash.Add(VehicleImpact);
hash.Add(IsStartDateVerified);
hash.Add(IsEndDateVerified);
hash.Add(IsStartPositionVerified);
hash.Add(IsEndPositionVerified);

if (Lanes is not null)
{
foreach (Lane lane in Lanes)
{
hash.Add(lane);
}
}

hash.Add(BeginningCrossStreet);
hash.Add(EndingCrossStreet);
hash.Add(BeginningMilepost);
hash.Add(EndingMilepost);

if (TypesOfWork is not null)
{
foreach (TypeOfWork typeOfWork in TypesOfWork)
{
hash.Add(typeOfWork);
}
}

hash.Add(WorkerPresence);
hash.Add(ReducedSpeedLimitKph);

if (Restrictions is not null)
{
foreach (Restriction restriction in Restrictions)
{
hash.Add(restriction);
}
}

if (ImpactedCdsCurbZones is not null)
{
foreach (CdsCurbZonesReference cdsCurbZonesReference in ImpactedCdsCurbZones)
{
hash.Add(cdsCurbZonesReference);
}
}

hash.Add(WorkZoneType);

return hash.ToHashCode();
}
}
2 changes: 2 additions & 0 deletions src/IBI.WZDx/Models/RoadEvents/WorkZones/WorkZoneRoadEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using IBI.WZDx.Equality;

#pragma warning disable CS0618 // Type or member is obsolete

namespace IBI.WZDx.Models.RoadEvents.WorkZones;

/// <summary>
Expand Down
24 changes: 10 additions & 14 deletions src/IBI.WZDx/Serialization/RoadEventConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,20 @@ internal class RoadEventConverter : JsonConverter<IRoadEvent>
/// </returns>
public override IRoadEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
var jsonObject = jsonDocument.RootElement.GetRawText();
var eventType = jsonDocument.RootElement.GetProperty("core_details")
using JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader);
string jsonObject = jsonDocument.RootElement.GetRawText();
string? eventType = jsonDocument.RootElement.GetProperty("core_details")
.GetProperty("event_type")
.GetString();

Func<Type, IRoadEvent?> deserializeAsType = type =>
JsonSerializer.Deserialize(jsonObject, type, options) as IRoadEvent;

return eventType switch
{
"work-zone" => deserializeAsType(typeof(WorkZoneRoadEvent)),
"detour" => null,
"work-zone" => DeserializeAsType(typeof(WorkZoneRoadEvent)),
"detour" => DeserializeAsType(typeof(DetourRoadEvent)),
_ => throw new JsonException($"Unsupported event type '{eventType}'.")
};

IRoadEvent? DeserializeAsType(Type type) => JsonSerializer.Deserialize(jsonObject, type, options) as IRoadEvent;
}

/// <inheritdoc />
Expand All @@ -60,12 +59,9 @@ internal class RoadEventConverter : JsonConverter<IRoadEvent>
/// <param name="options">An object that specifies serialization options to use.</param>
/// <exception cref="ArgumentNullException"><paramref name="value"/> cannot be null.</exception>
public override void Write(Utf8JsonWriter writer, IRoadEvent value, JsonSerializerOptions options)
{
if (value == null)
throw new ArgumentNullException("value");

var type = value.GetType();

{
ArgumentNullException.ThrowIfNull(value);
Type type = value.GetType();
JsonSerializer.Serialize(writer, value, type, options);
}
}
Loading