Skip to content

Upgrading to 2.0 from 1.x

Joe Amenta edited this page Oct 17, 2020 · 12 revisions

In order to provide a cleaner, simpler experience for users and to better match the functionality of JTS, 2.0 includes many breaking changes that will affect users upgrading from 1.x.

Entity Framework Core provider support

Spatial providers for Entity Framework Core 2.x are compatible with NetTopologySuite 1.x packages.

Spatial providers for Entity Framework Core 3.x and above are compatible with NetTopologySuite 2.x packages.

Upgrading just one or the other will not work; you must upgrade both together.

Target frameworks

Throughout the years, NetTopologySuite 1.x targeted many platforms, and until (relatively) late in the 1.x series, it even supported the long obsolete .NET Framework 2.0. The last 1.x release still even supported .NET Framework 3.5.

In addition, NetTopologySuite 1.x used multi-targeting to allow it to run on all platforms that implement any version of the .NET Standard, with separate targets for each higher .NET Standard version that offered something useful (e.g., operations in NetTopologySuite that require file I/O are missing from the .NET Standard 1.0 target because file I/O is only available in .NET Standard 1.3 or higher; because of this, we chose to also build a version that targets .NET Standard 1.3, which enables this and certain other things that we use).

All of this caused a (minor) drain on development efforts, and we were still effectively limited to using only the lowest-common-denominator of tools available to us (in essence, .NET Framework 3.5 only, unless there was a compelling benefit to use something that was added in later releases).

Starting with NetTopologySuite 2.0, .NET Standard 2.0 is now the new "least common denominator" that will be supported throughout the entire 2.x series. See this table for an up-to-date list of platforms that support .NET Standard 2.0; reproduced here, current as of 2020-01-13:

  1. .NET Framework 4.6.1 thru .NET Framework 4.7.1 (with varying degrees of stress)
  2. .NET Framework 4.7.2+ (mostly smoothly)
  3. .NET Core 2.0 or higher
  4. Mono 5.4 or higher
  5. Xamarin.iOS 10.14 or higher
  6. Xamarin.Mac 3.8 or higher
  7. Xamarin.Android 8.0 or higher
  8. Universal Windows Platform 10.0.16299 or higher
  9. Unity 2018.1 or higher

GeoAPI is gone

1.x referenced a package called "GeoAPI.Core". This often caused friction during NetTopologySuite development and made it possible to run into method binding errors when package versions were out-of-date with respect to one another.

As of 2.0, all relevant types from "GeoAPI.Core" have been either moved into NetTopologySuite or (see later) deleted outright.

Package naming

(Relatively) late in the 1.x series, the NetTopologySuite packages were split in order to enable most of the software to be relicensed under the more permissive BSD 3-clause license. "NetTopologySuite" turned into a metapackage that referenced "NetTopologySuite.Core" (BSD 3-clause) and "NetTopologySuite.CoordinateSystems" (LGPL 2.1+).

As of 2.0, "NetTopologySuite.Core" was renamed to "NetTopologySuite". "NetTopologySuite.CoordinateSystems" has no equivalent as of the 2.0 release.

NetTopologySuite.CoordinateSystems

ProjNet4GeoAPI (now ProjNet) also had several breaking changes in its own 2.0, and with the removal of GeoAPI packages, there is no longer a convenient common "thing" for a 2.0 equivalent of NetTopologySuite.CoordinateSystems to reference in order to do what it does.

That package had just one class with just one helper method that can actually be rewritten short enough to put here:

public static Geometry Transform(this Geometry geometry, MathTransform mathTransform)
{
    geometry = geometry.Copy();
    geometry.Apply(new MathTransformFilter(mathTransform));
    return geometry;
}

private sealed class MathTransformFilter : ICoordinateSequenceFilter
{
    private readonly MathTransform _mathTransform;

    public MathTransformFilter(MathTransform mathTransform)
        => _mathTransform = mathTransform;

    public bool Done => false;
    public bool GeometryChanged => true;

    public void Filter(CoordinateSequence seq, int i)
    {
        var (x, y, z) = _mathTransform.Transform(seq.GetX(i), seq.GetY(i), seq.GetZ(i));
        seq.SetX(i, x);
        seq.SetY(i, y);
        seq.SetZ(i, z);
    }
}

Coordinate

Coordinate instances in NetTopologySuite 1.x always had X, Y, and Z values.

As of 2.0, the base Coordinate class only stores X and Y values. Subclasses add Z, M, Z+M, and other combinations.

Special note: in JTS, Coordinates.Create methods can only return an instance with XY, XYZ, XYM, or XYZM values. NetTopologySuite will return one of those four if specifically requested, but otherwise, it will return a special internal subclass that can store practically anything, as long as both X and Y are present.

Interfaces

1.x had many interfaces (many of which were defined in "GeoAPI.Core") that were, effectively, just duplicates of classes. On the surface, these appear to enable programming against a model that does not depend on any particular implementation, which we all know is a "good thing".

In reality, there were few-to-no alternative implementations of these interfaces, and every "alternative" implementation would either be required to fully implement a huge stack of members that all need to be consistent with one another (a daunting task), or just implement the ones that they directly need, leaving many "unimplemented" by having them throw an exception at runtime.

These interfaces also had a cost to the development team: because not all .NET Standard 2.0 platforms support C# 8 default interface implementations, every addition to an interface is potentially a breaking change. Furthermore, because JTS did not use its own interfaces for many of the things we did, there were some cases where NetTopologySuite would cast to one of its classes anyway in order to continue its work.

