PleasanterとLeafletを組み合わせた地図機能を導入する


## はじめに

Pleasanterの利用例として住所録や顧客リストとして利用されている事例も多いかと思います。そのような場合、Pleasanterの画面上で地図を表示させたいケースもあるのではないでしょうか。  
この記事では、PleasanterとLeafletと組み合わせて、Pleasanterに格納された位置情報をもとに対象地点を地図上に表示させる例をご紹介します。

以下の画像が最終的な実現例です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/c8681e84-1313-6339-397b-61ef602d9705.gif">

## Leafletとは

Leafletは、オープンソースのJavaScript地図ライブラリで、軽量でありながら様々な地図機能を備えています。Leafletについては、公式サイトや QiitaのLefletに関する投稿を確認してください。

https://leafletjs.com/

https://qiita.com/tags/leaflet

## サンプルデータ

地図に表示するサンプルデータとして、東京都のオープンデータカタログサイトで公開されている中野区の医療機関データを利用しました。


https://portal.data.metro.tokyo.lg.jp/

今回は予め対象となる地点の緯度と経度が記載されたデータを利用しています。  
所有しているデータが住所情報のみの場合はジオコーディングサービスを利用するなどして、緯度、経度の情報を別途用意してください。

https://qiita.com/tags/%e3%82%b8%e3%82%aa%e3%82%b3%e3%83%bc%e3%83%87%e3%82%a3%e3%83%b3%e3%82%b0

## 事前の準備

### Pleasanterの動作環境

Pleasanterのセットアップについては割愛します。
今回のデモではPleasanterのバージョンは ver.1.3.47.0 を使用しています。  

