Brief Intro To NetTopology in .NET Core

Aizeem Paroya
Aizeem Paroya
Contents

Somehow someway you’ve ended up at this rudimentary blog but it most likely went in the following ways. You started googling along the lines of “SqlGeography .NET Core”, “SqlGeography functions and types .NET Core ”, “Why won’t SQL geography work in my .NET Core app?” and from there started going down some disappointing rabbit holes to find that SQL geography isn’t supported yet in .NET Core. Now you’re stressed and contemplating how you’re going to port dinosaur code that has a million SQL geography types and functions splattered everywhere over to this wonderful world of .net core (and probably Linux)… times may start to seem bleak….

BUT rest assured! Where there’s a framework problem there’s a Stack Overflow thread with the answer somewhere. This brings us NetTopologySuite.

In this blog, I’m mostly going to be talking about some basics that I have found useful. I will be using the 2.0.0 release of the NetTopology Suite for the code snippets and will also try to post links to some useful sites that have helped me understand the massive iceberg that is NTS (an abbreviation I’ll use throughout the article.)

Geometries are the bread and butter of dealing with data in the GIS world (useful link on shapes and jargon.) So let’s take a second to talk about creating geometries and playing around with them. The different types of geometries talked about later inherit the base class of geometry. One of the most commonly used geometries is a linestring:

public LineString SimpleLineString()
{
    //A simple line connected from one coordinate to the next
    //Example: A route that goes from LA to San Fran
    //Note: Have to have at least two points you know because its a LINEstring

    Coordinate coord1 = new Coordinate(74.6523332, 21.213213);
    Coordinate coord2 = new Coordinate(80.2321312, 25.563213);
    Coordinate coord3 = new Coordinate(85.6522352, 25.983223);

    Coordinate[] coordArr = new Coordinate[] {coord1, coord2, coord3};
    return new LineString(coordArr);
}

Another commonly used geometry type is polygons as they can be used to represent top-down representations of buildings for example. They are just as easy to create:

public Polygon SimplePolygon()
{
    //A polygon as you may know is a enclosed linestring (it has a name yes)
    // Now you can represent many different types of polygons
    var polygon = new Polygon(new LinearRing(new Coordinate[]
    {
        new Coordinate(1.0, 1.0),
        new Coordinate(1.05, 1.1),
        new Coordinate(1.1, 1.1),
        new Coordinate(1.1, 1.05),
        new Coordinate(1, 1),
    }));
    return polygon;
}

One can make a range of different types of polygons and don’t have to be simple enclosed shapes. For example, a donut can be created by passing not just a shell but also an array of coordinates that represent the smaller inner circle as the second parameter when creating the polygon.

Next, let’s take a second to talk about Multi-polygons and Multi-linestrings.

public MultiLineString MultiLineString()
{
    //Multi...Line Strings are you guessed it just a few line strings
    //Jammed into an array
    //Ex: LA to San Fran is one route and then San Fran to Portland can
    // be another route but stored into one data structure

    LineString ls1 = SimpleLineString();
    LineString ls2 = SimpleLineString();
    LineString ls3 = SimpleLineString();

    LineString[] lsArr = new LineString[] {ls1, ls2, ls3};
    MultiLineString mls = new MultiLineString(lsArr);

    var lineStr = mls[0]; //Return Geometry You can cast to LineString
    lineStr = (LineString) lineStr;

    return mls;
}

And MultiPolygons:

public  MultiPolygon SimpleMultiPolygon()
{
    //This one is ez just a bunch of polygons....jammed into an array! allows you
    //to bundle your polygons
    Polygon p1 = SimplePolygon();
    Polygon p2 = SimplePolygon();
    Polygon p3 = SimplePolygon();

    MultiPolygon multiPolygon = new MultiPolygon(new Polygon[]
    {
        p1, p2, p3
    });
    return multiPolygon;
}

You might ask “Aizeem, why do I wanna create arrays of geometries”? Well for a few reasons it allows you bundle similar types of geometries that you know will be of a certain type but it also allows you to run functional methods on the class without looping through everything!

public Geometry FindIntersects()
{

    MultiPolygon mp = GeometryExamples.SimpleMultiPolygon(); //Previous Example
    LineString ls = GeometryExamples.SimpleLineString();
    Geometry ezIntersect = mp.Intersection(ls);

    //Versus
    Geometry intersectGeometry = null;
    foreach (var polygon in mp.Geometries)
    {
        var intersect = polygon.Intersection(ls);
        if (intersectGeometry == null)
        {
            intersectGeometry = intersect;
        }
        else
        {
            intersectGeometry = intersectGeometry.Union(intersect);
        }
    }
    //do you hate yourself? and enjoy writing more code than you don't have to?
    return intersectGeometry;
}

