フリーダムの日記

GIS(地理情報システム)を中心に技術的なことを書いています。

kintone × MapLibre × ArcGIS で地図アプリの作成

はじめに

この記事は MapLibre Advent Calendar 202420日目の記事です。

qiita.com

以前に kintone と地図連携による顧客リストの可視化という内容で、kintone と ArcGIS の連携について紹介しました。

freedom-tech.hatenablog.com

kintone 連携の地図 API として、ArcGIS Maps SDK for JavaScript(当時は ArcGIS API for JavaScript)を使用して紹介しましたが、今回は、地図 API として、MapLibre GL JS を使用して、不動産物件マスタから物件情報の表示と物件などから到達圏解析も行えるように機能を追加しました。

kintone × MapLibre × ArcGISkintone × MapLibre × ArcGIS

youtu.be

MapLibre GL JS から背景地図到達圏解析には、ArcGISLocation Platform を使用しました。もちろん、無償の範囲で作成しています。

今回は、MapLibre GL JS から kintone 上の物件不動産マスタのデータ表示と、背景地図、到達圏解析について簡単に紹介したいと思います。具体的な kintone との連携方法等は、前回の記事cybozu が開発者向けなどに提供している 「cybozu developer network」などを参考にしていただけたらと思います。

MapLibre GL JS と kintone 連携

kintone の環境は、開発環境を使用します。はじめに kintone の開発環境でアプリを作成します。幾つかデフォルトでアプリが用意されており、今回は不動産物件マスタを使用しました。不動産物件マスタのフォームからアプリで使用する項目を作成して、ダミーの不動産データを登録するなどして、アプリを作成します。

kintone × MapLibre × ArcGIS

その後、アプリの設定の JavaScript/CSS カスタマイズにて、MapLibre GL JS で使用する各ライブラリの 参照先として、CDN の登録や JavaScript のファイルを登録していきます。kintone 上のデータ表示や地図表示などの実装は、index.js というファイルを作成して、そこで実装しています。index.js がエラーとならなければ、開発環境上で確認することができます。

MapLibre GL JS - ArcGIS

MapLibre GL JS と ArcGIS 連携

MapLibre から ArcGIS のサービスを使用するため、ArcGIS Location Platform を使用していきます。ArcGIS Location Platform は無償から使用できるサービスで、高品質なマップとロケーションサービスを使用することができます。ArcGIS Location Platform は、開発者アカウントを作成することで、無償から使用できます。開発者アカウントの作成や ArcGIS Location Platform の使い方などについては、ESRIジャパンが運用している ArcGIS 開発リソース集の開発者アカウントの作成が参考になるかと思います。

その他、ArcGIS のサービスを利用するためには API キーなどのアクセス トークンが必要となります。API キーの作成は、ArcGIS 開発リソース集の API キーの取得が参考になります。

また、MapLibre GL JS から ArcGIS のロケーション サービスを利用する方法や Get started など、ガイドでも詳しく紹介されていますので、以下のガイドを確認していただけたらと思います。

developers.arcgis.com

ここでは、背景地図の表示や kintone 上のデータを地図で表現する方法、到達圏解析について簡単に紹介します。

まずは、MapLibre GL JS から背景地図を表示していきます。 以下のガイドでは、MapLibre GL JS からの背景地図の表示や ArcGIS で利用できる背景地図なども紹介しています。

developers.arcgis.com

以下のコードは、背景地図の表示と各地図コントロールの追加部分になります。

let basemapEnum = "arcgis/community";

// 背景地図の設定
const map = new maplibregl.Map({
    container: "viewDiv", // the id of the div element
    style: `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapEnum}?token=${accessToken}&language=ja`,
    zoom: 13, // starting zoom
    center: [139.701636, 35.658034], // starting location [longitude, latitude]
    pitch: 70,
    hash: true
});

map.addControl(
    new maplibregl.NavigationControl({
        visualizePitch: true,
        showZoom: true,
        showCompass: true
    })
);

let scale = new maplibregl.ScaleControl({
    maxWidth: 200,
    unit: 'metric'
});

map.addControl(scale, "bottom-right");   

次のコードでは、kintone のレコード情報を取得しています。各属性情報を取得して、地図上での物件情報の表示(マーカー表示)とポップアップで物件情報の表示を実現しています。

// レコード情報を取得します
let records = event['records'];

