Python で GIS データハンドリング

## はじめに

![AutoGIS2018のバナー画像](https://automating-gis-processes.github.io/2018/_images/AutoGIS_banner.jpg)

本記事は、地理情報を扱うために学習した内容をまとめた記事になります。
学習教材として、ヘルシンキ大学の [Automating GIS-processes 2018](https://automating-gis-processes.github.io/2018/index.html) という講座を利用しました。
Google 翻訳を駆使すれば、問題なく演習が可能です。

この講座を受講すれば、『Python』 を使って『無料』で以下のようなことができるようになります。

- 人口増減率が大きいエリアの導出
  <img src="https://qiita-image-store.s3.amazonaws.com/0/184077/ff48b3dc-2c71-18e6-c4ed-a9205df5e419.png" width=50%>

- 営業マンが効率よく回れるルートの導出
  <img src="https://qiita-image-store.s3.amazonaws.com/0/184077/1832d5af-393c-56c8-63af-9dcd216ad3f4.png" width=50%>

- 新規出店に適したエリアの導出
  <img src="https://qiita-image-store.s3.amazonaws.com/0/184077/1a398756-c5dd-de36-fc55-132a32b3e9ae.png" width=50%>

※上記画像は [esri ジャパン様](https://www.esrij.com/getting-started/what-is-gis/) の画像を引用しています。
※参考のためイメージを載せています。
※本記事に誤りがあれば、ご指摘を頂けると幸いです。

## 本記事の対象者

以下の条件を満たしていること

- Dockerfile を作成してイメージ作成が可能なこと
- Jupyter Notebook の利用経験があること
- Python での GISデータハンドリングに興味があること

## 環境

- OS: Windows10 Pro(64ビット)
- CPU: Intel(R) Core(TM) i7-2600@3.40GHz 3.40GHz
- メモリ: 16.0GB
- ツール
  - Docker for Windows: 2.0.0.3

## GIS とは

GIS（ジー アイ エス）とは、Geographic Information System の略称です。
日本語では地理情報システムと訳されます。
地球上に存在する地物や事象はすべて地理情報と言えますが、これらを
コンピューターの地図上に可視化して、情報の関係性、パターン、傾向を
わかりやすいかたちで導き出すのが GIS の大きな役割です。

GIS の詳細は [ESRI ジャパン GIS 基礎解説](https://www.esrij.com/getting-started/what-is-gis/) を参照してください。
「GIS(地理情報システム)とは」「GIS 基礎解説」を読めば、GIS の概要を把握できます。

## GIS データフォーマット

### 1.シェープファイル

シェープファイルは GIS データ フォーマットの 1 つです。
病院などの目標物や道路や建物などの位置や形状、属性情報を持つ
ベクターデータ（ポイント、ライン、ポリゴン）を格納することができます。
ベクターデータについては後述します。

<img src="https://www.esrij.com/cgi-bin/wp/wp-content/uploads/2015/12/0c245edc37eb51544b0bbd28f82cb8a91.png" width="600" height="300">

画像はESRI ジャパンより引用

シェープファイルは複数のファイルから構成されています。
必須のファイルは ３ つあり、このうち 1 つでも欠けると
GIS アプリケーション上でシェープファイルと認識することができません。
また、シェープファイルを構成するファイルのサイズは
それぞれ 2GB のサイズ制限があります。

| 拡張子 | 概要                                                     |
| ------ | -------------------------------------------------------- |
| .shp   | エリアや地点を表す図形の情報を格納するファイル（必須）   |
| .dbf   | 図形の属性情報(国名・県名など)を格納するファイル（必須） |
| .shx   | .shp と .dbf の対応関係を示すファイル（必須）            |
| .prj   | 図形の持つ座標系の定義情報を格納するファイル             |

データ処理をする上で、シェープファイルを「GeoJSON」「TopoJSON」という
データ形式に変換することが多々あります。
GIS データを Web アプリで活用する場合は、特にこれらの形式で扱われることが多いです。

本記事で使用するfoliumライブラリ(詳細は後述)では「GeoJSON」に変換することで、
容易にインタラクティブマップを作成可能となります。
TopoJSON は利用したことがないのですが、以下に説明を記載しておきます。

### 2.GeoJSON

JavaScript Object Notation (JSON) を用いて空間データをエンコードし
非空間属性を関連付けるファイルフォーマットです。
属性にはポイント（住所や座標）、ライン（各種道路や境界線）、 ポリゴン（国や地域）などが含まれます。

ファイルフォーマット

```json
{ "type": "FeatureCollection",
    "features": [
      { "type": "Feature",
        "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
        "properties": {"prop0": "value0"}
        },
      { "type": "Feature",
        "geometry": {
          "type": "LineString",
          "coordinates": [
            [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
            ]
          },
        "properties": {
          "prop0": "value0",
          "prop1": 0.0
          }
        },
       ]
     }
```

### 3.TopoJSON

地理空間情報をエンコードするためのフォーマットである GeoJSON の拡張形式です。
詳細は[こちら](https://github.com/topojson/topojson/wiki/Home.ja)を参照してください。

> TopoJSON ファイルは GeoJSON よりずっと軽量になります。
> 上のアメリカ合衆国のシェイプファイルは GeoJSON の場合 2.2M ですが、TopoJSON では、
> 単純化していない境界線メッシュ形式でも 436K、すなわち 80.4% サイズが削減されます。
> TopoJSON では描写効率も改善されます。共有点を一度しか描かなくて済むからです。
> さらなるファイル容量削減のために、TopoJSON は整数座標のデルタエンコーディングのために、
> 浮動小数点ではなく固定小数点を用います。これにより、正確性を犠牲にすることなく
> 座標値の精度の丸め処理を省略できます（例：LilJSON )。また、GeoJSON と同様、
> ファイルは簡単にテキストエディタで編集したり、gzip で圧縮することができます。

## GISデータモデル
空間的な情報についてはさまざまな表現形態があり、それらに応じた GIS データモデルがあります。
GISデータモデルには、[ベクターデータ](https://www.esrij.com/gis-guide/gis-datamodel/vector-data/)、[ラスターデータ](https://www.esrij.com/gis-guide/gis-datamodel/raster-data/)、[3Dデータ](https://www.esrij.com/gis-guide/gis-datamodel/3d-gis-data/)、といった種類があります。
ここでは、本記事で使用するベクターデータについて説明します。

### ベクターデータとは
現実世界に存在する地物（目に見えないものも含む）をの3つの要素で表現したものです。
格納される座標値は、緯度・経度、あるいは地図投影法により定義された値です。
詳細は [esri ジャパンのページ](https://www.esrij.com/gis-guide/gis-datamodel/vector-data/) を参照して下さい。

| 種類           | 説明                                         | 代表             |
| -------------- | -------------------------------------------- | ---------------- |
| ポイント（点） | 図形情報を（x, y）の座標値で格納             | 電柱、信号、ATM  |
| ライン（線）   | （x, y）の座標値を結んだ線分として格納       | 鉄道路線、排水管 |
| ポリゴン（面） | （x, y）の座標値を結んだ閉じた線分として格納 | 建物、行政界     |

![](https://automating-gis-processes.github.io/2018/_images/SpatialDataModel.PNG)
画像は Automating GIS-processes 2018 より引用


## 環境構築手順

以下の2ファイルを利用して、GIS データハンドリング用の Python 環境を構築します。

| ファイル名   | 用途             |
| ------------ | ---------------- |
| Dockerfile   | コンテナのビルド |
| env_name.yml | 環境設定ファイル |


### 1. Dockerfile の準備

```Dockerfile:Dockerfile
FROM continuumio/miniconda3:4.5.12

COPY env_name.yml /tmp/

RUN conda env create -f=/tmp/env_name.yml

RUN echo "source activate gis" > ~/.bashrc
ENV PATH /opt/conda/envs/gis/bin/:$PATH

WORKDIR /opt/notebooks

EXPOSE 8888
CMD jupyter notebook --notebook-dir=/opt/notebooks --ip=0.0.0.0 --port=8888 --no-browser --allow-root --NotebookApp.token='gis'
```

### 2. 環境設定ファイルの準備

Anacondaでは、環境設定をYAML形式で書き出すことで、そこから簡単に環境を再構築することができます。
使用するライブラリは [このページ](https://automating-gis-processes.github.io/2018/course-info/Installing_Anacondas_GIS.html) を参考にしています。

conda または pip で、versionを指定せずに1つずつインストールすると、ライブラリ間の依存関係が壊れます。
依存関係を解決するために、[ここ](https://automating-gis-processes.github.io/2018/course-info/install-using-yml.html)を参考にしました。(他に良い方法があったら教えてください)

```yml:env_name.yml
name: gis
channels:
  - conda-forge
  - defaults
dependencies:
  - geopandas==0.4.0
  - geoplot==0.2.3
  - geoviews==1.6.2
  - haversine==2.0.0
  - osmnx==0.9
  - pysal==1.14.4
  - contextily==1.0rc1
  - rasterio==1.0.18
  - rasterstats==0.13.0
  - shapely==1.6.4
  - scikit-learn==0.20.2
  - lightgbm==2.2.2
  - pip:
    - dash==0.19.0
    - dash-core-components==0.14.0
    - dash-html-components==0.8.0
    - dash-renderer==0.11.1
    - pycrs==1.0.0
    - mplleaflet==0.0.5
    - jupyter==1.0.0
    - bokeh==1.0.4
    - vega_datasets==0.7.0
```

### 3. Jupyter Notebook の起動

- PowerShell を起動。
Dockerfile と env_name.yml が格納されているディレクトリで以下のコマンドを実行します。

  ```bash
  docker build -t gis .
  docker run -d --rm --name gis -p 8888:8888 -v C:/path//:/opt/notebooks gis
  ```

  proxy 環境の場合は以下のコマンドを実行

  ```
  docker build --build-arg http_proxy=http://proxysrv:port --build-arg https_proxy=http://proxysrv:port --no-cache -t gis .
  docker run -d --rm --name gis -p 8888:8888 -v C:/path/:/opt/notebooks gis
  ```

  ※`http://proxysrv:port` の `proxysrv` と `port` は適宜設定すること。
  ※`C:/path/`に関してもボリュームしたいディレクトリを指定すること。

- ブラウザを起動して以下のリンクを開くと Jupyter Notebook が起動
  - http://localhost:8888/?token=gis

## GISデータハンドリングで利用するライブラリ

特に geopandas, folium, osmnx は GIS データのハンドリングで利用されるので、
利用方法を抑えておきたいです。

| ライブラリ名                                             | 説明                                                                       |
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
| [geopandas](http://geopandas.org/)                       | 地理空間データを操作するのが簡単になり、Pandasの機能と形の組み合わせが可能 |
| [folium](https://python-visualization.github.io/folium/) | データを地図上に可視化するライブラリ                                       |
| [osmnx](https://osmnx.readthedocs.io/en/stable/)         | OpenStreetMapから街路ネットワークを取得、構築、分析、および視覚化する      |
| shapely                                                  | 平面幾何学的オブジェクトの操作と分析のためのPythonパッケージ               |
| fiona                                                    | 空間データの読み書き（geopandasの代替）                                    |
| pyproj                                                   | 地図作成変換と測地計算を実行（PROJ.4に基づく）                             |
| geopy                                                    | Geocoding library：住所への座標 <->座標への住所                            |

## 講座内容

全部で7講座あります。
私は Lesson1~6 を翻訳しながら写経しました。
Lesson7 は業務上不要だったため実施していません。
また、不明点があったときは、都度ググりながら調べて進めました。

| Lesson | Learning goals                                                                                                                                                                                                                                                                  |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1      | ・Python で GIS を実行するためにどのようなツールが利用可能か分かること<br>・geometry オブジェクトが利用可能で、それらの用途を理解できること<br>・shapely を使用してさまざまな種類の形状を作成できること<br>・ファイルから座標を読み取り、それらに基づきポイントを作成できること |
| 2      | ・シェープファイルの Read/Write ができること<br>・GeoDataFrame に geometry を作成できること<br>・データの座標系を変更できること                                                                                                                                                 |
| 3      | ・住所 ⇔ ポイントの変換ができること<br>・Point in Polygon（PIP）クエリを実行できること<br>・KMLファイルを読み込めること<br>・レイヤー間で空間結合ができること<br>・特定の点からの最近傍点を探せること                                                                           |
| 4      | ・空間データの分類ができること<br>・オーバーレイ解析ができること<br>・ジオメトリオブジェクトの集約ができること<br>・ジオメトリの単純化ができること                                                                                                                              |
| 5      | ・Geopandasを使用して背景ベースマップで静的マップを作成できること<br>・Foliumを使用して簡単なインタラクティブマップを作成できること<br>・GitHubページを使ってインターネット上で地図（静的/インタラクティブ）を共有できること                                                    |
| 6      | ・Pythonを使ってOpenStreetMapからデータを取得して保存できること<br>・単純な街路ネットワークの特性と統計を抽出できること<br>・osmnx / networkxで最短経路アルゴリズムを使用して単純な経路最適化ができること                                                                       |
| 7      | ・QGIS Processing Toolboxでスクリプト用のシンプルなユーザーインターフェースを作成できること<br>・QGISアルゴリズムを実行できること<br>・PyQGISとQGIS Processingのドキュメントをオンラインで読む方法を                                                                            |

## GIS データハンドリング

ヘルシンキ大学の講座から、私の仕事で使いそうな部分をピックアップしました。
また、自分で調べた内容も一部記載しています。

| #   | 見出し                             |
| --- | ---------------------------------- |
| 1   | シェープファイルのread/write       |
| 2   | 座標参照系の設定 (Map projections) |
| 3   | Point in Polygon(PIP)              |
| 4   | 点間の距離計算                     |
| 5   | ポイント ⇔ 住所(名称) の変換       |
| 6   | 最近傍点の探索                     |
| 7   | オーバーレイ解析                   |
| 8   | インタラクティブマップの作成       |


### 1. シェープファイルのread/write

#### 1.1. データ準備

[L2_data.zip](https://github.com/AutoGIS/data/blob/master/L2_data.zip) をダウンロードします。
zip を展開すると以下のような構成になっています。

```bash
L2_data
  ├ DAMSELFISH_distributions.cpg
  ├ DAMSELFISH_distributions.dbf
  ├ DAMSELFISH_distributions.prj
  ├ DAMSELFISH_distributions.shp
  ├ DAMSELFISH_distributions.shx
  ├ Europe_borders.cpg
  ├ Europe_borders.dbf
  ├ Europe_borders.prj
  ├ Europe_borders.sbn
  ├ Europe_borders.sbx
  ├ Europe_borders.shp
  └ Europe_borders.shx
```

#### 1.2. read

シェープファイルを読み込みます。
geometry 列にはベクターデータが格納されています。

```python:In[1]
import geopandas as gpd

fp = 'L2_data/DAMSELFISH_distributions.shp'
data = gpd.read_file(fp)
display(data.head(2))
```

<div>
<table border="1" class="dataframe">
                <thead>
                    <tr style="text-align: right;">
                        <th></th>
                        <th>ID_NO</th>
                        <th>BINOMIAL</th>
                        <th>ORIGIN</th>
                        <th>COMPILER</th>
                        <th>YEAR</th>
                        <th>CITATION</th>
                        <th>SOURCE</th>
                        <th>DIST_COMM</th>
                        <th>ISLAND</th>
                        <th>SUBSPECIES</th>
                        <th>...</th>
                        <th>RL_UPDATE</th>
                        <th>KINGDOM_NA</th>
                        <th>PHYLUM_NAM</th>
                        <th>CLASS_NAME</th>
                        <th>ORDER_NAME</th>
                        <th>FAMILY_NAM</th>
                        <th>GENUS_NAME</th>
                        <th>SPECIES_NA</th>
                        <th>CATEGORY</th>
                        <th>geometry</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <th>0</th>
                        <td>183963.0</td>
                        <td>Stegastes leucorus</td>
                        <td>1</td>
                        <td>IUCN</td>
                        <td>2010</td>
                        <td>International Union for Conservation of Nature...</td>
                        <td>None</td>
                        <td>None</td>
                        <td>None</td>
                        <td>None</td>
                        <td>...</td>
                        <td>2012.1</td>
                        <td>ANIMALIA</td>
                        <td>CHORDATA</td>
                        <td>ACTINOPTERYGII</td>
                        <td>PERCIFORMES</td>
                        <td>POMACENTRIDAE</td>
                        <td>Stegastes</td>
                        <td>leucorus</td>
                        <td>VU</td>
                        <td>POLYGON ((-115.6437454219999 29.71392059300007...</td>
                    </tr>
                    <tr>
                        <th>1</th>
                        <td>183963.0</td>
                        <td>Stegastes leucorus</td>
                        <td>1</td>
                        <td>IUCN</td>
                        <td>2010</td>
                        <td>International Union for Conservation of Nature...</td>
                        <td>None</td>
                        <td>None</td>
                        <td>None</td>
                        <td>None</td>
                        <td>...</td>
                        <td>2012.1</td>
                        <td>ANIMALIA</td>
                        <td>CHORDATA</td>
                        <td>ACTINOPTERYGII</td>
                        <td>PERCIFORMES</td>
                        <td>POMACENTRIDAE</td>
                        <td>Stegastes</td>
                        <td>leucorus</td>
                        <td>VU</td>
                        <td>POLYGON ((-105.589950704 21.89339825500002, -1...</td>
                    </tr>
                </tbody>
            </table>
</div>

#### 1.3. write(読み込んだファイルから書き込み)

さきほど読み込んだファイルから50行抽出したものをファイル出力します。

```python:In[2]
outfp = "L2_data/DAMSELFISH_distributions_SELECTION.shp"
selection = data[0:50]
selection.to_file(outfp)
```

ファイルが出力されていることを確認できます。

```bash
L2_data
  ├ ・・・
  ├ DAMSELFISH_distributions_SELECTION.cpg
  ├ DAMSELFISH_distributions_SELECTION.dbf
  ├ DAMSELFISH_distributions_SELECTION.prj
  ├ DAMSELFISH_distributions_SELECTION.shp
  ├ DAMSELFISH_distributions_SELECTION.shx
  ├ ・・・
  └ Europe_borders.shx
```

#### 1.4. write(ゼロベース)

GeoDataFrame を作ってシェープファイルの書き込みをします。

```python:In[3]
import geopandas as gpd
from shapely.geometry import Point, Polygon

# 列「geometry」を追加
newdata = gpd.GeoDataFrame()
newdata['geometry'] = None

# 列「geometry」に Polygon オブジェクトを設定
coordinates = [(24.950899, 60.169158), (24.953492, 60.169158), (24.953510, 60.170104), (24.950958, 60.169990)]
poly = Polygon(coordinates)
newdata.loc[0, 'geometry'] = poly

# シェープファイルの出力
outfp = "L2_data/Senaatintori.shp"
newdata.to_file(outfp)

display(newdata.head())
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>geometry</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>0</th>
        <td>POLYGON ((24.950899 60.169158, 24.953492 60.16...</td>
      </tr>
    </tbody>
  </table>
</div>

### 2. 座標参照系の設定 (Map projections)

#### 2.1. CRSとは

![](https://automating-gis-processes.github.io/2018/_images/Map-projections.png)
画像は Automating GIS-processes 2018 より引用

GeoDataFrame内のベクターデータは、任意の空間内の座標の集合であるため、
座標参照系（CRS）の設定が必要になります。
CRS とは 「Coordinate reference system」の略称です。

CRSは、座標が地球上の場所とどのように関連しているかをPythonに伝えます。
地図投影法（または投影座標系）は、緯度と経度を平面単位に体系的に変換したもので、
単位は通常（メートル単位ではなく）メートルで表されます.
この変換は、平面の2次元マップ上に3次元の地球を表示するために使用されます。

データセットの CRS は異なることが多いです。
CRS が同一になるように再定義するのが一般的な手法になります。
レイヤー間の空間的関係を分析するためには、レイヤーが同じ座標参照系を持つことが重要です。

Shapefileには、.prj ファイルに座標参照系に関する情報が格納されています。
Geopandasを使用してデータをGeoDataFrameに読み込むと、
自動的にGeoDataFrameの.crs属性に座標参照系が格納されます。

長々と書きましたが、Geopandasでは投影法の定義は簡単です。
Europe_borders.shpファイルからデータを読み取り、crsを確認してみましょう。

```python:In[1]
import geopandas as gpd

fp = 'L2_data/Europe_borders.shp'
data = gpd.read_file(fp)
print(data.crs)
```

```bash:Out[1]
{'init': 'epsg:4326'}
```

CRSの値を確認すると、epsg4326 という値が設定されています。
epsgは「EPSGコード」を指しており、これに関しては後述します。

#### 2.2 EPSG コードとは

EPSGコードとは、GISで使用される様々な要素に必要なパラメータを1つにまとめ、
そのパラメータの集合体どうしを区別するためにコードを割り振ったコード体系のことです。
様々な要素とは、座標参照系、測地系、本初子午線、地図投影法等があります。
EPSG（European Petroleum Survey Group）という団体によって作成されましたが、
現在では様々なGISで使用されています。

詳細は 「[EPSGコードとは](http://club.informatix.co.jp/?p=1225) 」を参照してください。

座標参照系を変更して違いを確認してみましょう。
まずは、変更前の座標参照系で geometry 列を確認します。

```python:In[2]
print(data['geometry'].head(2))
```

```bash:Out[2]
0    POLYGON ((8.457777976989746 54.56236267089844,...
1    POLYGON ((8.71992015838623 47.69664382934571, ...
Name: geometry, dtype: object
```

次に、座標参照系を変更して geometry 列を確認します。

```python:In[3]
# 座標参照系_変更後
orig = data.copy()  # 退避
data = data.to_crs(epsg=3035)
print(data['geometry'].head(2))
```

```bash:Out[3]
0    POLYGON ((4221214.558088431 3496203.404338956,...
1    POLYGON ((4224860.478308966 2732279.319617757,...
Name: geometry, dtype: object
```

座標の値が変更されたことが確認できました。

#### 2.3. 座標参照系の変更前後をそれぞれ投影

座標参照系の変更前後をプロットして、違いを確認してみましょう。

```python:In[4]
import matplotlib.pyplot as plt
%matplotlib inline

fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12, 8))

orig.plot(ax=ax1, facecolor='gray');
ax1.set_title("WGS84");

data.plot(ax=ax2, facecolor='blue');
ax2.set_title("ETRS Lambert Azimuthal Equal Area projection");

plt.tight_layout()
```

![](https://automating-gis-processes.github.io/2018/_images/notebooks_L2_projections_9_0.png)

きちんと変わっていますね。

### 3. Point in Polygon(PIP)

基本的な地理空間操作として以下2点があります。

- ある点が領域の内側にあるか外側にあるかを調べること
- 線が別の線または多角形と交差するかどうかを調べること

これらを調べるために、Python の shapley というライブラリを使用します。
shapely で PIP を実行するためには、以下2点の方法があります。

- 点が多角形内にあるかどうかを調べる `within（）`という関数を使用 
- 多角形に点が含まれているかどうかを調べる `contains（）`という関数を使用

説明を割愛しますが、LineString または Polygon が別の Polygon の内側にあるかどうかの確認も可能です。
まずは、座標タプルのリストと Point オブジェクトを使って Polygon を作成しましょう。

```python:In[5]
from shapely.geometry import Point, Polygon

# Point オブジェクトの作成
p1 = Point(24.952242, 60.1696017)
p2 = Point(24.976567, 60.1612500)

# Polygon オブジェクトの作成
coords = [(24.950899, 60.169158), (24.953492, 60.169158), (24.953510, 60.170104), (24.950958, 60.169990)]
poly = Polygon(coords)

# 確認
print(p1)
print(p2)
print(poly)
```

```bash:Out[5]
POINT (24.952242 60.1696017)
POINT (24.976567 60.16125)
POLYGON ((24.950899 60.169158, 24.953492 60.169158, 24.95351 60.170104, 24.950958 60.16999, 24.950899 60.169158))
```

これらの点がポリゴン内にあるかどうかを確認しましょう

```python:In[6]
print(p1.within(poly))
print(p2.within(poly))
```

```bash:Out[6]
True
False
```

最初の点はその多角形の内側にあり、他の点はそうではないことがわかります。
最初の点は、点の位置を多角形の重心と比較するとわかるように、多角形の中心に近いです。

```python:In[7]
print(p1)
print(poly.centroid)
```

```bash:Out[7]
POINT (24.952242 60.1696017)
POINT (24.95224242849236 60.16960179038188)
```

ポリゴンに点が含まれているかどうかを確認してみましょう。

```python:In[8]
print(poly.contains(p1))
print(poly.contains(p2))
```

```bash:Out[8]
True
False
```

### 4. 点間の距離計算

Europe_borders.shp を使って、すべてのヨーロッパ諸国の重心（中点）から
フィンランドのヘルシンキまでのユークリッド距離を計算します。

ヘルシンキと他のヨーロッパ諸国との間の距離は,メートル単位の距離を表す
[正距方位図法](https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%B7%9D%E6%96%B9%E4%BD%8D%E5%9B%B3%E6%B3%95)（Azimuthal Equidistant-projection）を使用して計算します。

#### 4.1. GeoDataFrame の作成

まず、フィンランドのヘルシンキの場所を表す単一のPointを含むGeoDataFrameを作成しましょう。

```python:In[1]
from shapely.geometry import Point
import pycrs

hki_lon = 24.9417
hki_lat = 60.1666

helsinki = gpd.GeoDataFrame([[Point(hki_lon, hki_lat)]], geometry='geometry', crs={'init': 'epsg:4326'}, columns=['geometry'])
```

#### 4.2. 正距方位図法への変換

この GeoDataFrameを正距方位図法に変換します。
変換を実行するために、`pyproj` と呼ばれる座標参照系のライブラリを使用します。

`proj = 'aeqd'`は正距方位図法を指します。

`ellps = 'WGS84'`は[地球楕円体](https://ja.wikipedia.org/wiki/%E5%9C%B0%E7%90%83%E6%A5%95%E5%86%86%E4%BD%93)を指します。
World Geodetic System(世界測地系）1984の略語で、
米国が構築・維持している世界測地系です。GPSの軌道情報で使われているほか、
GPSによるナビゲ－ションの位置表示の基準として使われています。

`datum = 'WGS84'`は[測地基準系](http://www.gsi.go.jp/LAW/G2000-g2000-h2.htm)を指します。
地球上の位置を経度・緯度で表わすための基準を「測地基準系（測地系）」といい、
地球の形に最も近い回転楕円体で定義されています。

`lat_0`は中心点の緯度座標です。
`lon_0`は中心点の経度座標です。

```python:In[2]
import pyproj

aeqd = pyproj.Proj(proj='aeqd', ellps='WGS84', datum='WGS84', lat_0=hki_lat, lon_0=hki_lon).srs
helsinki = helsinki.to_crs(crs=aeqd)

print(helsinki)
print('\nCRS:\n', helsinki.crs)
```

```bash:Out[2]
      geometry
0  POINT (0 0)

CRS:
 +units=m +proj=aeqd +ellps=WGS84 +datum=WGS84 +lat_0=60.1666 +lon_0=24.9417 
```

x と y の座標を確認すると、ともに 0 になりました。
ヘルシンキを中心に、ポイントが投影されていることが分かります。

次に、Europe_borders.shp を正距方位図法に変換します。

```python:In[3]
fp = 'L2_data/Europe_borders.shp'
europe_borders_aeqd = gpd.read_file(fp)
europe_borders_aeqd = europe_borders_aeqd.to_crs(crs=aeqd)
print(europe_borders_aeqd.head(2))
```

```bash:Out[3]
            TZID                                           geometry
0  Europe/Berlin  POLYGON ((-1057542.597130521 -493724.801682817...
1  Europe/Berlin  POLYGON ((-1216418.435359255 -1243831.63520634...
```

列「geometry」の座標は、ヘルシンキからさまざまな方向へのメートル単位の距離を表しているので、
かなり大きな数であることがわかります。

#### 4.3. ヨーロッパの国境とヘルシンキの位置をプロット

次に、ヨーロッパの国境とヘルシンキの位置をプロットして、座標の再投影が機能したか確認してみましょう。

```python:In[4]
fig, ax = plt.subplots(figsize=(10,10))

europe_borders_aeqd.plot(ax=ax)
helsinki.plot(ax=ax, color='black', markersize=10)
```

![](https://automating-gis-processes.github.io/2018/_images/notebooks_L2_projections_28_1.png)

地図からわかるように、x軸とy軸の0位置はヘルシンキが配置されている場所にあるため、
投影は実際にヘルシンキの中心にあります。
座標値は、ヘルシンキ（黒点）からさまざまな方向（南,北,東,西）までの距離をメートルで示しています。

#### 4.4. すべての国からヘルシンキへの距離を計算

次に、すべての国からヘルシンキへの距離を計算してみましょう。
計算するために、ヨーロッパ諸国の境界を表すすべてのPolygonの重心を計算します。
`geopandas` では、`centroid` を使用することで簡単に計算できます。

```python:In[5]
europe_borders_aeqd['centroid'] = europe_borders_aeqd.centroid
print(europe_borders_aeqd.head(2))
```

```bash:Out[5]
            TZID                                           geometry  \
0  Europe/Berlin  POLYGON ((-1057542.597130521 -493724.801682817...   
1  Europe/Berlin  POLYGON ((-1216418.435359255 -1243831.63520634...   

                                        centroid  
0  POINT (-1057718.135423443 -492420.5658204998)  
1  POINT (-1218235.216971495 -1242668.589667923)  
```

各Polygonの重心を表すPointジオメトリを持つ`centroid`という新しい列を作成しました。
次に、国の重心とヘルシンキの間の距離を計算します。
まず,`calculate_distance（）`という距離を計算するための専用関数を作成します.

```python:In[6]
def calculate_distance(row, dest_geom, src_col='geometry', target_col='distance'):
    """
    Calculates the distance between Point geometries.

    Parameters
    ----------
    dest_geom : shapely.Point
       A single Shapely Point geometry to which the distances will be calculated to.
    src_col : str
       A name of the column that has the Shapely Point objects from where the distances will be calculated from.
    target_col : str
       A name of the target column where the result will be stored.

    Returns
    -------

    Distance in kilometers that will be stored in 'target_col'.
    """
    
    # Calculate the distances
    dist = row[src_col].distance(dest_geom)
    
    # Convert into kilometers
    dist_km = dist / 1000
    
    # Assign the distance to the original data
    row[target_col] = dist_km
    return row
```

ヘルシンキの中心点(`dest_geom`パラメータに)の座標を取得します。

```python:In[7]
helsinki_geom = helsinki.loc[0, 'geometry']
print(helsinki_geom)
```

```bash:Out[7]
POINT (0 0)
```

すべての準備が整ったので、各国の重心とヘルシンキの間の距離を計算してみましょう。

```python:In[8]
europe_borders_aeqd = europe_borders_aeqd.apply(calculate_distance, 
                                                dest_geom=helsinki_geom, 
                                                src_col='centroid',
                                                target_col='dist_to_Hki',
                                                axis=1)
print(europe_borders_aeqd.head(10))
```

```bash:Out[8]
            TZID                                           geometry  \
0  Europe/Berlin  POLYGON ((-1057542.597130521 -493724.801682817...   
1  Europe/Berlin  POLYGON ((-1216418.435359255 -1243831.63520634...   
2  Europe/Berlin  POLYGON ((-1194521.638643211 -571726.459328881...   
3  Europe/Berlin  POLYGON ((-1185933.276237431 -571780.052772927...   
4  Europe/Berlin  POLYGON ((-1182416.219837206 -569097.571220090...   
5  Europe/Berlin  POLYGON ((-1172799.400650826 -565749.438722840...   
6  Europe/Berlin  POLYGON ((-1162805.427517967 -563558.434197177...   
7  Europe/Berlin  POLYGON ((-1129053.540904054 -568388.469960322...   
8  Europe/Berlin  POLYGON ((-1109126.532709598 -570899.989413469...   
9  Europe/Berlin  POLYGON ((-703490.1465879158 -664009.791857453...   

                                        centroid  dist_to_Hki  
0  POINT (-1057718.135423443 -492420.5658204998)  1166.724332  
1  POINT (-1218235.216971495 -1242668.589667923)  1740.207536  
2   POINT (-1194210.78929945 -568987.1532380251)  1322.832487  
3  POINT (-1185320.605845876 -571340.3134827728)  1315.832319  
4   POINT (-1182191.163363772 -567293.763983083)  1311.258236  
5  POINT (-1175758.089721244 -564846.4759828376)  1304.399719  
6  POINT (-1157868.191291224 -565162.3607219103)  1288.435968  
7  POINT (-1124883.959682355 -567850.4028534386)  1260.086506  
8   POINT (-1113376.309723986 -569037.768865205)  1250.364263  
9   POINT (-703970.559180466 -663710.5756044324)   967.515517  
```

これで、ヨーロッパ諸国の重心とヘルシンキの間の距離を計算できました。
他のヨーロッパ諸国の重心からヘルシンキまでの最長距離および平均距離を確認してみましょう。

```python:In[9]
max_dist = europe_borders_aeqd['dist_to_Hki'].max()
mean_dist = europe_borders_aeqd['dist_to_Hki'].mean()

print("Maximum distance to Helsinki is %.0f km, and the mean distance is %.0f km." % (max_dist, mean_dist))
```

```bash:Out[9]
Maximum distance to Helsinki is 3470 km, and the mean distance is 1177 km.
```

#### 4.5. 補足

今回はヨーロッパ諸国間の距離を計算しましたが、
世界中の複数の場所間の距離を計算したい場合は、Python の [harversine](https://github.com/mapado/haversine) パッケージを使いましょう。
環境構築の際にこのパッケージをインストールしているので、興味のある方は試してください。

### 5. ポイント ⇔ 住所(名称) の変換

OpenStreetMap データに基づくジオコーダーである Nominatim を使って、ポイント ⇔ 住所 の変換をします。
Nominatim は小規模のジオコーディングジョブに使用される場合、サービスの使用にAPIキーを必要としません。
理由は、1秒あたり1リクエスト（3600 /時）に制限されるためです。
大規模のジオコーディングジョブを実行する場合は、Google API を利用するのが良いでしょう。

#### 5.1. 住所(名称) ⇒ ポイント

```python:In[1]
import geopy.geocoders
from geopy.geocoders import Nominatim

geolocator = Nominatim(format_string="%s, 日本")
location = geolocator.geocode("東京タワー")

print('location.address: ', location.address)
print('location.altitude: ', location.altitude)
print('location.latitude: ', location.latitude)
print('location.longitude: ', location.longitude)
print('location.point: ', location.point)
```

```bash:Out[1]
location.address:  東京タワー, 東京タワー通り, 南青山6, 港区, 東京都, 関東地方, 105-0011, 日本
location.altitude:  0.0
location.latitude:  35.65858645
location.longitude:  139.745440057962
location.point:  35 39m 30.9112s N, 139 44m 43.5842s E
```

プロキシ環境の場合は、プロキシを設定してから実行しましょう。

```python:In[2]
proxies= {'http' : 'http://proxysrv:port',
          'https': 'http://proxysrv:port'
         }

geopy.geocoders.options.default_user_agent = 'mamurata0924'
geopy.geocoders.options.default_timeout = 7
geopy.geocoders.options.default_proxies = proxies
```

#### 5.2. ポイント ⇒ 住所(名称)

```python:In[3]
location = geolocator.reverse("35.6806303 139.768196161")

print('location.address: ', location.address)
print('location.altitude: ', location.altitude)
print('location.latitude: ', location.latitude)
print('location.longitude: ', location.longitude)
print('location.point: ', location.point)
```

```bash:Out[3]
location.address:  東京駅一番街, 1, 外堀通り, 中央区, 東京都, 関東地方, 100-6701, 日本
location.altitude:  0.0
location.latitude:  35.68190185
location.longitude:  139.768425758104
location.point:  35 40m 54.8467s N, 139 46m 6.33273s E
```

#### 5.3. Geocoding in Geopandas

geopandasでは、統合された geopy 機能を使用してジオコーディングを行うことができます。  
geopandasには`geocode()`という名前の関数があり、住所のリスト（文字列）をジオコーディングして、
列「geometry」に結果のポイントオブジェクトを含むGeoDataFrameを返すことができます。

```python:In[4]
import pandas as pd
import geopandas as gpd
from geopandas.tools import geocode

data = [
    [1000, 'Itamerenkatu 14, 00101 Helsinki, Finland'],
    [1001, 'Kampinkuja 1, 00100 Helsinki, Finland'],
    [1002, 'Kaivokatu 8, 00101 Helsinki, Finland'],
    [1003, 'Hermannin rantatie 1, 00580 Helsinki, Finland'],
]
columns = ['id', 'addr']
df = pd.DataFrame(data=data, columns=columns)

geo = geocode(data['addr'], provider='nominatim', user_agent='autogis_student_xx')

# プロキシ版
# geo = geocode(data['addr'], provider='nominatim', user_agent='autogis_student_xx', proxies=proxies)

geo.head()
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>geometry</th>
        <th>address</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>0</th>
        <td>POINT (24.9155624 60.1632015)</td>
        <td>Ruoholahti, 14, Itämerenkatu, Ruoholahti, Läns...</td>
      </tr>
      <tr>
        <th>1</th>
        <td>POINT (24.9316914 60.1690222)</td>
        <td>Kamppi, 1, Kampinkuja, Kamppi, Eteläinen suurp...</td>
      </tr>
      <tr>
        <th>2</th>
        <td>POINT (24.9416849 60.1699637)</td>
        <td>Bangkok9, 8, Kaivokatu, Keskusta, Kluuvi, Etel...</td>
      </tr>
      <tr>
        <th>3</th>
        <td>POINT (24.9787146 60.1909075)</td>
        <td>Hermannin rantatie, Kalasatama, Sörnäinen, Hel...</td>
      </tr>
    </tbody>
  </table>
</div>

プロキシ環境の場合は、プロキシを設定してから実行しましょう。

```python:In[5]
import urllib.request

proxies= {'http' : 'http://proxysrv:port',
          'https': 'http://proxysrv:port'
         }
proxy_support = urllib.request.ProxyHandler(proxies)
opener = urllib.request.build_opener(proxy_support)
urllib.request.install_opener(opener)

# プロキシ版
geo = geocode(data['addr'], provider='nominatim', user_agent='autogis_student_xx', proxies=proxies)

geo.head()
```

### 6. 最近傍点の探索

GISタスクの1つに、最近傍点の探索が挙げられます。
shapely と geopandas を使って最近傍点の探索を見ていきましょう。

#### 6.1. shapely 編

始点から最も近い終点を見つけることができるようにするには、
終点からMultiPointオブジェクトを作成する必要があります.

```python:In[1]
from shapely.geometry import Point, MultiPoint
from shapely.ops import nearest_points

orig = Point(1, 1.67)
dest1, dest2, dest3 = Point(0, 1.45), Point(2, 2), Point(0, 2.5)
destinations = MultiPoint([dest1, dest2, dest3])
nearest_geoms = nearest_points(orig, destinations)  # 最近傍点

print(nearest_geoms[0])
print(nearest_geoms[1])
```

```bash:Out[1]
POINT (1 1.67)
POINT (0 1.45)
```

`nearest_points()`は,最初の項目（インデックス0）が原点のジオメトリで、
2番目の項目（インデックス1）が目的点から実際に最も近いジオメトリのタプルを返します。
つまり、座標(1, 1.67) から最も近い座標は (0, 1.45)となります。

#### 6.2 geopandas 編

GeoDataFrameを使用して、一連の原点から一連の終点に
最も近い点を見つける方法を見ていきましょう。

検索元の GeoDataFrame を`df1`、検索先の GeoDataFrame を`df2`とします。

```python:In[2]
data = [
    ['Suur-Espoonlahti', Point(24.76754037242762, 60.0440879200116)],
    ['Suur-Kauklahti', Point(24.57415010885406, 60.19764302289445)],
    ['Pohjoinen', Point(24.94156264995636, 60.24654213027523)],
]
columns = ['Name', 'centroid']
df1 = gpd.GeoDataFrame(data=data, columns=columns)

display(df1)
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>Name</th>
        <th>centroid</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>0</th>
        <td>Suur-Espoonlahti</td>
        <td>POINT(24.76754037242762 60.0440879200116)</td>
      </tr>
      <tr>
        <th>1</th>
        <td>Suur-Kauklahti</td>
        <td>POINT(24.57415010885406 60.19764302289445)</td>
      </tr>
      <tr>
        <th>2</th>
        <td>Pohjoinen</td>
        <td>POINT(24.94156264995636 60.24654213027523)</td>
      </tr>
    </tbody>
  </table>
</div>


```python:In[3]
data = [
    [1000, Point(24.9155624, 60.1632015)],
    [1001, Point(24.9316914, 60.1690222)],
    [1002, Point(24.9416849, 60.1699637)],
    [1003, Point(24.9787146, 60.1909075)],
]

columns = ['id', 'geometry']
df2 = gpd.GeoDataFrame(data=data, columns=columns)

display(df2)
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>id</th>
        <th>geometry</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>0</th>
        <td>1000</td>
        <td>POINT (24.9155624 60.1632015)</td>
      </tr>
      <tr>
        <th>1</th>
        <td>1001</td>
        <td>POINT (24.9316914 60.1690222)</td>
      </tr>
      <tr>
        <th>2</th>
        <td>1002</td>
        <td>POINT (24.9416849 60.1699637)</td>
      </tr>
      <tr>
        <th>3</th>
        <td>1003</td>
        <td>POINT (24.9787146 60.1909075)</td>
      </tr>
    </tbody>
  </table>
</div>


以下の関数を使用して、`df2`から`df1`の重心に最も近いポイント（id列から値を取得）を見つけましょう。

```python:In[4]
from shapely.geometry import Point, MultiPoint
from shapely.ops import nearest_points

def nearest(row, geom_union, df1, df2, geom1_col='geometry', geom2_col='geometry', src_column=None):
    # Find the nearest point and return the corresponding value from specified column.

    # Find the geometry that is closest
    nearest = df2[geom2_col] == nearest_points(row[geom1_col], geom_union)[1]

    # Get the corresponding value from df2 (matching is based on the geometry)
    value = df2[nearest][src_column].get_values()[0]

    return value

unary_union = df2.unary_union  # MultiPoint オブジェクトの作成
df1['nearest_id'] = df1.apply(nearest, geom_union=unary_union, df1=df1, df2=df2, geom1_col='centroid', src_column='id', axis=1)

display(df1.head())
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>Name</th>
        <th>centroid</th>
        <th>nearest_id</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>0</th>
        <td>Suur-Espoonlahti</td>
        <td>POINT(24.76754037242762 60.0440879200116)</td>
        <td>1000</td>
      </tr>
      <tr>
        <th>1</th>
        <td>Suur-Kauklahti</td>
        <td>POINT(24.57415010885406 60.19764302289445)</td>
        <td>1000</td>
      </tr>
      <tr>
        <th>2</th>
        <td>Pohjoinen</td>
        <td>POINT(24.94156264995636 60.24654213027523)</td>
        <td>1003</td>
      </tr>
    </tbody>
  </table>
</div>

### 7. オーバーレイ解析

オーバーレイ解析とは、地理的な要素を重ね合わせることにより、
情報同士の関係性や新たな発見を導き出す解析を指します。
詳細は [ESRI ジャパンのオーバーレイ解析](https://www.esrij.com/gis-guide/spatial/spatial-overlay/)のページを参照してください。

基本的なオーバーレイ操作
![基本的なオーバーレイ解析](https://automating-gis-processes.github.io/2018/_images/overlay_operations.png)
画像は Automating GIS-processes 2018 より引用

#### 7.1. データ準備

[L4_data.zip](https://github.com/AutoGIS/data/raw/master/L4_data.zip) をダウンロードします。
zip を展開すると以下のような構成になっています。

```bash
L4_data
  ├ Amazon_river.cpg
  ├ Amazon_river.dbf
  ├ Amazon_river.prj
  ├ Amazon_river.shp
  ├ Amazon_river.shx
  ├ Helsinki_borders.cpg
  ├ Helsinki_borders.dbf
  ├ Helsinki_borders.prj
  ├ Helsinki_borders.shp
  ├ Helsinki_borders.shx
  ├ TravelTimes_to_5975375_RailwayStation.cpg
  ├ TravelTimes_to_5975375_RailwayStation.dbf
  ├ TravelTimes_to_5975375_RailwayStation.prj
  ├ TravelTimes_to_5975375_RailwayStation.shp
  └ TravelTimes_to_5975375_RailwayStation.shx
```

#### 7.2. データ読込

以下のファイルを読み込んで、可視化してみましょう。

- `TravelTimes_to_5975375_RailwayStation_Helsinki.shp`
- `Helsinki_borders.shp`

```python:In[1]
import geopandas as gpd
import matplotlib.pyplot as plt
import shapely.speedups
%matplotlib inline

# shape ファイルの読込が速くなるおまじない
shapely.speedups.enable()

border_fp = './lesson4/Helsinki_borders.shp'
grid_fp = './lesson4/TravelTimes_to_5975375_RailwayStation.shp'

grid = gpd.read_file(grid_fp)
hel = gpd.read_file(border_fp)

ax = grid.plot(facecolor='gray')
hel.plot(ax=ax, facecolor='None', edgecolor='blue')
```

![レイヤーが重なった図](https://automating-gis-processes.github.io/2018/_images/notebooks_L4_geometric-operations_1_1.png)

オーバーレイ解析をして、ヘルシンキレイヤ(青い領域)と交差する
グリッドのポリゴンから新しいレイヤを作成しましょう。

#### 7.3. CRS の一致確認

解析の前に、CRSの確認をしましょう。
複数のレイヤーを含む GIS 操作を実行する場合は、レイヤーの CRS が一致している必要があります。

```python:In[2]
hel.crs == grid.crs
```

```bash:Out[30]
True
```

一致していますね。

#### 7.4. オーバーレイ解析

geopandas を使えば、簡単にオーバーレイ解析が可能です。
ここでは、`intersection` の操作をしてみましょう。

```python:In[3]
intersection = gpd.overlay(grid, hel, how='intersection')
intersection.plot(color="b")
```

ヘルシンキの境界線と交差するグリッドセルのみが表示されるようになりました。
グリッドセルは境界に基づいて切り取られています。

![オーバーレイ解析_intersection](https://automating-gis-processes.github.io/2018/_images/notebooks_L4_geometric-operations_7_1.png)

データセットには両方の入力レイヤからの属性が含まれています。

```python:In[4]
display(intersection.head())
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>car_m_d</th>
        <th>car_m_t</th>
        <th>car_r_d</th>
        <th>car_r_t</th>
        <th>from_id</th>
        <th>pt_m_d</th>
        <th>pt_m_t</th>
        <th>pt_m_tt</th>
        <th>pt_r_d</th>
        <th>pt_r_t</th>
        <th>pt_r_tt</th>
        <th>to_id</th>
        <th>walk_d</th>
        <th>walk_t</th>
        <th>GML_ID</th>
        <th>NAMEFIN</th>
        <th>NAMESWE</th>
        <th>NATCODE</th>
        <th>geometry</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>94</th>
        <td>29476</td>
        <td>41</td>
        <td>29483</td>
        <td>46</td>
        <td>5876274</td>
        <td>29990</td>
        <td>76</td>
        <td>95</td>
        <td>24984</td>
        <td>77</td>
        <td>99</td>
        <td>5975375</td>
        <td>25532</td>
        <td>365</td>
        <td>27517366</td>
        <td>Helsinki</td>
        <td>Helsingfors</td>
        <td>091</td>
        <td>POLYGON ((402250.0001322526 6685750.000039577,...</td>
      </tr>
      <tr>
        <th>95</th>
        <td>29456</td>
        <td>41</td>
        <td>29462</td>
        <td>46</td>
        <td>5876275</td>
        <td>29866</td>
        <td>74</td>
        <td>95</td>
        <td>24860</td>
        <td>75</td>
        <td>93</td>
        <td>5975375</td>
        <td>25408</td>
        <td>363</td>
        <td>27517366</td>
        <td>Helsinki</td>
        <td>Helsingfors</td>
        <td>091</td>
        <td>POLYGON ((402367.8898583531 6685750.000039573,...</td>
      </tr>
      <tr>
        <th>96</th>
        <td>36772</td>
        <td>50</td>
        <td>36778</td>
        <td>56</td>
        <td>5876278</td>
        <td>33541</td>
        <td>116</td>
        <td>137</td>
        <td>44265</td>
        <td>130</td>
        <td>146</td>
        <td>5975375</td>
        <td>31110</td>
        <td>444</td>
        <td>27517366</td>
        <td>Helsinki</td>
        <td>Helsingfors</td>
        <td>091</td>
        <td>POLYGON ((403250.000132058 6685750.000039549, ...</td>
      </tr>
      <tr>
        <th>97</th>
        <td>36898</td>
        <td>49</td>
        <td>36904</td>
        <td>56</td>
        <td>5876279</td>
        <td>33720</td>
        <td>119</td>
        <td>141</td>
        <td>44444</td>
        <td>132</td>
        <td>155</td>
        <td>5975375</td>
        <td>31289</td>
        <td>447</td>
        <td>27517366</td>
        <td>Helsinki</td>
        <td>Helsingfors</td>
        <td>091</td>
        <td>POLYGON ((403456.4840652301 6685750.000039542,...</td>
      </tr>
      <tr>
        <th>193</th>
        <td>29411</td>
        <td>40</td>
        <td>29418</td>
        <td>44</td>
        <td>5878128</td>
        <td>29944</td>
        <td>75</td>
        <td>95</td>
        <td>24938</td>
        <td>76</td>
        <td>99</td>
        <td>5975375</td>
        <td>25486</td>
        <td>364</td>
        <td>27517366</td>
        <td>Helsinki</td>
        <td>Helsingfors</td>
        <td>091</td>
        <td>POLYGON ((402000.0001323065 6685500.000039622,...</td>
      </tr>
    </tbody>
  </table>
</div>

#### 7.5. 解析結果を GeoJSON に出力

shape ファイルに出力でも良いのですが、
今回は試しに、GeoJSON に出力します。

※注意
GeoPandas の仕様上、geometry の 列タイプが 'MultiPolygon' の必要あり  
2019/2/18時点で、[issue 867](https://github.com/geopandas/geopandas/issues/867) で対応中

```python:In[5]
from shapely.geometry.multipolygon import MultiPolygon

intersection['geometry'] = intersection['geometry'].apply(lambda x: MultiPolygon([x]))
outfp = "./L4_data/TravelTimes_to_5975375_RailwayStation_Helsinki.geojson"
intersection.to_file(outfp, driver="GeoJSON")
```

### 8. インタラクティブマップの作成

インタラクティブマップの作成には、Python ライブラリの folium を使用します。
マップには、国土交通省が作成した「地形分類図」「土壌図」を表示していきます。

また、マップ作成には [folium の Gallery](https://nbviewer.jupyter.org/github/python-visualization/folium/tree/master/examples/) を参考にしました。
Jupyter Notebook 形式で非常に分かりやすいので、これから folium に触れる方は必見です。

#### 8.1. データ準備

[国土調査 土地分類基本調査 50万分の１土地分類基本調査](http://nrb-www.mlit.go.jp/kokjo/inspect/landclassification/download/)から、
「地形分類図」「土壌図」をダウンロードします。

```bash:地形分類図
landform
  ├ landform.avl
  ├ landform.dbf
  ├ landform.shp
  └ landform.shx
```

```bash:土壌図
soil
  ├ soil.avl
  ├ soil.dbf
  ├ soil.shp
  └ soil.shx
```

#### 8.2. データ前処理

ダウンロードしたデータを読み込み、ラベルエンコーディングします。
今回は東京都を表示対象にします。

```python:In[1]
from sklearn.preprocessing import LabelEncoder
import pandas as pd
import geopandas as gpd
import folium
import branca

def label_encode(df, column_name):
    le = LabelEncoder()
    le = le.fit(df[column_name])
    return le.transform(df[column_name]).astype(int)

# 地形分類図の読み込み
gdf_land = gpd.read_file('./landform/landform.shp', encoding='shift-jis')
gdf_land = gdf_land[gdf_land['PREF']=='東京都']
gdf_land['terrain'] = label_encode(gdf_land, '地形大区分')
display(gdf_land.head(3))
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>ID</th>
        <th>NO</th>
        <th>PREF</th>
        <th>地形区分名</th>
        <th>地形大区分</th>
        <th>geometry</th>
        <th>terrain</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>10605</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>埋立地</td>
        <td>POLYGON ((139.8134010579003 35.59388260180398,...</td>
        <td>3</td>
      </tr>
      <tr>
        <th>10606</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>埋立地</td>
        <td>POLYGON ((139.8051275277766 35.59496175790707,...</td>
        <td>3</td>
      </tr>
      <tr>
        <th>10607</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>埋立地</td>
        <td>POLYGON ((139.7799472187043 35.61276783360819,...</td>
        <td>3</td>
      </tr>
    </tbody>
  </table>
</div>

```python:In[2]
# 土壌図の読み込み
gdf_soil = gpd.read_file('./soil/soil.shp', encoding='shift-jis')
gdf_soil = gdf_soil[gdf_soil['PREF']=='東京都']
gdf_soil['soil_type'] = label_encode(gdf_soil, '土壌区分名')
display(gdf_soil.head(3))
```

<div>
  <table border="1" class="dataframe">
    <thead>
      <tr style="text-align: right;">
        <th></th>
        <th>ID</th>
        <th>NO</th>
        <th>PREF</th>
        <th>土壌区分名</th>
        <th>SOILNAME11</th>
        <th>土壌大区分</th>
        <th>SOILNAME12</th>
        <th>SOILNAME22</th>
        <th>SOILNAME21</th>
        <th>geometry</th>
        <th>soil_type</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th>5429</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>c</td>
        <td>埋立地</td>
        <td>c</td>
        <td>c</td>
        <td>c</td>
        <td>POLYGON ((139.8134010579003 35.59388260180398,...</td>
        <td>3</td>
      </tr>
      <tr>
        <th>5430</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>c</td>
        <td>埋立地</td>
        <td>c</td>
        <td>c</td>
        <td>c</td>
        <td>POLYGON ((139.8051275277766 35.59496175790707,...</td>
        <td>3</td>
      </tr>
      <tr>
        <th>5431</th>
        <td>0</td>
        <td>c</td>
        <td>東京都</td>
        <td>埋立地</td>
        <td>c</td>
        <td>埋立地</td>
        <td>c</td>
        <td>c</td>
        <td>c</td>
        <td>POLYGON ((139.7799472187043 35.61276783360819,...</td>
        <td>3</td>
      </tr>
    </tbody>
  </table>
</div>

#### 8.3. カラーマップ作成

branca というライブラリを使ってカラーマップを作成します。
このライブラリは folium から派生したもので、マップ固有ではない機能を提供します。
詳しくは[こちら](https://github.com/python-visualization/branca)のページを参照してください。


まずは、地形分類図における地形区分のカラーマップを作成します。

```python:In[3]
import branca.colormap as cm

linear_land_max = gdf_land['terrain'].max()
linear_land_min = gdf_land['terrain'].min()
linear_land_length = len(gdf_land['terrain'].unique())

linear_land = cm.linear.YlGnBu_09.scale(linear_land_min, linear_land_max).to_step(linear_land_length)
linear_land.caption = "地形区分"

display(linear_land)
```

![cmap_land.png](https://qiita-image-store.s3.amazonaws.com/0/184077/a2704ee7-012d-49b3-5816-74020139d641.png)

次に、土壌図における土壌区分のカラーマップを作成します。

```python:In[4]
linear_soil_max = gdf_soil['soil_type'].max()
linear_soil_min = gdf_soil['soil_type'].min()
linear_soil_length = len(gdf_soil['soil_type'].unique())

linear_soil = cm.linear.YlOrBr_07.scale(linear_soil_min, linear_soil_max).to_step(linear_soil_length)
linear_soil.caption = "土壌区分"
display(linear_soil)
```

![cmap_soil.png](https://qiita-image-store.s3.amazonaws.com/0/184077/1242f4ed-fcb8-7d4b-86b9-206050a7dca4.png)

#### 8.4. EPSGコードの設定

デフォルトだと crs が None になっているため、EPSG コードを設定します。
設定値は [ここ](https://epsg.io/2450) を参考にしました。

```python:In[5]
from fiona.crs import from_epsg

gdf_land.crs = from_epsg(2450)
gdf_soil.crs = from_epsg(2450)
```

#### 8.5. コロプレス図(地形分類図編)の作成

コロプレス図とは、統計数値を地図に表現する場合の一方法として、数値をいくつかの階級に区分し、
区域単位、たとえば市区町村別に、各階級に応じた色彩または明暗によって表す地図を指します。

ここでは、地形分類図のデータをコロプレス図として表現します。
folium には　コロプレス図を作成する [choropleth](https://python-visualization.github.io/folium/modules.html#folium.features.Choropleth) というメソッドがありますが、
このメソッドはマップ作成の自由度が低いです。
そのため、[GeoJSON](https://python-visualization.github.io/folium/modules.html#folium.features.GeoJson)というメソッドを使用して、コロプレス図を作成します。

```python:In[6]
# ベースマップの作成
tokyo23_location = [35.658593, 139.745441]
m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10)

# コロプレス図の追加
folium.GeoJson(
    data=gdf_land.to_json(),
    style_function=lambda feature: {
        'fillColor': linear_land(feature['properties']['terrain']),
        'color': 'gray',
        'weight': 2,
        'fill_opacity': 0.6,
        'line_opacity': 0.2,
    },
    highlight_function=lambda feature: {
        'fillColor': linear_land(feature['properties']['terrain']),
        'color': 'gray',
        'weight': 4,
        'dashArray': '1, 1',
        'fill_opacity': 0.6,
        'line_opacity': 0.2,
    },
    tooltip=folium.features.GeoJsonTooltip(
        fields=['terrain', '地形区分名'],
        aliases=['地形区分コード', '地形区分名'],
        localize=True
    ),
    name="【図】地形分類"
).add_to(m)

# 凡例の追加
linear_land.add_to(m)

# マップ表示
display(m)
```

コードの解説をします。

最初にベースマップについて説明します。
`tiles` には `cartodbpositron` を指定していますが、他の値でも設定可能です。
以下の記事を見ながら、タイルを切り替えてマップを作成するとよいでしょう。
[Python folium 指定できる 地図の タイル について](https://www.monotalk.xyz/blog/python-folium-%E6%8C%87%E5%AE%9A%E3%81%A7%E3%81%8D%E3%82%8B-%E5%9C%B0%E5%9B%B3%E3%81%AE-%E3%82%BF%E3%82%A4%E3%83%AB-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)

次に、`folium.GeoJson` 内の引数について説明します。
`data=gdf_land.to_json()` の `gdf_land.to_json()` は、data に GeoJSON を設定するという意味です。
geopandas の `to_json()`関数は、GeoDataFrame を GeoJSON に変換します。

```json:GeoJSONの内容(一部のみ)
{
    "type": "FeatureCollection",
    "features": [{
        "id": "10605",
        "type": "Feature",
        "properties": {
            "ID": 0,
            "NO": "c",
            "PREF": "東京都",
            "地形区分名": "埋立地",
            "地形大区分": "埋立地",
            "terrain": 3
        },
        "geometry": {
            "type": "Polygon",
            "coordinates": [
                [
                    [138.5015508233247, 36.00032080614344],
                    [138.50155081335006, 36.00032082883919],
                    [138.50155088118132, 36.000320862881736],
                    [138.50155093704217, 36.00032088395551],
                    [138.5015509729516, 36.000320838563816],
                    [138.50155099090588, 36.00032079479348],
                    [138.50155098691576, 36.00032078993018],
                    [138.5015509769406, 36.00032078506696],
                    [138.50155092107954, 36.00032075588761],
                    [138.50155089115398, 36.000320739676845],
                    [138.50155087918387, 36.000320739676994],
                    [138.50155086920887, 36.00032074291936],
                    [138.50155085324923, 36.00032076885741],
                    [138.5015508233247, 36.00032080614344]
                ]
            ]
        },
        "bbox": [138.50155081335006,
            36.000320739676845,
            138.50155099090588,
            36.00032088395551
        ]
    }]
}
```

`style_function` と `highlight_function` の `feature['properties']['terrain']` の部分は
GeoJSON上の `feature['properties']['terrain']`と紐づいています。
この部分を理解すれば、コロプレス図でのハイライト・スタイル設定は容易になります。

上記以外の箇所については、[公式ドキュメント](https://python-visualization.github.io/folium/index.html)を確認してください。

次に表示されたマップの内容を見ていきましょう。

地形区分に対応した色でエリアが表現されていますね。
右上にはカラーマップの凡例があり、フォーカスを合わせると、
ハイライトされた上でツールチップが表示されます。

![choropleth_land.png](https://qiita-image-store.s3.amazonaws.com/0/184077/c7b3bdfc-240c-28d8-934a-08e973e70cc3.png)

#### 8.6. コロプレス図(土壌図編)の作成

同様に土壌図のデータもコロプレス図にしてみましょう。

```python:In[7]
# コロプレス図の追加
folium.GeoJson(
    data=gdf_soil.to_json(),
    style_function=lambda feature: {
        'fillColor': linear_soil(feature['properties']['soil_type']),
        'color': 'gray',
        'weight': 2,
        'fill_opacity': 0.6,
        'line_opacity': 0.2,
    },
    highlight_function=lambda feature: {
        'fillColor': linear_land(feature['properties']['soil_type']),
        'color': 'gray',
        'weight': 4,
        'fill_opacity': 0.6,
        'line_opacity': 0.2,
    },
    tooltip=folium.features.GeoJsonTooltip(
        fields=['soil_type', '土壌区分名'], 
        aliases=['土壌区分コード', '土壌区分名'], 
        localize=True
    ),
    name="【図】土壌"
).add_to(m)

# 凡例の追加
linear_soil.add_to(m)

# マップ表示
display(m)
```

土壌図のデータをコロプレス図にできました。
地形分類図のレイヤーと重なっているため、非常に見にくいという問題があります。
この問題を解消するために、次にレイヤコントロールの説明をします。

![choropleth_soil.png](https://qiita-image-store.s3.amazonaws.com/0/184077/dbe366bf-d8cf-9ce3-10d8-64dea9ee19af.png)

#### 8.7. レイヤーコントロールの追加

レイヤコントロールの追加は非常に簡単です。
たった1行で済みます。

```python:In[8]
folium.LayerControl(collapsed=False).add_to(m)
display(m)  # マップ表示
```

マップにチェックボックスが追加されました。
これで図の種類を切り替え可能です。

![layer_control.png](https://qiita-image-store.s3.amazonaws.com/0/184077/abe62efa-b8dc-76ae-973c-68fc448c9235.png)

#### 8.8. マーカーの追加

地図上にマーカーを追加します。
マーカーのアイコン設定には[Font Awesome Icons](https://fontawesome.com/v3.2.1/icons/)を参照してください。

```python:In[9]
import folium.plugins

# ベースマップ
tokyo23_location = [35.658593, 139.745441]
m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10)

# マーカークラスター
mcg = folium.plugins.MarkerCluster(control=False)
m.add_child(mcg)

# グループ分け
g1 = folium.plugins.FeatureGroupSubGroup(mcg, '【敷地】訓練用')
m.add_child(g1)

g2 = folium.plugins.FeatureGroupSubGroup(mcg, '【敷地】テスト用')
m.add_child(g2)

# マーカー作成
folium.Marker(
    location=[35.7, 139.80],
    tooltip='訓練用_1',
    icon=folium.Icon(color='green', icon='heart')
).add_to(g1)

folium.Marker(
    location=[35.75, 139.85],
    tooltip='訓練用_2',
    icon=folium.Icon(color='green', icon='heart')
).add_to(g1)

folium.Marker(
    location=[35.80, 139.90],
    tooltip='テスト用_1',
    icon=folium.Icon(color='blue', icon='info-sign')
).add_to(g2)

folium.Marker(
    location=[35.85, 139.95],
    tooltip='テスト用_2',
    icon=folium.Icon(color='blue', icon='info-sign')
).add_to(g2)

# レイヤーコントロールの追加
folium.LayerControl(collapsed=False).add_to(m)

# マップ表示
display(m)
```

マーカークラスターにグループを2つ登録したので、
地図上でもそのようになっていますね。

![marker_1.png](https://qiita-image-store.s3.amazonaws.com/0/184077/df699015-0685-c33d-4667-faae292968e4.png)

地図を拡大すると、マーカーが4つ打たれていることが分かります。
画像では表示していませんが、マーカーにフォーカスを合わせるとツールチップが表示されます。
また、右上にあるグループ切り替えのチェックボックスで、マーカーの表示を切り替えられます。

![marker_2.png](https://qiita-image-store.s3.amazonaws.com/0/184077/cf1835a7-8cc7-aa17-0204-a16984050e54.png)

#### 8.9. タイルの追加

今までのタイルは `cartodbpositron` でした。
`google maps` と `google street view` を追加してみましょう。

```python:In[10]
tokyo23_location = [35.658593, 139.745441]
m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10) 

folium.raster_layers.TileLayer(
    tiles='http://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}',
    attr='google',
    name='google maps',
    max_zoom=20,
    subdomains=['mt0', 'mt1', 'mt2', 'mt3'],
    overlay=False,
    control=True,
).add_to(m)

folium.raster_layers.TileLayer(
    tiles='http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
    attr='google',
    name='google street view',
    max_zoom=20,
    subdomains=['mt0', 'mt1', 'mt2', 'mt3'],
    overlay=False,
    control=True,
).add_to(m)

folium.LayerControl(collapsed=False).add_to(m)
display(m)
```

タイルをラジオボタンで切り替えられるようになりました。

![tiles_google_maps.png](https://qiita-image-store.s3.amazonaws.com/0/184077/6f7391d7-3911-6534-f20c-a564f6526289.png)
![tiles_google_street_view.png](https://qiita-image-store.s3.amazonaws.com/0/184077/063c5e71-cb94-9cfe-4e8c-2fd916417549.png)

#### 8.10 ミニマップの追加

「東京を見ていたけど、次は北海道を見たい」というときに、
マップ上でスライドして移動するのは非常に面倒です。
そんな場合はミニマップを活用しましょう。

```python:In[11]
tokyo23_location = [35.658593, 139.745441]
m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10) 

minimap = folium.plugins.MiniMap(toggle_display=True, width=200, height=200)
m.add_child(minimap)

display(m)
```

このようになります。右下の矢印をクリックするとミニマップの表示切替が可能です。

![minimap_tokyo.png](https://qiita-image-store.s3.amazonaws.com/0/184077/c12d08fc-f9cb-5fcb-fc26-f3db138ff655.png)
![minimap_sapporo.png](https://qiita-image-store.s3.amazonaws.com/0/184077/34635282-4507-1789-efd5-93e0e3294a31.png)

#### 8.11. マウスポジションの追加

マップ上でマウスオーバーしているときに、その箇所の座標を知りたいときがあります。(きっとあるはずです・・・)
そういったときは、マウスポジションを追加しましょう。

```python:In[12]
tokyo23_location = [35.658593, 139.745441]
m = folium.Map(location=tokyo23_location,tiles='cartodbpositron',zoom_start=10) 

formatter = "function(num) {return L.Util.formatNum(num, 3) + ' º ';};"

folium.plugins.MousePosition(
    position='topright',
    separator=' | ',
    empty_string='NaN',
    lng_first=True,
    num_digits=20,
    prefix='Coordinates:',
    lat_formatter=formatter,
    lng_formatter=formatter
).add_to(m)

display(m)
```

マップの右上に座標が表示されるようになりました。
![mouseover.png](https://qiita-image-store.s3.amazonaws.com/0/184077/de85da4d-8b44-8b22-acda-95705bfdfcec.png)

#### 8.12. HTMLに出力

マップに可視化した結果を共有したい場合があります。
folium では HTML に出力して保存が可能です。

```python:In[13]
m.save('./test.html')
```

### まとめ

ヘルシンキ大学講座には、この記事に記載していない、役立つ内容が盛りだくさんです。
Python で GIS データを活用したい人は、ぜひ受講して下さい。

### 参考

- [ESRI ジャパン GIS 基礎解説](https://www.esrij.com/getting-started/what-is-gis/)
- [Automating GIS-processes 2018](https://automating-gis-processes.github.io/2018/index.html)
- [EPSGコードとは](http://club.informatix.co.jp/?p=1225)
- [日本のEPSGコード_EPSG:2450](https://epsg.io/2450)
- [測地系 Wikipedia](https://ja.wikipedia.org/wiki/%E6%B8%AC%E5%9C%B0%E7%B3%BB#%E6%97%A5%E6%9C%AC%E6%B8%AC%E5%9C%B0%E7%B3%BB)
- [Python folium 指定できる 地図の タイル について](https://www.monotalk.xyz/blog/python-folium-%E6%8C%87%E5%AE%9A%E3%81%A7%E3%81%8D%E3%82%8B-%E5%9C%B0%E5%9B%B3%E3%81%AE-%E3%82%BF%E3%82%A4%E3%83%AB-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)