v2.2.1.1 Developer Guide


CoordinateSharp is a simple .NET Standard library designed to assist with geographic coordinate conversions, formatting and location based celestial calculations. CoordinateSharp has the ability to convert various GeoDetic lat/long formats, UTM, MGRS(NATO UTM), ECEF and Spherical Cartesian (X, Y, Z). It also provides a wide array of location based solar/lunar information such as rise/set time, lunar phase info and more.

Change notes can be viewed here

If you have any issues or questions create an issue on our GitHub Project Page.

Contents


Introduction

Usage Instructions

Credit

Getting Started


These instructions will get a copy of the library running on your local machine for development and testing purposes.

Prerequisites

.NET 4.0 or greater or .NET Standard 2.0, 1.4, 1.3 Supported Platforms

Installation

CoordinateSharp is available as a nuget package from nuget.org

Alternatively, you may download the library directly here.

Usage Instructions


Creating a Coordinate

The Coordinate object is the "main" class of this library. For the most part, it contains all of the information you will need. The following method is one of the most basic ways to create a Coordinate.

//Seattle coordinates on 5 Jun 2018 @ 10:10 AM (UTC)
Coordinate c = new Coordinate(47.6062, -122.3321, new DateTime(2018,6,5,10,10,0));

Console.WriteLine(c);                       // N 47º 36' 22.32" W 122º 19' 55.56"
Console.WriteLine(c.CelestialInfo.SunSet);  // 5-Jun-2018 4:02:00 AM
Console.WriteLine(c.UTM);                   // 10T 550200mE 5272748mN

Creating a Coordinate using TryParse()

CoordinateSharp has the ability to try and parse a coordinate from a provided string. The parser will return false if it fails. The advantage of this, is that you do not have to control the format in which a user inputs a coordinate. The disadvantage of this is that it will fail with a bool value instead of a detailed error message.

string s = "34X 551586mE 8921410mN"; //UTM Coordinate
Coordinate c; //Create new Coordindate to populate
if(Coordinate.TryParse(s, out c))
{
    //Coordinate parse success
    //Coordinate object has now been created and populated
    Console.WriteLine(c); //N 80º 20' 44.999" E 23º 45' 22.987"   
}

You may also parse with a GeoDate using the TryParse() overload method.

If you need to work with Latitudes and Longitudes individually you may use the CoordinatePart.TryParse() method.

   Coordinate c = new Coordinate();
   c.GeoDate = DateTime.Now //Date needed if grabbing celestial info

   CoordinatePart lat;
   CoordinatePart.TryParse("N 45.65", out lat);

   CoordinatePart lng;
   CoordinatePart.TryParse("W 15.58", out lng);
   c.Latitude = lat;
   c.Longitude = lng;   

Because there are multiple types of Cartesian coordinates, you may need to specify what cartesian system you intend for your parser to work in. Spherical Cartesian is the default method, but it may be changed using an overload.

Coordinate.TryParse(s, CartesianType.ECEF, out c); //Will assume input is in ECEF if X, Y, Z coordinate parses successfully.

The coordinate parser will be expanded constantly so provided suggestions on formats that should parse are greatly appreciated.


Creating a Coordinate from a non-signed degree (secondary method)

The Coordinate constructor only accepts Latitude and Longitude in signed degrees. With that said you can still create a Coordinate with other formats. For latitude / longitude type coordinates you can build a Coordinate using the method below.

It should be noted that this method is expensive if eager loading is being used (explained later in this guide). Eager loading is turned on by default, so using this method will cause all conversions to occur 3 separate times (once during Coordinate creation, and once for each CoordinatePart). This expense is usually negligible, but may become a factor during bulk or heavy usage.

//DMS Formatted: N 40º 34' 36.552" W 70º 45' 24.408.
Coordinate c = new Coordinate();
c.Latitude = new CoordinatePart(40,34, 36.552, CoordinatesPosition.N);
c.Longitude = new CoordinatePart(70, 45, 24.408, CoordinatesPosition.W);
c.Latitude.ToDouble();  // Returns 40.57682  (Signed Degree)
c.Longitude.ToDouble(); // Returns -70.75678 (Signed Degree)