for (let record of records) {
        
    let lat = record['緯度']['value'];
    let lon = record['経度']['value'];
          
    let name = record['物件名']['value'];
    let address = record['物件所在地']['value'];
    let price = record['価格']['value'];
    let bedrooms = record['寝室数']['value'];
    let bathrooms = record['バスルーム数']['value'];
    let meters = record['平方メートル']['value'];

    let marker = new maplibregl.Marker()
        .setLngLat([lon, lat])
        .addTo(map);

    let popup = new maplibregl.Popup();

    popup.on('open', () => {
      
      let popupContents = `<b>${name}</b><br>`;
      if (address) popupContents += `物件所在地:${address}<br>`;
      if (price) popupContents += `価格:${price.toLocaleString()}円<br>`;
      if (bedrooms) popupContents += `寝室数:${bedrooms}<br>`;
      if (bathrooms) popupContents += `バスルーム数:${bathrooms}<br>`;
      if (meters) popupContents += `平方メートル:${meters}㎡`;
      
      popup.setHTML(popupContents);
    });

    marker.setPopup(popup)
}    

最後に、到達圏解析になります。 以下のガイドでは、ある場所から指定された走行時間で到達可能なエリアを計算する方法について紹介しています。

developers.arcgis.com

到達圏解析では、ArcGIS REST JS を使用して、serviceArea メソッドを使用しています。

// 到達圏解析
map.on("click", (e) => {

  let coordinates = e.lngLat.toArray();
  let point = {
    type: "Point",
    coordinates
  };
  map.getSource("start").setData(point);
  
  let authentication = arcgisRest.ApiKeyManager.fromKey(accessToken);

  let addParams = { defaultBreaks: [0.25, 0.5, 0.75, 1], // km
                      impedanceAttributeName: "Kilometers",
                  }; 
  // ArcGIS Rest JS によるリクエスト
  arcgisRest
    .serviceArea({
      authentication,
      facilities: [coordinates],
      params: addParams
    })

    .then((response) => {

      map.getSource("servicearea").setData(response.saPolygons.geoJson);

      let features = [];
      for (let value of response.saPolygons.features) {
          //console.log(value.attributes.ToBreak);
          let feature = {
              "type": "Feature",
              "geometry": {
                  "type": "Point",
                  "coordinates": value.geometry.rings[0][0]
              },
              "properties": {
                  "ToBreak": value.attributes.ToBreak * 1000 + " m"
              }
          }
          features.push(feature);
      }
      let geojson = {type: "FeatureCollection", features: features}
      map.getSource("servicearealabel").setData(geojson);
      //console.log(features)
    });

});

全体のコードは以下となります。今回は、一覧表示のみの実装となっています。