However, this is not exclusive meaning its a list of coordinates in both geometries. Therefore, the loop might be necessary for tasks that require special logic and is good to know about. This can be between any two geometries which help on answering questions like “Does my line string(a route) touch this polygon (a house)?”.

Now that brings me to how can you read in SQL geography from the DB easily. Entity Framework provides a pretty simple way to specify the use of NTS to read and write. Take this example:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using NetTopologySuite.Geometries;
namespace NetTopologyBasics
{
    public class RoutePathContext : DbContext
    {
        private readonly string _connectionString;
        public RoutePathContext(string connectionString)
        {
            _connectionString = connectionString;
        }
        public DbSet<RoutePathDto> RoutePaths { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_connectionString, x => x.UseNetTopologySuite());
        }
    }
    public class RoutePathDto
    {
        public int RouteId { get; set; }
        public Geometry Route { get; set; } //Notice this is nts geometry and not sqlgeography
    }
    public class TripRoutePathProvider
    {
        private readonly RoutePathContext _routePathContext;
        public TripRoutePathProvider(RoutePathContext RoutPathDbContext)
        {
            _routePathContext = _routePathContext;
        }
        public RoutePathDto GetActiveRoutePaths(long routeId)
        {
            var resp = _routePathContext.RoutePaths
                .FromSqlRaw<RoutePathDto>($"Execute SPR_GET_ROUTE_BY_ROUTEID {routeId}");
            RoutePathDto routePathDto = resp.FirstOr(new RoutePathDto());
            return routePathDto;
        }
    }
}

Now let’s talk about some common functions and uses as well as their limitations. Comparing two coordinates. Sometimes coordinates don’t match up perfectly down to every decimal but are “close enough” — well you can use:

public bool ComparingTwoCoords(Coordinate coord1, Coordinate coord2, double threshold)
{
    // You can play around with these values so lets say we only
    // care down to sixth decimal, threshold can be ".000001"
    // That means Coord1 (1.123456,2.123456)
    // Coord2 (1.123457,2.123457) will return true
    // Better than comparing x and y every time

   return coord1.Equals2D(coord2, threshold);
}

Maybe I want to do distance calculations between two coordinates or two geometries:

public double DistanceBetweenGeometeries(Geometry geo1, Geometry geo2)
{
    double dist = geo1.Distance(geo2);
    // This is the same as doing the following

    //Finding the closest points
    Coordinate[] closestPoints = DistanceOp.NearestPoints(geo1,geo2);

    //Then Running Distance Calculations on the pair of coordinates (or more returned)
    //Something like this
    double distBetweenClosestPoints = DistanceOp.Distance(new Point(closestPoints[0]),
        new Point(closestPoints[1]));

    return dist; // Or closest Points
}

“Aizeem I want to create some simple shapes and don’t feel like putting in every coordinate like a chimp”. Simple, use geometric shape factory to create geometries.

using NetTopologySuite.Geometries;
using NetTopologySuite.Utilities;
namespace NetTopologyBasics
 {
     public class SimpleShapes
     {
         private readonly GeometricShapeFactory _geometricShapeFactory;

         public SimpleShapes(GeometricShapeFactory geometricShapeFactory)
         {
             _geometricShapeFactory = geometricShapeFactory;
         }

         public Polygon CreateCircle(Coordinate center, double radius)
         {
             _geometricShapeFactory.Base = center;
             _geometricShapeFactory.Size = radius * 2; //Diameter
             return _geometricShapeFactory.CreateCircle();
         }
     }
 }

The geometric shape factory allows the creation of other geometries as well including rectangles, arcs and the best of them all a squircle.

The above examples are quick ways of creating simple geometries but say you are working with different projected coordinate systems. That requires you to do some math to go from one system to another system. Therefore, might want to create geometries with the right coordinate values (obviously.) This is where one might want to create a common class that handles the transform function which if using dependency injection (useful article) can be injected into components that are interacting with geometries. First, let’s create the coordinate system representations.

public MathTransform GetTransformFilter()
{
    CoordinateTransformationFactory ctFact = new CoordinateTransformationFactory();

    ProjectedCoordinateSystem googleMapsSys = ProjectedCoordinateSystem.WebMercator;
    ProjectedCoordinateSystem anotherSystem = ProjectedCoordinateSystem.WGS84_UTM(1, true);

    ICoordinateTransformation tranformer = ctFact.CreateFromCoordinateSystems(googleMapsSys, anotherSystem);
    return tranformer.MathTransform;
}

