Z-GISの農地データをQtのウィジェットに表示する

[Z-GIS](https://z-gis.net/99/index.html)という農地管理システムがあります。
これは、自分でプロットした農地の位置情報をEXCELファイルで管理してくれるというものなのですが、今回はこれで記録した自分の農地情報を読み出して、Qtで表示出来るようにするというお話です。

これが出来れば、Pythonで[Z-GIS](https://z-gis.net/99/index.html)のデータを扱う事が出来るので、Z-GISで出来ない事も、自分でなんとかする事が出来る可能性が出てきます。

本稿では、GISの知識が必要になります。
かといって、僕も専門家ではなくて、わかんない事をググって調べてるレベルなので、特に注釈やら解説はしません。もしかしたら用語の使い間違いもあるかもしれないですが、わからないので、とりあえずごめんなさい。
これ以上専門家になるつもりもないので、気づいたら笑ってください。

# 環境について
今回の開発にあたって使用していた環境は以下の通りです。
OS:Windows10
IDE:Pycharm 2021.2.3 (Community Edition)
Python:3.10.0
QGIS:3.16.15
Qt Designer：6.2.1
(Qtは、Pysideと一緒にインストールしたものを使います）

これらのインストール手順や使い方については、説明しません。
それぞれのドキュメントを参照してください

# Python環境の準備
PyCharmを起動してプロジェクトを作成します。
**PyCharmでプロジェクトを作る時に、仮想環境にしています**

プロジェクトを作ったら、ライブラリを入れる準備をします。
PyCharmの「ファイル」「設定」メニューから「プロジェクト」「Pythonインタプリタ」を選びます

**画面左上のPythonインタプリタのリストコントロールの、歯車ボタンから「追加」を押して、「既存の環境」でプロジェクトの仮想環境を選びます。**
最初にインストールされているpip、setuptool、wheelのバージョンが最新でなかったら、**三角アイコンを押して、アップデートしておきます。**
（一部、最新のpipでないとインストールできないものがあります）

![SetupInstaller.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/18709cba-e197-e047-ad37-5487f722cc00.jpeg)


### 使用するライブラリのインストール
使用するライブラリのうち、一部はPyCharmのインストーラーからは入れられないものがありましたので、バイナリファイルをダウンロードしてインストールします。
本来は、それぞれのライブラリの元サイトに行って、自分でコンパイルなどをするものですが、[Unofficial Windows Binaries for Python Extension Packages](https://www.lfd.uci.edu/~gohlke/pythonlibs/)に、Windows用にコンパイルしたものがあるので、それを使います

仮想環境にインストールするので、プロジェクト固有のものになりますから、プロジェクトのフォルダの配下にダウンロード先を用意しました。

ここから、ダウンロードしたファイルは以下の通りです。
GDAL-3.3.3-cp310-cp310-win_amd64.whl
Fiona-1.8.20-cp310-cp310-win_amd64.whl
Shapely-1.8.0-cp310-cp310-win_amd64.whl
Rtree-0.9.7-cp310-cp310-win_amd64.whl
うちでは、プロジェクトの下に作った**LibSrcフォルダ**に入れました

それ以外にインストールするライブラリは次の通りです
geopandas
openpyxl
matplotlib
PySide6
これらを一括でインストールするため、通常の「Pythonインタプリタ」画面からはインストールせずに、インストール用のバッチファイルを作り、プロジェクトの配下に置きました。

```install.bat
venv\Scripts\pip.exe install LibSrc\GDAL-3.3.3-cp310-cp310-win_amd64.whl
venv\Scripts\pip.exe install LibSrc\Fiona-1.8.20-cp310-cp310-win_amd64.whl
venv\Scripts\pip.exe install LibSrc\Shapely-1.8.0-cp310-cp310-win_amd64.whl
venv\Scripts\pip.exe install LibSrc\Rtree-0.9.7-cp310-cp310-win_amd64.whl
venv\Scripts\pip.exe install geopandas==0.10.2
venv\Scripts\pip.exe install openpyxl==3.0.9
venv\Scripts\pip.exe install matplotlib==3.5.1
venv\Scripts\pip.exe install PySide6==6.2.2.1
```
このバッチファイルは、PyCharmのターミナル(ALT+F12)を起動して実行します。

# 地図データの準備
開発環境が出来たので、地図データを作成します。

[Z-GIS](https://z-gis.net/99/index.html)で記録されるデータは、自分が登録した農地のポリゴンだけなので、背景として表示するデータを用意します

用意するデータは、
1)国土地理院の「基盤地図情報ダウンロードサービス」の基盤地図情報基本項目
2)農水省の筆ポリゴン
3)Z-GISで作った自分の圃場データ
になります。
3)は、自分で作るので説明は割愛します。

また、ここで作成したファイルは、プロジェクトの下の**Dataフォルダ**に保存しました

### 基盤地図情報ダウンロードサービス」の基盤地図情報基本項目
[基盤地図情報ダウンロードサービス」](https://fgd.gsi.go.jp/download/menu.php)のページの「の基盤地図情報基本項目」から「ファイル選択へ」のボタンを押します
![基盤地図情報ダウンロードサービス.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/32654b41-6383-ea6b-ffa7-0d4008165042.jpeg)

表示された日本地図を拡大して、自分の圃場がある地域のデータをクリックすると、左下の選択リストに追加されるので、「ダウンロードファイル確認へ」のボタンを押します

ログイン画面になって（予め、利用者登録（無料）が必要です）、ダウンロードファイルの確認画面が出るので、「基盤地図情報　最新データ」のダウンロードボタンを押します
ダウンロードしたら、適当なフォルダに解凍します。

### 農水省の筆ポリゴン
[筆ポリゴンダウンロードページ](https://www.maff.go.jp/j/tokei/porigon/hudeporidl.html)から、**スクロールして下の方にある**「2．市町村単位（市町村コード順）［外部リンク］」の欄にある、目的の市町村のデータをダウンロードします。ダウンロードしたら、適当なフォルダに解凍します。

![hudeporidl.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/ec019ba6-02ec-972c-53b4-3c7fd14c2777.jpeg)

# 背景データを作る
基盤地図情報と筆ポリゴンの背景地図を作ります
QGISを起動します

![QGIS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/68f44c6b-e283-0204-f163-64a91ec2e258.jpeg)

### 基盤地図情報を読み込む
まずは、基盤地図情報を読み込みます。
基盤地図情報はベクタデータですが、ライン形式と、ポリゴン形式があります。
また、全部のデータを描画するとゴチャゴチャするので、

ポリゴンデータ
・水域(WA)
・建築物(BldA)
ラインデータ
・水涯線(WL)
・道路縁(RdEdg)
のみを採用しました。この辺は地域の事情とお好みで決めたら良いと思います。

QGISを開いて、左上のブラウザペインで、解凍したファイルのフォルダを選択します。
ファイルの一覧が出てきます。
２つ同じファイル名が並んでいますが、市松模様のアイコンの方**ではない**アイマスクみたいなアイコンの方のファイルを選んでください。右クリックで「レイヤをプロジェクトに追加する」を選んで追加します。

どのファイルに何が入っているのかは、とりあえず表示してみたらわかりますが、上記のWA、BldA、WL、RdEdgの各文字列を含むファイルを選択すると、上記のそれぞれデータが読み込まれ表示されます。

座標変換をするかどうかの、でかいダイアログが表示されますが、今回はEPSG6668で統一しようと思っているので、ここでは必要ありません。

### 筆ポリゴンを読み込む
同じようにダウンロードした筆ポリゴンを読み込みます。
こちらはファイルはひとつしかありません（実際は複数のファイルで１セットになっています）

こっちはレイヤへ追加する時に、座標変換のでっかいダイアログは出ないのですが、こちらの方はEPSG6672になっているので変換が必要になります。

レイヤペインの該当ファイルを右クリックすると「レイヤのCRS」という項目があるのですが、それでは出来ません。
その下の「エクスポート」「地物の保存」を選択します。
出てきたダイアログの「ファイル名」にファイル名を入れ、「座標参照系(CRS)」を「EPSG:6668 - JGD2011」にしてOKボタンを押すと変換されたファイルが出力され、レイヤに登録されます

先に登録した筆ポリゴンの方は、もう必要ないので、筆ポリゴンのファイルを選んで、右クリックしてコンテキストメニューから「レイヤの削除」を選びます

### ベクターデータを合成する
このままでも扱えない事はないのですがxmlをshpファイルにしたいのと、数が多いと面倒なので、ファイルをまとめます

画面上のベクターメニューから　[データ管理ツール][ベクターレイヤのマージ]と選択します
ダイアログが表示されたら「入力レイヤ」でマージするファイルを選択します。
ラインとポリゴンを混ぜてマージ出来なかったので、ラインであるRdEdgとWL、ポリゴンであるBldAとWAを分けてマージします。
座標変換した筆ポリゴンは、後々分けて表示したいので、ここでは合成しません

変換先の座標参照系はEPSG:6668を選びます。

出力レイヤは、今はこのままで構いません。

「実行」ボタンを押して実行します。

うまくいったら、ポリゴンのBldAとWAもマージします。

### 切り取り範囲を作る
このままだと、自分の農地を管理するには、あまりにも広すぎるので、データ量削減の為にも、データの必要な部分だけを切り取ります。

切り取るためには、まず、切り取る範囲を示すポリゴンを作成する必要があります。

「レイヤ」メニューから「レイヤを作成」「新規シャープファイルレイヤ」を選びます。
「ファイル名」には自分で決めたファイル名を入力します。
座標系が「EPSG:6668」になってなかったら、そうなるように選択します
「ジオメトリタイプ」を「ポリゴン(Polygon)」にします
フィールドは、とりあえず気にしなくても構いません。
OKで作成します

レイヤペインに、指定したファイル名のレイヤが出来ているのでそれを選択します。
右クリックで出てくるコンテキストメニューから「編集モード切り替え」を選択します。
上に表示されている、一本鉛筆のアイコンが引っ込むはずです。
その２つ右の、アイマスクみたいな形のアイコンを押します。

![EditLayer.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/23a01af3-c8c5-d657-8b44-7b4ff0e73739.jpeg)

アイコンの形が変わるので、切り取る点を指定していきます。
シフトキーで直角になるとかいう機能は無いみたいなので、座標もしくは目視で四角く切り取る場所を指定します。
４点指定したら右クリックします。
そうすると、さっきレイヤの作成時に無視した、フィールドに入力しろと言ってくるので、適当に入力してOKを押すと、ポリゴンが作成されます

### 指定した領域で切り取る
次に「ベクタ」メニューの「空間演算ツール」から「切り抜く(clip)」を選びます。

出てきたダイアログの「入力レイヤ」は、先程マージしたレイヤになります。
「オーバーレイレイア」で、今作ったポリゴンのレイヤをしていします。
クリップ済グリッドには、今度は一時レイヤではなく、ファイルにしたいので、右側のボタンを押して「ファイルに保存」でファイルを指定します

この作業は、合成した２つのレイヤと、もうひとつ、座標変換した筆ポリゴンのレイヤに対しても行います。

完成したら、レイヤペインのうち、今作成した３つのレイヤ以外のチェックを外すと、クリップされた事が確認できます。

### データ準備のおわり
以上で、QGIS上の作業は終わります。
プロジェクトに対し「名前をつけて保存」してから終了します。

**Dataフォルダ**の中には、３つのレイヤのファイルが入っています。
それぞれのレイヤのデータは、.shp、.shx、.dbf、.prjの拡張子を持つ４つのファイルで構成されています。
作成したファイル名はそれぞれ

**ClippedLineVector.**       切り取った基盤地図情報のうちライン形式
**ClippedPolygonVector.**    切り取った基盤地図情報のうちポリゴン形式
**ClippedFieldsPolygon.**    切り取った筆ポリゴン
にしました。

#　GUIの準備
Qt Designerを使って、GUIの部分を作ります。
大したものじゃないので、 Qt Designerを使わなくても出来るのですが、あとでちゃんとしたものを作る時には、Qt Designerが必要なので、今回もそれでやります

Main Windowの名前はTestWindowに変更しました。
![Qt1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/4928d2e0-152f-d178-53b9-5339d43d73a9.jpeg)
QtDesignerを開いたら、[ファイル]から[新規]を選んでMain Windowを選択します。


配置するものは、WidgetとSpinBoxです。
画面左のウィジェットボックスからWidgetを選んで、TestWindowの上に置きます。
置いたらTestWindowの上で右クリックし、コンテキストメニューから[レイアウト][垂直に並べる]を選びます
次にSpinBoxを選んでWidgetの下に配置します

部品の配置が終わったら、Widgetとspin boxの名前を変更します。
今回は
Widget：field_map
spin box：field_no
にしました。

次に、Widgetを格上げします。
field_mapを選んで、コンテキストメニューから「格上げ先を指定」を選択します
「格上げされたクラス名」に、これから作成する「地図を描画するウィジェットの名前」を入れます。
今回はPlotWidgetにしました
ヘッダファイルも同じ名前にします。

次に、画面右下の「シグナル/スロット　エディタ」にシグナルとスロットを入力します
「＋」アイコンを押して
発信者：field_no
シグナル：valueChanged(int)
受信者：TestWindow
スロット：update()
にします。
この辺は、あえてここでは指定しないで、コード中で独自の場所にconnectする事も出来ますが、今回はこうしました。

ここまで用意したら、保存して終了します。
今回はTestWindow.uiというファイルで保存しました

# コードの作成
地図データと画面の準備が出来ましたので、コードを作成します


### Qt designerのデータをpythonのコードに変換する
まず、Qt designerのデータをpythonのコードに変換します。

毎回、コマンドを打ち込むのが面倒なので、バッチファイルを作ります

```convert.bat
pyside6-uic TestWindow.ui -o TestWindow.py
```
pyside6-uicが変換コマンドで、pyside6をインストールした時にくっついてきた、、、はずです。
PyChaemのターミナルから実行するとTestWindow.pyというファイルが出来ます

### PlotWidget.py
Qt　Designerでウィジェットを格上げした先のクラスです。
ウィジェットにmatplotlibで描画した地図を貼り付けています。

```PlotWidget.py
from matplotlib import pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from PySide6.QtWidgets import QWidget, QVBoxLayout


class PlotWidget(QWidget):
    def __init__(self, parent=None):
        """
        地図と圃場を描画するウィジェット
        :param parent: 親ウィジェット
        """
        super().__init__(parent)

        #  create widgets
        self.fig, self.axes = plt.subplots()
        self.canvas = FigureCanvas(self.fig)
        self.fig.subplots_adjust(left=0.001, right=0.999, bottom=0.001, top=0.999)  # FigいっぱいにAxesを広げる
        self.axes.axis("off")       # 地図以外の座標軸などはすべて表示しない

        #  Create layout
        layout = QVBoxLayout()          # レイアウトを作って
        layout.addWidget(self.canvas)   # figを載せて
        self.setLayout(layout)          # このウィジェットに登録する

    def get_ax(self):
        """
        Axesを得る
        :return: 描画させる為のAxes
        """
        print("type(self.axes)={0}".format(type(self.axes)))
        return self.axes

    def clear(self):
        """
        描画領域クリア
        :return:
        """
        self.axes.cla()
        self.axes.axis("off")

```

__init__()では、matplotlibのFigureCanvasオブジェクトを作成して、レイアウトに載せ、さらにこのウィジェットにそのレイアウトを載せています。
実際に地図を表示するエリアはaxesなのですが、その外側にfigureのレイヤがあるので、figureいっぱいまで、axesを広げています。
初期値ではaxesには、座標軸などが表示されて煩わしいので
self.axes.axis("off")でそれらを消しています。

get_ax()は、axesを提供するメソッドで、このaxesにプロットする事で、表示が更新されます。

clear()は、前回まで表示していたものを一度クリアする為のもので、これをしないと、どんどん地図が追加されていきます。

### main.py

プログラムの本体の部分です。
大きく５つに別れています

1) qt_setup
2) FarmData
3) BackGroundVector
4) TestForm
5) if __name__ == "__main__":
説明はコードの後ろでします