To create a Coordinate using a format other than latitude & longitude (such as UTM), either use the TryParse() method OR reference the appropriate section of this document for further instruction.


Formatting a Coordinate

The default output of a Coordinate or Coordinate.ToString() is in DMS format. Formats may be changed by passing or editing the FormatOptions property contained in the Coordinate object.

Coordinate c = new Coordinate(40.57682, -70.75678);

c.FormatOptions.CoordinateFormatType = CoordinateFormatType.Degree_Decimal_Minutes;
c.FormatOptions.Display_Leading_Zeros = true;
c.FormatOptions.Round = 3;

c.ToString();           // N 40º 34.609' W 070º 045.407'
c.Latitude.ToString();  // N 40º 34.609'
c.Longitude.ToString(); // W 070º 45.407'

Universal Transverse Mercator and Military Grid Reference System

UTM and MGRS (NATO UTM) formats are available for display. They are converted from the lat/long decimal values. The default ellipsoid is WGS84 but a custom ellipsoid may be passed. These formats are accessible from the Coordinate object.

Coordinate c = new Coordinate(40.57682, -70.75678);
c.UTM.ToString(); // Outputs 19T 351307mE 4493264mN
c.MGRS.ToString(); // Outputs 19T CE 51307 93264

The UTM/MGRS systems should not be used in circumpolar regions. You may check the WithinCoordinateSystemBounds property to see if you are outside the limitations of the system. It should also be noted to both UTM and MGRS ToString() methods will return empty (not null) if a conversion from Lat/Long is outside the systems limitations.

Coordinate c = new Coordinate(-82,10);
if(!c.MGRS.WithinCoordinateSystemBounds)
{
     Console.WriteLine("MGRS cannot be used at this latitude");
     Console.WriteLine(c.MGRS); //Will return empty string.
}

To convert UTM or MGRS coordinates into Lat/Long.

UniversalTransverseMercator utm = new UniversalTransverseMercator("T", 32, 233434, 234234);
Coordinate c = UniversalTransverseMercator.ConvertUTMtoLatLong(utm);

You may change or pass a custom ellipsoid by using the Equatorial Radius (Semi-Major Axis) and Inverse of Flattening of the datum. This will cause UTM/MGRS conversions to be based on the new ellipsoid.

Note Regarding Datums: When setting a custom "ellipsoid" you aren't truly setting a datum point, but a reference ellipsoid. This library isn't designed for mapping and has no way of knowing how a coordinate correlates with an actual map. This is solely used to changed the earth's shape during calculations to increase accuracy in specific regions. In most cases the default datum value is sufficient.

To change the current ellipsoid

c.Set_Datum(6378160.000, 298.25);

To create an object with the custom ellipsoid.

UniversalTransverseMercator utm = new UniversalTransverseMercator("Q", 14, 581943.5, 2111989.8, 6378160.000, 298.25);
c = UniversalTransverseMercator.ConvertUTMtoLatLong(utm);

Some UTM formats may contain a "Southern Hemisphere" boolean value instead of a Lat Zone character. If this is the case for a UTM you are converting, just use the letter "C" for southern hemisphere UTMs and "N" for northern hemisphere UTMs.

//MY UTM COORD ZONE: 32 EASTING: 233434 NORTHING: 234234 (NORTHERN HEMISPHERE)
UniversalTransverseMercator utm = new UniversalTransverseMercator("N", 32, 233434, 234234);
Coordinate c = UniversalTransverseMercator.ConvertUTMtoLatLong(utm);

NOTE: UTM conversions below and above the 85th parallels become highly inaccurate. MGRS conversion may suffer accuracy loss even sooner. No attempts are made by this library to correct for Norway's modified grid. Lastly, due to grid overlap the MGRS coordinates input, may not be the same ones output in the created Coordinate class. If accuracy is in question you may test the conversion by following the steps below.

MilitaryGridReferenceSystem mgrs = new MilitaryGridReferenceSystem("N", 21, "SA", 66037, 61982);
Coordinate c = MilitaryGridReferenceSystem.MGRStoLatLong(mgrs);
Coordinate nc = MilitaryGridReferenceSystem.MGRStoLatLong(c.MGRS); //c.MGRS is now 20N RF 33962 61982
Debug.WriteLine(c.ToString() + "  " + nc.ToString());              // N 0º 33' 35.988" W 60º 0' 0.01"   N 0º 33' 35.988" W 60º 0' 0.022"