You can also create coordinate systems with code (link) or well-known text representations (link). Now we can simply use the code given to us on the documentation site to create a filter function from the math transform provided above:

internal class MathTransformFilter : ICoordinateSequenceFilter
{
    //https://github.com/NetTopologySuite/ProjNet4GeoAPI/wiki/Projecting-points-from-one-coordinate-system-to-another
    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);
    }
}

Used:

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

//Or if injected in
public Geometry Transform(Geometry geometry)
{
  geometry = geometry.Copy();
  geometry.Apply(_mathTransformFilter);
  return geometry;
}

You might find yourself often comparing geometries with each other and I wanted to take a second and look at some of those comparisons and how they behave. There are a lot of handheld function

using NetTopologySuite.Geometries;
namespace NetTopologyBasics
{
    public class PropertiesExploration
    {
        public bool Compare()
        {
            Coordinate[] coordsArray = new Coordinate[0];
            LineString ls = new LineString(coordsArray);

            bool objComparison = (ls == LineString.Empty); //True
            bool coordLengthComparison = (ls.Coordinates == ls.Coordinates); //True, will do a direct array compare
            bool isEmpty = ls.IsEmpty; //True

            bool compareWithFunction = ls.Equals(LineString.Empty); //False
            bool compareWithFunctionOnItself = ls.Equals(ls); //False
            bool compareWithFunctionNull = ls.Equals(null); //False
            bool compareTopologyOnItself = ls.EqualsTopologically(ls); //False

            //Why? you might ask in order to be equal they must have at least one point in common, nulls are always false
            //Lets make sure
            coordsArray = new[] {new Coordinate(1, 1), new Coordinate(2,2)}; //Need to have two points min
            LineString lsWithCoords = new LineString(coordsArray);
            bool compareTopologyOnItselfWithCommon = lsWithCoords.EqualsTopologically(lsWithCoords); //True!

            bool comparisonsOnLength = (ls.Length == 0); //True, Length of linestring
            bool comparisonOnCount = (ls.Count == 0); //True, Array count
            return true;
        }
    }
}

GeoJson

If you’re still reading this you probably are interested in knowing how NTS interacts with incoming or outgoing data that is GeoJson. You can easily convert from any geometry to geoJson with:

public class GeometriesToGeoJson
{
    GeoJsonWriter _geoJsonWriter = new GeoJsonWriter();
    GeoJsonReader _geoJsonReader = new GeoJsonReader();
    public string convert(Geometry geometry)
    {
       var str =  _geoJsonWriter.Write(geometry);
       return str;
    }
    public Geometry read(string json)
    {
        return _geoJsonReader.Read<Geometry>(json);
    }
}

If we write a quick test like the following:

[Test]
public void geoTests()
{
    Polygon poly = GeometryExamples.SimplePolygon();
    var retString =
        "{\"type\":\"Polygon\",\"coordinates\":[[[1.0,1.0],[1.05,1.1],[1.1,1.1],[1.1,1.05],[1.0,1.0]]]}";

    Polygon readPoly = (Polygon) _geometriesToGeoJson.read(retString);

    Assert.IsTrue(_geometriesToGeoJson.convert(poly) == retString); //True
    Assert.IsTrue(poly.EqualsExact(readPoly)); //True
}

We’ll see that indeed what we put in is what we get out (life motto as well.)

This is useful as you can pass data in a standard format back to your frontend to be consumed and not have to do a lot of conversions (everything is awesome). Personally, I do like visualizing my geometries if I do not know what a function is doing or if the geometry I am trying to create is close to what I am picture. If you’re gifted at reading a sequence of coordinates and knowing the exact shape of geometry in your MatLab of a brain, well good for you. However, If you can’t auto plot in your head then you can take the output from the above function a paste it into a visualizer like geojson.io.

That brings me to the conclusion of the article. There is a lot under the umbrella of netTopology suite and it can be daunting to get started. The goal of this “short” intro was to provide a starting point for using netTopology suite in your application. Hopefully, this blog helped do exactly that so thanks for reading!

Share this article:

You May Also Like

Experimenting With LangChain AI

| By Ganesan Senthilvel

My recent experiences working with LangChain technology.

New Frameworks Provide New Ways to Create Cross Platform, Cross Mobile Applications

| By Ganesan Senthilvel

My recent experiences working with Flutter and Capacitor.