色々なマップライブラリでポイント表示数の限界を探ってみた

![sample_2011_02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/9bc0499c-bdb8-1ee1-d98a-b90121ca3d05.gif)

<br>

![try-051_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/0697262b-a3e9-ee9b-d1f9-122f0bc26a30.png)

<br>

この記事は、「[FOSS4G Advent Calendar 2020](https://qiita.com/advent-calendar/2020/foss4g)」の9日目の記事です。

### Mapbox GL JS・Leaflet・OpenLayersでのポイント表示数の限界を探ってみました :tada:  

<br>

JSマップライブラリで頑張ってアプリケーションを構築したのにブラウザの**表示動作が重く**感じたことはありませんか？

それはアレです...ライブラリとブラウザの限界です...

一昔前は、Leafletの前バージョンで**数千件**のポイント表示でもブラウザが固まり限界でした。そのため、ラスタタイル化をしたりバックエンドを構築して画面範囲のみを描画したりと工夫が必要でした。

しかし、近年の充実したJSマップライブラリの登場により、**驚くほど大量なデータ**がフロンエンドのみでも表示できるようになり選択肢が広がりました :thumbsup: 

今回はその限界を探ってみようということで、どのマップライブラリが適しているのか、フロントエンドのみの仕組みで問題ないのか、バックエンドの構築も必要なのか等、大体この辺で最初にはまったりすると思うので技術選定の参考にしていただければと思います。

**結果的には、Mapbox GL JS v1のバイナリベクトルタイル(pbf)読み込みが圧倒的勝利で、70万件以上のデータをフロントエンドのみで表示することが可能でした :tada:**

※ジオメトリタイプや属性のサイズによって最大表示件数は変わります。

<br>

### 事前準備

手軽に始めるビルド環境公開しているので今回はこちらを利用しています。

- 各マップライブラリの環境準備
[mapboxgljs-starter](https://github.com/dayjournal/mapboxgljs-starter)
[leaflet-starter](https://github.com/dayjournal/leaflet-starter)
[openlayers-starter](https://github.com/dayjournal/openlayers-starter)

- 検証した各マップライブラリのバージョン
Mapbox GL JS v1.11.0
Leaflet v1.60
OpenLayers v6.3.1

- 背景地図
背景地図には[Maptiler](https://maptiler.jp)を読み込んでみました :thumbsup:
読み込み方法 → [色々なマップライブラリでMapTilerを表示してみた](https://qiita.com/dayjournal/items/8fe865169e89c8bad343)

<br>

### 利用データ

膨大なポイントデータを利用したかったので、国土地理院の[電子国土基本図(地名情報)「住居表示住所」](https://www.gsi.go.jp/kihonjohochousa/jukyo_jusho.html)データを利用してみました。サンプルでは札幌市(一部地域を除く)のデータ**約70万レコード**を利用しています :bulb: 

データはCSVとShapeで提供されているので、今回はShapeをQGISでいい感じに結合したり、GeoPackageやGeoJSONでエクスポートしたり、tippecanoeでpbfに変換したりしてみました。この辺は話すと色々あるので、興味あるかたはゼヒこちらのイベント「[GeoSaturday 2020 Online](https://connpass.com/event/198040)」でお話ししましょう。(宣伝)

![](https://day-journal.com/memo/images/try-051_16.png)

属性値は少なすぎず多すぎずの4カラム程度にしてみました。
![](https://day-journal.com/memo/images/try-051_17.png)

<br>

### 実装例

ポイントを表示するためには、基本的にはスターターのscript.jsにコード追記するのみになります。今回はこんな感じで設定してみました。

#### Leaflet

```javascript
// ポイント設定
fetch('https://xxxxx/sample.geojson')
    .then(data => {
        return data.json();
    })
    .then(geojson => {
        L.geoJson(geojson, {
            pointToLayer: function (feature, layer) {
                return L.circleMarker(layer, {
                    color: '#014c86',
                    radius: 3,
                    weight: 1,
                    opacity: 0.7,
                    fill: true,
                    fillColor: '#014c86',
                    fillOpacity: 0.7
                });
            }
        }).addTo(map);
    });
```

#### OpenLayers

```javascript
// ポイントスタイル設定
const styles = {
    'Point': new Style({
        image: new Circle({
            radius: 3,
            stroke: new Stroke({
                color: 'rgba(1, 76, 134, 0.7)',
                width: 1
            }),
            fill: new Fill({
                color: 'rgba(1, 76, 134, 0.7)'
            })
        })
    })
};
const styleFunction = function(feature) {
    return styles[feature.getGeometry().getType()];
};
// ポイントソース設定
const vectorSource = new VectorSource({
    url: 'https://xxxxx/sample.geojson',
    format: new GeoJSON()
});
// ポイントレイヤ設定
const vectorLayer = new VectorLayer({
    source: vectorSource,
    style: styleFunction
});
```

#### Mapbox GL JS v1 (GeoJSON)

```javascript
// ポイントソース設定
map.addSource('point_sample', {
    type: 'geojson',
    data: 'https://xxxxx/sample.geojson'
});
// ポイントスタイル設定
map.addLayer({
    id: 'point_sample',
    type: 'circle',
    source: 'point_sample',
    layout: {},
    paint: {
        'circle-color': '#014c86',
        'circle-radius': 3,
        'circle-opacity': 0.7
    }
});
```

#### Mapbox GL JS v1 (pbf)

```javascript
// ポイントソース設定
map.addSource('point_sample', {
    type: 'vector',
    tiles: ['https://xxxxx/sample/{z}/{x}/{y}.pbf']
});
// ポイントスタイル設定
map.addLayer({
    id: 'point_sample',
    type: 'circle',
    source: 'point_sample',
    source-layer: 'xxxxx',
    layout: {},
    paint: {
        'circle-color': '#014c86',
        'circle-radius': 3,
        'circle-opacity': 0.7
    }
});
```

<br>

### 比較検証

それでは、比較しながら表示数の限界を探っていこうと思います :thumbsup: 
今回はこの5種類の**GeoJSON**と**pbf**で試してみました。サイズはGeoJSONのサイズになります。

| 件数 | サイズ |
|:-:|:-:|
| 50,000件 | 11MB |
| 100,000件 | 22MB |
| 200,000件 | 45MB | 
| 450,000件 | 108MB |
| 700,000件 | 162MB |

検証環境はざっくりこんな感じです。

- ネット: テザリング環境 (18Mbps)
- ブラウザ: Chrome最新版
- OS: macOS Catalina (MacBook Pro 13インチ)
- CPU: Intel Core i7 (2.5GHz デュアルコア)
- メモリ: 16GB
- 参照先: ローカル環境ではなくサーバーにデプロイして参照

#### 50,000件 - 11MB

##### Leaflet
表示されるけど動き重い。表示までに6秒。
![try-051_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/d89de0f6-d74a-3519-2def-8b6438389bb9.png)
![try-051_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/b918442d-e433-e5f9-8b7b-fc336a9b5564.png)

##### OpenLayers
表示OK。表示までに7秒。
![try-051_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/4f8ef8dd-1ce2-1973-60fd-6e0d4de2dbb9.png)
![try-051_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/4a4e386d-db40-8a0a-f29e-46a7db693f42.png)

##### Mapbox GL JS v1 (GeoJSON)
表示OK。表示までに8秒。

##### Mapbox GL JS v1 (pbf)
表示OK。表示までに2秒。
![try-051_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/d8262f57-d698-b090-72f0-7a84ca2138a9.png)
![try-051_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/3dfd17fc-403e-5486-e234-f5e6f33644fa.png)


#### 100,000件 - 22MB

##### Leaflet
表示されない。

##### OpenLayers
表示OK。表示までに10秒。
![try-051_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/5e1f6b00-d9e3-5c7e-4be1-1f80d1d196eb.png)

##### Mapbox GL JS v1 (GeoJSON)
表示OK。表示までに28秒。

##### Mapbox GL JS v1 (pbf)
表示OK。表示までに3秒。
![try-051_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/f76492cb-598f-f8a7-3c13-9076fbb747e8.png)


#### 200,000件 - 45MB

##### OpenLayers
表示されるけど広域表示が動き重い。表示までに23秒。
![try-051_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/5b527147-9b6e-cc21-8d7d-f9f0d2852cbc.png)

##### Mapbox GL JS v1 (GeoJSON)
表示OK。表示までに38秒。

##### Mapbox GL JS v1 (pbf)
表示OK。表示までに4秒。

![try-051_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60444/f857f5a6-54ac-5e3d-1f9b-5bda2f26bf98.png)


#### 450,000件 - 108MB

##### OpenLayers
表示されるけど広域表示が動き重い。表示までに45秒。
![](https://day-journal.com/memo/images/try-051_12.png)

##### Mapbox GL JS v1 (GeoJSON)
表示OK。表示までに75秒。

##### Mapbox GL JS v1 (pbf)
表示OK。表示までに4秒。
![](https://day-journal.com/memo/images/try-051_13.png)


#### 700,000件 - 162MB

##### OpenLayers
表示されない。

##### Mapbox GL JS v1 (GeoJSON)
表示されるけどブラウザ落ちる。表示までに120秒。

##### Mapbox GL JS v1 (pbf)
表示OK。表示までに5秒。
![](https://day-journal.com/memo/images/try-051_15.png)

<br>

#### Mapbox GL JS・Leaflet・OpenLayersでのポイント表示数の限界を探ることがでました :thumbsup: 

表にまとめるとこんな感じになります :bulb: 

**ポイントデータ表示可否**

| マップライブラリ | 50,000件 | 100,000件 | 200,000件 | 450,000件 | 700,000件 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| Leaflet  | △ | ✕ | ✕ | ✕ | ✕ |
| OpenLayers  | :thumbsup: | :thumbsup: | △  | △ | ✕ |
| Mapbox GL JS v1 (GeoJSON)  | :thumbsup: | :thumbsup: | :thumbsup: | :thumbsup: | △ |
| Mapbox GL JS v1 (pbf)  | :thumbsup: | :thumbsup: | :thumbsup: | :thumbsup: | :thumbsup: |

<br>

**初期表示までの時間 (秒)**

| マップライブラリ | 50,000件 | 100,000件 | 200,000件 | 450,000件 | 700,000件 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| Leaflet  | 6秒 | ✕ | ✕ | ✕ | ✕ |
| OpenLayers  | 7秒  | 10秒  | 23秒  | 45秒 | ✕ |
| Mapbox GL JS v1 (GeoJSON)  | 8秒 | 28秒 | 38秒 | 75秒 | 120秒 |
| Mapbox GL JS v1 (pbf)  | 2秒 | 3秒 | 4秒 | 4秒 | 5秒 |

※検証はテザリング環境(18Mbps)であえておこなってみたので、通常の自宅でのインターネット環境を利用すると何十倍で表示される可能性はあります。

### Mapbox GL JS・Leaflet・OpenLayersでのポイント表示数の限界を探ることができました :tada:  

Leafletは、**約5万件程度**。
OpenLayersは、**約20万件程度**。
Mapbox GL JS v1 (GeoJSON)は、**約50万件程度**。
Mapbox GL JS v1 (pbf)は、**70万件以上**。

Leafletは、v1になった時にレンダリングが改善されたとはいえ、やはり手軽な分他のライブラリに比べて厳しそうで、OpenLayersとMapbox GL JS v1 (GeoJSON)は表示されれば問題なく動作するのですが、読み込みに時間がかかるのがWebアプリケーションとして現実的ではない気もします。Mapbox GL JS v1 (pbf)は、表示時間等特に問題ないですが、間引き処理の問題で一部欠けたように見えたり再描画処理について少し時間かかることがあったりしました。

今回の検証で、フロントエンドのみでもMapbox GL JS v1を利用することにより、**GeoJSONで約50万件程度**、**バイナリベクトルタイル(pbf)で70万件以上**を表示できることがわかりました :bulb: 

ただ、データといってもポイントをアイコンに変更したり、クラスタ化したり、バイナリベクトルタイルの変換時に簡素化したり、ポリゴンやラインもあるのでサイズは大きくなりますし、ブラウザの種類も影響しますし、検索やスタイル変更についても規模が大きくなってきたり、データの登録削除機能が必要だとバックエンドの導入も必要になってきます。 

フロントエンドでできる幅は広がったとはいえ、バックエンドの処理が必要とされる機会は今後もたくさんあると思うので、要件に合わせた技術選定の参考にしていただければと思います :pencil2:

これを見たどなたかが、deck.glやCesium等の他のマップライブラリでも探ってくれることを祈ります :pray:

**最後にブラウザで数千件以上表示できるだけでもすごい時代になったなと思いました :muscle:**　(いまさら)

<br>

Mapbox GL JS・Leaflet・OpenLayersについて、他にも記事を書いています。よろしければぜひ :bow:
<a href="https://day-journal.com/memo/tags/MapboxGLJS" target="_blank">tags - Mapbox GL JS</a>
<a href="https://day-journal.com/memo/tags/Leaflet" target="_blank">tags - Leaflet</a>
<a href="https://day-journal.com/memo/tags/OpenLayers" target="_blank">tags - OpenLayers</a>

やってみたシリーズ :grinning:
<a href="https://day-journal.com/memo/tags/Try" target="_blank">tags - Try</a>

<br>

※「このアプリケーションの作成に当たっては、国土地理院長の承認を得て、同院発行の電子国土基本図(地名情報)住居表示住所を使用した。(承認番号 平30情使、第928号)」

<br>
<br>

<a href="https://dayjournal.booth.pm" target="_blank"><img src="https://day-journal.com/memo/images/etc/book.png" alt="book"></a>