PowerAppsで静止画地図を活用する時のお決まり計算をコンポーネントに

※当初のタイトルは「PowerAppsでBingMapsを活用する時のお決まり計算をコンポーネントに」でしたが、本稿の内容は特定の地図サービスに依存しない計算なので「静止画地図を活用する時の」に変更しました。本文はそのままです。
#はじめに
PowerAppsからBingMapsを使う時、単に表示するだけならBingMapsコネクタやStatic MAPを呼び出して使えば良いのですが、地図上に表示したポイントをタッチで選択させたくなったり、地図上のポイントを動かして、移動後の緯度経度を計算で取得したくなったりすると、ピクセルと距離や度数の相互変換が必要になります。
これまではアプリを作る度にひな形からコピペを繰り返していたのですが、コンポーネントの機能が追加されたことから、基本的な計算をコンポーネントにしてしまおう、というのが今回の概要です。
なお、計算式については、以下のページで勉強させていただきました。ありがとうございます。
[link-1]:https://docs.microsoft.com/en-us/bingmaps/articles/understanding-scale-and-resolution
[link-2]:https://easyramble.com/latitude-and-longitude-per-kilometer.html

* [Microsoft Docs: Understanding Scale and Resolution][link-1]
* [EasyRamble: 距離1kmあたりの緯度・経度の度数を計算（日本・北緯35度）][link-2]

