lazarus-ccr/components/lazmapviewer/source/mvgeomath.pas

730 lines
21 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

unit mvGeoMath;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Math;
const
EARTH_EQUATORIAL_RADIUS = 6378137;
EARTH_POLAR_RADIUS = 6356752.3142;
EARTH_CIRCUMFERENCE = 2 * pi * EARTH_EQUATORIAL_RADIUS;
EARTH_ECCENTRICITY = sqrt(1 - sqr(EARTH_POLAR_RADIUS / EARTH_EQUATORIAL_RADIUS));
EARTH_FLATTENING = 1.0 - EARTH_POLAR_RADIUS / EARTH_EQUATORIAL_RADIUS;
DMS_Decimals: Integer = 1;
type
TDistanceUnits = (duMeters, duKilometers, duMiles);
TEarthShape = (esSphere, esEllipsoid);
function NormalizeLon(const Lon: Double): Double; inline;
function HaversineDist(Lat1, Lon1, Lat2, Lon2, Radius: Double): Double;
function LambertsDist(Lat1, Lon1, Lat2, Lon2, Radius, Flattening: Double): Double;
function CalcGeoDistance(Lat1, Lon1, Lat2, Lon2: double;
AUnits: TDistanceUnits = duKilometers; AShape: TEarthShape = esSphere): double;
function CalcBearing(Lat1, Lon1, Lat2, Lon2: Double): Double;
procedure CalcLatLon(const Lat1, Lon1, ADist, ABearing: Double; out Lat2, Lon2: Double);
procedure CalcMidpoint(const Lat1, Lon1, Lat2, Lon2: Double; out Lat, Lon: Double);
procedure CalcIntermedPoint(const Lat1, Lon1, Lat2, Lon2, AFrac: Double; out Lat, Lon: Double);
procedure VertexOfGreatCircle(
const AStartLat, AStartLon, ADestinationLat, ADestinationLon: Double;
out AVertexLat, AVertexLon: Double);
procedure LatFromLonAtGreatCircle(
const AStartLat, AStartLon, ADestinationLat, ADestinationLon : Double;
const ASearchLon: Double; out AFoundLat: Double);
function DMSToDeg(Deg, Min: Integer; Sec: Double): Double;
function GPSToDMS(Angle: Double): string;
function GPSToDMS(Angle: Double; AFormatSettings: TFormatSettings): string;
function LatToStr(ALatitude: Double; DMS: Boolean): String;
function LatToStr(ALatitude: Double; DMS: Boolean; AFormatSettings: TFormatSettings): String;
function LonToStr(ALongitude: Double; DMS: Boolean): String;
function LonToStr(ALongitude: Double; DMS: Boolean; AFormatSettings: TFormatSettings): String;
function TryStrToGps(const AValue: String; out ADeg: Double): Boolean;
function TryStrDMSToDeg(const AValue: String; out ADeg: Double): Boolean;
procedure SplitGps(AValue: Double; out ADegs, AMins, ASecs: Double);
function ZoomFactor(AZoomLevel: Integer): Int64;
implementation
const
_K = 1024;
_M = _K*_K;
_G = _K*_M;
ZOOM_FACTOR: array[0..32] of Int64 = (
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, // 0..9
_K, 2*_K, 4*_K, 8*_K, 16*_K, 32*_K, 64*_K, 128*_K, 256*_K, 512*_K, // 10..19
_M, 2*_M, 4*_M, 8*_M, 16*_M, 32*_M, 64*_M, 128*_M, 256*_M, 512*_M, // 20..29
_G, 2*_G, 4*_G // 30..32
);
function ZoomFactor(AZoomLevel: Integer): Int64;
begin
if (AZoomLevel >= Low(ZOOM_FACTOR)) and (AZoomLevel < High(ZOOM_FACTOR)) then
Result := ZOOM_FACTOR[AZoomLevel]
else
Result := round(IntPower(2, AZoomLevel));
end;
{ Protected version of arcin which does not crash when the argument is outside
domain due to round-off error. }
function SafeArcsin(x: Double): Double;
begin
if x >= +1.0 then
Result := pi * 0.5
else
if x <= -1.0 then
Result := -pi * 0.5
else
Result := arcsin(x);
end;
{ Calculation of distance on a sphere
https://stackoverflow.com/questions/73608975/pascal-delphi-11-formula-for-distance-in-meters-between-two-decimal-gps-point
}
// angles in radians
function HaversineAngle(Lat1, Lon1, Lat2, Lon2: Double): Double;
var
latFrom, latTo, lonDiff: Double;
sinLatFrom, cosLatFrom: Double;
sinLatTo, cosLatTo: Double;
sinLonDiff, cosLonDiff: Double;
dx, dy, dz: Double;
begin
lonDiff := Lon1 - Lon2;
latFrom := Lat1;
latTo := Lat2;
SinCos(latFrom, sinLatFrom, cosLatFrom);
SinCos(latTo, sinLatTo, cosLatTo);
SinCos(lonDiff, sinLonDiff, cosLonDiff);
dz := sinlatFrom - sinlatTo;
dx := coslonDiff * coslatFrom - coslatTo;
dy := sinlonDiff * coslatFrom;
Result := SafeArcsin(sqrt(sqr(dx) + sqr(dy) + sqr(dz)) / 2.0) * 2.0;
end;
// Angles in degrees
function HaversineDist(Lat1, Lon1, Lat2, Lon2, Radius: Double): Double;
begin
Result := HaversineAngle(DegToRad(Lat1), DegToRad(Lon1), DegToRad(Lat2), DegToRad(Lon2)) * Radius;
end;
{ Calculation of distance on an ellipsoid
https://en.wikipedia.org/wiki/Geographical_distance
}
// Angles in degrees
function LambertsDist(Lat1, Lon1, Lat2, Lon2, Radius, Flattening: Double): Double;
var
reducedLat1, reducedLat2, sigma: Double;
P, Q: Double;
X, Y: Double;
sinP, cosP, sinQ, cosQ: Double;
sinSigma, sinSigma2, cosSigma2: Double;
begin
Lat1 := DegToRad(Lat1);
Lon1 := DegToRad(Lon1);
Lat2 := DegToRad(Lat2);
Lon2 := DegToRad(Lon2);
reducedLat1 := arctan((1.0 - flattening) * tan(Lat1));
reducedLat2 := arctan((1.0 - flattening) * tan(Lat2));
sigma := HaversineAngle(reducedLat1, Lon1, reducedLat2, Lon2);
P := (reducedLat1 + reducedLat2) * 0.5;
Q := (reducedLat2 - reducedLat1) * 0.5;
SinCos(P, sinP, cosP);
SinCos(Q, sinQ, cosQ);
SinCos(sigma*0.5, sinSigma2, cosSigma2);
sinSigma := sin(sigma);
X := (sigma - sinSigma) * sqr(sinP * cosQ / cosSigma2);
Y := (sigma + sinSigma) * sqr(cosP * sinQ / sinSigma2);
Result := (sigma - 0.5*Flattening*(X + Y)) * Radius;
end;
{ Returns the direct distance (air-line) between two geo coordinates
If latitude is NOT between -90°..+90° and longitude is NOT between -180°..+180°
the function returns NaN.
Usage example:
CalcGeoDistance(51.53323, -2.90130, 51.29442, -2.27275, duKilometers, esEllipsoid);
}
function CalcGeoDistance(Lat1, Lon1, Lat2, Lon2: double;
AUnits: TDistanceUnits = duKilometers; AShape: TEarthShape = esSphere): double;
begin
// Validate
if (Lat1 < -90.0) or (Lat1 > 90.0) then exit(NaN);
if (Lat2 < -90.0) or (Lat2 > 90.0) then exit(NaN);
case AShape of
esSphere:
Result := HaversineDist(Lat1, Lon1, Lat2, Lon2, EARTH_EQUATORIAL_RADIUS);
esEllipsoid:
Result := LambertsDist(Lat1, Lon1, Lat2, Lon2, EARTH_EQUATORIAL_RADIUS, EARTH_FLATTENING);
end;
case AUnits of
duMeters: ;
duKilometers: Result := Result * 0.001;
duMiles: Result := Result * 0.62137E-3;
end;
end;
{ Calculate initial bearing (in °) from the start point Lat1,Lon1 to
the end point Lat2,Lon2. No parameter checks, result normalized to 0°..360°.
}
function CalcBearing(Lat1, Lon1, Lat2, Lon2: Double): Double;
var
latFrom, latTo, lonDiff: Double;
sin_LatFrom, cos_LatFrom: Double;
sin_LatTo, cos_LatTo: Double;
sin_LonDiff, cos_LonDiff: Double;
begin
lonDiff := DegToRad(Lon2 - Lon1);
latFrom := DegToRad(Lat1);
latTo := DegToRad(Lat2);
SinCos(lonDiff, sin_LonDiff, cos_LonDiff);
SinCos(latFrom, sin_LatFrom, cos_LatFrom);
SinCos(latTo, sin_LatTo, cos_LatTo);
Result := ArcTan2(sin_LonDiff * cos_LatTo,
cos_LatFrom * sin_LatTo - sin_LatFrom * cos_LatTo * cos_LonDiff);
Result := RadToDeg(Result);
if Result < 0.0 then
Result := Result + 360.0;
end;
{ Calculate end point Lat2,Lon2 by given start point Lat1,Lon1, distance
ADist (in meters) and bearing ABearing (in °). No parameter checks,
result Lon2 normalized to -180°..180°.
}
procedure CalcLatLon(const Lat1, Lon1, ADist, ABearing: Double; out Lat2,
Lon2: Double);
var
latFrom, lonFrom, brng, aD: Double;
sin_LatFrom, cos_LatFrom: Double;
sin_LonFrom, cos_LonFrom: Double;
sin_brng, cos_brng: Double;
sin_aD, cos_aD: Double;
begin
latFrom := DegToRad(Lat1);
lonFrom := DegToRad(Lon1);
brng := DegToRad(ABearing);
aD := ADist / EARTH_EQUATORIAL_RADIUS;
SinCos(latFrom, sin_LatFrom, cos_LatFrom);
SinCos(lonFrom, sin_lonFrom, cos_lonFrom);
SinCos(brng, sin_brng, cos_brng);
SinCos(aD, sin_aD, cos_aD);
Lat2 := SafeArcSin(sin_LatFrom * cos_aD + cos_LatFrom * sin_aD * cos_brng);
Lon2 := lonFrom + ArcTan2(sin_brng * sin_aD * cos_latFrom, cos_aD - sin_latFrom * Sin(Lat2));
Lat2 := RadToDeg(Lat2);
Lon2 := {%H-}NormalizeLon(RadToDeg(Lon2));
end;
{ Calculate midpoint Lat,Lon by given start point Lat1,Lon1 and end point
Lat2,Lon2. No parameter checks, result Lon normalized to -180°..180°.
}
procedure CalcMidpoint(const Lat1, Lon1, Lat2, Lon2: Double; out Lat,
Lon: Double);
var
latFrom, lonDiff, latTo, lonTo, Bx, By: Double;
sin_latFrom, cos_latFrom: Double;
begin
lonDiff := DegToRad(Lon2 - Lon1);
latFrom := DegToRad(Lat1);
latTo := DegToRad(Lat2);
lonTo := DegToRad(Lon2);
Bx := Cos(latTo) * Cos(lonDiff);
By := Cos(latTo) * Sin(lonDiff);
SinCos(latFrom, sin_latFrom, cos_latFrom);
Lat := ArcTan2(sin_latFrom + Sin(latTo), Sqrt(Sqr(cos_latFrom + Bx) + Sqr(By)));
Lon := lonTo + ArcTan2(By, cos_latFrom + By);
Lat := RadToDeg(Lat);
Lon := {%H-}NormalizeLon(RadToDeg(Lon));
end;
{ Calculate intermediate point Lat,Lon by given start point Lat1,Lon1, end point
Lat2,Lon2 and fraction AFrac (0.0-1.0). No parameter checks for
Lat1,Lon1,Lat2 and Lon2. Result Lon normalized to -180°..180°.
}
procedure CalcIntermedPoint(const Lat1, Lon1, Lat2, Lon2, AFrac: Double; out
Lat, Lon: Double);
var
latFrom, lonFrom, latTo, lonTo: Double;
A, B, aD, X, Y, Z: Double;
sin_latFrom, cos_latFrom: Double;
sin_lonFrom, cos_lonFrom: Double;
sin_latTo, cos_latTo: Double;
begin
if (Lat1 = Lat2) and (Lon1 = Lon2) or (AFrac < 0.001) then
begin
Lat := Lat1;
Lon := Lon1;
Exit;
end;
if AFrac > 0.999 then
begin
Lat := Lat2;
Lon := Lon2;
Exit;
end;
aD := CalcGeoDistance(Lat1, Lon1, Lat2, Lon2, duMeters) / EARTH_EQUATORIAL_RADIUS;
latFrom := DegToRad(Lat1);
lonFrom := DegToRad(Lon1);
latTo := DegToRad(Lat2);
lonTo := DegToRad(Lon2);
SinCos(latFrom, sin_LatFrom, cos_LatFrom);
SinCos(lonFrom, sin_LonFrom, cos_lonFrom);
SinCos(latTo, sin_latTo, cos_latTo);
A := Sin((1.0 - AFrac) * aD) / Sin(aD);
B := Sin(AFrac * aD) / Sin(aD);
X := A * cos_latFrom * cos_lonFrom + B * cos_latTo * Cos(lonTo);
Y := A * cos_latFrom * sin_lonFrom + B * cos_latTo * Sin(lonTo);
Z := A * sin_latFrom + B * sin_latTo;
Lat := ArcTan2(Z, Sqrt(Sqr(X) + Sqr(Y)));
Lon := ArcTan2(Y, X);
Lat := RadToDeg(Lat);
Lon := {%H-}NormalizeLon(RadToDeg(Lon));
end;
procedure VertexOfGreatCircle(const AStartLat, AStartLon, ADestinationLat,
ADestinationLon: Double; out AVertexLat, AVertexLon: Double);
var
lStartLat, lStartLon, lDestinationLat, lDestinationLon : Double;
lDestBearing, lDestBearingRad : Double;
d : Double;
lDestLatRad : Double;
SecondLoop : Boolean = False;
begin
AVertexLon := 0.0;
lStartLat := AStartLat;
lStartLon := AStartLon;
lDestinationLat := ADestinationLat;
lDestinationLon := ADestinationLon;
// Problem: If the Latitude is 0.0 then the computation fails, we try then the opposite direction
repeat
lDestLatRad := DegToRad(lDestinationLat);
lDestBearing := CalcBearing(lDestinationLat,lDestinationLon,lStartLat,lStartLon);
if lDestBearing > 180.0 then
lDestBearing := lDestBearing - 360.0;
lDestBearingRad := DegToRad(lDestBearing);
// Vertex Latitude
// Latitude of Vertex: cos phiS = sin alpha * cos phiA
// cos φS = sin α · cos φPoS = sin 46,87° · cos 10,6722° = 0,7172; φS = 44,1762°
d := ArcCos(Sin(lDestBearingRad) * Cos(lDestLatRad));
AVertexLat := RadToDeg(d);
// The latitude has a sign problem if the signs of DestinationLat and Bearing are different
if ((lDestinationLat > 0.0) and (lDestBearing < 0.0)) or
((lDestinationLat < 0.0) and (lDestBearing > 0.0)) then
AVertexLat := -AVertexLat;
// Vertex Longitude
// Longitude of Vertex: tan(lamda A - lambda S) = 1 / (sin phi A * tan alpha)
// tan (λS - λPoS) = cot α sin φPoS =
// = cot 46,87° sin 10,6722° = 0,937 0,185 = 5,065;
// ⇒ λS - λPoS = 78,83°, und daraus
// λS = 78,83° + (-61,53°) = 17,3°
if (lDestLatRad <> 0.0) and (lDestBearingRad <> 0.0) then
begin
d := RadToDeg(ArcTan(Cot(lDestBearingRad) / Sin(lDestLatRad)));
AVertexLon := NormalizeLon(d + lDestinationLon);
Break;
end;
if SecondLoop then Break;
// If lDestLatRad is 0.0, then try the opposite way
SecondLoop := True;
lStartLat := ADestinationLat;
lStartLon := ADestinationLon;
lDestinationLat := AStartLat;
lDestinationLon := AStartLon;
until False;
end;
procedure LatFromLonAtGreatCircle(
const AStartLat, AStartLon, ADestinationLat, ADestinationLon : Double;
const ASearchLon: Double; out AFoundLat: Double);
var
lVertexLat, lVertexLon : Double;
begin
// tan φWP = tan φS · cos(λS - λWP)
// φS = Lat of Vertex
// λS = Lon of Vertex
// λWP = Lon of sdearched point
VertexOfGreatCircle(AStartLat, AStartLon, ADestinationLat, ADestinationLon, lVertexLat, lVertexLon);
AFoundLat := RadToDeg(ArcTan(Tan(DegToRad(lVertexLat)) * Cos(DegToRad(lVertexLon-ASearchLon))));
end;
function NormalizeLon(const Lon: Double): Double;
begin
if InRange(Lon, -180.0, 180.0)
then Result := Lon
else Result := FMod(Lon + 540.0, 360.0) - 180.0;
end;
{ Converts an angle given as degrees, minutes and seconds to a single
floating point degrees value. }
function DMSToDeg(Deg, Min: Integer; Sec: Double): Double;
var
isNeg: Boolean;
sgn: Integer;
begin
isNeg := Deg < 0;
Result := abs(Deg) + Min/60.0 + Sec/3600.0;
if isNeg then Result := -Result;
end;
procedure SplitGps(AValue: Double; out ADegs, AMins: Double);
begin
AValue := abs(AValue);
AMins := frac(AValue) * 60;
if abs(AMins - 60) < 1E-3 then
begin
AMins := 0;
ADegs := trunc(AValue) + 1;
end else
ADegs := trunc(AValue);
if AValue < 0 then
ADegs := -ADegs;
end;
procedure SplitGps(AValue: Double; out ADegs, AMins, ASecs: Double);
begin
SplitGps(AValue, ADegs, AMins);
ASecs := frac(AMins) * 60;
AMins := trunc(AMins);
if abs(ASecs - 60) < 1E-3 then
begin
ASecs := 0;
AMins := AMins + 1;
if abs(AMins - 60) < 1e-3 then
begin
AMins := 0;
ADegs := ADegs + 1;
end;
end;
if AValue < 0 then
ADegs := -ADegs;
end;
function GPSToDMS(Angle: Double): string;
begin
Result := GPSToDMS(Angle, DefaultFormatSettings);
end;
function GPSToDMS(Angle: Double; AFormatSettings: TFormatSettings): string;
var
deg, min, sec: Double;
begin
SplitGPS(Angle, deg, min, sec);
Result := Format('%.0f° %.0f'' %.*f"', [deg, min, DMS_Decimals, sec], AFormatSettings);
end;
function LatToStr(ALatitude: Double; DMS: Boolean): String;
begin
Result := LatToStr(ALatitude, DMS, DefaultFormatSettings);
end;
function LatToStr(ALatitude: Double; DMS: Boolean; AFormatSettings: TFormatSettings): String;
begin
if DMS then
Result := GPSToDMS(abs(ALatitude), AFormatSettings)
else
Result := Format('%.6f°',[abs(ALatitude)], AFormatSettings);
if ALatitude > 0 then
Result := Result + ' N'
else
if ALatitude < 0 then
Result := Result + ' S';
end;
function LonToStr(ALongitude: Double; DMS: Boolean): String;
begin
Result := LonToStr(ALongitude, DMS, DefaultFormatSettings);
end;
function LonToStr(ALongitude: Double; DMS: Boolean; AFormatSettings: TFormatSettings): String;
begin
if DMS then
Result := GPSToDMS(abs(ALongitude), AFormatSettings)
else
Result := Format('%.6f°', [abs(ALongitude)], AFormatSettings);
if ALongitude > 0 then
Result := Result + ' E'
else if ALongitude < 0 then
Result := Result + ' W';
end;
{ Combines up to three parts of a GPS coordinate string (degrees, minutes, seconds)
to a floating-point degree value. The parts are separated by non-numeric
characters:
three parts ---> d m s ---> d and m must be integer, s can be float
two parts ---> d m ---> d must be integer, s can be float
one part ---> d ---> d can be float
Each part can exhibit a unit identifier, such as °, ', or ". BUT: they are
ignored. This means that an input string 50°30" results in the output value 50.5
although the second part is marked as seconds, not minutes!
Hemisphere suffixes ('N', 'S', 'E', 'W') are supported at the end of the input string.
}
function TryStrToGps(const AValue: String; out ADeg: Double): Boolean;
const
NUMERIC_CHARS = ['0'..'9', '.', ',', '-', '+'];
var
mins, secs: Double;
i, j, len: Integer;
n: Integer;
s: String = '';
res: Integer;
sgn: Double;
begin
Result := false;
ADeg := NaN;
mins := 0;
secs := 0;
if AValue = '' then
exit;
len := Length(AValue);
i := len;
while (i >= 1) and (AValue[i] = ' ') do dec(i);
sgn := 1.0;
if (AValue[i] in ['S', 's', 'W', 'w']) then sgn := -1;
// skip leading non-numeric characters
i := 1;
while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do
inc(i);
// extract first value: degrees
SetLength(s, len);
j := 1;
n := 0;
while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin
if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i];
inc(i);
inc(j);
inc(n);
end;
if n > 0 then begin
SetLength(s, n);
val(s, ADeg, res);
if res <> 0 then
exit;
end;
// skip non-numeric characters between degrees and minutes
while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do
inc(i);
// extract second value: minutes
SetLength(s, len);
j := 1;
n := 0;
while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin
if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i];
inc(i);
inc(j);
inc(n);
end;
if n > 0 then begin
SetLength(s, n);
val(s, mins, res);
if (res <> 0) or (mins < 0) then
exit;
end;
// skip non-numeric characters between minutes and seconds
while (i <= len) and not (AValue[i] in NUMERIC_CHARS) do
inc(i);
// extract third value: seconds
SetLength(s, len);
j := 1;
n := 0;
while (i <= len) and (AValue[i] in NUMERIC_CHARS) do begin
if AValue[i] = ',' then s[j] := '.' else s[j] := AValue[i];
inc(i);
inc(j);
inc(n);
end;
if n > 0 then begin
SetLength(s, n);
val(s, secs, res);
if (res <> 0) or (secs < 0) then
exit;
end;
// If the string contains seconds then minutes and deegrees must be integers
if (secs <> 0) and ((frac(ADeg) > 0) or (frac(mins) > 0)) then
exit;
// If the string does not contain seconds then degrees must be integer.
if (secs = 0) and (mins <> 0) and (frac(ADeg) > 0) then
exit;
// If the string contains minutes, but no seconds, then the degrees must be integer.
Result := (mins >= 0) and (mins < 60) and (secs >= 0) and (secs < 60);
// A similar check should be made for the degrees range, but since this is
// different for latitude and longitude the check is skipped here.
if Result then
ADeg := sgn * (abs(ADeg) + mins / 60 + secs / 3600);
end;
{ Convert Lat/Lon string to degrees.
Recognized formats:
Degrees, Minutes and Seconds:
DDD°MM'SS.S"
32°18'23.1"N 122°36'52.5"W
Degrees and Decimal Minutes:
DDD° MM.MMM'
32°18.385'N 122°36.875'W
Decimal Degrees:
DDD.DDDDD°
32.30642°N 122.61458°W
+32.30642 -122.61458
}
function TryStrDMSToDeg(const AValue: String; out ADeg: Double): Boolean;
const
NUMERIC_CHARS = ['0'..'9', '.', ',', '-', '+'];
WS_CHARS = [' '];
var
I, Len, N: Integer;
S: String;
D, Minutes, Seconds: Double;
R: Word;
Last, Neg: Boolean;
function EOL: Boolean; inline;
begin
Result := Len < I;
end;
procedure SkipWS; inline;
begin
while not EOL and (AValue[I] in WS_CHARS) do
Inc(I);
end;
function NextNum: String;
begin
Result := '';
SkipWS;
while not EOL and (AValue[I] in NUMERIC_CHARS) do
begin
if AValue[I] = ','
then Result := Result + '.'
else Result := Result + AValue[I];
Inc(I);
end;
end;
begin
Result := False;
ADeg := NaN;
Len := Length(AValue);
I := 1;
// Degrees
S := NextNum;
if S = '' then
Exit;
Val(S, ADeg, R);
if R > 0 then
Exit;
// It must be the only part if negative or fractional
Neg := ADeg < 0.0;
Last := Neg or (Frac(ADeg) > 0.0);
// Eat the degree symbol if present
if not EOL and (Utf8CodePointLen(@AValue[I], 2, False) = 2)
and (AValue[I] = #$c2) and (AValue[I + 1] = #$b0)
then
Inc(I, 2);
Minutes := 0.0;
Seconds := 0.0;
N := 1;
while not (EOL or Last) and (N < 3) do
begin
S := NextNum;
if S = '' then
Break
else
begin
Val(S, D, R);
// Invalid part or negative one
if (R > 0) or (D < 0.0) then
Exit;
// No more parts when fractional
Last := Frac(D) > 0.0;
if not EOL then
case AValue[I] of
'''': // Munutes suffix
begin
Minutes := D;
Inc(I); // Eat the '
end;
'"': // Seconds suffix
begin
Seconds := D;
Last := True; // Last part
Inc(I); // Eat the "
end;
otherwise
if N = 1
then Minutes := D
else Seconds := D;
end;
end;
Inc(N);
end;
// Merge parts
ADeg := ADeg + Minutes / 60 + Seconds / 3600;
// Check for N-S and E-W designators
SkipWS;
if not (EOL or Neg) and (AValue[I] in ['S', 's', 'W', 'w', 'N', 'n', 'E', 'e']) then
begin
if AValue[I] in ['S', 's', 'W', 'w']
then ADeg := -1 * ADeg;
Inc(I);
end;
SkipWS;
// It must be entirely consumed
Result := EOL;
end;
end.