- ตอนก่อนหน้านี้ ผมเขียนโปรแกรมแปลงพิกัดระหว่าง UTM และ Geographic (Lat/Long) และและถ้าไม่เขียนการหาระยะทางและอะซิมัท (เมื่อกำหนดจุด Latitude, Longitude ให้สองจุด) ก็ดูจะขาดอะไรไปอย่าง
Model ที่ใช้ในการคำนวณ
- สัณฐานหรือรูปทรงที่ใช้แทนโลก ใช้กันอยู่ 2 แบบ คือ ทรงกลม(Spherical)และทรงรี(Ellipsoid) แต่ทรงรีจะใกล้เคียงกับกับโลกเรามากกว่า ดังนั้นการหาระยะทางและทิศทาง(อะซิมัท) บนทรงรีจะให้ความละเอียดถูกต้องมากกว่า
- การคำนวณระยะทางและอะซิมัทบนทรงกลมนั้นง่ายกว่าการคำนวณบนทรงรี การคำนวณระยะทางบนทรงกลมเช่นต้องการทราบระยะทางจากจุด A ไปจุด B เราจะใช้ plane คือแผ่นเรียบตัดผ่านจุด A และจุด B และที่สำคัญคือต้องผ่านจุดศูนย์กลางทรงกลมด้วย เส้นที่เกิดจากแผ่น plane มาตัดกับทรงกลมจะเรียกว่า Great Circle ระยะที่สั้นที่สุดบนผิวทรงกลมก็คือระยะที่ไปตาม Great circle นี่เอง
- ส่วนทรงรีที่สัณฐานใกล้เคียงกับโลกมากกว่า จะยุบลงที่ขั้วโลก โป่งออกที่เส้นศูนย์สูตร เราจะหาระยะทางและอะซิมัทโดยอิงสัณฐานของทรงรีโดยใช้สูตรอันลือชื่อของ Thaddeus Vincenty
สูตรที่ใช้อ้างอิงในการคำนวณ (Vincenty’s formulae)
- Thaddeus Vincenty เกิดที่โปแลนด์ อพยพเข้าสหรัฐอเมริกาเมื่อสงครามโลกครั้งที่สอง ทำงานให้กับ U.S. Air Force และ National Geodetic Survey ตามลำดับ ที่เป็นที่รู้จักกันมากที่สุดก็คือ Vincenty’s Formulae นี้เอง เขาเผยแพร่สูตรนี้เมื่อปี 1975 ซึ่งให้ความละเอียดสูงมากจนถึงครึ่งมิลลิเมตร
- สำหรับสูตรของคุณ Thaddeus Vincenty ถ้าเห็นสูตรยาวๆแล้วสารอะดรีนาลีนหลั่งออกมาก็ ไปดาวน์โหลดได้ที่ http://www.ngs.noaa.gov/PUBS_LIB/inverse.pdf
- สำหรับสูตรที่จะนำเสนอในที่นี้ผมแปลมาจาก Javascript ดูได้ที่ http://www.movable-type.co.uk/scripts/latlong-vincenty.html สำหรับสูตรของ Vincenty จะมีการอินทิเกรต แต่ในทางโปรแกรมมิ่งจะใช้เทคนิค iteration แทนทำให้สูตรที่จะโปรแกรมดูกระชับมาก
- สำหรับสูตรที่นำสรุปจาก Vincenty’s formulae คำนวณหาระยะทางและอะซิมัทจากจุด 2 จุด แสดงให้เห็นดังข้างล่าง
a, b = major & minor semiaxes of the ellipsoid | ||
f = flattening (a−b)/a | ||
φ1, φ2 = geodetic latitude | ||
L = difference in longitude | ||
U1 = atan((1−f).tanφ1) (U is ‘reduced latitude’) | ||
U2 = atan((1−f).tanφ2) | ||
λ = L (first approximation) | ||
iterate until change in λ is negligible (e.g. 10-12 ≈ 0.06mm) { | ||
sinσ = √[ (cosU2.sinλ)² + (cosU1.sinU2 − sinU1.cosU2.cosλ)² ] | (14) | |
cosσ = sinU1.sinU2 + cosU1.cosU2.cosλ | (15) | |
σ = atan2(sinσ, cosσ) | (16) | |
sinα = cosU1.cosU2.sinλ / sinσ | (17) | |
cos²α = 1 − sin²α (trig identity; §6) | ||
cos2σm = cosσ − 2.sinU1.sinU2/cos²α | (18) | |
C = f/16.cos²α.[4+f.(4−3.cos²α)] | (10) | |
λ′ = L + (1−C).f.sinα.{σ+C.sinσ.[cos2σm+C.cosσ.(−1+2.cos²2σm)]} | (11) | |
u² = cos²α.(a²−b²)/b² | ||
A = 1+u²/16384.{4096+u².[−768+u².(320−175.u²)]} | (3) | |
B = u²/1024.{256+u².[−128+u².(74−47.u²)]} | (4) | |
Δσ = B.sinσ.{cos2σm+B/4.[cosσ.(−1+2.cos²2σm) − B/6.cos2σm.(−3+4.sin²σ).(−3+4.cos²2σm)]} | (6) | |
s = b.A.(σ−Δσ) | (19) | |
α1 = atan2(cosU2.sinλ, cosU1.sinU2 − sinU1.cosU2.cosλ) | (20) | |
α2 = atan2(cosU1.sinλ, −sinU1.cosU2 + cosU1.sinU2.cosλ) | (21) |
where :
- s is the distance (in the same units as a & b)
- α1 is the initial bearing, or forward azimuth
- α2 is the final bearing (in direction p1→p2)
สิ่งที่ต้องเตรียมก่อนจะโปรแกรม
- ยูนิต GeoEllipsoids (geoellipsoids.pas) เก็บสัณฐานทรงรีที่ใช้กันทั่วโลก ที่ผมเขียนไว้เรื่อง การเขียนโปรแกรมคำนวณการแปลงพิกัดระหว่าง UTM และ Geographic (ตอนที่ 2)
เริ่มต้นโปรแกรม
- ผมจะสร้างยูนิตที่ทำหน้าที่คำนวณชื่อ GeodesicCompute (อยู่ในไฟล์ geodesiccompute.pas) โค๊ดดังที่กล่าวไปแล้วจาก Javascript แต่ดัดแปลงให้เป็นโค๊ด OOP ในยูนิตนี้มี 2 class คือ TGeodesicInverse ทำหน้าที่คำนวณระยะทางและอะซิมัทเมื่อป้อนค่าพิกัดเป็น Latitude,Longitude 2 จุด เรียกการคำนวณนี้ว่า Inverse
- และอีกยูนิตหนึ่งคือ TGeodesicDirect(อยู่ในไฟล์ geodesiccompute.pas) ทำหน้าที่คำนวณหาค่าพิกัดจุดที่สอง (Lat/Long) เมื่อกำหนดค่าพิกัดจุดที่หนึ่งให้และ กำหนดอะซิมัทและระยะทาง เรียกการคำนวณนี้สั้นๆว่า Direct
GeodesicCompute Unit
- โค๊ดที่ผมเขียนดัดแปลงมาจาก Javascript เป็นไปดัง source code ด้านล่าง
unit GeodesicCompute;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, GeoEllipsoids, Math;
type
TZoneHemisphere = (hemEast, hemWest, hemNorth, hemSouth);
TCoordinate = record
X : double;
Y : double;
end;
//Geodesic computation by Vincenty's formulae.
//Compute geodesic azimuth & distance.
TGeodesicInverse = class
private
FEllipsoid : TEllipsoid;
FGeoCoor1 : TCoordinate;
FGeoCoor2 : TCoordinate;
FEllDist, FInitAzi, FFinalAzi : double;
procedure SetGeoCoordinate1 (const geocoor1 : TCoordinate);
procedure SetGeoCoordinate2 (const geocoor2 : TCoordinate);
procedure SetEllipsoid(const Ellipsoid : TEllipsoid);
function GeodesicInverse(lat1, lon1, lat2, lon2 : double) : double;
public
property GeoCoordinate1 : TCoordinate write SetGeoCoordinate1;
property GeoCoordinate2 : TCoordinate write SetGeoCoordinate2;
property Distance : double read FEllDist;
property InitialAzimuth : double read FInitAzi;
property FinalAzimuth : double read FFinalAzi;
property Ellipsoid : TEllipsoid write SetEllipsoid;
procedure Compute;
constructor Create;
destructor Destroy; override;
end;
TGeodesicDirect = class
private
FEllipsoid : TEllipsoid;
FGeoCoor1 : TCoordinate;
FGeoCoor2 : TCoordinate;
FAzimuth : double;
FDistance : double;
procedure SetGeoCoordinate1 (const geocoor1 : TCoordinate);
procedure SetEllipsoid(const Ellipsoid : TEllipsoid);
procedure SetAzimuth(const azi : double);
procedure SetDistance(const dist : double);
function GeodesicDirect(lat, lon, azimuth, distance : double) : TCoordinate;
public
property GeoCoordinate1 : TCoordinate write SetGeoCoordinate1;
property GeoCoordinate2 : TCoordinate read FGeoCoor2;
property Azimuth : double write SetAzimuth;
property Distance : double write SetDistance;
property Ellipsoid : TEllipsoid read FEllipsoid write SetEllipsoid;
procedure Compute;
constructor Create;
destructor Destroy; override;
end;
implementation
// TGeodesicInverse
procedure TGeodesicInverse.SetGeoCoordinate1 (const geocoor1 : TCoordinate);
begin
FGeoCoor1 := geocoor1;
end;
procedure TGeodesicInverse.SetGeoCoordinate2 (const geocoor2 : TCoordinate);
begin
FGeoCoor2 := geocoor2;
end;
procedure TGeodesicInverse.SetEllipsoid(const Ellipsoid : TEllipsoid);
begin
FEllipsoid.EllipsoidName := Ellipsoid.EllipsoidName;
FEllipsoid.MajorAxis := Ellipsoid.MajorAxis;
FEllipsoid.InvFlattening := Ellipsoid.InvFlattening;
end;
procedure TGeodesicInverse.Compute;
begin
FEllDist := GeodesicInverse(FGeoCoor1.Y, FGeoCoor1.X, FGeoCoor2.Y, FGeoCoor2.X);
end;
function TGeodesicInverse.GeodesicInverse(lat1, lon1, lat2, lon2 : double) : double;
var
a, b, f , e : double;
dlat1, dlon1, dlat2, dlon2 : double;
L, U1, U2 : double;
sinU1, cosU1, sinU2, cosU2 : double;
lambda, lambdaP, sinlambda, coslambda, sinsigma : double;
cossigma, sinalpha, sigma, cosSqalpha, cos2sigmaM : double;
cossigmaM, C, uSq, AA, BB, deltaSigma : double;
az1, az2 : double;
iterLimit : integer;
diff : double;
begin
dlat1 := PI/180 * lat1;
dlon1 := PI/180 * lon1;
dlat2 := PI/180 * lat2;
dlon2 := PI/180 * lon2;
a := FEllipsoid.MajorAxis;
f := 1/FEllipsoid.InvFlattening;
b := a*(1 - f);
L := dlon2-dlon1;
U1 := arctan((1-f) * tan(dlat1));
U2 := arctan((1-f) * tan(dlat2));
sinU1 := sin(U1);
cosU1 := cos(U1);
sinU2 := sin(U2);
cosU2 := cos(U2);
lambda := L;
iterLimit := 100;
repeat
begin
sinlambda := sin(lambda);
coslambda := cos(lambda);
sinsigma := sqrt((cosU2*sinLambda) * (cosU2*sinlambda) +
(cosU1*sinU2-sinU1*cosU2*coslambda) * (cosU1*sinU2-sinU1*cosU2*coslambda));
if (sinsigma = 0) then
begin
result := 0; // co-incident points
exit;
end;
cossigma := sinU1*sinU2 + cosU1*cosU2*coslambda;
sigma := arctan2(sinsigma, cossigma);
sinalpha := cosU1 * cosU2 * sinlambda / sinsigma;
cosSqAlpha := 1 - sinAlpha*sinAlpha;
cos2SigmaM := cosSigma - 2*sinU1*sinU2/cosSqAlpha;
if (IsNaN(cos2sigmaM)) then cosSigmaM := 0; // equatorial line: cosSqAlpha=0 (6)
C := f/16*cosSqalpha*(4+f*(4-3*cosSqalpha));
lambdaP := lambda;
lambda := L + (1-C) * f * sinAlpha *
(sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));
dec(iterLimit);
end;
diff := abs(lambda-lambdaP);
Until ((diff < 1e-12) and (iterLimit > 0));
if (iterLimit = 0) then
begin
result := NaN; // formula failed to converge
exit;
end;
uSq := cosSqAlpha * (a*a - b*b) / (b*b);
AA := 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
BB := uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
deltaSigma := BB*sinsigma*(cos2sigmaM+BB/4*(cossigma*(-1+2*cos2sigmaM*cos2sigmaM)-
BB/6*cos2sigmaM*(-3+4*sinsigma*sinsigma)*(-3+4*cos2sigmaM*cos2sigmaM)));
result := b*AA*(sigma-deltaSigma); //return Ellipse Distance.
//initial azimuth.
az1 := arctan2((cosU2*sinlambda),(cosU1*sinU2 - sinU1*cosU2*coslambda));
//final azimuth.
az2 := arctan2((cosU1*sinlambda),(-sinU1*cosU2 + cosU1*sinU2*coslambda));
FInitAzi := az1 * 180/PI;
if (FInitAzi < 0) then FInitAzi := 360 + FInitAzi;
FFinalAzi := az2 * 180/PI;
if (FFinalAzi < 0) then FFinalAzi := 360 + FFinalAzi;
end;
constructor TGeodesicInverse.Create;
begin
inherited create;
FEllipsoid := TEllipsoid.Create;
end;
destructor TGeodesicInverse.Destroy;
begin
FEllipsoid.Free;
inherited Destroy;
end;
// TGeodesicDirect
procedure TGeodesicDirect.SetGeoCoordinate1 (const geocoor1 : TCoordinate);
begin
FGeoCoor1 := geocoor1;
end;
procedure TGeodesicDirect.SetAzimuth (const azi : double);
begin
FAzimuth := azi;
end;
procedure TGeodesicDirect.SetDistance (const dist : double);
begin
FDistance := dist;
end;
procedure TGeodesicDirect.SetEllipsoid(const Ellipsoid : TEllipsoid);
begin
FEllipsoid.EllipsoidName := Ellipsoid.EllipsoidName;
FEllipsoid.MajorAxis := Ellipsoid.MajorAxis;
FEllipsoid.InvFlattening := Ellipsoid.InvFlattening;
end;
procedure TGeodesicDirect.Compute;
begin
FGeoCoor2 := GeodesicDirect(FGeoCoor1.Y, FGeoCoor1.X, FAzimuth, FDistance);
end;
function TGeodesicDirect.GeodesicDirect(lat, lon, azimuth, distance : double) : TCoordinate;
var
a, b, f , e, s : double;
dlat1, dlon1, dlat2, dlon2 : double;
sinU1, alpha1, sinAlpha, sinAlpha1, cosAlpha1, tanU1, cosU1, sigma1 : double;
cos2SigmaM, sinSigma, cosSigma, cosSqAlpha, uSq, deltaSigma : double;
AA, BB, sigma, sigmaP, tmp, lambda, C, L, lat2, revAz : double;
begin
dlat1 := PI/180 * lat;
dlon1 := PI/180 * lon;
a := FEllipsoid.MajorAxis;
f := 1/FEllipsoid.InvFlattening;
b := a*(1 - f);
s := distance;
alpha1 := azimuth * PI/180.0;
sinAlpha1 := sin(alpha1);
cosAlpha1 := cos(alpha1);
tanU1 := (1-f) * tan(dlat1);
cosU1 := 1 / sqrt((1 + tanU1*tanU1));
sinU1 := tanU1*cosU1;
sigma1 := arctan2(tanU1, cosAlpha1);
sinAlpha := cosU1 * sinAlpha1;
cosSqAlpha := 1 - sinAlpha*sinAlpha;
uSq := cosSqAlpha * (a*a - b*b) / (b*b);
AA := 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
BB := uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
sigma := s / (b*AA);
sigmaP := 2*PI;
while (abs(sigma-sigmaP) > 1e-12) do
begin
cos2SigmaM := cos(2*sigma1 + sigma);
sinSigma := sin(sigma);
cosSigma := cos(sigma);
deltaSigma := BB*sinSigma*(cos2SigmaM+BB/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)-
BB/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));
sigmaP := sigma;
sigma := s / (b*AA) + deltaSigma;
end;
tmp := sinU1*sinSigma - cosU1*cosSigma*cosAlpha1;
dlat2 := arctan2(sinU1*cosSigma + cosU1*sinSigma*cosAlpha1,
(1-f)*sqrt(sinAlpha*sinAlpha + tmp*tmp));
lambda := arctan2(sinSigma*sinAlpha1, cosU1*cosSigma - sinU1*sinSigma*cosAlpha1);
C := f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));
L := lambda - (1-C) * f * sinAlpha *
(sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));
revAz := arctan2(sinAlpha, -tmp); // final bearing
result.X := lon + L * 180/PI;
result.Y := dlat2 * 180/PI;
end;
constructor TGeodesicDirect.Create;
begin
inherited create;
FEllipsoid := TEllipsoid.Create;
end;
destructor TGeodesicDirect.Destroy;
begin
FEllipsoid.Free;
inherited Destroy;
end;
end.
- สำหรับ source code ที่แสดงดังข้างต้น เป็น class ง่ายๆ เช่น TGeodesicInverse จะมี property สำหรับ input อยู่ 3 ตัวคือ ค่ารูปทรงสัณฐาน Ellipsoid, GeoCoordinate1 และ GeoCoordinate2 เมื่อ input ค่า lat,long 2 ค่าพิกัดให้เสร็จ แล้วก็เรียกเมธอด Compute เพื่อทำการคำนวณ จากนั้นเรียกค่าจาก 2 property คือ Azimuth และ Distance ซึ่งจะเป็นผลลัพธ์การคำนวณ
- ส่วนคลาส TGeodesicDirect ก็เหมือนๆกัน มี property 4 ตัว คือ Ellipsoid, GeoCoordinate1, Azimuth และ Distance เมื่อรับค่ามาก็จะเรียกเมธอด Compute และให้ผลลัพธ์เป็นค่าพิกัด (lat,long) ของจุดที่ต้องการหา
ปัญหาความเข้ากันของ Widget ในแต่ละ Platform
- รูปที่จะแสดงให้ดูเป็นปัญหาของ Widget ผมเพิ่งจะประสบความสำเร็จติดตั้ง Widget ของ Qt บน PCLinuxOS เมื่อรันโปรแกรมด้วยชุด source code เดียวกันเปรียบเทียบกับ Win32 บนวินโดส์ และ Gtk2 บน Ubuntu พบว่า ขนาดของ Lable, EditBox, ComboBox, Groupbox ทั้งหลายบน Gtk2 มีขนาดใหญ่กว่าเพื่อน แต่พบว่าขนาดของ object พวกนี้บน Win32 และ Qt มีขนาดใกล้เคียงกัน เพื่อให้เข้ากับ Gtk2 จึงต้องมาขยับ เช่นตัว Label กับ EditBox จะต้องอยู่ห่างกันพอสมควร ไม่งั้นตัว Label จะล้ำเข้าไปหา EditBox แต่ความสวยงามดูๆ QT จะสวยเนียนกว่าเพื่อน รองลงมาเป็น Gtk2 กับ Win32 ดูรูปผลลัพธ์ของโปรแกรมที่ผมเขียนในตอนนี้
- ทิ้งท้ายกันก่อนในตอนนี้ ติดตามกันตอนหน้า