In the above example, the MGRS values are different once converted, but the Lat/Long is almost the same once converted back.


Cartesian Format

Spherical Earth Cartesian (X, Y, Z) is available for display. They are converted from the lat/long radian values. This format is accessible from the Coordinate object. You may also convert a Cartesian coordinate into a lat/long coordinate. This conversion uses the Haversine formula. It is sufficient for basic application only as it assumes a spherical earth.

To Cartesian:

Coordinate c = new Coordinate(40.7143538, -74.0059731);
c.Cartesian.ToString(); //Outputs 0.20884915 -0.72863022 0.65228831

To Lat/Long:

Cartesian cart = new Cartesian(0.20884915, -0.72863022, 0.65228831);
Coordinate c = Cartesian.CartesianToLatLong(cart);
//OR
Coordinate c = Cartesian.CartesianToLatLong(0.20884915, -0.72863022, 0.65228831);

ECEF Format

Earth Centered Earth Fixed (ECEF) Cartesian coordinates are available. The are converted using the WGS84 ellipsoid by default, but the ellipsoid may be changed by using the SetDatum() function. There is no geoid model included in this conversion.

To ECEF:

Coordinate c = new Coordinate(40.7143538, -74.0059731);
c.ECEF.ToString(); //Outputs 1333.97 km, -4653.936 km, 4138.431 km

To Lat/Long:

ECEF ecef = new ECEF(1333.97, -4653.936, 4138.431);
Coordinate c = ECEF.ECEFToLatLong(ecef);
//OR
Coordinate c = ECEF.ECEFToLatLong(1333.97, -4653.936, 4138.431);

When converting from ECEF to GeoDetic Lat/Long, the altitude or height from the conversion may be desired. The post conversion height may be accessed in the ECEF class using the Distance object GeoDetic_Height. You may also set the height of the coordinate for ECEF conversions by using the Set_GeoDetic_Height(). ECEF height starts at Mean Sea Level (MSL) based on the provided ellipsoid.

NOTE: ECEF.GeoDetic_Height will always be set at 0 unless a coordinate is converted from ECEF to Lat/Long or the value is manually updated using ECEF.Set_GeoDetic_Height().

Setting the GeoDetic height for ECEF conversions:

Coordinate c = new Coordinate(45 , 45);
c.ECEF.ToString(); //Outputs 3194.419 km, 3194.419 km, 4487.348 km

c.ECEF.Set_GeoDetic_Height(c, new Distance(1000, DistanceType.Meters));
c.ECEF.ToString(); //3194.919 km, 3194.919 km, 4488.056 km

Getting the GeoDetic height after converting from ECEF to Lat/Long:

ECEF ecef = new ECEF(1333.97, -4653.936, 4138.431);
Coordinate c = ECEF.ECEFToLatLong(ecef);
c.ECEF.GeoDetic_Height.Meters; //Outputs 1000.2

The GeoDetic height will need to be set if creating a new Coordinate based on an existing Coordinate. These fundementals may become applicible if working with EagerLoading.

Distance geoHeight = c.ECEF.GeoDetic_Height;_
Coordinate newCoordinate = new Coordinate(c.Latitude.ToDouble(), c.Longitude.ToDouble());
newCoordinate.ECEF.Set_GeoDetic_Height(newCoordinate, geoHeight);

Calculating Distance and Moving a Coordinate

Distance is calculated with 2 methods based on how you define the shape of the earth. If you pass the shape as a Sphere calculations will be more efficient, but less accurate. The other option is to pass the shape as an Ellipsoid. Ellipsoid calculations have a higher accuracy. The default ellipsoid of a coordinate is WGS84, but can be changed using the Coordinate.SetDatum function.

Distance can be calculated between two Coordinates. Various distance values are stored in the Distance object.

Distance d = new Distance(coord1, coord2); //Default. Uses Haversine (Spherical Earth)
//OR
Distance d = new Distance(coord1, coord2, Shape.Ellipsoid); 
d.Kilometers;
d.Bearing;