(function () {
    "use strict";
    // ArcGIS Location Platform 
    const accessToken = "<accessToken> ";

    // 一覧画面を開いた時に実行します
    kintone.events.on('app.record.index.show', function(event) {

        // 一覧の上部部分にあるスペース部分を定義します       
        let elAction = kintone.app.getHeaderSpaceElement();

        // すでに地図要素が存在する場合は、削除します
        // ※ ページ切り替えや一覧のソート順を変更した時などが該当します
        let mapCheck = document.getElementsByName('viewDiv');
        if (mapCheck.length !== 0) {
            elAction.removeChild(mapCheck[0]);
        }

        // 地図を表示する div 要素を作成します
        let viewDiv = document.createElement('div');
        viewDiv.setAttribute('id', 'viewDiv');
        viewDiv.setAttribute('name', 'viewDiv');
        viewDiv.setAttribute('style', 'width: auto; height: 450px; margin-right: 30px; border: solid 2px #c4b097');
        elAction.appendChild(viewDiv);
        
        // レコード情報を取得します
        let records = event['records'];

        let basemapEnum = "arcgis/community";
        
        // 背景地図の設定
        const map = new maplibregl.Map({
            container: "viewDiv", // the id of the div element
            style: `https://basemapstyles-api.arcgis.com/arcgis/rest/services/styles/v2/styles/${basemapEnum}?token=${accessToken}&language=ja`,
            zoom: 13, // starting zoom
            center: [139.701636, 35.658034], // starting location [longitude, latitude]
            pitch: 70,
            hash: true
        });
        
        map.addControl(
            new maplibregl.NavigationControl({
                visualizePitch: true,
                showZoom: true,
                showCompass: true
            })
        );
        
        let scale = new maplibregl.ScaleControl({
            maxWidth: 200,
            unit: 'metric'
        });
      
        map.addControl(scale, "bottom-right");        
        
        for (let record of records) {
                
            let lat = record['緯度']['value'];
            let lon = record['経度']['value'];
                  
            let name = record['物件名']['value'];
            let address = record['物件所在地']['value'];
            let price = record['価格']['value'];
            let bedrooms = record['寝室数']['value'];
            let bathrooms = record['バスルーム数']['value'];
            let meters = record['平方メートル']['value'];
        
            let marker = new maplibregl.Marker()
                .setLngLat([lon, lat])
                .addTo(map);
        
            let popup = new maplibregl.Popup();

            popup.on('open', () => {
              
              let popupContents = `<b>${name}</b><br>`;
              if (address) popupContents += `物件所在地:${address}<br>`;
              if (price) popupContents += `価格:${price.toLocaleString()}円<br>`;
              if (bedrooms) popupContents += `寝室数:${bedrooms}<br>`;
              if (bathrooms) popupContents += `バスルーム数:${bathrooms}<br>`;
              if (meters) popupContents += `平方メートル:${meters}㎡`;
              
              popup.setHTML(popupContents);
            });

            marker.setPopup(popup)
        }    
                
        map._controls[0].options.customAttribution += " | Powered by Esri "
        map._controls[0]._updateAttributions()
        
        map.on("load", () => {
          addServiceAreaLayer();
          addServiceAreaLabelLayer();
          addStartingPointLayer();
        });
        
        // 到達圏解析
        map.on("click", (e) => {

          let coordinates = e.lngLat.toArray();
          let point = {
            type: "Point",
            coordinates
          };
          map.getSource("start").setData(point);
          
          let authentication = arcgisRest.ApiKeyManager.fromKey(accessToken);

          let addParams = { defaultBreaks: [0.25, 0.5, 0.75, 1], // km
                              impedanceAttributeName: "Kilometers",
                          }; 
          // ArcGIS Rest JS によるリクエスト
          arcgisRest
            .serviceArea({
              authentication,
              facilities: [coordinates],
              params: addParams
            })
  
            .then((response) => {
  
              map.getSource("servicearea").setData(response.saPolygons.geoJson);
  
              let features = [];
              for (let value of response.saPolygons.features) {
                  //console.log(value.attributes.ToBreak);
                  let feature = {
                      "type": "Feature",
                      "geometry": {
                          "type": "Point",
                          "coordinates": value.geometry.rings[0][0]
                      },
                      "properties": {
                          "ToBreak": value.attributes.ToBreak * 1000 + " m"
                      }
                  }
                  features.push(feature);
              }
              let geojson = {type: "FeatureCollection", features: features}
              map.getSource("servicearealabel").setData(geojson);
              //console.log(features)
            });

        });
        
        // 各レイヤー定義
        function addServiceAreaLayer() {
  
          map.addSource("servicearea", {
            type: "geojson",
            data: {
              type: "FeatureCollection",
              features: []
            }
          });
  
          map.addLayer({
            id: "servicearea-fill",
            type: "fill",
            source: "servicearea",
            paint: {
              "fill-color": [
                "match",
                ["get", "ObjectID"],
                1,
                "hsl(210, 80%, 40%)",
                2,
                "hsl(210, 80%, 60%)",
                3,
                "hsl(210, 80%, 80%)",
                4,
                "hsl(210, 80%, 100%)",
                "transparent"
              ],
              "fill-outline-color": "black",
              "fill-opacity": 0.5
            }
          });
  
        }
    
        function addServiceAreaLabelLayer() {

          map.addSource("servicearealabel", {
            type: "geojson",
            data: {
              type: "FeatureCollection",
              features: []
            }
          });
          
          map.addLayer({
            id: "servicearea-label",
            type: "symbol",
            source: "servicearealabel",
            layout: {
              "icon-allow-overlap": true,
              'text-field': ['get', 'ToBreak'],
              'text-font': ["Arial Italic"],
              'text-size': 20,
              'text-anchor': 'top'
            },
            'paint': {
              'text-color': 'rgba(0,0,0,0.5)',
              "text-halo-color": "#fff",
              "text-halo-width": 2
             }
          });

        }

        function addStartingPointLayer() {

          map.addSource("start", {
            type: "geojson",
            data: {
              type: "FeatureCollection",
              features: []
            }
  
          });
          map.addLayer({
            id: "start-circle",
            type: "circle",
            source: "start",
  
            paint: {
              "circle-radius": 6,
              "circle-color": "white",
              "circle-stroke-color": "black",
              "circle-stroke-width": 2
            }
          });

        }
        
    });
    
})();

まとめ

今回、オープンソースMapLibre GL JS を使用して、kintone との連携を試してみました。MapLibre GL JS がオープンソースということで、プラグインも多く提供されており、ArcGIS との連携も比較的に簡単に実現することができました。 有償だと思われている ArcGIS のサービスですが、到達圏解析など、その他多くのサービスは、無償から利用することができますので、お試して開発することもすぐに可能です。

MapLibre GL JS を利用する際の参考にしていただければ幸いです。

location.arcgis.com

esri.github.io

developers.arcgis.com