```python:main.py
import geopandas as gpd
import openpyxl
import pandas as pd
from shapely.geometry import Polygon
from fiona.crs import from_epsg
from pyproj import CRS
import sys
from TestWindow import Ui_TestWindow
from PySide6.QtWidgets import QMainWindow


def qt_setup():
    """
    仮想環境内のQtを参照出来るようにする為
    :return:
    """
    import os
    import PySide6

    # PySide6がインストールされているフォルダ名を取得
    dir_name = os.path.dirname(PySide6.__file__)
    # PySide6のライブラリ下にある「plugins」フォルダと「platforms」フォルダの絶対パスを取得
    plugin_path = os.path.join(dir_name, 'plugins', 'platforms')
    # プログラムの実行中だけ変数「plugin_path」のパスを通す
    os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path


class FarmData:
    """
    圃場全体
    """

    class FieldData:
        def __init__(self, field_series, margin_x, margin_y):
            """
            ひとつ分圃場
            :param field_series: 圃場一つ分のデータが格納されている
            :param margin_x: 圃場の外側に表示する地図のサイズ
            :param margin_y: 圃場の外側に表示する地図のサイズ
            """
            print("type(FieldData::field_series)={0}".format(field_series))
            self.data = field_series
            self.margin_x = margin_x
            self.margin_y = margin_y

        def draw_coordinates(self):
            """
            指定した圃場の表示範囲を得る
            圃場の範囲よりmargin_xとmargin_yの分だけ広い矩形のポリゴンを作成する
            :return: 圃場の表示範囲を示す矩形
            """
            left_upper = (self.data.total_bounds[0] + self.margin_x, self.data.total_bounds[3] - self.margin_y)
            right_upper = (self.data.total_bounds[2] - self.margin_x, self.data.total_bounds[3] - self.margin_y)
            left_lower = (self.data.total_bounds[0] + self.margin_x, self.data.total_bounds[1] + self.margin_y)
            right_lower = (self.data.total_bounds[2] - self.margin_x, self.data.total_bounds[1] + self.margin_y)
            coordinates = [left_upper, right_upper, right_lower, left_lower]
            print("type(draw_coordinates)={0}".format(type(coordinates)))
            return coordinates

        def draw_area_polygon(self):
            """
            指定した圃場の切り出し用のポリゴンを得る
            :return: 切り出し範囲を示すポリゴン
            """
            clipping_data = gpd.GeoDataFrame()
            clipping_data['geometry'] = None

            coordinates = self.draw_coordinates()
            print("coordinates={0}".format(coordinates))
            poly = Polygon(coordinates)
            clipping_data.loc[0, 'geometry'] = poly
            clipping_data.crs = from_epsg(6668)
            clipping_data = clipping_data.to_crs(epsg=6668)  # これやらんと、ワーニングが出る
            print("type(clipping_data.crs)={0}".format(type(clipping_data.crs)))
            print("clipping_data.total_bounds={0}".format(clipping_data.total_bounds))
            print("type(draw_area_polygon)={0}".format(type(clipping_data)))
            return clipping_data

        def plot(self, ax):
            """
            この圃場をAxesにプロットする
            :param ax:プロット先のAxes
            :return:プロットしたAxes
            """
            ax = self.data.plot(ax=ax, color='black', alpha=0.9)
            # print("type(plot)={0}".format(ax))
            return ax

    def __init__(self):
        """
        全圃場のデータ
        """
        # z-gisのデータを読み込む
        work_book = openpyxl.load_workbook("Data/Z-GIS2021_1.xlsx")
        work_sheet = work_book["圃場情報"]
        print("work_sheet={0}".format(work_sheet.title))
        data = work_sheet.values
        attribute = next(data)[0:]  # 1行目　読み込み
        columns = next(data)[0:]  # DaraFrameのカラム名にする
        data_frame_gpd = pd.DataFrame(data, columns=columns)
        print("data_frame_gpd={0}".format(data_frame_gpd.at[1, '.']))
        data_frame_gpd['geometry'] = gpd.GeoSeries.from_wkt(data_frame_gpd['.'])

        self.geo_df = gpd.GeoDataFrame(data_frame_gpd, geometry='geometry', crs="EPSG:6668")

        self.firm_list = []
        for i, row in self.geo_df.iterrows():
            print(type(row))
            sim_geo = gpd.GeoSeries(row['geometry'])
            print(type(sim_geo))
            sim_geo.crs = CRS(6668)
            sim_geo.to_crs(epsg=6668)
            print("sim_geo.crs={0}".format(sim_geo.crs))
            self.firm_list.append(sim_geo)

        self.margin_x = 0.0015
        self.margin_y = 0.0015

    def get_field(self, n):
        """
        指定した圃場のデータフレームを得る
        :param n:圃場番号
        :return: 圃場データ
        """
        field = self.FieldData(self.firm_list[n], self.margin_x, self.margin_y)
        print("type(get_field)={0}".format(field))
        return field


class BackGroundVector:
    def __init__(self, clipping_data):
        """
        背景として表示する地図のデータ
        :param clipping_data:切り取りエリアを指定するポリゴン
        """
        self.clipping_data = clipping_data

        # 基盤地図情報のポリゴンデータを用意する
        file_name = 'Data/ClippedLineVector.shp'
        data_line = gpd.read_file(file_name)  # geopandasのデータフレームに読み込む
        # ax = data_line.plot(ax = ax, color = 'lightgrey', alpha=0.9 )
        # 指定された範囲で切り取る
        clipped_line_data = gpd.clip(data_line, self.clipping_data)
        # 切り取ったデータに座標系の情報をセットする
        clipped_line_data.crs = from_epsg(6668)
        self.clipped_line_data = clipped_line_data.to_crs(epsg=6668)  # これやらんと、ワーニングが出る
        print("clipped_data.total_bounds={0}".format(clipped_line_data.total_bounds))

        # 基盤地図情報のラインデータを用意する
        file_name = 'Data/ClippedPolygonVector.shp'
        data_polygon = gpd.read_file(file_name)  # geopandasのデータフレームに読み込む
        # 指定された範囲で切り取る
        clipped_polygon_data = gpd.clip(data_polygon, self.clipping_data)
        # 切り取ったデータに座標系の情報をセットする
        clipped_polygon_data.crs = from_epsg(6668)
        self.clipped_polygon_data = clipped_polygon_data.to_crs(epsg=6668)  # これやらんと、ワーニングが出る

        # 筆ポリゴンを用意する
        file_name = 'Data/ClippedFieldsPolygon.shp'
        field_polygon = gpd.read_file(file_name)  # geopandasのデータフレームに読み込む
        # 指定された範囲で切り取る
        clipped_farms_polygon_data = gpd.clip(field_polygon, self.clipping_data)
        # 切り取ったデータに座標系の情報をセットする
        clipped_farms_polygon_data.crs = from_epsg(6668)
        self.clipped_farms_polygon_data = clipped_farms_polygon_data.to_crs(epsg=6668)  # これやらんと、ワーニングが出る

    def plot(self, ax):
        """
        背景の地形を描画する
        :param ax:プロット先のAxes
        :return:プロットしたAxes
        """
        ax = self.clipped_line_data.plot(ax=ax, color='lightgray', alpha=0.5)
        ax = self.clipped_polygon_data.plot(ax=ax, color='grey', alpha=0.5)
        ax = self.clipped_farms_polygon_data.plot(ax=ax, color='pink', alpha=0.3)
        return ax


class TestForm(QMainWindow, Ui_TestWindow):
    def __init__(self, parent=None):
        """
        今回のテスト用GUI画面
        :param parent:
        """
        super().__init__(parent)
        self.setupUi(self)  # Ui_TestWindowのセットアップ
        self.ax = None
        self.farm_data = FarmData()
        self.update()  # 初期表示

    def update(self):
        """
        スピンボックスで数値が変更された時に呼び出されるスロット
        :return:
        """
        # スピンボックスから入力されている数値を得る
        no = self.field_no.value()
        print("update={0}".format(no))

        # 指定された圃場番号のデータと、背景地図を得る
        self.target_field = self.farm_data.get_field(no)
        self.back_ground = BackGroundVector(self.target_field.draw_area_polygon())

        # 前回の表示をクリアして新たに圃場を描画する
        self.field_map.clear()
        ax = self.field_map.get_ax()  # 描画先のAxesを得る
        ax = self.back_ground.plot(ax)  # 地図を描画
        self.target_field.plot(ax)  # 圃場を描画


if __name__ == "__main__":
    from PySide6.QtWidgets import QApplication

    qt_setup()
    app = QApplication(sys.argv)

    main_window = TestForm()

    main_window.show()
    sys.exit(app.exec())
```