You may also grab a distance by passing a second Coordinate to an existing Coordinate.

coord1.Get_Distance_From_Coordinate(coord2).Miles;

If you wish to move a coordinate based on a known distance and bearing you can do so with the Move function. Distance must be passed in meters. The coordinate values will update in place.

//1000 Meters
//270 degree bearing
coord1.Move(1000, 270, Shape.Ellipsoid);

You may also move a specified distance toward a target coordinate if you do not have a bearing toward it.

//Move coordinate 1 10,000 meters toward coordinate 2
coord1.Move(coord2, 10000, Shape.Ellipsoid);

The option to create a Distance for conversion purposes only also exists.

Distance d = new Distance(20, DistanceType.NauticalMiles);
d.Meters; //Convert to meters.

Binding and MVVM

The properties in CoordinateSharp implement INotifyPropertyChanged and may be bound. If you wish to bind to the entire CoordinatePart bind to the Display property. This property can be notified of changes, unlike the overridden ToString(). The Display will reflect the formats previously specified for the Coordinate object in the code-behind.

Output Example:

<TextBlock Text="{Binding Latitude.Display, UpdateSourceTrigger=PropertyChanged}"/>

Input Example:

<ComboBox Name="latPosBox" VerticalAlignment="Center" SelectedItem="{Binding Path=DataContext.Latitude.Position, UpdateSourceTrigger=LostFocus, Mode=TwoWay}"/>
<TextBox Text="{Binding Latitude.Degrees, UpdateSourceTrigger=LostFocus, Mode=TwoWay, ValidatesOnExceptions=True}"/>
<TextBox Text="{Binding Latitude.Minutes, UpdateSourceTrigger=LostFocus, Mode=TwoWay, ValidatesOnExceptions=True}"/>
<TextBox Text="{Binding Latitude.Seconds, StringFormat={}{0:0.####}, UpdateSourceTrigger=LostFocus, Mode=TwoWay, ValidatesOnExceptions=True}"/>

NOTE: It is important that input boxes be set with 'ValidatesOnExceptions=True'. This will ensure UIElements display input errors when incorrect values are passed.


Celestial Information

You may pull the following pieces of celestial information by passing a UTC date to a Coordinate. You can initialize an object with a date or pass it later. CoordinateSharp operates in UTC so all dates will be assumed in UTC regardless of the specified DateTimeKind. With that said, the ability to convert to local time after all celestial calculations have been accomplished exists and is explained in this section.

Accessing celestial information (all times in UTC).

Coordinate c = new Coordinate(40.57682, -70.75678, new DateTime(2017,3,21));
c.CelestialInfo.SunRise.ToString(); //Outputs 3/21/2017 10:44:00 AM

Getting times in local is more involved then just adding or subtracting hours to a specified property. This is due to the fact that a moon rise or sunset may not occur on the local day, even though it can on a UTC day. Because of this, you must create a new Celestial object using the Celestial.Celestial_LocalTime(Coordinate c, double offset) function. This will create a new Celestial object populated in local time.

Let's assume a user input a date into a box that's intended to be local instead of UTC.

DateTime d = UsersSpecifiedDate.
  
//Get the local offset time from UTC
//For ease we will manually input an offset
double offset = -4; //Eastern time is -4 hours from UTC. 
  
//Convert users date to UTC time
d.AddHours(offset*-1);
  
//Create a Coordinate with the UTC time
Coordinate c = new Coordinate(39.0000,-72.0000, d); 
 
//Create a new Celestial object by converting the existing one to Local
Celestial celestial = Celestial.Celestial_LocalTime(c, offset);

//There are also solar/lunar only options for increased performance
//This will cut benchmarks in half as conversions are expensive
celestial = Celestial.Solar_LocalTime(c, offset);
celestial = Celestial.Lunar_LocalTime(c, offset);

NOTE ABOUT LOCAL TIME CONVERSIONS: Conversions are currently made by grabbing celestial information for the day before and after the specified date. It then compares values to the user specified date to find correct local times. This will be reworked for efficiency in the future.

