ArcGIS Pro のマップフレームをポリゴンにする

**対象読者** ArcGIS Pro で図面を作成する方

**実行環境** Windows10, Python 3.7.9

## はじめに
図枠の異なる図面を複数作成すると、それぞれの図面の位置関係を把握するための索引図が欲しくなります。

**ここでは索引図に使えるような図枠を、ポリゴンタイプのシェープファイルで作成します。**

処理としては、ArcGIS Pro レイアウト内のマップフレームからポリゴンデータへの変換になります。

レイアウトやマップフレームの設定の読み取りは、素直に [ArcPy](https://www.esrij.com/gis-guide/arcgis-basic/arcpy/) を使います。

ポリゴンデータの作成には ArcPy のデータアクセスモジュールを使ってもよいのですが、個人的趣味で [pyshp](https://pypi.org/project/pyshp/) ライブラリを使います。

なお pyshp は、ArcGIS Pro のデフォルト環境にインストールされています。

[スクリプトをツールボックスにツールとして登録](https://pro.arcgis.com/ja/pro-app/latest/help/analysis/geoprocessing/basics/create-a-python-script-tool.htm)し、ArcGIS Pro アプリケーションから実行します。

## マップの角度について

マップの角度設定が0であれば、マップの範囲の南西端・北東端座標が作成したいポリゴンの四隅座標と一致するので、取得した座標値で四角形のポリゴンを作成することができます。

マップに角度をつけている場合、すなわち北がマップの上を向いてない場合は工夫が必要になります。

ここでは、マップの角度をもとに戻してからマップ範囲の南西端・北東端座標を取得した後、マップの中心座標を中心にマップに設定されていた角度だけ回転させてポリゴンの四隅座標を計算することにしています。

つまり、点Aを中心として点Bをθ度回転する処理を四隅座標に対して行います。

numpy で行列計算を使う方法がスマートですが、行列があまり得意ではないという個人的理由により、以下の式[^1]で計算しています。


[^1]: フリー百科事典『ウィキペディア（Wikipedia）』 回転 (数学)


二次元直交座標系の点 (x, y) を原点 (0, 0) を中心として**反時計回り**に角度θ度回転する式です。

*x' = x*cosθ - y*sinθ
y' = x*sinθ + y*cosθ*

マップ中心 (X, Y) は座標系の原点ではないので、式を変形して使います。

*x' = (x-X)*cosθ - (y-Y)*sinθ + X
y' = (x-X)*sinθ + (y-Y)*cosθ + Y*


## コード

```py
import os.path
import math
import arcpy
import shapefile

# 関数:(x0, y0)を中心として(x_in, y_in)を時計回りにrotation[度]回転した座標を返す
def rotate(x0, y0, rotation, x_in, y_in):
	# 式の定義を考慮して角度の正負を逆転し、ラジアンに変換
	rad = math.radians(-1*rotation)
	# 相対座標
	x = x_in - x0
	y = y_in - y0
	# 回転
	x_out = x*math.cos(rad) - y*math.sin(rad) + x0
	y_out = x*math.sin(rad) + y*math.cos(rad) + y0
	return x_out, y_out

# レイアウト名
lyt_name = arcpy.GetParameterAsText(0)
# マップフレーム名
mpf_name = arcpy.GetParameterAsText(1)
# 出力フォルダ
path_fold = arcpy.GetParameterAsText(2)

# シェープファイル名
path_shp = os.path.join(path_fold, mpf_name + '.shp')
# 座標系定義情報ファイル名
path_prj = os.path.join(path_fold, mpf_name + '.prj')

# ArcGIS Pro で現在開いているプロジェクトを取得
aprx = arcpy.mp.ArcGISProject('CURRENT')
# レイアウトを取得
lyt = aprx.listLayouts(lyt_name)[0]
# マップフレームを取得
mpf = lyt.listElements('MAPFRAME_ELEMENT', mpf_name)[0]
# マップの座標系を取得
sr = mpf.map.spatialReference
# マップフレームの縮尺を取得
scale = mpf.camera.scale
# マップフレームの傾き（反時計回りの回転角[度]）を取得
rotation = mpf.camera.heading
# 中心座標を取得
x0 = mpf.camera.X
y0 = mpf.camera.Y

# マップフレームの回転を解除（それぞれの座標軸と辺の方向をそろえるため）
mpf.camera.heading = 0

# マップフレームの範囲を取得
extent = mpf.camera.getExtent()
# 四隅座標を取得
xmin, ymin, xmax, ymax = [float(coord) for coord in str(extent).split(' ')[:4]]

# マップフレームの角度を戻す
mpf.camera.heading = rotation

# ポリゴンの四隅座標を取得
x1, y1 = rotate(x0, y0, rotation, xmin, ymin)
x2, y2 = rotate(x0, y0, rotation, xmin, ymax)
x3, y3 = rotate(x0, y0, rotation, xmax, ymax)
x4, y4 = rotate(x0, y0, rotation, xmax, ymin)

# シェープファイル書き込み
with shapefile.Writer(path_shp) as w:
	w.field('scale', 'N')
	w.field('rotation', 'N')
	w.poly([
		[[x1, y1], [x2, y2], [x3, y3], [x4, y4], [x1, y1]]
	])
	w.record(scale, rotation)

# プロジェクトファイル作成
with open(path_prj, 'w') as f:
	# 座標系を定義するパラメータの文字列を取得
	text = sr.exportToString()
	f.write(text)
```

## ツールボックスへの実装
ツールボックスに上述のスクリプトを追加します。
![パラメータの定義.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/c5a5ea0e-36e5-0def-3f61-c1416ed931c7.png)

第２パラメータにレイアウトとの依存関係をもたせたマップフレームを指定したかったのですが、該当しそうなデータタイプが見当たらなかったため、String としました。


## 実行
マップを30度（反時計回り）回転させたマップフレームをレイアウトに配置し、ツールを実行します。
![レイアウト_30deg.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/e3fd7755-666e-f09b-73e3-d6f6f8429f2f.jpeg)

## 結果
マップフレームで表示している範囲を示すのポリゴンデータが作成されました。
![マップ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/9fc530ec-4c8f-7140-8c86-b1fa4335cb4f.jpeg)
テーブルには縮尺と角度が格納されています。
![テーブル.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1409923/49085de4-673f-0ccc-8fae-8434744f7fe1.png)

## 最後に
マップフレームをポリゴンに変換しました。

アプリケーションに同様のツールが標準で組み込まれていても良さそうなものですが、どうも無いようです。

なお [ArcGIS Pro の Python リファレンス](https://pro.arcgis.com/ja/pro-app/latest/arcpy/main/arcgis-pro-arcpy-reference.htm) はほとんど英語ですが、マップやレイアウトをクラスとして扱えるので、英語が分からなくてもそれらの設定値の取得は結構簡単です。