##### 1) qt_setup
これは、仮想環境にあるQt本体へのパスを通す為のものです。
パソコンにQtがインストールされていれば必要ないものですが、バージョンの変更などで影響を受けないよう、PySide6をインストールした時に入った仮想環境のQt本体を参照するようにしています。
起動時に一度呼び出します

##### 2) FarmData
圃場のデータを管理します。
サブクラスFieldDataは、一枚の圃場のデータを管理するものです。

FieldDataは、背景地図を表示する余白分のサイズを受け取り、FieldData::draw_area_polygon()が呼ばれたら、自分の圃場のサイズに余白分を加えた矩形のポリゴンを返します。
FieldData::draw_coordinates()は、そのポリゴンの座標を求めるためのものです。

FieldData::plot()は指定されたaxesに圃場を描画します。

FarmData::__init___では、Z-GISのファイルを取り込んで、geopandasのDataFrameを作成しています。
Excelファイルの読み込みにはopenpyxlが使用されています。
今回のデータでは、"圃場情報"という名前のシートに圃場のデータが格納されています。

Z-GISは、ExcelのA1セルには"__xl$gis__"の文字列が入り、2行目にカラム名が入る仕様になっているので、１行目を読み飛ばし、２行目のデータをとってきてDataFrameのカラム名にするようにしています。
Excelの次の行からは、実際の圃場データが格納されているので、残りのデータを使ってDataFrameを作成します。

