静岡県の点群データDBをVue+Leafletで表示してみた

## 今回作ったもの
[静岡県点群データマップ](https://shizuoka-pcdb.herokuapp.com/)
[GitHub - shizokaPCDB](https://github.com/Kanahiro/shizuokaPCDB)
![スクリーンショット 2020-01-16 1.35.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/ab1fccb5-5423-2c37-0543-7f6d5dda839d.png)

## 注意
この記事はLIDARデータをLeafletに表示する記事ではありません。
点群データベースの情報（データの取得地や工事日など）を表示するものです。

## はじめに
![スクリーンショット 2020-01-16 1.49.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73197/9a7c4882-95a2-a542-43d7-2e19eef08d7a.png)
Rockな静岡県庁さんは、自前のウェブサイト[Shizuoka Point Cloud DB](https://pointcloud.pref.shizuoka.jp/)（以下「静岡PCDB」）で工事成果品のLIDARデータをオープンデータとして多数公開しています。興味本位でソースを眺めてみると、外部からでもデータを取得出来そうだなとわかりました（htmlにJavaScriptが直書きだったため）。ただこのサイト、上記画像を見るとわかるとおり①マーカーの数が多くパフォーマンスに影響が出ている事と、②地図領域が変わるたびにその領域に含まれるデータをサーバーから取得しておりさらにパフォーマンスが低下している事、という2つの問題があると思いました。データが外部からでも取得出来るなら改善の余地があるかもと思い、開発に至った次第です。

そんな訳でデータベースへのアクセス・整形とフロントでの表示の二本立てでお送りします。

## 使用技術
- Flask
- Vue.js
- Vue2-leaflet

## データベースへのアクセス・整形
###アクセス
静岡PCDBのソースを見ると、サーバーへのアクセス・Leafletでの表示は100行程度で実装されていました。
サーバーからデータを取得する部分を以下に示します。

```javascript

var data = { request : "MarkerSet",
                Xmax : map.getBounds().getNorthEast().lng,
                Xmin : map.getBounds().getSouthWest().lng ,
                Ymax : map.getBounds().getNorthEast().lat,
                Ymin : map.getBounds().getSouthWest().lat };
    $.ajax("ankenmapsrc",{
        type: "GET",
        data: data,
        success: function(data, dataType){
            var myIcon = L.icon({
                iconUrl: 'http://cdn.leafletjs.com/leaflet-0.5/images/marker-icon@2x.png',
                iconSize: [15,25],
                iconAnchor: [5, 5],
            });
//以下処理が続きます
```

dataというクエリでankenmapsrcにアクセスすれば、データが得られそうです。
ここでdataはリクエスト名"MarkerSet"と取得する領域を示すXmax,Xmin,Ymax,Yminを持ちます。
領域の4つの変数には、Leafletで表示している領域が格納されます。
この処理はLeafletの表示領域が更新されるたびに呼ばれます。つまりその都度サーバーからデータを取得している訳です。

さてこのリクエストの返り値を見てみましょう。

```
30XXX01010001:平成30年度韮山反射炉計測業務:138.96214537214:35.03962001009?
28XXX00030007:白糸の滝滝見橋周辺整備事業　その７:138.58870495572:35.312506370532?
28XXX00030008:白糸の滝滝見橋周辺整備事業　その８:138.58881502806:35.312596432406?
28XXX00030009:白糸の滝滝見橋周辺整備事業　その９:138.58892510063:35.312686494178?
29C2001011361:平成２９年度［第２９-Ｃ２００１-０１号］　伊豆半島の屋外広告物の実態調査業務委託（函南町道_1-2号線）:138.93794860595:35.083520492945
...
```
本来は改行はありませんが、説明のため追加しています。
このデータを見ると、工事ごとの区切り文字は"?"で、データ要素ごとの区切りは":"である事がわかります。

###整形
Flaskで静岡PCDBにアクセスし、整形したデータをレスポンスするAPIサーバをつくります。

```python
import urllib.request, urllib.parse
import json
@app.route('/markers')
def getMarkers():
    #全件取得するために、静岡県全域が含まれる緯度経度を整数値で設定
    xMax = 140
    xMin = 137
    yMax = 36
    yMin = 33

    params = {
        'request':'MarkerSet',
        'Xmax':xMax,
        'Xmin':xMin,
        'Ymax':yMax,
        'Ymin':yMin
    }
    p = urllib.parse.urlencode(params)
    url = "https://pointcloud.pref.shizuoka.jp/lasmap/ankenmapsrc?" + p

    #上記で生成したURLパラメータでSIZUOKA POINT CLOUD DBにリクエストし案件一覧文字列を取得
    allAnkenStr = ""
    with urllib.request.urlopen(url) as res:
        allAnkenStr = res.read().decode()

    #returnするjsonを作成
    ankensObj = {
        "ankenList":[]
    }

    ankenList = allAnkenStr.split('?')
    for anken in ankenList:
        ankenInfo = anken.split(':')
        #不適切なデータがあった場合、スキップする
        if len(ankenInfo) != 4:
            continue

        #和暦を西暦に変換
        yy = int(ankenInfo[0][:2])
        #令和
        if yy < 24:
            yyyy = 2018 + yy
        else:
            yyyy = 1988 + yy

        ankenObj = {
            "no":ankenInfo[0],
            "name":ankenInfo[1],
            "lon":ankenInfo[2],
            "lat":ankenInfo[3],
            "year":yyyy
        }
        ankensObj['ankenList'].append(ankenObj)
    return jsonify(ankensObj)
```

冒頭で説明した問題点②地図領域が変わるたびにその領域に含まれるデータをサーバーから取得しておりさらにパフォーマンスが低下している事については、静岡県の領域を全て含むXmax,Xmin,Ymax,Yminとする事で解決しました。というのも、全件でも1400件程度であり、実際に全件取得してみても遅くはなかったため、最初に全部取得しておけば良いという結論になりました。

取得したankenデータをパースして、工事番号、工事名、経度・緯度、工事年度をjsonとしてreturnする事としました。


## フロントでの表示
残る問題点①マーカーの数が多くパフォーマンスに影響が出ている事ですが、動作の早いMapbox GL JSで表示すれば解決すると考えていました。しかしながら実装してみたところ、マーカーの表示はLeafletより遅くなってしまいました。という事で、LeafletのMarkerClusterを採用する事でこの問題を解決しました。

LeafletとVueの環境構築については[Try #027 – Vue.jsでLeafletとMapbox GL JSの開発環境を構築してみた](https://day-journal.com/memo/try-027/)が詳しいので割愛します。
くわえて、[vue2-leaflet-markercluster](https://github.com/jperelli/vue2-leaflet-markercluster)をインストールします。

```
npm install vue2-leaflet-markercluster
```


### VueでAPIサーバに非同期通信
FetchAPIを使います。
App.vueでデータを取得しMapPane.vueへ渡します。

```vue:App.vue
<MapPane :ankens="ankens"/>
```

```javascript:App.vue
  data() {
    return {
      ankens:[],
    }
  },
  created() {
    let vm = this
    fetch("/markers")
    .then(response => {
        return response.json()
    })
    .then(data => {
        //sortしてからdataを渡しています
        vm.ankens = data.ankenList.sort(function (a, b) {
            if (a.no < b.no) {
                return 1
            }
            if (a.no > b.no) {
                return -1
            }
            return 0 
        }).sort(function (a, b) {
            if (a.year < b.year) {
                return 1
            }
            if (a.year > b.year) {
                return -1
            }
            return 0 
        })
    })
    .catch(error => {
        console.log(error)
        alert("エラーが発生しました。")
    });
  }
```

```vue:MapPane.vue
<template>
    <div class="mapPane">
        <l-map
            :zoom="zoom"
            :center="center"
            :preferCanvas="true"
        >
            <l-control-scale
                position="bottomleft"
                :imperial="false"
                :metric="true"
            ></l-control-scale>

            <l-tile-layer
                :name="tileProvider.name"
                :visible="tileProvider.visible"
                :url="tileProvider.url"
                :attribution="tileProvider.attribution"
            ></l-tile-layer>

            <Vue2LeafletMarkerCluster :options="clusterOptions" >
                <LMarker v-for="anken in ankens" :key="anken.no" :lat-lng="makeLatLng(anken)" @click="onMarkerClick(anken.no)">
                    <LPopup :content="makeMarkerContent(anken)" ></LPopup>
                </LMarker>
            </Vue2LeafletMarkerCluster>
        </l-map>
    </div>
</template>
```

```vue:MapPane.vue

props: {
    ankens:Array
},
```

```vue:MapPane.vue
<style scoped>
    @import "~leaflet.markercluster/dist/MarkerCluster.css";
    @import "~leaflet.markercluster/dist/MarkerCluster.Default.css";

</style>
```

LMarkerをVue2LeafletMarkerClusterで包めば良きにはからってくれます。
以上により、密接したマーカーはクラスター（束）となり、拡大するまでまとめてひとつの地物として表示されます。結果問題点①も解決しました。

## おわりに
今後はデータの一覧表示をソート出来たり検索出来たり、ローディング中のアニメーションを表示したいなと思っています。LIDARデータがアツいらしく調べていると静岡PCDBに辿り着きましたが、思わぬ大脱線となりました。これほど多数の貴重なデータをオープンデータとする静岡県さんはマジでRockであり、最近では[兵庫県さんもRockerの仲間入りをしたようです。](https://qiita.com/Yfuruchin/items/2a9b017d840c46c37f3e)オープンデータ活用の本流とは若干逸れている気がする今回の案件ですが、少なくとも自己研鑽にはなったかなと思います。これからもオープンデータ界隈にはアンテナを張っておきたいと思います。