##コンポーネント特有の悩み
最初はコンポーネントにコピペするだけで済むかと思ったのですが、すぐに変数がうまく使えないことに気づきました。
まずコンテキスト変数は使えず、グローバル変数は使えますが、OnVisibleなどの属性が無いのでそもそもSet関数を書ける場所が殆どない。では出力項目の所に冗長覚悟で一気に式を書いてしまうのか？と思って試してみたりしたのですが、後から絶対理解できない仕上がりに。
悩んでいた時、計算結果確認のために表示していたラベルを見て思い出したのが、OTAさんのこちらの記事（[idea.toString(); PowerApps の変数の使い方と使いどころ](https://idea.tostring.jp/?p=4154)）でした。
「そうだ、ラベルを変数代わりに使おう。どうせ計算結果しか使わないコンポーネントだから、使うときは非表示になるし」

#まずは出来上がりから
コンポーネントの入力は、計算の基準となる緯度（Latitude）と、BingMapsのZoomLevelです。
対して出力は、１ピクセルあたりの距離（MeterPerPixel）、1000ピクセルあたりの緯度（LatitudePer1000Pixels）と経度（LongitudePer1000Pixels）の３つです。
カスタムプロパティにも、この５つを設定します。
![kansei.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/d03784b4-58ac-24ef-c4f7-194a64d9facf.jpeg)
##新しいコンポーネント
では新しいコンポーネントを作りましょう。
現時点ではコンポーネントを作るには、新規アプリを開いた後で「ファイル」-「アプリの設定」-「詳細設定」-「コンポーネント」をオンにする必要があります。
設定したら、ツリービューに「コンポーネント」というタブが現れるので、そこから「＋新しいコンポーネント」を選びます。
##入力項目の設定
次に右側メニューの「＋新しいカスタムプロパティ」を選び、以下のように入力します。ZoomLevelについても同様に作成してください。
![CustomProperty.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/66da1503-14bd-ec31-c342-496e830d31c8.jpeg)
カスタムプロパティが出来たら、詳細設定タブから初期値を設定しておきましょう。ここではLatitudeを43.65、ZoomLevelを13と設定します。
![CustomDefault.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/d598a6df-49d7-02bf-80cc-5840d76e9609.jpeg)
##入力項目の参照方法
ここから入力項目を使って計算をしていく訳ですが、まずは確認の意味も込めて、入力値をラベルに表示してみましょう。
入力値は「コンポーネント名」．「カスタムプロパティの名前」で引用できます。

| ラベルコントロールの名前 | プロパティ名 | 設定値 |
|:-:|:-:|:---------------------------:|
| Latitude | Text | Component1.Latitude |
| ZoomLevel | Text | Component1.ZoomLevel |
「43.65」「13」と表示されたでしょうか。
##レゾリューションの計算
地図上の１ピクセルが何メートルに相当するかがレゾリューションです。
[Microsoft Docs: Understanding Scale and Resolution][link-1]の「Calculating Resolution」の部分に記載されている式を使います。

| ラベルコントロールの名前 | プロパティ名 | 設定値 |　コメント |
|:-:|:-:|:---------------------------:|:------------------------:|
| MapResolution | Text | 156543.04*Cos(Latitude * Pi() / 180) / (2^ZoomLevel) | Cos(Radians(Latitude))のほうがカッコイイ |
ここで使っているLatitudeやZoomLevelというコントロール名は、本当はValue(Latitude.Text）のように書かないといけないのかも知れませんが、参照できているのでそのまま使ってしまいます。
さて、計算結果は如何でしたか？同章に記載されているトロントの例「about 13.8 meters/pixel」になりましたでしょうか。
確認できましたら、いつまでもトロントにいても仕方がないので、初期値を日本に近い緯度「35」と、スマホでタッチできるレベルの「17」に変更しておきましょう。

##距離１キロメートルあたりの緯度・経度の度数を計算
本当は１ピクセルが何度にあたるかを計算したいのですが、前段処理としてこれを計算しておきます。
[EasyRamble: 距離1kmあたりの緯度・経度の度数を計算（日本・北緯35度）][link-2]のサンプルをPowerAppsの式に置き換えます。

| ラベルコントロールの名前 | プロパティ名 | 設定値 | コメント |
|:-:|:-:|:-----------------:|:-:|
| EQUATOR_RADIUS | Text | 6378137 | 地球の赤道半径 |
| POLE_RADIUS | Text | 6356752.314 | 地球の極半径 |
| LATITUDE_PER_KM | Text | 360*1000/(2*Pi()*POLE_RADIUS) | 1kmあたりの緯度の大きさ |
| LONGITUDE_PER_KM | Text | 360*1000/(2*Pi()*EQUATOR_RADIUS*Cos(Radians(Latitude))) | 1kmあたりの経度の大きさ |
##１０００ピクセルあたりの緯度・経度の度数を計算
最初に計算したレゾリューションの単位が（メートル／ピクセル）、次に計算した度数の単位が（度数／１０００メートル）なので、掛け合わせるとメートルが消えて（度数／１０００ピクセル）になります。

| ラベルコントロールの名前 | プロパティ名 | 設定値 |
|:-:|:-:|:---------------------------:|
| LatitudePer1000Pixels | Text | MapResolution*LATITUDE_PER_KM |
| LongitudePer1000Pixels | Text | MapResolution*LONGITUDE_PER_KM |

さて、これで計算ができました。数字は「出来上がり」の画像と合っていましたか？

##出力項目の設定
最後に計算結果をコンポーネント外に出力するカスタムプロパティを作りましょう。
以下の要領でLatitudePer1000Pixels、LongitudePer1000Pixels、MeterPerPixelの３つを作ります。
![custom-out.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/14d1936b-a0e6-578d-4fb0-74c5b97f29d1.jpeg)
カスタムプロパティと計算結果を紐づけます。
![out_default.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/dc3bd225-354b-364a-191a-8eb942e03f20.jpeg)

お疲れ様でした。これでコンポーネントの設定は終了です。保存が終わったら試してみましょう。

#コンポーネントを確認してみよう
それでは作ったコンポーネントの動作を確認してみましょう。
ツリービューを「画面」タブに切り替えます。
「挿入」メニューの右端に「コンポーネント」が追加されており、そこから先程作った「Component1」を選びます。
挿入されたComponent1_1のプロパティの一覧には、カスタムプロパティとして設定した「Latitude」「ZoomLevel」が表示されています。
![insert_component.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/bdca9810-1836-e1de-de05-5037cf6579b0.jpeg)　![1_1Property.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/46f3b398-4fbb-4bea-0687-e021f31cb880.jpeg)
ここから以下のような確認用のアプリを作ります。
![kakunin.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/9b774efd-2a50-4d17-7af4-5b9ffbf7668c.jpeg)
##コントロールの設定
###1.緯度入力用のスライダーコントロール（Slider1）
南緯をマイナスで表現すると、緯度は-90度から90度の間で動きます。

| プロパティ | 設定値 | コメント |
|:-:|:-:|:-:|
| Default | 0 | 特に意味はないですが、とりあえず赤道で |
| Max | 90 |   |
| Min | -90  |   |

###2.ズームレベル入力用のスライダーコントロール（Slider2）
BingMapsのズームレベルは1から19の間で動くので、以下のように設定します。

| プロパティ | 設定値 | コメント |
|:-:|:-:|:-:|
| Default | 10 | 特に意味はないですが、真ん中あたりで |
| Max | 19 |   |
| Min | 1  |   |
###3.出力確認用のラベルコントロール（Label6,Label7,Label8）
コンポーネントからの出力は「コンポーネントコントロール名」．「カスタムプロパティ名」で引用できます。

| コントロール名 | プロパティ | 設定値 | コメント |
|:-:|:-:|:-:|:-:|
| Label6 | Text | "1ピクセルあたりの距離（ｍ）" & Component1_1.MeterPerPixel |   |
| Label7 | Text | "1000ピクセルあたりの緯度　" & Component1_1.LatitudePer1000Pixels |   |
| Label8 | Text | "1000ピクセルあたりの経度　" & Component1_1.LongitudePer1000Pixels |   |
###4.入力値の紐づけ（Component1_1）
スライダーコントロールの値を、コンポーネントコントロールのカスタムプロパティに設定します。

| プロパティ | 設定値 | コメント |
|:-:|:-:|:-:|
| Latitude | Slider1.Value |   |
| ZoomLevel | Slider2.Value |   |
#動かしてみよう
出来上がったら実行してみてください。スライダーの動きに応じて、計算結果が引き渡されて来ることがわかると思います。
極端な入力では使い物になりそうにないですね。

#エクスポートしよう
うまく動作したら、アプリ作成時に使えるようにコンポーネントをエクスポートしましょう。
「挿入」メニューの「コンポーネント」から「コンポーネントをエクスポート」を選び、名前を付けて保存します。
![export.jpg](https://qiita-image-store.s3.amazonaws.com/0/278581/c00fbf10-c398-f0ef-76bf-061c23cb6954.jpeg)
次回からは、このメニューの「コンポーネントのインポート」から保存したコンポーネントを呼び出して使用できます。

#最後に
今回は地図が一切出てこない地味な記事になってしまいましたが、これを使うことによって地図を単なる画像から、ユーザーインターフェースへと引き上げることが可能になります（と信じています）。

利用例は追々投稿するつもりですが、待ってられないので自分で作るよ、という方に、ひとつだけハマったポイントをお伝えします。
計算で出すのは地図上のピクセル位置ですが、PowerAppsが扱えるのは画像コントロール上のピクセル位置です。したがってこの２つを一致させることが大変重要になります。例えば画像コントロールの「画像の位置（ImagePosition）」プロパティの初期値が「自動調整（Fit）」になっていますが、デバイスによって拡大縮小されて合わないことがありました。「中央（Center）」にすると合うようなので、私はこれで使っています。