The following pieces of celestial information are available:

  • -Sun Set
  • -Sun Rise
  • -Sun Altitude
  • -Sun Azimuth
  • -MoonSet
  • -Moon Rise
  • -Moon Distance
  • -Moon Altitude
  • -Moon Azimuth
  • -Moon Illumination (Phase, Phase Name, etc)
  • -Additional Solar Times (Civil/Nautical/Astronomical/Bottom of Solar Disc Times)
  • -Astrological Information (Moon Sign, Zodiac Sign, Moon Name If Full Moon")
  • -Solar/Lunar Eclipse information.
  • -Perigee/Apogee information.

You may check if the Sun or Moon is currently Up by using the IsSunUp and IsMoonUp boolean properties. The value returned is based on the provided location and GeoDate. The Sun and Moon are considered "Up" based on the body's rise/set times.

c.CelestialInfo.IsSunUp; //returns true or false

Sun/Moon Set and Rise DateTimes are nullable. If a null value is returned the Sun or Moon Condition needs to be viewed to see why. In the below example we are using a lat/long near the North Pole with a date in August. The sun does not set that far North during the specified time of year.

Coordinate c = new Coordinate(85.57682, -70.75678, new DateTime(2017,8,21));
c.CelestialInfo.SunCondition; //Outputs UpAllDay

Moon Illumination returns a value from 0.0 to 1.0. The table shown is a basic break down. You may determine Waxing and Waning types between the values shown or you may get the phase name from the Celestial.MoonIllum.PhaseName property.

c.Celestial.MoonIllum.PhaseName
Value Phase
0.0 New Moon
0.25 First Quarter
0.5 Full Moon
0.75 Third Quarter

You may also grab celestial data through static functions if you do not wish to create a Coordinate object.

Celestial cel = Celestial.CalculateCelestialTimes(85.57682, -70.75678, new DateTime(2017,8,21));
cel.SunRise.Value.ToString();

Pergee and Apogee information is available in the Celestial class but may be called specifically as it is not location dependent.

Perigee p = Celestial.GetPerigee(date);
p.LastPerigee.Date;
p.LastPerigee.Distance.Kilometers;

Solar and Lunar Eclipse.

Coordinate seattle = new Coordinate(47.6062, -122.3321, DateTime.Now);
//Solar
SolarEclipse se = seattle.CelestialInfo.SolarEclipse;
se.LastEclipse.Date;
se.LastEclipse.Type;
//Lunar
LunarEclipse le = seattle.CelestialInfo.LunarEclipse;
se.NextEclipse.Date;
se.NextEclipse.Type;

You may also grab a list of eclipse data based on the century for the location's date.

List<SolarEclipseDetails> events = Celestial.Get_Solar_Eclipse_Table(seattle.Latitude.ToDouble(), seattle.Longitude.ToDouble(),  DateTime.Now);

NOTE REGARDING ECLIPSE DATA: Eclipse data can only be obtained from the years 1601-2600. Thas range will be expanded with future updates.

NOTE REGARDING SOLAR/LUNAR ECLIPSE PROPERTIES: The Date property for both the Lunar and Solar eclipse classes will only return the date of the event. Other properties such as PartialEclipseBegin will give more exact timing for event parts.

Solar eclipses sometimes occur during sunrise/sunset. Eclipse times account for this and will not start or end while the sun is below the horizon.

Properties will return 0001/1/1 12:00:00 if the referenced event didn't occur. For example if a solar eclipse is not a Total or Annular eclipse, the AorTEclipseBegin property won't return a populated DateTime.

NOTE REGARDING CALCULATIONS: The formulas used take into account the locations altitude. Currently all calculations for eclipse timing are set with an altitude of 100 meters. Slight deviations in actual eclipse timing may occur based on the locations actual altitude. Deviations are very minimal and should suffice for most applications.

NOTE REGARDING REFRACTION: Averages are used to calculate refraction. You may see slight deviations in actual event occurance depending on actual atmospheric conditions. Furthmore, you will see deviations in times near circumpolar regions if a celestial body spends a large quantity of time near the event horizon. For example, if the events Civil Dusk & Civil Dawn are with an hour or two from eachother, you will see deviations in time as the solar disc maintains position at the event horizon.


Julian Date Conversions

The Julian date converters used by the library have been exposed for use. The converters account for both Julian and Gregorian calendars.

//To Julian
double jul = JulianConversions.GetJulian(date);
 
//From Julian
DateTime date = JulianConversions.GetDate_FromJulian(jul));
 
