Developer Guide
INTRODUCTION
What is CoordinateSharp?
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), Cartesians (X, Y, Z), GEOREF, and Web Mercator (EPSG:3857). It also provides a wide array
of location based solar/lunar information such as rise/set times, phase info and more.
Prerequisites
.NET 4.0+ Framework, .NET Standard 1.3+ and .NET 5.0+ Supported Platforms
Installation
CoordinateSharp is available as a nuget package from nuget.org
Alternatively, you may download the library directly here.
Updates and Changes
Change notes can be viewed here
Questions and Issues
If you have any issues or questions create an issue on our GitHub Project Page.
CONTENTS
- Coordinates Basics
- Coordinate System Conversion
- Celestial Information
- Distances, Bearings and Moving Coordinates
- Extension Libraries
- Binding
- Working with Time
- Geo-Fencing
- Eager Loading
- Global Settings
- Benchmarks
COORDINATE BASICS
Creating a Coordinate Object
The Coordinate
object is the "main" class of this library. It represents a standard geodetic "lat/long" coordinate.
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
Parsing a Coordinate From a String
CoordinateSharp has the ability to parse a Coordinate
from a provided string.
Coordinate.Parse()
The Coordinate.Parse()
method is a quick way to parse a coordinate string into a Coordinate
object. It will throw
exceptions if it fails however, so it important that proper exception handling be implemented when using this method. If
the input string is uncontrolled, it is recommend that the TryParse()
method below be used instead.
string s = "34X 551586mE 8921410mN"; //UTM Coordinate
Coordinate c = Coordinate.Parse(s);
Coordinate.TryParse()
The advantage of TryParse()
is that you do not have worry about an invalid format throwing an exception.
TryParse()
will return a bool
value instead signaling whether the parse was successful.
string s = "34X 551586mE 8921410mN"; //UTM Coordinate
Coordinate c; //Create new Coordindate to populate
if(Coordinate.TryParse(s, out c))
{
//Coordinate parse was successful
//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
and/or EagerLoading
specification using an overload with either parser.
If you need to work with Latitudes and Longitudes individually you may use the CoordinatePart.Parse()
or
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;
As some coordinate systems may contain similar string formats (ex. Geodetic signed and Web Mercator), you
may wish to disable parsing of certain systems. This may be done using a Parse()
and/or TryParse()
overload.
You may also adjust the Global Default Parse Settings on application startup.
//Limit parsable systems on demand
//Limit to geodetic lat/long and MGRS systems
Allowed_Parse_Format formats = Allowed_Parse_Format.Lat_Long | Allowed_Parse_Format.MGRS;
Coordinate c = Coordinate.Parse("91 1", formats);
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.
//Specifies input is in ECEF if X, Y, Z coordinate parses successfully.
Coordinate.TryParse(s, CartesianType.ECEF, out c);
Whereas Coordinate.Parse
will parse into any available system in CoordinateSharp, the ability to parse into a few specific systems exists
if you desire to be restrictive.
MilitaryGridReferenceSystem mgrs = MilitaryGridReferenceSystem.Parse(coordString);
The coordinate parser will be expanded constantly so provided suggestions on formats that should parse are greatly appreciated.
Creating a Coordinate From Other Degree Values
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.
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.Format = 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'
CoordinateFormatType |
Output |
Decimal |
40.577 -70.757 |
Decimal_Degree |
N 40.577º W 70.757º |
Degree_Decimal_Minutes |
N 40º 34.609' W 70º 45.407' |
Degree_Minutes_Seconds |
N 40º 34' 36.552" W 70º 45' 24.408" |
Coordinate System Conversion
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
UTM/MGRS coordinates are truncated per standard. Though this makes sense in terms of operating in those systems, it can have
adverse impacts when converting back and forth between UTM/MGRS and Lat/Long as precision loss will occur.
Rounded centimeter versions of these systems are also available for output. These values may be more reliable if
converting back and forth between UTM/MGRS and Lat/Long systems.
Coordinate c = new Coordinate(40.57682, -70.75678);
c.MGRS.ToRoundedString(); // Outputs 19T CE 51308 93265
c.MGRS.ToRoundedString(5); // Outputs 19T CE 51307.55707 93264.83597
The UTM/MGRS systems will automatically switch to and display their polar system counterparts once polar regions are entered.
UTM's polar counterpart is the Universal Polar Stereographic (UPS) and MGRS uses the MGRS Polar system.
You may check which system is in use with the SystemType
property. This property has replaced the
WithinCoordinateSystemBounds
property which was previously checked.
Coordinate c = new Coordinate(-85,10);
Console.WriteLine(c.MGRS.SystemType); //MGRS_Polar;
Console.WriteLine(c.UTM.SystemType); //UPS;
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 or
one of the built in ellipsoid specifications Earth_Ellipsoid_Spec
.
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
//Ellipsoid is set on the Coordinate object for UTM/MGRS
c.Set_Datum(6378137, 298.257222101);
//OR
c.Set_Datum(Earth_Ellipsoid_Spec.GRS80_1979);
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);
There may be times in which you wish to remain in a single grid zone and "over-project" beyond its boundaries.
This is often useful if a small section of a mapped area extends into a neighboring grid zone. We can accomplish
over-projection by locking the latitudinal grid zone. It should be noted however, that over-projection will result in
a loss of precision. It is up to each user to determine if precision loss is acceptable.
//Create a coordinate that projects to UTM Grid Zone 31
Coordinate coord = new Coordinate(51.5074,1);
//Display normal projected UTM
Console.WriteLine(coord.UTM); //31U 361203mE 5708148mN
//Lock coordinate conversions to Grid Zone 30 (over-project);
coord.Lock_UTM_MGRS_Zone(30);
//Display over-projected UTM
Console.WriteLine(coord.UTM); //30U 777555mE 5713840mN
//Approximately 1 meter of precision lost with a 1° (111 km / 69 miles) over-projection.
While over projection is allowed, users may wish to detect UniversalTransverseMercator
out of bounds initialization and block it.
You may detect out of bounds creation by checking the UniversalTransverseMercator.Out_Of_Bounds
bool
property.
This property will return true if a UniversalTransverseMercator
object is created outside of the system specified boundaries.
This property will NOT trigger when zone locking is used from within a Coordinate
object.
MilitaryGridReferenceSystem
does not allow this behavior and will throw an
exception if an object is created outside of system specifications.
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 any southern hemisphere letter (ex. "C")
for southern hemisphere UTMs and any northern hemisphere letter (ex. "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: Certain softwares handle near polar region coordinates differently due to the conic shape of grid zones when nearing the edges of system limitations.
Some softwares choose to skip grid zone designations in order to widen the grid zone being returned during conversion.
This is normal and both softwares are accurate due to grid overlap. This is most obvious in the N80-N84 regions prior to entering UPS/MGRS Polar.
For example N 80, E 7
will return different UTM/MGRS coordinates between www.earthpoint.us and CoordinateSharp.
Software |
UTM |
MGRS |
Earthpoint |
31X 577483mE 8884250mN |
31X EJ 77483 84250 |
CoordinateSharp |
32X 461235mE 8882252mN |
32X MP 61235 82252 |
Both coordinates will convert back to approximately N 80, E 7
in both softwares.
This discrepancy is important however, as it could lead to test/expectation failures if not handled correctly.
NOTE: US ARMY TEC-SR-7 1996 was referenced for UPS/MGRS POLAR conversions implemented in 2.4.1.1.
During testing it was noted that the formulas suffer accuracy loss during convert backs
(converting UPS/MGRS POLAR back to GEODETIC LAT/LONG). This accuracy loss ranges from 0-33
meters below the 88th parallel and 66 meters up to 2.2 kilometers above the 88th parallel
(greatest precision loss occurs near poles).
It is important that users working in polar regions understand these precision limitations.
Other software/tools will experience similar precision losses due to the nature of the system,
but there may be more accurate polar tools available if higher precision is required.
NOTE: CoordinateSharp uses math based calculations to determine converted coordinates. This
library does not attempt to correct for modified zones (ex Norway).
MGRS Grid Boxes
It may be necessary at times to determine MGRS Grid Zone Designator (GZD) 100km square corner points. You can calculate estimated grid boundaries
using the MGRS_GridBox
class. Both MGRS and Lat/Long points can be obtained. It should be noted that while CoordinateSharp will
automatically attempt to correct false MGRS points, the MGRS_GridBox
cannot. Users should always determine
if the provided MGRS coordinate is valid. Example below.
Note: This feature will only return GZD 100km square corner points, not MGRS grid squares which may be different based on specified precision.
//Create MGRS Coordinate at a Grid Zone Junction Point (partial square)
MilitaryGridReferenceSystem mgrs = new MilitaryGridReferenceSystem("N", 21, "SA", 66037, 61982);
//Set EagerLoad to UTM/MGRS only for efficiency if needed
EagerLoad el = new EagerLoad(EagerLoadType.UTM_MGRS);
var gb = mgrs.Get_Box_Boundaries(el);
//Check if box is valid first (if not corners will be null)
if(!gb.IsBoxValid){return;}
//Get Bottom Left MGRS Object
gb.Bottom_Left_MGRS_Point; //21N SA 66022 00000
//Get Bottom Left Coordinate Object
//Will throw exception if MGRS is not valid.
gb.Bottom_Left_Coordinate_Point; //N 0º 0' 0" W 59º 59' 59.982"
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);
Earth Centered Earth Fixed (ECEF) Cartesian coordinates are available. They 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);
Web Mercator EPSG:3857
Web Mercator EPSG:3857 coordinates are available. Web Mercator coordinates can only be converted using the WGS84 default ellipsoid.
To Web Mercator:
Coordinate c = new Coordinate(45, 64);
Console.WriteLine(c.WebMercator.ToString()); //7124447.411mE 5621521.486mN
To Lat/Long:
WebMercator wm = new WebMercator(3624447.411, 2721521.486);
Coordinate c = WebMercator.ConvertWebMercatortoLatLong(wm); //N 23º 44' 17.003" E 32º 33' 32.274"
NOTE: As Web Mercator is limited to WGS84, Coordinate.WebMercator
will throw a FormatException
if called after a Coordinate
object ellipsoid has been changed to anything other than WGS84.
NOTE: The Web Mercator system is not realiable above/below +/- 85.06 latitude. Developers should build truncations into their systems as needed.
World Geographic Reference System GEOREF
World Geographic Reference System - GEOREF coordinates are available.
To GEOREF:
Coordinate c = new Coordinate(45, 64);
Console.WriteLine(c.GEOREF.ToString()); //SKEA000000000000
To Lat/Long:
GEOREF geo = new GEOREF("SK","EA","425698","152658");
Coordinate c = GEOREF.ConvertGEOREFtoLatLong(geo); //N 45º 15' 15.948" E 64º 42' 34.188"
You may also quickly create a GeoFence
based on a precision OR extract box corners. This may become
unreliable at the poles.
int precision = 6;
//Get GeoFence
GeoFence fence = geo.ToGeoFence(precision);
//Get Corners
GEOREF bl = geo.Get_BottomLeftCorner(precision);
GEOREF tr = geo.Get_TopRightCorner(precision);
GEOREF
precision is set at 10 with a default string output of 6. A custom precision may be set via the
GEOREF.ToString(int precision)
override.
Celestial Information
Accessing Celestial Data
Solar and lunar information is made available by passing a date to a Coordinate
.
You may initialize a Coordinate
object with a date or pass it later.
CoordinateSharp operates in UTC by default, so all dates will be assumed in UTC regardless of the specified DateTimeKind
.
You may however operate in local time by specifying the Coordinate
object's Offset
value should you choose.
With minimal code you can calculate sunset, sunrise, moonset, moon illumination and more in C#.
Accessing celestial information example (all times will be in UTC).
//UTC Date 21-MAR-2019 @ 11:00 AM
DateTime d = new DateTime(2017,3,21,11,0,0);
Coordinate c = new Coordinate(40.57682, -70.75678, d);
c.CelestialInfo.SunRise.ToString(); //Outputs 3/21/2017 10:45:00 AM
Local time operation example.
//EST Date 21-MAR-2019 @ 07:00 AM Local
DateTime d = new DateTime(2017,3,21,7,0,0);
Coordinate c = new Coordinate(40.57682, -70.75678, d);
//Coordinate still assumes the date is UTC, so we must specify the local offset hours.
c.Offset = -4; //EST is UTC -4 hours
c.CelestialInfo.SunRise.ToString(); //Outputs 3/21/2017 06:45:00 AM
You may also set the entire application to run in local time based on the user's environment by modifying the GlobalSettings
.
This should be done only at application start up and requires all DateTimeKind
objects to be set to local
. If set to
UTC
then Coordinates will operate in UTC
.
//Set at application startup
GlobalSettings.Allow_Coordinate_DateTimeKind_Specification = true;
//EST Date 21-MAR-2019 @ 07:00 AM Local
DateTime d = new DateTime(2017,3,21,7,0,0);
Coordinate c = new Coordinate(40.57682, -70.75678, d);
c.CelestialInfo.SunRise.ToString(); //Outputs 3/21/2017 06:45:00 AM
Note: If Allow_Coordinate_DateTimeKind_Specification
is set to true
then adjusting Coordinate.Offset
will throw an
InvalidOperationException
.
Available Celestial Data
The following pieces of celestial information are currently available:
DATA TYPE |
SUN |
MOON |
Rise |
* |
* |
Set |
* |
* |
Noon |
* |
|
Altitude |
* |
* |
Azimuth |
* |
* |
Distance |
|
* |
Illumination |
|
* |
Perigee |
|
* |
Apogee |
|
* |
Dawns |
* |
|
Dusks |
* |
|
Eclipse |
* |
* |
Solstice |
* |
|
Equinox |
* |
|
Coordinate |
* |
* |
Hours of Day |
- |
- |
Hours of Night |
- |
- |
Checking if Celestial Body Is Up
You may check if the Sun or Moon is currently "up" by using the IsSunUp
or IsMoonUp
boolean properties.
The value returned is based on the provided location, GeoDate
and Offset
hours provided.
The Sun and Moon are considered "Up" based on the body's rise/set times.
c.CelestialInfo.IsSunUp; //returns true or false
Checking Celestial Body Conditions
IMPORTANT: 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. It is recommended that you always check this. Because CoordinateSharp works on a "event per day"
basis and not a "next event" basis, you may have days with no rise or no set.
This becomes especially apparent in the spring when solar cycles are greater than 24 hours due to growing daylight. Once a year
you will have a day with no rise or set in certain regions of the world if working in UTC/ZULU time.
Coordinate c = new Coordinate(85.57682, -70.75678, new DateTime(2017,8,21));
c.CelestialInfo.SunRise; //Outputs null
c.CelestialInfo.SunCondition; //Outputs UpAllDay
If you wish to work in a "last" or "next" event basis, you may do so using static functions. This is advantageous if you
wish to return a guaranteed rise or set time. It should be noted that this method can become expensive as you move towards the
poles as rise and sets may not occur for weeks or month at a time.
DateTime d = new DateTime(2019, 2, 6);
DateTime val = Celestial.Get_Next_SunRise(40.0352, -74.5844, d);
Moon Illumination and Phase
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 using Celestial.MoonIllum.Phase
or you may get the phase name from the Celestial.MoonIllum.PhaseName
(string) or Celestial.MoonIllum.PhaseNameEnum
(enum) properties.
Value |
Phase |
0.0 |
New Moon |
0.25 |
First Quarter |
0.5 |
Full Moon |
0.75 |
Third Quarter |
It should be noted that when working in local time, the moons "phase name" may change if the UTC day is the previous or next day. There will also be times when the phase name
does not match the news or certain websites. This normally occurs when the source is using the following morning's phase name. CoordinateSharp works in the exact
time passed to capture phase name, not the phase name on a "certain night".
If you are unsure or do not prefer this behavior, you may tap into the Phase
property to determine your own phase name.
Solar and Lunar Coordinates
Coordinate information (to include subsolar and sublunar) is available. This can be extremely useful when creating tracks over the earth.
//Accessible through Coordinate
Coordinate c = new Coordinate(45, 45, new DateTime(2020, 11, 19, 12, 30, 0));
Console.WriteLine(c.CelestialInfo.LunarCoordinates.SublunarLatitude);//-24.0341
Console.WriteLine(c.CelestialInfo.LunarCoordinates.SublunarLongitude);//51.6318
//Static Method
lc = Celestial.Get_Lunar_Coordinate(new DateTime(2020, 11, 19, 12, 30, 0));
Console.WriteLine(lc.SublunarLatitude);//-24.0341
Console.WriteLine(lc.SublunarLongitude);//51.6318
Static Retrieval of Celestial Data
You may also grab celestial data through static functions if you do not wish to create a Coordinate object.
//UTC EXAMPLE
Celestial cel = Celestial.CalculateCelestialTimes(45.57682, -70.75678, new DateTime(2017,8,21));
cel.SunRise.Value.ToString();
//LOCAL TIME (EST) EXAMPLE
double utcOffsetHours = -4;
Celestial cel = Celestial.CalculateCelestialTimes(45.57682, -70.75678, new DateTime(2017,8,21), utcOffsetHours);
cel.SunRise.Value.ToString();
Determine Time of Day from Solar Altitude or Solar Azimuth
If you wish to determine the time of day based on a provided date, coordinate point and solar altitude or azimuth you may do so.
When determining a time of day based on solar altitude, a rising and setting transit event time will be returned as altitudes normally hit twice in a day.
These values are nullable
and should be checked for value before use.
You may also check the Condition
property to determine why a rise or set through the specified altitude
is null. This works just like the normal Sun/Moon Rise/Set condition check.
Caution: These methods are only reliable during daylight hours when the sun is observable.
Time calculations will not be accurate after sunset when the altitude is negative.
//lat, long, date, altitude in degrees, UTC offset (if desired).
AltitudeEvents aev = Celestial.Get_Time_at_Solar_Altitude(47.4, -122.6, new DateTime(2020,8,11), 41.6, -7);
aev.Condition;//RiseAndSet
//Altitude point crossed time during solar rising
if(aev.Rising.HasValue)
{
aev.Rising; //8/11/2020 10:22:12 AM
}
//Altitude point crossed time during solar setting
if(aev.Setting.HasValue)
{
aev.Setting; //8/11/2020 4:11:33 PM
}
Determining the time of day from the sun's azimuth is more strait forward and will return a nullable DateTime
value. When a
null value is returned, it means the sun is either down/set (too inaccurate to reliably return a value) OR the azimuth provided did not
occur within the specified date / error delta.
//Create a coordinate and specify a date.
Coordinate c = new Coordinate(49, -122, new DateTime(2023, 9, 30));
//Set local UTC offset as desired.
c.Offset = -7;
//Set current sun azimuth in degrees E of N
double az = 120;
//Determine time of day. Default azimuth accuracy error delta is 1 degree by default,
//but it is set at .5 for this example.
DateTime? t = Celestial.Get_Time_At_Solar_Azimuth(az, c, .5);
Console.WriteLine($"{t}"); //9/30/2023 9:21:44 AM
Lunar Perigee and Apogee
Perigee 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;
Eclipses
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);
Celestial Data Notes
NOTES REGARDING ACCURACY: Most celestial calculations use math based approximation algorithms. These approximations use average values for things like
altitude, refraction and other atmospheric conditions. It is up to each user to determine if precision meets use case.
Circumpolar regions may experience severe degradation of solar times during the seasonal transition from rising and setting to up and down all-day conditions due to near horizon transition precision. For example, if the sun hangs near -0.8333 degrees during direction transition, a rise or set may or may not register. This may cause an up or down all-day solar condition even though the sun technically sets if it hits that altitude. Degradation should be negligible to observers as they are likely not concerned if a set is registered or not as the sun sits on the horizon. This occurrence is extremely rare and would only happen in circumpolar regions.
The library's design emphasizes usability, efficiency of implementation, and lightweightedness, so precision in this region is slightly sacrificed at times.
Celestial conversions occurring in the pre-Gregorian era (before 1582) may see accuracy degradation at times due to differences in leap year
calculations between calendars. It is up to each user to determine if precision meets use case.
NOTE REGARDING ECLIPSE DATA: Eclipse data can only be obtained from the years 1601-2600. This 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.
Distances, Bearings and Moving Coordinates
Calculating Distance
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;
Moving Coordinates
Coordinates may be moved or shifted using spherical or ellipsoidal earth calculations. You may execute
a move using a specified distance towards a target coordinate or via an initial start bearing.
//Move 1000m towards a target
Coordinate startCoordinate = new Coordinate(45, 65);
Coordinate targetCoordinate = new Coordinate(46, 66);
startCoordinate.Move(targetCoordinate, 1000, Shape.Ellipsoid);
//Move 1000m starting with a 93 degree bearing.
Coordinate coordinate = new Coordinate(45, 65);
coordinate.Move(1000, 93, Shape.Ellipsoid);
NOTE: Due to the shape of the earth, the end bearing of a move will not equal the start bearing. This needs
to be accounted for when drawing shapes.
//The following example completes a box based on two known coordinates.
//Set string output for ease
GlobalSettings.Default_CoordinateFormatOptions.Format = CoordinateFormatType.Decimal; //Set output to decimal string
GlobalSettings.Default_CoordinateFormatOptions.Round = 14; //Precision 14
//Set coordinates
var crdInitialPointA = new Coordinate(52.073018535963556, 8.69649589116307);
var crdInitialPointB = new Coordinate(52.073783497352025, 8.695117235729413);
//Get distance from A to B
Distance distanceAB = new Distance(crdInitialPointA, crdInitialPointB, Shape.Ellipsoid);
//Set width of box as distance between A and B
double width = distanceAB.Meters; //127.202
//Set height of box at 100m
double height = 100;
//Turn -90 degrees
double baseBearing = distanceAB.Bearing-90;
//Calculate Point C from A
var crdInitialPointC = new Coordinate(crdInitialPointA.Latitude.ToDouble(), crdInitialPointA.Longitude.ToDouble());
crdInitialPointC.Move(height, distanceAB.Bearing-90, Shape.Ellipsoid);
//Calculate Point D from C
//Get new "initial" bearing by reverse calculating the distance from C to A
Distance distanceCA = new Distance(crdInitialPointC, crdInitialPointA, Shape.Ellipsoid);
var crdInitialPointD = new Coordinate(crdInitialPointC.Latitude.ToDouble(), crdInitialPointC.Longitude.ToDouble());
crdInitialPointD.Move(width, distanceCA.Bearing-90, Shape.Ellipsoid);
List<string> points = new List<string>(){crdInitialPointB.ToString(),crdInitialPointC.ToString(),crdInitialPointD.ToString(),crdInitialPointA.ToString()};
string contentFile = string.Join("\n", points);
Console.WriteLine(contentFile);
//52.073783497 8.695117236
//52.072350665 8.695519989
//52.073115613 8.69414134
//52.073018536 8.696495891
Points projected on a map.
Extension Libraries
There is often a need for certain tools that would work well with CoordinateSharp, but
may not necessarily fit well within the base library. This may be due to required data models or performance issues.
To work around this, we are creating extension libraries that are designed to work with CoordinateSharp. These extensions must be
downloaded separately.
Magnetic Fields
Package Name: CoordinateSharp.Magnetic
Location based magnetic data may be calculated via the CoordinateSharp.Magnetic package.
Currently available magnetic models:
The following examples show how to obtain a magnetic declination's value, variation and uncertainty.
Example 1 (Magnetic Constructor):
Coordinate c = new Coordinate(45, -121, new DateTime(2020, 10, 1), new EagerLoad(false));
//Height is 0km MSL unless specified in overload constructor.
Magnetic m = new Magnetic(c, DataModel.WMM2020);
m.MagneticFieldElements.Declination; //14.673069
m.SecularVariations; //-0.09198
m.Uncertainty; //0.38141
Example 2 (Coordinate Extension):
Coordinate c = new Coordinate(45, -121, new DateTime(2020, 10, 1), new EagerLoad(false));
//Height is 0km MSL unless specified in overload constructor.
Magnetic m = c.GetMagnetic(DataModel.WMM2020);
m.MagneticFieldElements.Declination; //14.673069
m.SecularVariations; //-0.09198
m.Uncertainty; //0.38141
One of the main advantages of CoordinateSharp is its eager loaded approach. This approach allows a Coordinate
object's values to
update as properties changes. For example, if you change the Coordinate
object's latitude, the sunset time will update. Because extension
packages are not part of the base library, they cannot be eager loaded by default. If you want to automatically update magnetic data as a
Coordinate
changes, you may do so by subscribing to the Coordinate.CoordinateChanged
event. This event will trigger every time the
Coordinate
object is updated.
Magnetic m;
public void Main()
{
Coordinate c = new Coordinate(45, -121, new DateTime(2020, 10, 1), new EagerLoad(false));
c.CoordinateChanged += CoordinateChanged; //Subscribe changes
m = c.GetMagnetic(DataModel.WMM2020);
}
//Will fire every time the subscribed object changes
private void CoordinateChanged(object sender, EventArgs e)
{
Coordinate c = (Coordinate)sender;
//Update the magnetic object
m = c.GetMagnetic(DataModel.WMM2020);
}
Binding
Binding Data
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.
Working with Time
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. Due to differences in leap years between the Julian and Gregorian calendars, there may be Julian dates
of February 29th that are not valid on the Gregorian calendar. These dates will be converted to March 1st for safety. As such,
you may see two Julian days that equal March 1st of rare occasions for conversions that are before 1582.
Julian dates occurring in B.C. Gregorian cannot be converted due to DateTime
class limitations in .NET
//To Julian
double jul = JulianConversions.GetJulian(date);
//From Julian
DateTime date = JulianConversions.GetDate_FromJulian(jul));
//Epoch options also exist
JulianConversions.GetJulian_Epoch2000(date);
Getting Time Zone at Location
CoordinateSharp does not contain the ability by itself to acquire time zones for offsetting a UTC DateTime
. This is a complicated process that involves keeping up with
ever changing map data and time zone rules. With that said, it is understood that users of this library often have the need for this. The goal of this section is to assist with methods of accomplishing that goal.
The Two Part Process
Gathering time zone information based solely on a coordinate is a two part process.
- Acquire the IANA time zone ID using either a web service or a library.
- Determine the UTC offset using an IANA compliant date tool such as NodaTime.
1. Acquire the Time Zone ID
There are many tools that can grab a time zone ID based on a provided coordinate, but all have issues. For instance, implementing a library is great in that
you do not rely on a web service to provide that data for you. The problem with this method however is that time zones change globally. Every time there is a change
you must not only update the library you are using, but hope the developers maintaining the library get the change out in time.
Web services like Google or Bing Maps on the other hand are great in that they are usually up to date. The problem with web services however is that they will rate limit you
unless you pay money. Furthermore, they require an internet connection to work. It's really a "pick your poison" type situation. Check out this Stack Overflow
post for a great list of services and libraries that can help you decide which tool works best for you.
For the purposes of this very basic example, we'll be using the Google Maps API. You will most likely need an API key to use this. It can be
obtained from the Google Developer Console
First we need a data model to handle the Google Map Request.
public class GoogleTimeZone
{
public double dstOffset { get; set; }
public double rawOffset { get; set; }
public string status { get; set; }
public string timeZoneId { get; set; }
public string timeZoneName { get; set; }
}
Next we need a DateTime
extension method to handle Google's time stamp requirements.
public static class ExtensionMethods
{
public static double ToTimestamp(this DateTime date)
{
DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
TimeSpan diff = date.ToUniversalTime() - origin;
return Math.Floor(diff.TotalSeconds);
}
}
Lastly we add a method to handle the RESTful API request to Google. This example is very basic. It is up to you to handle any
errors or failed/denied requests.
using RestSharp; //RestSharp can be downloaded via Nuget.
public static string GetTimeZone(double latitude, double longitude)
{
string ianaID;
string key = "<your secret key>"; //MAP API KEY. Get from Google Developer Console.
var client = new RestClient("https://maps.googleapis.com"); //Set the client
var request = new RestRequest("maps/api/timezone/json", Method.GET); //Set the request type (timezone)
//Add required parameters
request.AddParameter("location", latitude + "," + longitude);
request.AddParameter("timestamp", DateTime.Now.ToTimestamp());
request.AddParameter("key", key);
//Send the request to Google and await response
var response = client.Execute<GoogleTimeZone>(request);
return ianaID = response.Data.timeZoneId;
}
Ok now we are set up to grab a time zone ID based on our Lat / Long.
Coordinate c = new Coordinate(42, -112, DateTime.Now);
string timezoneID = GetTimeZone(c.Latitude.ToDouble(), c.Longitude.ToDouble());
Console.WriteLine(timezoneID); //America/Boise
2. Determine the UTC Offset.
Luckily hard part is over. Thanks to the master of C# John Skeet, we can get the offset of the time zone we just
acquired using his awesome library NodaTime.
using NodaTime; //Download from Nuget
DateTimeZone zone = DateTimeZoneProviders.Tzdb[tz];
DateTime d = DateTime.Now.ToUniversalTime(); //Date must have DateTimeKind of UTC
Instant instant = Instant.FromDateTimeUtc(Convert.ToDateTime(DateTime.Now.ToUniversalTime()));
Offset offset = zone.GetUtcOffset(instant);
Geo-Fencing
Creating Geo-Fences
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 or determine the distance to the nearest line.
NOTE: If creating a closed boundary, be sure to "close" the shape by making the first and last points of the list identical.
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 to "close" the shapr 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.
//Method 1 specify meters.
gf.IsPointInRangeOfLine(c, 1000);
//Method 2 specify Distance object.
gf.IsPointInRangeOfLine(c, new Distance(1, DistanceType.Kilometers));
//Get distance from nearest polyline.
Distance d = gf.DistanceFromNearestPolyline(c);
Left and Right Hand Ordering
GeoJSON polygons require specific point ordering to distinguish between outer boundaries and inner holes.
The GeoFence
class provides methods to order points as either left-handed (clockwise) for inner boundaries or
right-handed (counterclockwise) for outer boundaries. This ensures compatibility with GeoJSON specifications.
This is also useful if you are given a random set of points, and wish to order them to create a proper fence.
OrderPoints_LeftHanded
The OrderPoints_LeftHanded
method orders the points of a GeoFence
in a clockwise direction,
suitable for defining inner boundaries (holes) in a GeoJSON polygon.
// Create a GeoFence for an inner boundary (hole)
List<GeoFence.Point> innerPoints = new List<GeoFence.Point>
{
new Coordinate(46.8523, -121.7603), // Mount Rainier (center point)
new Coordinate(46.8625, -121.7401), // Slightly north-east
new Coordinate(46.8421, -121.7805), // Slightly south-west
new Coordinate(46.8650, -121.7850), // North-west
new Coordinate(46.8400, -121.7500) // South-east
};
GeoFence innerFence = new GeoFence(innerPoints);
// Order the points clockwise (left-handed) for a GeoJSON inner boundary
innerFence.OrderPoints_LeftHanded();
//Ensure polygon is closed to complete the shape.
//This will create a new point at the end of the polygon coordinate list the is identical to the first point.
innerFence.ClosePolygon();
Explanation: In this example, we create a GeoFence
representing an inner hole. The OrderPoints_LeftHanded
method
arranges the points in a clockwise direction, following GeoJSON standards for holes. We then call ClosePolygon()
to ensure the boundary is properly closed.
OrderPoints_RightHanded
The OrderPoints_RightHanded
method orders the points of a GeoFence
in a counterclockwise direction, suitable for defining
the outer boundary of a GeoJSON polygon.
// Create a GeoFence for the outer boundary
List<GeoFence.Point> outerPoints = new List<GeoFence.Point>
{
new Coordinate(47.6062, -122.3321), // Seattle
new Coordinate(48.7519, -122.4787), // Bellingham
new Coordinate(47.2529, -122.4443), // Tacoma
new Coordinate(48.0419, -122.9025), // Port Townsend
new Coordinate(47.6588, -117.4260), // Spokane
new Coordinate(46.6021, -120.5059), // Yakima
new Coordinate(46.7324, -117.0002), // Pullman
new Coordinate(48.3102, -122.6290), // Anacortes
new Coordinate(47.8225, -122.3123), // Edmonds
new Coordinate(46.9787, -123.8313), // Aberdeen
new Coordinate(47.0379, -122.9007), // Olympia
new Coordinate(47.6091, -122.2015), // Bellevue
new Coordinate(47.6787, -120.7141), // Leavenworth
new Coordinate(48.0812, -123.2643), // Port Angeles
new Coordinate(46.7152, -122.9522) // Centralia
};
GeoFence outerFence = new GeoFence(outerPoints);
// Order the points counterclockwise (right-handed) for a GeoJSON outer boundary
outerFence.OrderPoints_RightHanded();
//Ensure polygon is closed to complete the shape.
//This will create a new point at the end of the polygon coordinate list the is identical to the first point.
outerFence.ClosePolygon();
Explanation: This example creates a GeoFence
for an outer boundary. The OrderPoints_RightHanded
method arranges the
points in a counterclockwise direction, which is the convention for outer boundaries in GeoJSON.
We also call ClosePolygon()
to ensure the polygon is closed.
Outputting to GeoJson
GeoJsonPolygonBuilder
The GeoJsonPolygonBuilder method constructs a GeoJSON string representation of a polygon, including both the outer boundary and
optional inner boundaries (holes).
// Create the outer boundary and order (right-handed)
List<GeoFence.Point> outerPoints = new List<GeoFence.Point>
{
new Coordinate(47.6062, -122.3321), // Seattle
new Coordinate(48.7519, -122.4787), // Bellingham
new Coordinate(47.2529, -122.4443), // Tacoma
new Coordinate(48.0419, -122.9025), // Port Townsend
new Coordinate(47.6588, -117.4260), // Spokane
new Coordinate(46.6021, -120.5059), // Yakima
new Coordinate(46.7324, -117.0002), // Pullman
new Coordinate(48.3102, -122.6290), // Anacortes
new Coordinate(47.8225, -122.3123), // Edmonds
new Coordinate(46.9787, -123.8313), // Aberdeen
new Coordinate(47.0379, -122.9007), // Olympia
new Coordinate(47.6091, -122.2015), // Bellevue
new Coordinate(47.6787, -120.7141), // Leavenworth
new Coordinate(48.0812, -123.2643), // Port Angeles
new Coordinate(46.7152, -122.9522) // Centralia
};
GeoFence outerFence = new GeoFence(outerPoints);
outerFence.OrderPoints_RightHanded();
//Ensure polygon is closed to complete the shape.
//This will create a new point at the end of the polygon coordinate list the is identical to the first point.
outerFence.ClosePolygon();
// Create an inner boundary and order (left-handed)
List<Coordinate> innerPoints = new List<Coordinate>()
{
new Coordinate(46.8523, -121.7603), // Mount Rainier (center point)
new Coordinate(46.8625, -121.7401), // Slightly north-east
new Coordinate(46.8421, -121.7805), // Slightly south-west
new Coordinate(46.8650, -121.7850), // North-west
new Coordinate(46.8400, -121.7500) // South-east
};
GeoFence innerFence = new GeoFence(innerPoints);
innerFence.OrderPoints_LeftHanded();
//Ensure polygon is closed to complete the shape.
//This will create a new point at the end of the polygon coordinate list the is identical to the first point.
innerFence.ClosePolygon();
// Build the GeoJSON polygon with the outer and inner boundaries
string geoJson = GeoFence.GeoJsonPolygonBuilder(outerFence, new List<GeoFence> { innerFence });
Explanation: In this example, we define both an outer boundary and an inner boundary (hole) using the GeoFence
class.
The OrderPoints_RightHanded
method ensures the outer boundary is counterclockwise, while OrderPoints_LeftHanded
makes
the inner boundary clockwise. The GeoJsonPolygonBuilder
method then generates a valid GeoJSON string representing the
polygon, including the hole.
Output:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ -123.8313, 46.9787 ],
[ -122.4443, 47.2529 ],
[ -122.9007, 47.0379 ],
[ -122.9522, 46.7152 ],
[ -120.5059, 46.6021 ],
[ -117.0002, 46.7324 ],
[ -117.426, 47.6588 ],
[ -120.7141, 47.6787 ],
[ -122.4787, 48.7519 ],
[ -122.629, 48.3102 ],
[ -122.3123, 47.8225 ],
[ -122.9025, 48.0419 ],
[ -123.2643, 48.0812 ],
[ -122.2015, 47.6091 ],
[ -122.3321, 47.6062 ],
[ -123.8313, 46.9787 ]
],
[
[ -121.785, 46.865 ],
[ -121.7401, 46.8625 ],
[ -121.7603, 46.8523 ],
[ -121.75, 46.84 ],
[ -121.7805, 46.8421 ],
[ -121.785, 46.865 ]
]
]
}
}
]
}
Earth Shape Distortion and Densification
It is important to note that the above logic used to determine if a coordinate is within a
boundary does not account for the Earth's spherical distortion.
This means that accuracy will be extremely degraded over long distances.
To mitigate the impacts of distortion, users should use densification, which places
continuous points along the boundary lines to create a more accurate spherical representation
of the polygon as it lies on the Earth.
//Create a four point GeoFence around Utah
List<GeoFence.Point> points = new List<GeoFence.Point>();
points.Add(new GeoFence.Point(41.003444, -109.045223));
points.Add(new GeoFence.Point(41.003444, -102.041524));
points.Add(new GeoFence.Point(36.993076, -102.041524));
points.Add(new GeoFence.Point(36.993076, -109.045223));
points.Add(new GeoFence.Point(41.003444, -109.045223));
GeoFence gf = new GeoFence(points);
// Densify the geofence to plot a coordinate every 5 kilometers using Vincenty to account for Earth's shape
gf.Densify(new Distance(5, DistanceType.Kilometers));
Shape Drawing
There may be times in which you do not know the specific coordinates for a Geo-Fence, but rather need to draw a shape using distances
and bearings. Due to the shape of the earth, the initial bearing of a line may not match the final bearing. This
can cause shapes drawn to be very inaccurate. To assist with this, you can make use of the GeoFence.Drawer
class. This drawing class
will automatically adjust for bearing shifts due to the shape of the earth.
//DRAW A 5KM SQUARE
//THIS EXAMPLE TURNS RIGHT 90 DEGREES AFTER EACH LINE IS DRAWN
//Create a Coordinate at the starting location
//Eager loading is off for efficiency as every point will calculate if it's on.
Coordinate c = new Coordinate(35.68919, 51.38897, new EagerLoad(false));
//Create the GeoFence.Drawer with an initial bearing facing 0 degrees
GeoFence.Drawer gd = new GeoFence.Drawer(c, Shape.Ellipsoid, 0);
gd.Draw(new Distance(5), 0); //Draw the first line maintaining the initial bearing
gd.Draw(new Distance(5), 90); //Change the bearing 90 degrees and draw the second line
gd.Draw(new Distance(5), 90); //Change the bearing 90 degrees and draw the third line
gd.Close(); //Close the shape by drawing a line to the starting location
//Iterate each point drawn and print its location.
foreach (var coord in gd.Points)
{
Console.WriteLine("{0}, {1}", coord.Latitude.ToDouble(), coord.Longitude.ToDouble());
}
Eager Loading
Eager Loading Basics
CoordinateSharp values are all eager loaded upon initialization of a Coordinate
object by default.
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.
NOTE: The default eager loading behavior may be adjusted in the global settings. See Global Settings
for details.
Below is a basic example of how to adjust a Coordinate
object's eager loading behavior. The will turn off
eager loading of the CelestialInfo
property.
EagerLoad eagerLoad = new EagerLoad();
eagerLoad.Celestial = false;
//Create Coordinate with the specified eager loading settings
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 for specific areas after initialization.
coordinate.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);
Enum flags with a declared object or static function may be used. 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
A more in depth look at eager loading can be found on our Performance Tips page.
Global Settings
Global Settings Usage
Application wide global settings may be adjusted to modify the default behavior of certain aspects within CoordinateSharp.
This is a great feature if you wish to set a behavior one time for the entire application. This will save the developer from
having to specify an EagerLoad
or CoordinateFormatOptions
each time a Coordinate
is created.
Global settings should be specified
at application start up. Treating these settings as a dynamic values could have unwanted behavior, especially in multi-threaded environments.
It is highly recommend you do not adjust these values once they have been modified.
You may currently adjust the following default.
Default Setting |
Property Name |
Eager Loading Behavior |
Default_EagerLoad |
Coordinate Format Output Behavior |
Default_CoordinateFormatOptions |
Parsable Coordinate Systems |
Default_Parsable_Formats |
Cartesian Type |
Default_Cartesian_Type |
Equatorial Radius |
Default_EquatorialRadius |
Inverse Flattening |
Default_InverseFlattening |
UTC or Local Time |
Allow_Coordinate_DateTimeKind_Specification |
The below example will change the application wide default eager loading settings from all properties eager loading to UTM/MGRS
only.
GlobalSettings.Default_EagerLoad = new EagerLoad(EagerLoadType.UTM_MGRS);
The below example will change the application wide Coordinate
default output from DMS to DDM.
GlobalSettings.Default_CoordinateFormatOptions = new CoordinateFormatOptions()
{ Format = CoordinateFormatType.Degree_Decimal_Minutes };
The following example will restrict allowed parse settings to geodetic lat/long, UTM and MGRS.
//Restrict parsable formats to Lat/Long, UTM and MGRS.
GlobalSettings.Default_Parsable_Formats = Allowed_Parse_Format.Lat_Long |
Allowed_Parse_Format.UTM | Allowed_Parse_Format.MGRS;
This example sets the default Cartesian type to ECEF, so that Cartesian strings parse in ECEF vs standard Cartesian.
//Restrict parsable formats to Lat/Long, UTM and MGRS.
GlobalSettings..Default_Cartesian_Type = CartesianType.ECEF;
The below code demonstrates how to change the default Equatorial Radius (Semi-Major Axis) and Inverse Flattening values
of the application to from WGS84 datum values to AIRY1830 datum values.
GlobalSettings.Set_DefaultDatum(Earth_Ellipsoid_Spec.Airy_1830);
//OR
GlobalSettings.Set_DefaultDatum(Earth_Ellipsoid.Get_Ellipsoid(Earth_Ellipsoid_Spec.Airy_1830));
//OR
GlobalSettings.Default_EquatorialRadius = 6377563.396;
GlobalSettings.Default_InverseFlattening = 299.3249646;
The next example show you how to default Coordinates to run in local time. By default, Coordinates run
in UTC time regardless of the specified DateTimeKind
. User must pass an offset to specify local time
after the Coordinate
as been created. By adjusting the Allow_Coordinate_DateTimeKind_Specification
to true
we can default Coordinates to the environments local time.
GlobalSettings.Allow_Coordinate_DateTimeKind_Specification = true;
Benchmarks
Benchmark Results
The following Coordinate
procedures were benchmarked as follows.
Method |
i7-8550U 1.80GHz 1.99 GHz (x64) |
Standard Initialization |
8 ms |
TryParse() Initialization |
6-35 ms |
Secondary Initialization |
30 ms |
Initialization w/EagerLoad off |
< 1 ms |
Property Change |
7 ms |
Total Celestial (Local/UTC) |
8 ms |
Solar Cycle Only (Local/UTC) |
< 1 ms |
Lunar Cycle Only (Local/UTC) |
2 ms |