In 2.0, many of these interfaces were removed, in favor of their corresponding classes. This table attempts to list the most important ones:

1.x Interface 2.0 Equivalent Notes
IGeometry Geometry
IPoint Point
ICurve LineString
ILineString LineString
ILinearRing LinearRing
ISurface Polygon
IPolygon Polygon
IGeometryCollection GeometryCollection
IMultiPoint MultiPoint
IMultiCurve MultiCurve
IMultiLineString MultiLineString
IMultiSurface MultiPolygon
IMultiPolygon MultiPolygon
ICoordinateSequence CoordinateSequence 1
ICoordinateSequenceFactory CoordinateSequenceFactory 1
IGeometryServices NtsGeometryServices
IGeometryFactory GeometryFactory
IPrecisionModel PrecisionModel
IBufferParameters BufferParameters
ISpatialIndex ISpatialIndex<object>
ICoordinateBuffer N/A 2
IGeometryIOSettings N/A 3
IGeometryReader<TSource> N/A 3
IBinaryGeometryReader N/A 3
IGeometryWriter<TSink> N/A 3
IBinaryGeometryWriter N/A 3
ITextGeometryWriter N/A 3

Notes:

  1. See next section for more details
  2. Only implemented by internal types in external packages
  3. If you need an abstraction, you can probably build a much better one than what we tried to do with these.

CoordinateSequence(Factory) / Ordinate(s)

NetTopologySuite 1.x tried to address an issue with JTS where "ordinate" values (i.e., X/Y/Z/M/etc.) were identified using their indexes for performance reasons instead of by their names. Since .NET has value-type enums, it made sense to use them instead of integer indexes to enable retrieving values of ordinates by name instead of by number.

Unfortunately, in order to enable full compatibility with JTS, there was a major flaw: because everything used the actual numeric value of the enum behind-the-scenes, in a sequence that stored only X, Y, and M values, fetching by Ordinate.Z would get you the value of the M-ordinate. Since JTS 1.16 added stronger support for Z and M ordinate values, this problem went from "quirky" to "unacceptable".

Furthermore, as part of this, JTS 1.16 started using Java's own default interface implementations for its own CoordinateSequence type (which is an interface); of course, NetTopologySuite could not follow suit.

Our solution for NetTopologySuite 2.0 was to completely redesign coordinate sequences in NetTopologySuite, prioritizing both maximum JTS compatibility and ease of use in the .NET world. CoordinateSequence is now an abstract base class that works as follows:

  1. Subclasses must provide three pieces of information that cannot change throughout the lifetime of an instance:
    • Number of coordinates in the sequence
    • Number of dimensions stored for each coordinate (e.g., 4 for XYZM, 3 for XYZ / XYM, 2 for XY)
    • How many of those dimensions are "measures" (i.e., 1 for XYZM / XYM, 0 for XYZ / XY)
  2. Subclasses must also provide implementations of these methods:
    • Copy() returns a "deep" copy of the sequence with the same coordinate values
      • In this context, a "deep" copy just means that modifications to either sequence should not be publicly visible from the other sequence; a fully immutable sequence, for example, may return the original object instances.
    • GetOrdinate(int index, int ordinateIndex) gets a value
    • SetOrdinate(int index, int ordinateIndex, double value) sets the value
    • index refers to the index of the coordinate in the sequence
    • ordinateIndex refers to the index of the ordinate from 0 to Dimension - 1
  3. Subclasses may override some other virtual methods.
  4. Callers may get / set the value of any ordinate for any coordinate in the sequence by ordinate index (for JTS compatibility), or any of the first 16 spatial dimensions / first 16 measures by Ordinate enum value.
  5. Trying to get the value of an Ordinate that's not stored in the sequence will return NaN, and trying to set the value of an Ordinate that's not stored in the sequence will silently do nothing. Be careful.
  6. Because everything is based on the ordinate index, using the enum value is going to be slower than using the index directly. To help use the faster indexes if you need to make repeated calls in a performance-sensitive context, here are some tips:
    • The ordinateIndex for X is always 0, and for Y it's always 1. The constructor enforces that these two are present in all sequences.
    • bool TryGetOrdinateIndex(Ordinate ordinate, out int ordinateIndex) will give you the index of the requested ordinate, or return false if the requested ordinate is absent from the sequence.
    • int ZOrdinateIndex { get; } will give the index of the Z ordinate, or -1 if the sequence has only two spatial dimensions.
    • int MOrdinateIndex { get; } will give the index of the M ordinate, or -1 if the sequence has no measures.

The main limitation here comes from the JTS compatibility requirement: in order for a CoordinateSequence instance to contain a value for, e.g., the 6th spatial dimension and the 4th measure, it will also be considered to contain values for spatial dimensions 1-5 and measures 1-3.

NetTopologySuite.Features

The dependent package NetTopologySuite.Features also had some breaking changes to simplify things:

  1. This package no longer supplies types to model the CRS part of the pre-IETF GeoJSON specification.
  2. This package no longer provides an extension method to get an optional "id" attribute value.
    • GetOptionalId is back in 2.1.0, slightly modified to check for an interface to make this more reliable.
  3. FeatureCollection now is a collection of features (in earlier versions, it had a collection of features).
  4. IAttributesTable.AddAttribute is now spelled IAttributesTable.Add.
  5. IAttributesTable now has a GetOptionalValue(string) method to retrieve the value of an attribute if it's present, or null if the attribute is not present, in one call instead of two.
  6. AttributesTable is now backed by a Dictionary<string, object> instead of allowing any IDictionary<string, object>.
    • If you have a Dictionary<string, object>, then that instance may still be injected as-is.