Z-GISは、圃場のポリゴンが格納されているカラム名を"."に決めているので、そのカラムのデータを描画する時のカラム名である'geometry'にしています。

出来上がったDataFrameから、そのまま行を取り出すと、pandasのSeries型のオブジェクトになり、これはそのままプロット出来ないので、この段階で、geopandasのSeries型に変換してself.farm_listに格納しています。

圃場ポリゴンの周りの背景地図の大きさは、0.0015だけ大きくなるようにしています。
この単位はEPSG:6668の単位で、緯度経度の角度になります。
（実際には、そんな細かい事考えないで、表示してみて調整しました）

FarmData::get_fieldでは指定された圃場番号の圃場データを返します

##### 3) BackGroundVector
背景地図を作ります。

__init__では、QGISで作った基盤地図情報のポリゴンデータ、基盤地図情報のラインデータ、筆ポリゴンをファイルを読みこんで、指定された領域のポリゴンで切り取っています。
切り取ったポリゴンには座標情報がないので、改てセットしています。
これを書いていて気がついたのですが、いちいちファイルを読むと、時間がかかるので、本当なら最初に読み込んで、呼ばれた時に切り取った方がいいですね。

plotでは、指定されたaxesに切り取った地図を描画しています。

##### 4) TestForm
MainWindowの処理になります。
Qt Designerで作った画面に、圃場のデータを描画して表示しています。