[プリザンターのインストール | Pleasanter](https://pleasanter.org/manual?category=0090_0120)

### サンプルデータのインポート

前述のサンプルデータをPleasanterへインポートします。 利用したデータは医療施設のデータであるため、所在地情報のほか、診療科目など様々な情報が記載されていますが、今回のデモでは最低限、施設の名称と緯度、経度の情報があれば実現できます。

PleasanterでのCSVインポートの操作方法については以下のマニュアルに記載されています。

[テーブル機能：レコードのインポート | Pleasanter](https://pleasanter.org/manual/table-record-import)

### ヘッダーにHTMLを挿入

対象サイトのテーブルの管理から `HTML`を選択し、以下のコードを 挿入位置 `Head bottom`を指定します。出力先は `編集` と `一覧` を指定します。

```html
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
```
今回、Leafletのkライブラリは CDNに配置されたものを参照しています。  

[Download - Leaflet - a JavaScript library for interactive maps](https://leafletjs.com/download.html)

Pleasanter の ver.1.3.45.0 でリリースされた 「HTMLヘッダ及びBODYのスクリプトの位置に任意のHTMLを出力する機能を追加」という機能を使うことで、上記のように任意のHTMLコードを埋め込みができるようになりました。

[Pleasanter テーブルの管理：HTML | Pleasanter](https://pleasanter.org/manual/insert-html)


## 編集画面へ地図を埋め込む

まずは、取り込んだ複数の医療施設データのうち、一施設のみを表示させた編集画面に地図を埋め込んだ表示をさせてみます。

### スタイルの設定

対象サイトのテーブルの管理から`スタイル`を選択し、以下のコードを貼り付けます。出力先は `編集` を指定します。

```css
#mapid {
   width:calc(100% - 20px);
   height:300px;
   margin:10px;
}
```
地図を埋め込む領域のサイズを指定します。

### スクリプト

対象サイトのテーブルの管理から`スクリプト`を選択し、以下のコードを貼り付けます。出力先は `編集` を指定します。緯度、経度の格納先の項目名は適宜変更してください。

```javascript
$p.events.on_editor_load = function () {

    // 画面内の コメント欄の前に openstreetmap の地図を挿入する
    const mapDiv = document.createElement('div');
    mapDiv.id = 'mapid';
    $("#CommentField").before(mapDiv);

    //Zoomレベルを指定
    let zoom = 15;
    // 画面内の緯度lat ,経度 lng を読み取り、地図
    let lat = parseFloat($p.getControl('ClassF').val());
    let lng = parseFloat($p.getControl('ClassG').val());
    let map = L.map('mapid').setView([lat, lng], zoom);

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
    L.marker([lat, lng]).addTo(map);
}
```

`$p.events.on_editor_load` は編集画面を読み込んだときに実行されます。  
サンプルでは 分類F(ClassF)と分類G(ClassG)にそれぞれ緯度、経度を格納しており、この値をもとに地図にマーカーを設定します。

[開発者向け機能：スクリプト：$p.events.on_editor_load | Pleasanter](https://pleasanter.org/manual/script-events-on-editor-load)  
[Documentation - Leaflet - a JavaScript library for interactive maps](https://leafletjs.com/reference.html#marker)

Leafletの地図の操作に関するは、Leafletのクイックスタートガイドやリファレンスを参照してください。

[Quick Start Guide - Leaflet - a JavaScript library for interactive maps](https://leafletjs.com/examples/quick-start/)
[Documentation - Leaflet - a JavaScript library for interactiv](https://leafletjs.com/reference.html)

### 編集画面への地図埋め込み動作例
ここまでの設定を行い、編集画面を開くと以下のようにコメント欄の上部に地図が表示されます。
![edit-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/6536d0a0-2000-6e7c-16e4-d3569b751e40.png)
## 一覧画面への地図埋め込み

続いて一覧画面に表示された複数の施設を同様に地図に表示させてみます。

### 一覧表の項目にセルCSSを設定

テーブルの管理から`一覧`を選択、対象となる列の`詳細設定`で以下の例ようにセルCSSを指定します。設定するセルCSS名はどのような値でも構いません。

|項目の名称|設定するセルCSS名|
|:--|:--|
|名前|cell_name|
|所在地|cell_address|
|緯度|cell_lat|
|経度|cell_lng|

セルCSSをは一覧画面で表示される表の項目列（th要素やtd要素）毎にCSSクラス名を指定することができます。主な目的はスタイル（CSS）と組み合わせたセルの装飾ですが、ここで項目列毎にクラスを設定することで、スクリプトから列名を指定しての値の参照が容易になります。

[テーブルの管理：一覧画面：項目の詳細設定：セルCSS | Pleasanter](]https://pleasanter.org/manual/table-management-extended-cell-css)

### スタイルの設定
対象サイトのテーブルの管理から`スタイル`を選択し、以下のコードを貼り付けます。出力先は `一覧` を指定します。
```css
#MainForm {
    width:70%;
}
#mapid {
   position:fixed;
   top:0px;
   right:0px;
   width:calc(30% - 20px);
   height: calc(100% - 140px);
   margin: 50px 10px 10px 10px;
}
```
通常は画面幅いっぱい表示される一覧表を幅70%に制限し、残りの30%に地図を埋め込む領域を作成しています。

### スクリプトの設定

対象サイトのテーブルの管理から`スクリプト`を選択し、以下のコードを貼り付けます。出力先は `一覧` を指定します。

```javascript
// マーカーを格納するための変数
var markers = L.featureGroup();
var map;

//地図を表示するエリアを作成し、地図を初期化
function initializeMap() {

    // 画面内の MainForm の後に地図を挿入
    const mapDiv = document.createElement('div');
    mapDiv.id = 'mapid';
    $("#MainForm").after(mapDiv);

    // Leaflet.jsで地図オブジェクトを生成
    map = L.map('mapid');

    // OpenStreetMapのタイルレイヤーを設定
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 
    {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);
}

//一覧 table 内の各行のデータを取得し、マーカーを地図へ追加
function addMarkers(){

    //一覧表の最初の一行を除く各行を順に処理
    $('#Grid tr').not(':first').each(function(row, tr){

        //セルCSSで指定したクラス名をもとに各セルから値を取得
        let name = $(tr).find('.cell_name').text();
        let address = $(tr).find('.cell_address').text();
        let lat = parseFloat($(tr).find('.cell_lat').text());
        let lng = parseFloat($(tr).find('.cell_lng').text());

        // 緯度と経度が数値であり、かつ範囲内にある場合にマーカーを追加
        if( lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180){

            //マーカーを追加 ポップアップには施設名と所在地を表示
            markers.addLayer(L.marker([lat, lng]).bindPopup(name + '<br>' + address));

            //一覧表の行(tr)にmouseoverイベント追加
            $(tr).on('mouseover', function() {

                //マーカーの各レイヤーを探索し、一覧表の緯度、経度が一致する場合、マーカーのポップアップを開く
                markers.eachLayer(function(layer) {
                    if (layer.getLatLng().lat == lat && layer.getLatLng().lng == lng) {
                        layer.openPopup();
                    }
                });
            });
        }
    });

    // マーカーを地図に追加
    markers.addTo(map);
    // すべてのマーカが地図に収まるよう表示範囲を調整
    map.fitBounds(markers.getBounds());
}

//マーカーを削除
function clearMarkers(){
    markers.clearLayers();
}

//「一覧画面」を読み込んだとき、もしくはフィルタ等で表示する内容が変わったときに実行
$p.events.on_grid_load = function () {

    if(!map){
        initializeMap();
        addMarkers();
    }
}

// サーバへデータを送信後、画面内容を更新後に実行
$p.events.after_set = function () {
    clearMarkers();
    addMarkers();
}
```
`$p.events.on_grid_load` は、一覧画面を読み込んだとき、もしくはフィルタ等で表示する内容が変わったときに実行されます。初回のみ地図の基本的な設定を行う初期処理を実行しています。

`$p.events.after_set` は、サーバへデータを送信後、画面内容を更新後に実行されます。
一覧画面において、フィルタで検索条件を指定して検索結果が表示さたタイミングと、画面の初期表示行数よりも多いデータがスクロールにより追加でロードされたタイミングでも実行されます。  
ここでは、各タイミングでいったん地図上のマーカーを削除したうえで、再度一覧表上のすべての項目をマーカーとして登録しています。

[開発者向け機能：スクリプト：$p.events.on_grid_load | Pleasanter](https://pleasanter.org/manual/script-events-on-grid-load)
[開発者向け機能：スクリプト：$p.events.after_set | Pleasanter](https://pleasanter.org/manual/script-events-after-set)

Leafletのマーカーの操作は編集画面でのケースと基本的には同じことをしていますが、tableの各行をマウスオーバーした際に、施設名がポップアップ表示される処理を追加しています。

### 一覧画面での地図埋め込み動作例
#### フィルタ操作
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/b6a5f0de-fad1-da9a-3fd2-09333b01761f.gif)

#### ポップアップ表示
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/b805b9f0-5cbe-834e-929d-eafa69af23d5.gif)
#### スクロール操作
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492700/59e22a5a-1cd8-16ae-a338-285c54e1f181.gif)

## まとめ

以上のような手順でPlesanterとLeafletを組み合わせた簡単な地図表示を実現することができました。  

Leafletには標準機能のほか、プラグインを導入することで更に多くの機能を実現することができます。例えば、今回の記事では省略しましたが、住所の情報から緯度、経度を変換するプラグインなども存在します。これらを組み合わせて、Pleasanter内にある住所、位置情報を活用してはいかがでしょうか。

https://leafletjs.com/plugins.html#geocoding