Customizable LCD で地形図
拠点の Wide LCD パネルへ地形図を出力したプログラムに手を加え、地形図の拡大縮小と描画の常時更新ができるようにしたものを小型船に搭載しました。このプログラムの動作としては、現在地を画面の中央とする地形図を拡大縮小可能でリアルタイム表示します。ただし、表示エリアが極に近く、現在地を画面中央に表示できない場合は、現在地を表す×印を画面中央からずらして表示します。
1| public struct GeoPosition
2| {
3| public float LAT;
4| public float LNG;
5| public float ALT;
6| }
7|
8| int[,] map_data = new int[360, 181];
9| int count;
10| int max_count;
11| float ratioMax;
12| float ratioMin;
13| float ratio;
14| string status;
15| string exp_text;
16| string imp_text;
17| Vector2 current_pos;
18| Vector2 scan_pos;
19| IMyTextSurfaceProvider tsp;
20| IMyTextSurface surface;
21| MySprite sprite = MySprite.CreateSprite("SquareSimple", new Vector2(0, 0), new Vector2(1, 1));
22| MySpriteDrawFrame frame;
23|
24| public Program()
25| {
26| tsp = GridTerminalSystem.GetBlockWithName("Cockpit 2") as IMyTextSurfaceProvider;
27| surface = tsp.GetSurface(0);
28| surface.ContentType = ContentType.SCRIPT;
29| surface.Script = string.Empty;
30| max_count = (int)(Runtime.MaxInstructionCount * 0.99);
31| imp_text = Storage;
32| exp_text = imp_text;
33| ratioMax = 10;
34| ratioMin = Calc_ratioMin();
35| ratio = ratioMin;
36| status = "DataLoading";
37| count = 0;
38| Runtime.UpdateFrequency = UpdateFrequency.Update1;
39| }
40|
41| private float Calc_ratioMin()
42| {
43| float ratioWidth = surface.SurfaceSize.X/360;
44| float ratioHeight = surface.SurfaceSize.Y/181;
45| return ratioWidth > ratioHeight ? ratioWidth + 0.001f : ratioHeight + 0.001f;
46| }
47|
48| public void Save()
49| {
50| Storage = exp_text;
51| }
52|
53| public void Main(string argument, UpdateType updateSource)
54| {
55| var com = argument.Split(',');
56|
57| switch (com[0]){
58| case "ImportData":
59| exp_text = argument.Remove(0, 11);
60| break;
61| case "ExportData":
62| var pb = GridTerminalSystem.GetBlockWithName("PB Map System") as IMyProgrammableBlock;
63| string SendData = "ImportData," + exp_text;
64| pb.TryRun(SendData);
65| break;
66| case "ZoomIn":
67| ratio = ratio + 1 > ratioMax ? ratioMax : ratio + 1;
68| break;
69| case "ZoomOut":
70| ratio = ratio - 1 < ratioMin ? ratioMin : ratio - 1;
71| break;
72| }
73|
74| switch (status){
75| case "DataLoading":
76| Load();
77| break;
78| case "Ready":
79| status = "ReadyForUpdate";
80| Runtime.UpdateFrequency = UpdateFrequency.Update1;
81| break;
82| case "ReadyForUpdate":
83| GeoPosition geopos = ConvertGPSToPosition(Me.GetPosition());
84| current_pos = new Vector2(geopos.LNG, geopos.LAT);
85| scan_pos = new Vector2(0, 0);
86| status = "UpdatingMap";
87| frame = surface.DrawFrame();
88| break;
89| case "UpdatingMap":
90| UpdateMapData();
91| break;
92| }
93| }
94|
95| private void Load()
96| {
97| var data_line = imp_text.Split('\n');
98| while (count < data_line.Count()-1){
99| if (Runtime.CurrentInstructionCount > max_count)
100| return;
101| var data = data_line[count].Split(',');
102| map_data[int.Parse(data[0]), int.Parse(data[1])] = int.Parse(data[2]);
103| count++;
104| }
105| status = "Ready";
106| Runtime.UpdateFrequency = UpdateFrequency.Update100;
107| }
108|
109| private void UpdateMapData()
110| {
111| Vector2 DataPos;
112| while (scan_pos.Y < surface.SurfaceSize.Y){
113| while (scan_pos.X < surface.SurfaceSize.X){
114| if (Runtime.CurrentInstructionCount > max_count)
115| return;
116| DataPos = CalcDataPos(ratio, current_pos, scan_pos);
117| sprite.Color = ColorScale(map_data[(int)DataPos.X, (int)DataPos.Y]);
118| sprite.Position = scan_pos;
119| frame.Add(sprite);
120| scan_pos.X++;
121| }
122| scan_pos.X = 0;
123| scan_pos.Y++;
124| }
125| MarkPos(ratio, current_pos);
126| frame.Dispose();
127| status = "Ready";
128| Runtime.UpdateFrequency = UpdateFrequency.Update100;
129| }
130|
131| private Vector2 CalcDataPos(float ratio, Vector2 CurPos, Vector2 ScanPos)
132| {
133| Vector2 CenterPos = new Vector2(CurPos.X, (90f - CurPos.Y));
134| Vector2 ULPos = CenterPos - 0.5f * surface.SurfaceSize/ratio;
135| Vector2 LRPos = CenterPos + 0.5f * surface.SurfaceSize/ratio;
136| if (ULPos.Y < 0)
137| CenterPos.Y = 0.5f * surface.SurfaceSize.Y/ratio;
138| if (LRPos.Y > 180)
139| CenterPos.Y = 180 - 0.5f * surface.SurfaceSize.Y/ratio;
140|
141| Vector2 result = CenterPos - 0.5f * surface.SurfaceSize/ratio + ScanPos/ratio;
142|
143| if (result.X < 0)
144| result.X += 360;
145| if (result.X > 359)
146| result.X -= 360;
147| return result;
148| }
149|
150| private void MarkPos(float ratio, Vector2 current_pos)
151| {
152| int size = 25;
153| Vector2 CurPos = new Vector2(current_pos.X, 90f - current_pos.Y);
154| Vector2 ULPos = CurPos - 0.5f * surface.SurfaceSize/ratio;
155| Vector2 LRPos = CurPos + 0.5f * surface.SurfaceSize/ratio;
156| Vector2 plotPos = surface.SurfaceSize/2;
157|
158| if (ULPos.Y < 0)
159| plotPos.Y = surface.SurfaceSize.Y/2 + ULPos.Y * ratio;
160| else if (LRPos.Y > 180)
161| plotPos.Y = surface.SurfaceSize.Y/2 + (LRPos.Y - 180) * ratio;
162|
163| var sp = MySprite.CreateSprite("Cross", plotPos, new Vector2(size, size));
164| sp.Color = new Color(255, 0, 0, 255);
165| frame.Add(sp);
166| }
167|
168| private Color ColorScale(int alt)
169| {
170| Color result;
171| int min = 0;
172| int max = 6000;
173| float alt_ratio = (float)(alt - min)/(max - min);
174|
175| if (alt_ratio < 0.25)
176| result = new Color(0, (int)(255*alt_ratio/0.25), 255);
177| else if (alt_ratio >= 0.25 && alt_ratio <0.5)
178| result = new Color(0, 255, (int)(255-255*(alt_ratio-0.25)/0.25));
179| else if (alt_ratio >= 0.5 && alt_ratio < 0.75)
180| result = new Color((int)(255*(alt_ratio-0.5)/0.25), 255, 0);
181| else
182| result = new Color(255, (int)(255-255*(alt_ratio-0.75)/0.25), 0);
183| return result;
184| }
185|
186| private GeoPosition ConvertGPSToPosition(Vector3D GPS)
187| {
188| GeoPosition result;
189| float planet_radius = 60000;
190| Vector3D stdP = new Vector3D(-65166.17, -123372.01, -96148.62);
191| Vector3D planet_centerP = new Vector3D(-71294.114, -156858.579, -147893.096);
192| Vector3D polarV = new Vector3D(0.339268218472, -0.709210149516, -0.617995177779);
193| Vector3D stdP_UV = stdP - planet_centerP;
194| stdP_UV.Normalize();
195| Vector3D CurP_UV = GPS - planet_centerP;
196| CurP_UV.Normalize();
197| Vector3D std_NormalV = Vector3D.Cross(stdP_UV, polarV);
198| Vector3D cur_NormalV1 = Vector3D.Cross(CurP_UV, polarV);
199| Vector3D cur_NormalV2 = Vector3D.Cross(cur_NormalV1, polarV);
200| std_NormalV.Normalize();
201| cur_NormalV1.Normalize();
202| cur_NormalV2.Normalize();
203| double lng_ang1 = 180 * Math.Acos(Vector3D.Dot(std_NormalV, cur_NormalV1))/Math.PI;
204| double lng_ang2 = 180 * Math.Acos(Vector3D.Dot(std_NormalV, cur_NormalV2))/Math.PI;
205| result.LNG = lng_ang2 < 0 ? (float)(360 - lng_ang1) : (float)lng_ang1;
206| result.LAT = (float)(180 * (Math.PI/2 - Math.Acos(Vector3D.Dot(CurP_UV, polarV)))/Math.PI);
207| result.ALT = (float)(GPS-planet_centerP).Length() - planet_radius;
208| return result;
209| }
プログラム冒頭のグローバル変数宣言で、GeoPosition 構造体を定義しています。構造体メンバーの LAT, LNG, ALT (3-5 行)はそれぞれ惑星での緯度、経度、高度を格納し、惑星上での地点を表すのに使います。
1| public struct GeoPosition
2| {
3| public float LAT;
4| public float LNG;
5| public float ALT;
6| }
2 次元配列 map_data は、惑星地表面を緯度・経度それぞれ 1° キザミで計測した 65,160 地点の標高データです。この配列データから、LCD の地形図を描画しています。
8| int[,] map_data = new int[360, 181];
Program() のなかで、IMyTextSurfaceProvider としてコックピットブロックを取得します(26 行)。このコックピットブロックは内部に合計 4 つの LCD 画面を持ち、各画面は GetSurface(int index) メソッドで IMyTextSurface オブジェクトとして取得します(27 行)。このコックピットの場合、中央の画面が index = 0 です。取得した IMyTextSurface オブジェクトの ContentType および Script プロパティーをスプライト描画のためそれぞれ設定します(28-29 行)。
26| tsp = GridTerminalSystem.GetBlockWithName("Cockpit 2") as IMyTextSurfaceProvider;
27| surface = tsp.GetSurface(0);
28| surface.ContentType = ContentType.SCRIPT;
29| surface.Script = string.Empty;
Programmable block には、1 回の Run で実行できる命令数に制限があります。今回の地形データのように比較的大きいデータを扱う場合、この制限を超えてしまうため、制限に達しないように処理を分割する必要があります。変数 max_count は、この分割を実行するための基準値です。Rutime.MaxInstructionCount プロパティーは 1 回の Main メソッド呼び出しで実行できる命令数の最大を返しますが、この値に達してからでは遅いので、この値に 0.99 を掛けた値を max_count としています。
30| max_count = (int)(Runtime.MaxInstructionCount * 0.99);
変数 ratioMax と ratioMin は、地形図を拡大縮小表示するときの倍率の最大値および最小値です。最小値は、Calc_ratioMin メソッドで表示する画面解像度から計算しています。
33| ratioMax = 10;
34| ratioMin = Calc_ratioMin();
string型の exp_text は、全地形データを1つの文字列として保持しています。これを Save() メソッド中で string型 の Storage に代入することでこの値はセーブされ、次回起動時にもこの Storage 変数の中身は保持されます。
50| Storage = exp_text;
Main メソッド前半では、コマンドライン引数として渡される文字列を、コマンドとして処理します。地形データを保持するには Storage に文字列として保存しますが、この Storage は各プログラマブルブロックで独立しており、他のプログラマブルブロックと共有できません。このため、Storaage に保持しているデータを他のプログラマブルブロックにコピーしたい場合は、データをコマンドライン引数として他のプログラムを実行することで渡します(62-64 行)。呼ばれたプログラムは、コマンドライン引数からデータを取り出し、変数に保存します(59 行)。今回このプログラムでは、拠点のプログラマブルブロックに保持されている地形データを小型船内のプログラマブルブロックにコピーする必要があったので、これらのコマンドを用意しました。
58| case "ImportData":
59| exp_text = argument.Remove(0, 11);
60| break;
61| case "ExportData":
62| var pb = GridTerminalSystem.GetBlockWithName("PB Map System") as IMyProgrammableBlock;
63| string SendData = "ImportData," + exp_text;
64| pb.TryRun(SendData);
65| break;
Main メソッド後半では、このシステムのステータスに対応する処理をします。status が "ReadyForUpdate" に更新されると、実際のマップ描画に先立って変数の初期化等が行われます。ConvertGPSToPosition メソッドは、GPS 座標をこの惑星上の緯度・経度・高度に変換し、GeoPosition 構造体を返します(83 行)。最後に、スプライトを描画する MySpriteDrawFrame オブジェクトを生成し(87 行)、この後 UpdateMapData() で実際の描画処理を実行します。
83| GeoPosition geopos = ConvertGPSToPosition(Me.GetPosition());
84| current_pos = new Vector2(geopos.LNG, geopos.LAT);
85| scan_pos = new Vector2(0, 0);
86| status = "UpdatingMap";
87| frame = surface.DrawFrame();
Load() メソッドはプログラム実行時に一度だけ呼び出され、Storage に保持されている地形データが代入された imp_text から map_data 配列を復元します。1つ1つの地点データの区切りとして使っている "\n" で切り出して string型の配列 data_line に代入します(97 行)。この後の処理を地形データの個数分繰り返しますが(98 行)、1 度の Run で実行可能な命令数が制限されているため、while での繰り返しのたびに、実行可能命令数の上限として設定した max_count を超えてないかチェックします。Runtime.CurrentInstructionCount は、この段階までに実行された有効な命令数を返します(99 行)。もしすでに実行した命令数が max_count を超えていたら、return でこの Load メソッドを抜け(100 行)、処理は Main メソッドに戻りそのまま終了します。ただ Self-update が設定されているので、 1 tick 後に再び Main メソッドが呼び出され、処理は元の Load メソッドに戻ってきます。while ループのカウントに使っている count はグローバル変数として宣言されており、前回の最後の値を保持しているので、今回はその続きのデータから処理が繰り返されます。これにより、実行可能な命令数の制限を超えて大量のデータ処理を実行します。各地点のデータ data_line 配列からさらに、',' を区切りとして、緯度、経度、高度を切り出して map_data にセットします(101-102 行)。
97| var data_line = imp_text.Split('\n');
98| while (count < data_line.Count()-1){
99| if (Runtime.CurrentInstructionCount > max_count)
100| return;
101| var data = data_line[count].Split(',');
102| map_data[int.Parse(data[0]), int.Parse(data[1])] = int.Parse(data[2]);
103| count++;
104| }
UpdateMapData メソッドでは実際に地形データを基に、画面上のすべてのピクセルに対してスプライトを設定して frame に追加します(116-119 行)。すべてのスプライトを追加後に frame を Dispose メソッドで開放することで画面に実際の描画が実行されます(126 行)。この処理も命令数の制限を超える可能性が十分あるため、実行済み命令数のチェックと処理の分割を行っています(114-115 行)。また、MarkPos メソッドを呼び出して地形図上での自機の位置も描画します(125 行)。最後に status を "Ready" に更新して地形図の更新処理を終了します(127 行)。
112| while (scan_pos.Y < surface.SurfaceSize.Y){
113| while (scan_pos.X < surface.SurfaceSize.X){
114| if (Runtime.CurrentInstructionCount > max_count)
115| return;
116| DataPos = CalcDataPos(ratio, current_pos, scan_pos);
117| sprite.Color = ColorScale(map_data[(int)DataPos.X, (int)DataPos.Y]);
118| sprite.Position = scan_pos;
119| frame.Add(sprite);
120| scan_pos.X++;
121| }
122| scan_pos.X = 0;
123| scan_pos.Y++;
124| }
125| MarkPos(ratio, current_pos);
126| frame.Dispose();
127| status = "Ready";
CalcDataPos メソッドは、表示倍率、自機の緯度・経度、描画画面(IMyTextSurface)上の座標を引数に取り、これらのデータから画面上の座標に対応する map_data 配列の添え字(惑星上の緯度・経度に相当)を Vector2 オブジェクトで返します。まず、表示倍率と緯度・経度から計算した描画範囲の上部と下部が、 0 から 180 の範囲を超えないかチェックし、超えているなら超えないように描画画面の中心 CenterPos を設定します(133-139 行)。この CenterPos を基準にして描画画面上の座標に対応する配列の添え字を result に格納します(141 行)。最後にこの result の X 成分が 0 から 359 の範囲を超えていないかチェックし、範囲外ならば 360 で加減算を行い範囲内にします(143-146 行)。この処理により、惑星上の東西方向の移動に伴う地形図の表示は、緯度 0° をまたいでも連続的に行われます。
131| private Vector2 CalcDataPos(float ratio, Vector2 CurPos, Vector2 ScanPos)
132| {
133| Vector2 CenterPos = new Vector2(CurPos.X, (90f - CurPos.Y));
134| Vector2 ULPos = CenterPos - 0.5f * surface.SurfaceSize/ratio;
135| Vector2 LRPos = CenterPos + 0.5f * surface.SurfaceSize/ratio;
136| if (ULPos.Y < 0)
137| CenterPos.Y = 0.5f * surface.SurfaceSize.Y/ratio;
138| if (LRPos.Y > 180)
139| CenterPos.Y = 180 - 0.5f * surface.SurfaceSize.Y/ratio;
140|
141| Vector2 result = CenterPos - 0.5f * surface.SurfaceSize/ratio + ScanPos/ratio;
142|
143| if (result.X < 0)
144| result.X += 360;
145| if (result.X > 359)
146| result.X -= 360;
147| return result;
148| }
MarkPos は、表示倍率と自機の現在地(緯度・経度)を引数にとり、地形図上での現在地を×印でマークします。
さきほどと同じように表示倍率と現在地から描画範囲の上部、下部の Y 座標が 0 から 180 までに入っているかをチェックし、入っていれば描画画面の中心座標を、入っていなかったら画面上での座標を計算して plotPos に代入します(153-161 行)。この plotPos の位置にサイズ 25 x 25 の "Cross" をスプライトで生成します(163 行)。色を赤に設定し、最後に frame に追加します(164-165 行)。
150| private void MarkPos(float ratio, Vector2 current_pos)
151| {
152| int size = 25;
153| Vector2 CurPos = new Vector2(current_pos.X, 90f - current_pos.Y);
154| Vector2 ULPos = CurPos - 0.5f * surface.SurfaceSize/ratio;
155| Vector2 LRPos = CurPos + 0.5f * surface.SurfaceSize/ratio;
156| Vector2 plotPos = surface.SurfaceSize/2;
157|
158| if (ULPos.Y < 0)
159| plotPos.Y = surface.SurfaceSize.Y/2 + ULPos.Y * ratio;
160| else if (LRPos.Y > 180)
161| plotPos.Y = surface.SurfaceSize.Y/2 + (LRPos.Y - 180) * ratio;
162|
163| var sp = MySprite.CreateSprite("Cross", plotPos, new Vector2(size, size));
164| sp.Color = new Color(255, 0, 0, 255);
165| frame.Add(sp);
166| }
今回の地形図は、標高の低い地点から高い地点に向かって、青、緑、赤と徐々に色が変化するグラデーション表示をしました。この ColorScaleは、int 型の標高値を引数にとり、この標高データを Color オブジェクトに変換するメソッドです。グラデーション表現の最大最小に対応する標高をここでは、それぞれ 6,000 m と 0 m に設定しています(171-172 行)。実際の標高データは、最高で 7,000 m 程度、最低で -500 m 程度ですが、このように極端に標高の高い地点と低い地点の面積は非常に小さいため、カラースケール上は 0 から 6000 m までとしました。引数の標高値 alt が、0 から 6000 までを 1 とした場合の比に換算し(173 行)、これを使って色を計算します(175-182 行)。
168| private Color ColorScale(int alt)
169| {
170| Color result;
171| int min = 0;
172| int max = 6000;
173| float alt_ratio = (float)(alt - min)/(max - min);
174|
175| if (alt_ratio < 0.25)
176| result = new Color(0, (int)(255*alt_ratio/0.25), 255);
177| else if (alt_ratio >= 0.25 && alt_ratio <0.5)
178| result = new Color(0, 255, (int)(255-255*(alt_ratio-0.25)/0.25));
179| else if (alt_ratio >= 0.5 && alt_ratio < 0.75)
180| result = new Color((int)(255*(alt_ratio-0.5)/0.25), 255, 0);
181| else
182| result = new Color(255, (int)(255-255*(alt_ratio-0.75)/0.25), 0);
183| return result;
184| }
最後は、GPS座標を惑星上の緯度・経度・高度に変換する ConvertGPSToPosition メソッドです。この惑星の本初子午線(経度 0°) は、この惑星に最初に着陸した地点 stdP を通るように設定されています (190 行)。また、緯度・経度の計算に必要な惑星の中心座標 planet_centerP および 惑星の中心から北極に向かう単位ベクトル polarV もこのメソッド内で設定されています(190-192 行)。これらのデータから緯度・経度を計算し、result.LNG と result.LAT に代入します。高度 result.ALT は、与えられた GPS 座標と惑星中心までの距離から惑星半径を引くことで計算しています(207 行)。
186| private GeoPosition ConvertGPSToPosition(Vector3D GPS)
187| {
188| GeoPosition result;
189| float planet_radius = 60000;
190| Vector3D stdP = new Vector3D(-65166.17, -123372.01, -96148.62);
191| Vector3D planet_centerP = new Vector3D(-71294.114, -156858.579, -147893.096);
192| Vector3D polarV = new Vector3D(0.339268218472, -0.709210149516, -0.617995177779);
193| Vector3D stdP_UV = stdP - planet_centerP;
194| stdP_UV.Normalize();
195| Vector3D CurP_UV = GPS - planet_centerP;
196| CurP_UV.Normalize();
197| Vector3D std_NormalV = Vector3D.Cross(stdP_UV, polarV);
198| Vector3D cur_NormalV1 = Vector3D.Cross(CurP_UV, polarV);
199| Vector3D cur_NormalV2 = Vector3D.Cross(cur_NormalV1, polarV);
200| std_NormalV.Normalize();
201| cur_NormalV1.Normalize();
202| cur_NormalV2.Normalize();
203| double lng_ang1 = 180 * Math.Acos(Vector3D.Dot(std_NormalV, cur_NormalV1))/Math.PI;
204| double lng_ang2 = 180 * Math.Acos(Vector3D.Dot(std_NormalV, cur_NormalV2))/Math.PI;
205| result.LNG = lng_ang2 < 0 ? (float)(360 - lng_ang1) : (float)lng_ang1;
206| result.LAT = (float)(180 * (Math.PI/2 - Math.Acos(Vector3D.Dot(CurP_UV, polarV)))/Math.PI);
207| result.ALT = (float)(GPS-planet_centerP).Length() - planet_radius;
208| return result;
209| }