updateは、Spix Boxから送られてきたシグナルを受け取って、spix box内の数字に従って圃場を描画しています。

##### 5) if __name__ == "__main__":
試験コードです。
Qtを立ち上げて、ウィンドウを表示しています

### TestWindow.py
一応。Qt Designerから自動生成されたコードも載せておきます。

```python:TestWindow.py
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'TestWindow.ui'
##
## Created by: Qt User Interface Compiler version 6.2.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QMainWindow, QMenuBar, QSizePolicy,
    QSpinBox, QStatusBar, QVBoxLayout, QWidget)

from PlotWidget import PlotWidget

class Ui_TestWindow(object):
    def setupUi(self, TestWindow):
        if not TestWindow.objectName():
            TestWindow.setObjectName(u"TestWindow")
        TestWindow.resize(538, 550)
        self.centralwidget = QWidget(TestWindow)
        self.centralwidget.setObjectName(u"centralwidget")
        self.verticalLayout = QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.field_map = PlotWidget(self.centralwidget)
        self.field_map.setObjectName(u"field_map")

        self.verticalLayout.addWidget(self.field_map)

        self.field_no = QSpinBox(self.centralwidget)
        self.field_no.setObjectName(u"field_no")

        self.verticalLayout.addWidget(self.field_no)

        TestWindow.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(TestWindow)
        self.menubar.setObjectName(u"menubar")
        self.menubar.setGeometry(QRect(0, 0, 538, 22))
        TestWindow.setMenuBar(self.menubar)
        self.statusbar = QStatusBar(TestWindow)
        self.statusbar.setObjectName(u"statusbar")
        TestWindow.setStatusBar(self.statusbar)

        self.retranslateUi(TestWindow)
        self.field_no.valueChanged.connect(TestWindow.update)

        QMetaObject.connectSlotsByName(TestWindow)
    # setupUi

    def retranslateUi(self, TestWindow):
        TestWindow.setWindowTitle(QCoreApplication.translate("TestWindow", u"MainWindow", None))
    # retranslateUi
```

# おわりに

[complete.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99652/ace21015-1c4e-e396-58a4-f81945c77776.jpeg)

本当は、ベクタ地図ではなくて、ラスタの航空写真の上に表示しようと思っていました。
google mapを使う方法では、当然GoogleのAPIを叩く事になるのですが有料だし、いつ仕様が変わるかわ!からないので、やめました。

航空写真は国土地理院にもあって、無料の普通の画像と、有料のgetTiff画像があります。
GISでのラスタの扱いはrastarioというライブラリで行えるのですが、うまく動的な切り取りが出来なかったので、途中で方針を変更しました。

今の自分の目的の為には、ベクタで充分なので、これはそれ以上掘り下げていません。

# 謝辞
GISがなんなのかも知らない段階から作り始めたので、本当に多くの記事を参照しました。
あまりにも多すぎて、リストアップしませんが、貴重な情報を無料で公開している方々には、感謝しております。ありがとうございました。

この記事は、とてもニッチな用途ですが、どこかでどなたかのお役に立てれば幸いです