//Epoch options also exist
JulianConversions.GetJulian_Epoch2000(date);    

Geo-Fencing

Both line and polygon boundaries may be specified using the GeoFence class. Once Points have been specified you may compare them to a Coordinate to determine if it is within bounds.


List<GeoFence.Point> points = new List<GeoFence.Point>();

//Points specified manually to create a square in the USA.
//First and last points should be identical if creating a polygon boundary.
points.Add(new GeoFence.Point(31.65, -106.52));
points.Add(new GeoFence.Point(31.65, -84.02));
points.Add(new GeoFence.Point(42.03, -84.02));
points.Add(new GeoFence.Point(42.03, -106.52));
points.Add(new GeoFence.Point(31.65, -106.52));

GeoFence gf = new GeoFence(points);


Coordinate c = new Coordinate(36.67, -101.51);

//Determine if Coordinate is within polygon
gf.IsPointInPolygon(c);

//Determine if Coordinate is within specific range of shapes line.
gf.IsPointInRangeOfLine(c1, 1000); //Method 1 specify meters.

Distance d = new Distance(1, DistanceType.Kilometers);
gf.IsPointInRangeOfLine(c1, d); //Method 2 specify Distance object.

Eager Loading

CoordinateSharp values are all eager loaded upon initialization of a Coordinate. Anytime a Coordinate property changes, everything is recalculated. You may wish to turn off eager loading if you are trying to maximize performance. This will allow you to specify when certain calculations take place.

EagerLoad eagerLoad = new EagerLoad();
eagerLoad.Celestial = false;
Coordinate c = new Coordinate(40.0352, -74.5844, DateTime.Now, eagerLoad);
//To load Celestial data when ready
c.LoadCelestialInfo();           

The above example initializes a Coordinate with eager loading in place. You may however turn it on or off after initialization.

c.EagerLoadSettings.Celestial = false;    

You may also turn EagerLoading on/off for all available settings at once.

//Sets EagerLoading off for all properties (CelestialInfo, MGRS, UTM, Cartesian, ECEF)
EagerLoad eagerLoad = new EagerLoad(false);
Coordinate c = new Coordinate(40.0352, -74.5844, DateTime.Now, eagerLoad);

You may also create an object by passing enum flags with a declared object or static function. Only the passed flags will EagerLoad.

EagerLoadType et = EagerLoadType.Celestial | EagerLoadType.Cartesian;

EagerLoad eagerLoad = new EagerLoad(et);
//OR
EagerLoad eagerLoad = EagerLoad.Create(et); //Returns new EagerLoad

Benchmarks

The following Coordinate methods were benchmarked as follows.

Method i7-4720HQ 2.60 GHz (x64) i5-4210U 1.70 GHz (x64)
Standard Initialization 9 ms 14 ms
TryParse() Initialization 7-35 ms 11-42 ms
Secondary Initialization 45 ms 64 ms
Initialization w/EagerLoad off < 1 ms < 1 ms
Property Change 8 ms 12 ms

Acknowledgements

Most celestial calculations are based on "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.

Certain solar algorithms were adapted from NOAA and Zacky Pickholz 2008 "C# Class for Calculating Sunrise and Sunset Times" NOAA The Zacky Pickholz project

Certain lunar calculations were adapted from the mourner / suncalc project (c) 2011-2015, Vladimir Agafonkin suncalc & These Formulas by Dr. Louis Strous

Calculations for illumination parameters of the moon based on NASA Formulas and Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.

UTM & MGRS Conversions were referenced from Sami Salkosuo's j-coordconvert library & Steven Dutch, Natural and Applied Sciences,University of Wisconsin - Green Bay

ECEF Conversions were referenced from works by James R. Clynch

Solar and Lunar Eclipse calculations were adapted from NASA's Eclipse Calculator created by Chris O'Byrne and Fred Espenak.

Aspects of distance calculations referenced worked by Ed Williams Great Circle Calculator

Graphic and logo design work was donated by area55.

All GitHub users who contribute code and/or create issues!