C#で方位距離計算可能な電卓を作ってみた。

 電卓は、C#プログラミングのいろいろな要素があっていい演習材料である。実は昨年C#でプログミングを始めたばかりのころも作ったことがあった。むりやりClassとか入れ込んでみたりとか無駄なところに注力しすぎて電卓としてあまりいいものではなかった。依然C#が推奨するところのオブジェクト指向プログラミングは、あまり理解できていないけれども電卓第二弾を作ってみた。

 今回の目標は、地球上の基点座標から目標座標位置(緯度経度)の方位角度と距離が計算可能な電卓であること。球体の体積計算もできないと意味がない。16進数計算も一応できる。

(左)方位距離計算 (右)電卓計算

方位距離の計算方法

三角関数多用で正直どうしてこんな計算になるのか理解してないけれども計算方法は以下の通り。
エクセル使うとあっさり解が求められる。
以下の例は、東京スカイツリーからパリのエッフェル塔の方位と距離を求める。

東京スカイツリー
  北緯 lat1=rad(35.7100627°)
  東経 lon1=rad(139.8107004°)

パリのエッフェル塔
  北緯 lat2=rad(48.8583701°)
  東経 lon2=rad(2.2944813°)

rad = Radians

地球の半径 r =6371km

方位角度
  X= Cos(lat1) * Sin(lat2) – Sin(lat1) * Cos(lat2) * Cos(lon2 – lon1)
  Y=Sin(lon2 – lon1) * Cos(lat2)
  = Degrees(Atan2(X, Y)) + 360
  360を超えたら360減算する。

距離 = Acos(Sin(lat1) * Sin(lat2) + Cos(lat1) * Cos(lat2) * Cos(lon2 – lon1)) * r

これをメモリー操作[+M]ボタンと[RM]ボタンを使うことでなんとか解を求めることができる。

これで動作を確認していたら面倒になってきて座標情報を入力するだけで方位距離を計算できたら便利かもということでそれ用の入力ができるようにした。それが[Position]ボタンである。グリットロケーターを緯度経度に変換することもできるようにした。

地球の体積計算した後方位距離計算表示

 電卓なのでボタンをクリックして動作するがPositionにするとキーボードからアルファベットも入力可能になる。[Base][Target]は記録された座標を表示、+CTRLで表示中の座標を記録する。[MC]ボタンは、[RM]で表示させたメモリーを消去する。+CTRLで全消去となる。必要に応じて目的外ボタンの押下無効になる。
 全てのボタンは、キーボードでも操作できるようにした。Shift+[/]で[%]、Shift+[-]で[+/-]とか、[MC]~[∠θ]の4列づつF1~F8で連動する。などである。
 緯度経度は、コンマ区切りで入力する。グリットロケーターは、サブスクエアまでの6桁にしか対応していない。

C# 方位距離計算部分

//  Visual Studio 2019 C# 8.0 
//
        private (double lat, double lon) Base = (0, 0);
        private (double lat, double lon) Target = (0, 0); 
       //
       // Angle to Radian
        private double Radian(double angle)
        {
            return angle * Math.PI / 180;
        }
        // Radian to Angle
        private double Angle(double rad)
        {
            return rad * 180 / Math.PI;
        } 
        //
        // Base QTH から Target QTH の方位角度と距離を表示
        private void DirDx_Click(object sender, EventArgs e)
        {
            var lat1 = Radian(Base.lat);
            var lon1 = Radian(Base.lon);
            var lat2 = Radian(Target.lat);
            var lon2 = Radian(Target.lon);
            var DirDeg = Angle(Math.Atan2(Math.Sin(lon2 - lon1) * Math.Cos(lat2),
                (Math.Cos(lat1) * Math.Sin(lat2) - Math.Sin(lat1) * Math.Cos(lat2) * Math.Cos(lon2 - lon1)))) + 360;
            DirDeg = (double)Rounded((decimal)DirDeg);
            if (DirDeg > 360) DirDeg -= 360;
            var Dx = Math.Acos(Math.Sin(lat1) * Math.Sin(lat2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Cos(lon2 - lon1)) * 6371;
            Dx = (double)Rounded((decimal)Dx);
            label1.Text = String.Format("{0}° / {1}km", DirDeg, Dx);
            label2.Text = QTHtoGL(Base) + " → " + QTHtoGL(Target);
        }
        //
        // Nrからアルファベット変換
        private string NrToStr(ref Double Nr)
        {
            int ii = (int)Math.Truncate(Nr);
            char Cx = Convert.ToChar(ii + 65);
            Nr -= (double)ii;
            Nr *= 10;
            return Cx.ToString();
        }
        //
        // QTH(緯度,経度)からGLに変換
        private string QTHtoGL((double lat,double lon) Qth)
        {
            double xLon = (Qth.lon + 180) / 20;
            double xLat = (Qth.lat + 90) / 10;
            string GL = NrToStr(ref xLon);
            GL += NrToStr(ref xLat);
            int ii = (int)Math.Truncate(xLon);
            GL += ii.ToString();
            xLon = (xLon - (double)ii) * 10;
            ii = (int)Math.Truncate(xLat);
            GL += ii.ToString();
            xLat = (xLat - (double)ii) * 10;
            xLon *= 2.4;
            GL += NrToStr(ref xLon);
            xLat *= 2.4;
            GL += NrToStr(ref xLat);
            return GL;
        }
        //
        // GLをQTH(緯度、経度)に変換
        private (double,double ) GLtoQTH(string gloc)
        {
            (double lat, double lon) Qth = (0, 0);
            char Cx = Convert.ToChar(gloc.Substring(0, 1));
            Qth.lon = (double)Convert.ToInt32(Cx) - 65;
            string Cn = gloc.Substring(2, 1);
            Qth.lon += double.Parse(Cn) / 10;
            Cx = Convert.ToChar(gloc.Substring(4, 1));
            Qth.lon += ((double)Convert.ToInt64(Cx) - 65) / 240;
            Qth.lon = Qth.lon * 20 - 180 + 0.04;
            Qth.lon = (double)Rounded((decimal)Qth.lon, 6, 0);
            Cx = Convert.ToChar(gloc.Substring(1, 1));
            Qth.lat = (double)Convert.ToInt64(Cx) - 65;
            Cn = gloc.Substring(3, 1);
            Qth.lat += double.Parse(Cn) / 10;
            Cx = Convert.ToChar(gloc.Substring(5, 1));
            Qth.lat += ((double)Convert.ToInt64(Cx) - 65) / 240;
            Qth.lat = Qth.lat * 10 - 90 + 0.02;
            Qth.lat = (double)Rounded((decimal)Qth.lat, 6, 0);
            return Qth;
        }
        //
        // 四捨五入計算
        private decimal Rounded(decimal nr,int ddn=1,int rt =0)
        {
            decimal en = (decimal)Math.Pow(10, ddn);
            try
            {
                nr *= en;
                nr = rt switch
                {
                    1 => Math.Round(nr, MidpointRounding.ToEven),
                    2 => Math.Ceiling(nr),
                    3 => Math.Truncate(nr),
                    _ => Math.Round(nr, MidpointRounding.AwayFromZero)
                };
            }
            catch (System.Exception ex)
            {
                label2.Text = ex.Message;
                return nr;
            }
            return nr / en;
        }

方位距離計算を付加したのでソースコードは1000行ほどになってしまった。

 今回は、TextBoxを使わないでKeyDownだけでタイプ入力というのを組み込んだ。デリゲートの変更とかもやった。取り敢えず自分が考えた仕様通りに動いてくれることが確認できたら気が済むのである。何しろこれは演習である。

コメント