CkGeo
Job: read latitudes and longitudes from raw data, and do the arithmetic of the globe — distance, direction, midpoint, radius checks — with no map service involved.
A coordinate is two numbers: latitude (how far north or south, -90 to 90) and longitude
(how far east or west, -180 to 180). CkGeo reads them from a number, from decimal text
like "40.7128", from degrees-minutes-seconds text like "40°42'46"N", or from a whole
point written the way the spatial standards write one — "POINT(-74.006 40.7128)" or
{"type":"Point","coordinates":[-74.006,40.7128]} — and then
works with them: how far apart two points are, which way to head, what lies halfway.
When a read fails, the answer is the North Pole (latitude 90, longitude 0) — a real
place where nobody lives, so bad data stands out on a map instead of hiding at (0, 0),
which is a real spot in the ocean off Africa. When a calculation is handed a bad
coordinate, the answer is NaN ("not a number"), which no real distance or bearing is.
Quick look
using SafeCoder.CommonKit;
using SafeCoder.CommonKit.Enums;
CkGeo.Latitude("40.7128"); // -> 40.7128
CkGeo.Longitude("-74.006"); // -> -74.006
CkGeo.Latitude("junk"); // -> 90 (the North Pole — rejected)
CkGeo.Distance(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 51.5074,
longitude2: -0.1278
); // -> 5570.229873656523 (New York to London, in km)
CkGeo.IsCoordinate("40.7128, -74.0060"); // -> true
CkGeo.IsPlaceholder(0, 0); // -> true (Null Island: a failed geocode)
CkGeo.ToWkt(40.7128, -74.0060); // -> "POINT(-74.006 40.7128)" (longitude first)
CkGeo.BoundsAround(40.7128, -74.0060, 50); // -> (South ≈ 40.26, West ≈ -74.6, North ≈ 41.16, East ≈ -73.41)
CkGeo.Destination(0, 0, 90, 111.2); // -> (0, ≈ 1) (a degree east along the equator)
CkGeo.Explain("91"); // -> "outside -90 to 90"
Methods
Latitude and Longitude share their rules. Each reads one value: a number of any
type, decimal text, degrees-minutes-seconds text with a hemisphere letter (N, S, E
or W), or a whole point in WKT or GeoJSON, out of which each takes its own half. A letter
from the wrong axis is refused — an "N" in a longitude field means
someone swapped the columns, and CkGeo will not paper over it.
When the value cannot be read, the fallback is tried — and checked the same way, so an
impossible default is no fallback. When both fail, Latitude answers 90 and Longitude
answers 0: the two halves of the North Pole.
Latitude
How far north or south.
double Latitude(
object? input = null,
double? fallback = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The raw value: a number of any type, decimal text, or degrees-minutes-seconds text. |
fallback |
double? |
null |
Latitude to use when the value cannot be read. It is checked the same way, so an impossible default is no fallback. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering the North Pole, when neither the value nor the fallback can be used. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
The value (or the fallback) is a readable latitude |
The latitude, held to -90…90: 40.7128 |
The latitude, held to -90…90: 40.7128 |
Rejected: missing, unreadable, past a pole, or a wrong-axis letter — and the fallback fails too |
90 — the North Pole |
Throws CkGeoException — carries Input, Expected and Reason |
A DMS value with an E or W letter is a longitude in the wrong field — refused, not read.
Latitude holds the result to -90…90 — nothing is past the poles.
CkGeo.Latitude("40.7128"); // -> 40.7128
CkGeo.Latitude("40°42'46\"N"); // -> 40.71277777777778
CkGeo.Latitude("91"); // -> 90 (no latitude is past the pole — rejected)
CkGeo.Latitude(
input: "abc",
fallback: 0
); // -> 0
Longitude
How far east or west.
double Longitude(
object? input = null,
double? fallback = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The raw value: a number of any type, decimal text, or degrees-minutes-seconds text. |
fallback |
double? |
null |
Longitude to use when the value cannot be read. It is checked the same way, so an impossible default is no fallback. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering 0 — the North Pole's longitude — when neither the value nor the fallback can be used. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
The value (or the fallback) is a readable longitude |
The longitude, held to -180…180: -74.006 |
The longitude, held to -180…180: -74.006 |
Rejected: missing, unreadable, past 180, or a wrong-axis letter — and the fallback fails too |
0 — the North Pole's longitude |
Throws CkGeoException — carries Input, Expected and Reason |
A DMS value with an N or S letter is a latitude in the wrong field — refused, not read.
Works like Latitude, but Longitude holds the
result to -180…180, and a rejected value answers 0 — longitude's half of the pole.
CkGeo.Longitude("-74.006"); // -> -74.006
CkGeo.Longitude("40°42'46\"N"); // -> 0 (an N letter is a latitude — wrong field)
The two OrNull twins are the same read, but the answer for a missing value (null or
DBNull) or an unreadable one is null instead of the pole — made for nullable
database columns, which should receive an honest NULL, never a stand-in and never NaN.
Unlike the rest of the OrNull family (see CkNumber) there is no zero
switch here: the equator and the Greenwich meridian are real places, so 0 always
comes back as 0.
LatitudeOrNull
Null when there is nothing usable.
double? LatitudeOrNull(object? input = null)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The raw value: a number of any type, decimal text, or degrees-minutes-seconds text. |
Returns
| When | You get |
|---|---|
| The value is a readable latitude | The latitude: 40.7128 |
Missing (null or DBNull), unreadable, past a pole, or a wrong-axis letter |
null — this method never throws |
There is no zero switch here: 0 is the equator, a real place, and always comes back as 0.
The nullable read for a latitude: anything Latitude would reject to the pole comes
back as null instead.
CkGeo.LatitudeOrNull("40.7128"); // -> 40.7128
CkGeo.LatitudeOrNull(null); // -> null
CkGeo.LatitudeOrNull("junk"); // -> null
CkGeo.LatitudeOrNull("0"); // -> 0 (the equator is a real place)
LongitudeOrNull
Null for a longitude.
double? LongitudeOrNull(object? input = null)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The raw value: a number of any type, decimal text, or degrees-minutes-seconds text. |
Returns
| When | You get |
|---|---|
| The value is a readable longitude | The longitude: -74.006 |
Missing (null or DBNull), unreadable, past 180, or a wrong-axis letter |
null — this method never throws |
There is no zero switch here: 0 is the Greenwich meridian, a real place, and always comes back as 0.
Works like LatitudeOrNull, for the east-west number.
CkGeo.LongitudeOrNull("-74.006"); // -> -74.006
CkGeo.LongitudeOrNull("181"); // -> null (no longitude goes past 180)
TryParse
A whole "lat, lon" pair in one call.
bool TryParse(
string? input,
out double latitude,
out double longitude
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
string? |
— | The text holding the pair, latitude first. |
latitude |
out double |
— | The latitude read out; 90 (the North Pole's latitude) when the text could not be read. |
longitude |
out double |
— | The longitude read out; 0 (the North Pole's longitude) when the text could not be read. |
Returns
| When | You get |
|---|---|
| Both halves read as a latitude and a longitude | true — latitude and longitude carry the pair: 40.7128 and -74.006 |
| The text is a whole point in WKT, EWKT or GeoJSON | true — the two halves, swapped back into latitude, longitude |
| Anything else: missing text, not exactly two halves, or either half rejected | false — the outputs hold the North Pole (90 and 0); it never throws |
The true/false return is the whole answer — the library's one bool-verdict method.
Reads both halves from one piece of text — separated by a space, a comma or a semicolon,
with or without wrapping brackets. This is the library's one bool-verdict method: the
true/false return is the whole answer, and the coordinates ride out through the two
out parameters. On false the outputs hold the North Pole. It never throws.
bool ok = CkGeo.TryParse(
input: "40.7128, -74.0060",
latitude: out double lat,
longitude: out double lon
); // -> true (lat is 40.7128, lon is -74.006)
CkGeo.TryParse(
input: "91 0",
latitude: out double badLat,
longitude: out double badLon
); // -> false (badLat is 90 and badLon is 0 — the North Pole)
One catch: because a space separates the two halves, a degrees-minutes-seconds half must
be written without spaces here — "40°42'46"N", not "40 42 46 N". The spaced form is
fine for DmsToDecimal, which reads one angle on its own.
Points from a database or a map
A whole point written the way the spatial standards write one is read too — the shapes ToWkt and ToGeoJson write, plus the variants other systems emit:
CkGeo.TryParse("POINT(-74.0060 40.7128)", out double a, out double b); // -> true (40.7128, -74.006)
CkGeo.TryParse("POINT (-74.0060 40.7128)", out _, out _); // a space before the bracket
CkGeo.TryParse("POINT Z (-74.0060 40.7128 12.5)", out _, out _); // the height is read past
CkGeo.TryParse("SRID=4326;POINT(-74.0060 40.7128)", out _, out _); // the extended form
CkGeo.TryParse("{\"type\":\"Point\",\"coordinates\":[-74.0060,40.7128]}", out _, out _);
CkGeo.TryParse("SRID=3857;POINT(-8238310 4970071)", out _, out _); // -> false
Longitude first
WKT and GeoJSON both put the longitude before the latitude, which is the
opposite of the order people say a coordinate in and the mistake these formats
invite. TryParse swaps them back, so what comes out is latitude first as
everywhere else in this class — and ToWkt and ToGeoJson
take latitude first and write longitude first. Never reorder by hand.
Two rules go with them. A value that opens with POINT, SRID= or { is judged as a
point and only as a point: a bad one answers false, never retried as a plain pair. And
of the extended form, only SRID=4326 — WGS 84 — is a latitude and a longitude; every
other reference is metres on some projection, and is refused.
Latitude and Longitude read the same shapes and take their own half out of one, which
is what makes reading a stored geography column a single call.
Distance
How far apart two points are.
double Distance(
double latitude1,
double longitude1,
double latitude2,
double longitude2,
GeoUnit unit = GeoUnit.Kilometers,
double fallback = double.NaN,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude1 |
double |
— | Latitude of the first point, -90 to 90. |
longitude1 |
double |
— | Longitude of the first point, -180 to 180. |
latitude2 |
double |
— | Latitude of the second point. |
longitude2 |
double |
— | Longitude of the second point. |
unit |
GeoUnit |
GeoUnit.Kilometers |
The unit of the result; kilometres by default. |
fallback |
double |
double.NaN |
What comes back when a coordinate is invalid; NaN by default, which no real distance is. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the fallback. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| All four coordinates are valid | The great-circle distance in unit, 0 for the same point: 5570.229873656523 (New York to London, kilometres) |
The great-circle distance in unit, 0 for the same point: 5570.229873656523 (New York to London, kilometres) |
| Rejected: a coordinate is NaN, infinite or outside its range | The fallback (default NaN) |
Throws CkGeoException — carries Input, Expected and Reason |
Good to about 0.3% — a perfect sphere stands in for the slightly flattened real Earth.
The great-circle distance: the shortest path over the surface of the globe, the way a
plane flies. Pick the unit with GeoUnit; kilometres is the default. A bad coordinate
answers the fallback, which is NaN unless you pass your own.
CkGeo.Distance(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 51.5074,
longitude2: -0.1278
); // -> 5570.229873656523 (New York to London, kilometres)
CkGeo.Distance(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 51.5074,
longitude2: -0.1278,
unit: GeoUnit.Miles
); // -> 3461.1803776299676
CkGeo.Distance(
latitude1: 91,
longitude1: 0,
latitude2: 0,
longitude2: 0
); // -> NaN (91 is not a latitude)
Bearing
The compass direction to set off in.
double Bearing(
double latitude1,
double longitude1,
double latitude2,
double longitude2,
double fallback = double.NaN,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude1 |
double |
— | Latitude of the starting point, -90 to 90. |
longitude1 |
double |
— | Longitude of the starting point, -180 to 180. |
latitude2 |
double |
— | Latitude of the destination. |
longitude2 |
double |
— | Longitude of the destination. |
fallback |
double |
double.NaN |
What comes back when a coordinate is invalid; NaN by default, which no real bearing is. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the fallback. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| All four coordinates are valid | The initial bearing in degrees clockwise from north, 0 to just under 360: 51.2126168241972 |
The initial bearing in degrees clockwise from north, 0 to just under 360: 51.2126168241972 |
| Rejected: a coordinate is NaN, infinite or outside its range | The fallback (default NaN) |
Throws CkGeoException — carries Input, Expected and Reason |
The same point twice answers 0, not NaN.
The direction from the first point to the second, in degrees clockwise from north: 0 is north, 90 east, 180 south, 270 west. It is the initial bearing — on a long trip along the great circle the direction changes as you go; this is the one at the start.
CkGeo.Bearing(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 51.5074,
longitude2: -0.1278
); // -> 51.2126168241972 (set off north-east for London)
Midpoint
Halfway along the path.
(double Latitude, double Longitude) Midpoint(
double latitude1,
double longitude1,
double latitude2,
double longitude2,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude1 |
double |
— | Latitude of the first point, -90 to 90. |
longitude1 |
double |
— | Longitude of the first point, -180 to 180. |
latitude2 |
double |
— | Latitude of the second point. |
longitude2 |
double |
— | Longitude of the second point. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering the North Pole. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| All four coordinates are valid | The halfway point as a (Latitude, Longitude) pair: (52.36843958746243, -41.29030735624855) |
The halfway point as a (Latitude, Longitude) pair: (52.36843958746243, -41.29030735624855) |
| Rejected: a coordinate is NaN, infinite or outside its range | The North Pole pair (90, 0) |
Throws CkGeoException — carries Input, Expected and Reason |
Halfway along the great circle — not the average of the numbers.
The point halfway between two points along the great circle — not the average of the
numbers, which drifts off the true path over long distances. It comes back as a
(Latitude, Longitude) pair; a bad coordinate answers the North Pole pair (90, 0).
CkGeo.Midpoint(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 51.5074,
longitude2: -0.1278
); // -> (52.36843958746243, -41.29030735624855) (out in the Atlantic)
Destination
Where a bearing and a distance take you.
(double Latitude, double Longitude) Destination(
double latitude,
double longitude,
double bearing,
double distance,
GeoUnit unit = GeoUnit.Kilometers,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | Latitude of the starting point, -90 to 90. |
longitude |
double |
— | Longitude of the starting point, -180 to 180. |
bearing |
double |
— | The initial bearing, degrees clockwise from north. Any real number; 370 is 10, -350 is 10. |
distance |
double |
— | How far to travel, in unit. Zero stays where it is; a negative distance is rejected. |
unit |
GeoUnit |
GeoUnit.Kilometers |
Kilometers, Miles, NauticalMiles or Meters. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says what is invalid and why, instead of answering the North Pole. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| The start, the bearing and the distance are all usable | The point reached, as a (Latitude, Longitude) pair, its longitude wrapped into -180 to 180 |
The same pair |
| Rejected: a coordinate out of range, a bearing or a distance that is NaN or infinite, a negative distance | The North Pole pair (90, 0) |
Throws CkGeoException — carries Input, Expected and Reason |
The inverse of Distance and Bearing, on the same sphere.
Take the bearing and the distance from A to B, hand them back here with A, and B comes out. Useful for drawing the edge of a radius, for offsetting a point a known way, and for checking that a distance and a bearing agree.
CkGeo.Destination(
latitude: 40.7128,
longitude: -74.0060,
bearing: 51.2,
distance: 5570
); // -> about (51.52, -0.13) — London
CkGeo.Destination(0, 0, 90, 111.19492664455873); // -> about (0, 1) — a degree east
CkGeo.Destination(0, 0, 370, 100); // -> the same as bearing 10
CkGeo.Destination(0, 179.5, 90, 100); // -> about (0, -179.6) — across the antimeridian
The bearing is the one you start with, not one you hold: a great circle turns as you go, which is why a flight from New York to London leaves heading north-east and arrives heading south-east.
BoundsAround
The box around a radius.
(double South, double West, double North, double East) BoundsAround(
double latitude,
double longitude,
double distance,
GeoUnit unit = GeoUnit.Kilometers,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | Latitude of the centre, -90 to 90. |
longitude |
double |
— | Longitude of the centre, -180 to 180. |
distance |
double |
— | The radius, in unit. Zero gives the centre alone; a negative radius is rejected. |
unit |
GeoUnit |
GeoUnit.Kilometers |
Kilometers, Miles, NauticalMiles or Meters. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says what is invalid and why, instead of answering NaN. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| The centre and the radius are usable | The four edges as a (South, West, North, East) tuple — the order IsInBounds takes |
The same tuple |
| Rejected: a coordinate out of range, a radius that is NaN, infinite or negative | (NaN, NaN, NaN, NaN) |
Throws CkGeoException — carries Input, Expected and Reason |
The cheap half of a "within radius" query. IsWithin is the exact half.
The "within radius" query, in two steps
"Everyone within 50 km of the office" over a table of coordinates is two steps, and both
matter. BoundsAround gives a box an index can use, so the database narrows millions of
rows to a few hundred; IsWithin then measures those few and throws away the
corners of the box that lie outside the circle.
(double south, double west, double north, double east) =
CkGeo.BoundsAround(40.7128, -74.0060, 50);
// 1. the prefilter, in SQL, where an index on (Lat, Lon) can do the work
// WHERE Lat BETWEEN @south AND @north AND Lon BETWEEN @west AND @east
// 2. the exact check, in code, on what comes back
List<Member> near = rows
.Where(row => CkGeo.IsWithin(40.7128, -74.0060, row.Lat, row.Lon, 50))
.ToList();
Never use the box on its own: its corners are up to 40% further out than the radius, so a box-only answer includes people who are not within 50 km. And never skip it: without a box the database reads every row.
Three shapes of box to know about, all of which IsInBounds already reads:
- A box that wraps the antimeridian comes back with
Westeast ofEast— 170 and -170, say.IsInBoundsunderstands it; a hand-written SQLBETWEENdoes not, and needs two ranges joined byOR. - A circle that reaches a pole has no meaningful east or west, so the box spans every longitude, -180 to 180, and the pole's own latitude.
- The box is always a little wider than the circle — a corner of a box is outside the
circle it holds, and every edge carries
CkSettings.BoundsMarginDegrees(a millionth of a degree, about eleven centimetres) on top, so that a point lying exactly on the radius can never round its way out of the box that is meant to hold it.
ToWkt
The point as well-known text.
string ToWkt(
double latitude,
double longitude,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | The latitude, -90 to 90. |
longitude |
double |
— | The longitude, -180 to 180. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering "". |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| Both coordinates are valid | "POINT(-74.006 40.7128)" — longitude first |
The same text |
| Rejected: a coordinate is NaN, infinite or outside its range | Empty string | Throws CkGeoException — carries Input, Expected and Reason |
Longitude first, as the standard says. TryParse reads it back.
The spelling every spatial database and mapping library reads. The parameters are latitude first, as everywhere else in this class; the text that comes out is longitude first, as the standard requires.
CkGeo.ToWkt(40.7128, -74.0060); // -> "POINT(-74.006 40.7128)"
CkGeo.ToWkt(0, 0); // -> "POINT(0 0)"
CkGeo.ToWkt(91, 0); // -> "" (not a latitude)
Plain WKT, with no SRID in front: the value is WGS 84, which is what a column declared
geography in SQL Server or geography(Point,4326) in PostGIS already means. Where a
receiver wants the extended form, write "SRID=4326;" + CkGeo.ToWkt(lat, lon) —
TryParse reads that back too.
ToGeoJson
The point as a GeoJSON object.
string ToGeoJson(
double latitude,
double longitude,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | The latitude, -90 to 90. |
longitude |
double |
— | The longitude, -180 to 180. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says which coordinate is invalid and why, instead of answering "". |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| Both coordinates are valid | {"type":"Point","coordinates":[-74.006,40.7128]} — longitude first, no spaces |
The same text |
| Rejected: a coordinate is NaN, infinite or outside its range | Empty string | Throws CkGeoException — carries Input, Expected and Reason |
Longitude first, as GeoJSON says. TryParse reads it back.
The shape a web map, a browser API and a document database expect. No spaces, so the text goes straight into a payload.
CkGeo.ToGeoJson(40.7128, -74.0060); // -> "{\"type\":\"Point\",\"coordinates\":[-74.006,40.7128]}"
CkGeo.ToGeoJson(0, 181); // -> "" (not a longitude)
GeoJSON has no coordinate system of its own — it is WGS 84 by definition, which is what
this class reads and writes. The numbers are written by CkText.NumberToString, so they
are the same under every culture and read back as the values that went in.
IsWithin
Inside the radius?
bool IsWithin(
double latitude1,
double longitude1,
double latitude2,
double longitude2,
double distance,
GeoUnit unit = GeoUnit.Kilometers,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude1 |
double |
— | Latitude of the centre, -90 to 90. |
longitude1 |
double |
— | Longitude of the centre, -180 to 180. |
latitude2 |
double |
— | Latitude of the point to test. |
longitude2 |
double |
— | Longitude of the point to test. |
distance |
double |
— | The radius, inclusive, in unit. |
unit |
GeoUnit |
GeoUnit.Kilometers |
The unit of distance; kilometres by default. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says what is invalid and why, instead of answering false. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| All coordinates and the radius are valid | The verdict — true when the point is no further than distance, false when it is further |
The verdict — true when the point is no further than distance, false when it is further |
| Rejected: a coordinate is NaN, infinite or outside its range, or the radius is NaN, infinite or negative | false |
Throws CkGeoException — carries Input, Expected and Reason |
The radius is inclusive — a point exactly on the edge is within.
Answers whether the second point lies within distance of the first — "is this member
within 50 miles of the office". The radius is inclusive, in the GeoUnit you pick.
CkGeo.IsWithin(
latitude1: 40.7128,
longitude1: -74.0060,
latitude2: 40.0583,
longitude2: -74.4057,
distance: 50,
unit: GeoUnit.Miles
); // -> true (Trenton is within 50 miles of New York)
A bad coordinate or a negative radius answers false too — so where "false because the
data is broken" must not look like "false because they are far away", pass
throwIfInvalid: true or check the coordinates first.
The last two methods carry an angle both ways between decimal degrees and DMS. DMS is the map-and-survey way of writing an angle: whole degrees, then minutes (a minute is 1/60 of a degree), then seconds (1/60 of a minute), plus a hemisphere letter.
DmsToDecimal
Degrees, minutes and seconds into a decimal.
double DmsToDecimal(
string? input = null,
double? fallback = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
string? |
null |
The DMS text. |
fallback |
double? |
null |
Decimal degrees to use when the text cannot be read. It is checked too: NaN, infinite or beyond ±180 is no fallback. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering the North Pole, when neither the text nor the fallback can be used. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
The text (or the fallback) is a readable angle |
The decimal degrees: 40.71277777777778 |
The decimal degrees: 40.71277777777778 |
Rejected: not DMS text, a minus against an N or E letter, a fraction anywhere but the last part, minutes or seconds at 60 or more, or degrees past the limit — and the fallback fails too |
90 — the North Pole |
Throws CkGeoException — carries Input, Expected and Reason |
The fallback is checked too: NaN, infinite or beyond ±180 is no fallback.
DmsToDecimal reads DMS text — symbols, spaces or colons between the parts, letter
before or after — into plain decimal degrees. It reads one angle without knowing which
axis it belongs to, so it allows anything up to ±180 — though an N or S letter still
caps it at 90; Latitude and Longitude read DMS too and enforce the axis.
CkGeo.DmsToDecimal("40°42'46\"N"); // -> 40.71277777777778
CkGeo.DmsToDecimal("40 42 46 N"); // -> 40.71277777777778 (spaced form is fine here)
CkGeo.DmsToDecimal("-74 0 22"); // -> -74.00611111111111 (a minus works like W or S)
CkGeo.DmsToDecimal("junk"); // -> 90 (the North Pole)
DecimalToDms
A decimal back into degrees, minutes and seconds.
string DecimalToDms(
double degrees,
GeoAxis axis = GeoAxis.Latitude,
int secondsDecimals = CkSettings.DmsSecondsDecimals,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
degrees |
double |
— | The coordinate in decimal degrees. |
axis |
GeoAxis |
GeoAxis.Latitude |
Latitude (N/S, -90 to 90) or longitude (E/W, -180 to 180). |
secondsDecimals |
int |
CkSettings.DmsSecondsDecimals (1) |
Decimal places of the seconds, 0 to 6; values outside that are capped. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering "", when the value cannot be written. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| The value fits the axis | The DMS text with its hemisphere letter: "40°42'46.1"N" |
The DMS text with its hemisphere letter: "40°42'46.1"N" |
| Rejected: NaN, infinite, or outside the axis' range | Empty string | Throws CkGeoException — carries Input, Expected and Reason |
Rounded seconds carry into the minutes and degrees when they reach 60.
DecimalToDms writes decimal degrees back out as DMS text; tell it the axis with
GeoAxis (latitude is the default), and set the seconds' decimal places with
secondsDecimals (default 1, capped at 0 to 6). It is the one string method here, so
it rejects the string way: non-empty in, empty out
(see How it works).
CkGeo.DecimalToDms(40.7128); // -> "40°42'46.1"N"
CkGeo.DecimalToDms(
degrees: -74.006,
axis: GeoAxis.Longitude
); // -> "74°0'21.6"W"
CkGeo.DecimalToDms(91); // -> "" (past the pole — rejected)
IsLatitude
Does it read as a latitude?
bool IsLatitude(
object? input = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The value: a number, or text — decimal or degrees, minutes and seconds. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering false, when the value is not a latitude. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| A number, decimal text or DMS text inside -90 to 90 | true |
true |
| Rejected: out of range, minutes or seconds of 60 or more, junk, missing | false |
Throws CkGeoException — carries Input, Expected and Reason |
The yes-or-no form of Latitude: anything that reader accepts, and nothing it rejects. Call the reader instead when you want the decimal degrees back.
CkGeo.IsLatitude("40°42'46\"N"); // -> true
CkGeo.IsLatitude("90.0001"); // -> false
IsLongitude
Does it read as a longitude?
bool IsLongitude(
object? input = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The value: a number, or text — decimal or degrees, minutes and seconds. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering false, when the value is not a longitude. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| A number, decimal text or DMS text inside -180 to 180 | true |
true |
| Rejected: out of range, malformed DMS, junk, missing | false |
Throws CkGeoException — carries Input, Expected and Reason |
The yes-or-no form of Longitude.
CkGeo.IsLongitude("74°00'22\"W"); // -> true
CkGeo.IsLongitude(181); // -> false
IsCoordinate
Does the text read as a whole pair?
bool IsCoordinate(
string? input = null,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
string? |
null |
The raw text: latitude, then longitude, separated by a comma, a semicolon or a space, brackets allowed around the pair. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering false, when the text is not a coordinate pair. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| Two halves that read as a latitude and a longitude | true |
true |
| Rejected: one half, three parts, a half out of range, junk | false |
Throws CkGeoException — carries Input, Expected and Reason — Expected says whether the pair, the latitude or the longitude failed |
The named twin of TryParse, for validation code, with a throwIfInvalid that
says which half failed and why, which TryParse cannot. Latitude comes first; a pair
written longitude-first still reads when both halves happen to be in range, since the
text does not say which is which.
CkGeo.IsCoordinate("40.7128, -74.0060"); // -> true
CkGeo.IsCoordinate("40°42'46\"N 74°00'22\"W"); // -> true
CkGeo.IsCoordinate("91, -74.0060"); // -> false (the latitude is out of range)
IsInBounds
Inside this box?
bool IsInBounds(
double latitude,
double longitude,
double south,
double west,
double north,
double east,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | Latitude of the point, -90 to 90. |
longitude |
double |
— | Longitude of the point, -180 to 180. |
south |
double |
— | The southern edge of the box, inclusive. |
west |
double |
— | The western edge, inclusive. |
north |
double |
— | The northern edge, inclusive. |
east |
double |
— | The eastern edge, inclusive. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering false, when a coordinate is invalid or the edges contradict each other. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| Every coordinate is valid and the point lies inside the box, edges included | true |
true |
| Every coordinate is valid and the point lies outside | false |
false |
| Rejected: an invalid coordinate, or a southern edge north of the northern one | false |
Throws CkGeoException — carries Input, Expected and Reason — Expected names the coordinate or edge |
The service-area and map-viewport check: "is this address in our delivery region". A box
whose western edge is east of its eastern edge wraps across the antimeridian, so a box
around the date line works. Like Distance and IsWithin it takes numbers, since a box is
built from stored coordinates, not raw text.
CkGeo.IsInBounds(
latitude: 40.7128,
longitude: -74.0060,
south: 40.4,
west: -74.3,
north: 40.9,
east: -73.7
); // -> true (Manhattan, inside New York's box)
CkGeo.IsInBounds(0, 180, south: -10, west: 170, north: 10, east: -170); // -> true (across the antimeridian)
IsPlaceholder
A failed geocode rather than a place?
bool IsPlaceholder(
double latitude,
double longitude,
bool throwIfInvalid = CkSettings.DefaultThrowIfInvalid
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
latitude |
double |
— | The latitude, -90 to 90. |
longitude |
double |
— | The longitude, -180 to 180. |
throwIfInvalid |
bool |
CkSettings.DefaultThrowIfInvalid (false) |
Pass true to throw a CkGeoException that says why, instead of answering false, when a coordinate is invalid. |
Returns
| When | Default | With throwIfInvalid: true |
|---|---|---|
| The North Pole — latitude 90, whatever the longitude — or Null Island, (0, 0) | true |
true |
| Any other valid place | false |
false |
| Rejected: an invalid coordinate | false |
Throws CkGeoException — carries Input, Expected and Reason |
The two "nobody entered anything" locations: this library's own rejection value, and the
(0, 0) other systems emit when a geocoder failed. The CkDate.IsPlaceholder
of coordinates, for finding bad rows before they land on a map. A latitude of 90 with a
real longitude is the pole too — it is what a record holds when only its latitude failed
to read. A genuine reading at exactly (0, 0) is a buoy in the Gulf of Guinea; in business
data it is always a failed geocode.
CkGeo.IsPlaceholder(90, 0); // -> true (the rejection value)
CkGeo.IsPlaceholder(0, 0); // -> true (Null Island)
CkGeo.IsPlaceholder(51.5, 0); // -> false (Greenwich is real)
Explain
Why not — in words, without an exception.
string Explain(
object? input = null,
GeoAxis axis = GeoAxis.Latitude
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The value: a number, or text. |
axis |
GeoAxis |
GeoAxis.Latitude |
Which coordinate it is meant to be. |
Returns
| When | You get |
|---|---|
| The value passes the reader | "" |
| It does not | The reason, the same words the exception would carry — an answer, not a failure; it never throws |
The form-friendly twin of throwIfInvalid: the message to show beside the field,
without a try and catch. Every class with a reader has one, on the reader's own
rules, so the reason is exactly what the reader would say.
CkGeo.Explain("91"); // -> "outside -90 to 90"
CkGeo.Explain("181", GeoAxis.Longitude); // -> "outside -180 to 180"
Good to know
- Failed reads answer the North Pole, not zero. (0, 0) is a real spot in the Gulf of Guinea where bad data would hide; (90, 0) is a place no honest record points at. A longitude read on its own rejects to 0 — that is its half of the pole pair, since at the pole every longitude is the same place.
- Failed calculations answer NaN.
DistanceandBearingreturn the caller'sfallback, NaN by default — and NaN is a loud value: any comparison with it is false, so a bad distance can never accidentally pass a "less than 50" check. - The OrNull twins have no zero switch. The rest of the OrNull family treats zero as a sentinel by default; here 0 latitude is the equator and 0 longitude is Greenwich — real places, always kept.
- Fallbacks are checked too. A
fallbackof 91 for a latitude is no fallback; you still get the pole. TryParsehands back the pair;IsCoordinateonly judges it. Both read the same way;TryParsenever throws and gives you the numbers,IsCoordinatecan throw and says which half failed. The other validators —IsLatitude,IsLongitude,IsInBounds,IsPlaceholder,IsWithin— all answerfalsefor anything invalid and takethrowIfInvalid.IsPlaceholdercatches both sentinels. The library's North Pole and the (0, 0) other systems emit; check it before plotting, since both are real spots on a map.- Distance is good to about 0.3%. The maths uses a perfect sphere; the real Earth is
slightly flattened. Plenty for "who is within 50 miles", not for surveying.
Destinationuses the same sphere, so it andDistanceagree with each other exactly. - WKT and GeoJSON put the longitude first. Both formats do, and
ToWkt,ToGeoJson,TryParse,LatitudeandLongitudeall take and give latitude first, as the rest of this class does — the swap happens inside. Never reorder by hand. - A "within radius" query is
BoundsAroundthenIsWithin— the box for the index, the circle for the answer. See BoundsAround. - Two small enums ride along, both in
SafeCoder.CommonKit.Enums:GeoUnit(Kilometers— the default —Miles,NauticalMiles,Meters) andGeoAxis(Latitude,Longitude). You saw both above:unit: GeoUnit.Miles,axis: GeoAxis.Longitude.
If you want errors instead
Pass throwIfInvalid: true and a value that cannot be used raises a CkGeoException
(from SafeCoder.CommonKit.Exceptions) instead of the fallback. The exception carries the
Input, what was Expected (which of the four coordinates, for the calculations) and a
short Reason, flattened into a one-line message.
CkGeo.Latitude(
input: "abc",
throwIfInvalid: true
); // throws CkGeoException — the message says why
Two exceptions to the rule: TryParse takes no throwIfInvalid — its false is the
whole signal — and the OrNull twins never throw at all: null is their answer for
anything unusable, not an error. When a bad coordinate should be an error, read it with
Latitude or Longitude and throwIfInvalid: true instead.