kintone と ArcGIS の連携を試してみました ~第2弾 不動産物件マスタのアプリ拡張 ~

【Esri Community Blog】

以前にサイボウズ株式会社が提供している kintone と ArcGIS を連携した記事を紹介しました。今回は前回紹介した記事からさらに不動産物件マスタのアプリを拡張したので、拡張した内容にフォーカスして紹介したいと思います。

前回のおさら

前回の記事では、kintone が提供している業務アプリの一つとして不動産物件マスタがあり、不動産物件マスタのデータを ArcGIS と連携して地図表示を行いました。

具体的には、kintone JavaScript API を使用して、kintone 上で不動産物件マスタのデータを ArcGIS API for JavaScript を使用して地図上に表示をしました。さらに ArcGIS では多くのオンライン コンテンツを配信しているので、ArcGIS Living Atlas of the World に登録されている災害情報として、洪水浸水想定区域(国土数値情報 令和 2 年)を使用しました。

ArcGIS Living Atlas of the World には他にも災害情報が登録されています。その他の災害情報の活用方法として、ArcGIS ブログ(梅雨・台風の大雨の季節に備えた災害情報レイヤーの活用法 ~ハザードマップを作成してみよう~)でも紹介しています。

index.png

不動産物件マスタのアプリ拡張:詳細・編集・追加

前回は kintone JavaScript API のイベントのレコード一覧イベントを使用して物件マスタのデータを地図上に表示しました。今回は、レコードの詳細表示や追加、編集のイベントにも対応しました。これにより、物件マスタの詳細情報の確認やデータの編集、新規物件データの追加を行うことができるようになります。

kintone JavaScript APIによるレコード詳細イベント追加イベント編集イベントを使用することで kintone 上でデータの編集や追加などに関してカスタマイズをすることができます。カスタマイズなどの詳細は cybozu developer network で確認することができます。

以下に不動産物件マスタの詳細表示、編集や追加に関して実装した部分について紹介していきます。

不動産物件マスタ:詳細表示

不動産物件マスタの一覧から詳細画面に遷移した画面が以下です。kintone では標準でこの機能を提供しており、詳細画面に地図の機能を組み込みました。

info.gif

レコードの詳細イベントは “app.record.detail.show” で呼び出すことができます。イベントタイプやイベントが呼び出されるタイミングなど具体的な内容は kintone JavaScript API のレコードの詳細イベントで確認できます。レコード詳細イベントでは event オブジェクトが取得できますので、event オブジェクトから record オブジェクトを取得し、record オブジェクト内の緯度・経度を取得し、地図上に物件の位置を表示しています。

全体のソースは以下の通りです。

(function () {
    "use strict";
    // API キー
    const api_key = '<API キー>';

    // 一覧画面を開いた時に実行します
    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: 350px; margin-right: 30px; border: solid 2px #c4b097');
        elAction.appendChild(viewDiv);

        // レコード情報を取得します
        let records = event['records'];
        //console.log(records);
        require([
            "esri/config",
            "esri/Map",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/Graphic",
            "esri/layers/GraphicsLayer",
            "esri/widgets/Legend",
            "esri/widgets/LayerList",
        ], function (esriConfig, Map, MapView, MapImageLayer, Graphic, GraphicsLayer, Legend, LayerList) {
            
            // API キーの作成
            esriConfig.apiKey = api_key;
      
            const map = new Map({
                basemap: "osm-streets-relief" // Basemap layer
            });
      
            const view = new MapView({
                map: map,
                center: [139.69167, 35.68944],
                zoom: 10, // scale: 72223.819286
                container: "viewDiv",
                constraints: {
                    snapToZoom: false
                }
            });

            view.when(() => {
                // レイヤーリスト ウィジェット
                const layerList = new LayerList({
                    view: view
                });
                // ビューの右上にウィジェットを追加
                view.ui.add(layerList, "top-right");
            });

            // MapImageLayer の作成
            const mapImageLayer = new MapImageLayer({
                title: "浸水想定区域 ",
                url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/sinsuisoutei/MapServer"
            });
            map.add(mapImageLayer);

            // 凡例ウィジェット
            const legend = new Legend({
                view: view,
                layerInfos: [
                    {
                        layer: mapImageLayer
                    }
                ]
            });
            // ビューの右下にウィジェットを追加
            view.ui.add(legend, "bottom-left");

            // GraphicsLayer の作成
            const graphicsLayer = new GraphicsLayer({
                title: "不動産物件マスタ"
            });

            const simpleMarkerSymbol = {
                type: "simple-marker",
                color: [0, 0, 205],  // Orange
                outline: {
                    color: [255, 255, 255], // White
                    width: 1
                }
            };

            for (const record of records) {
                
                let lat = record['緯度']['value'];
                let lon = record['経度']['value'];
                
                let name = record['物件名']['value'];
                let address = record['物件所在地']['value'];
                let company = record['オーナー企業名_氏名']['value'];
                let tel = record['オーナー電話番号']['value'];

                // 属性情報の作成 
                let propertyAtt = {
                    Name: name,
                    Address: address,
                    Company: company,
                    Tel: tel
                };

                // point の作成
                let point = { 
                    type: "point",
                    longitude: lon,
                    latitude: lat
                };
                
                // Graphic オブジェクトの作成
                let pointGraphic = new Graphic({
                    geometry: point,
                    symbol: simpleMarkerSymbol,
                    attributes: propertyAtt,
                    popupTemplate : {
                        title: "不動産物件マスタ",
                        content: [
                            {
                                type: "fields",
                                fieldInfos: [
                                    {
                                        fieldName: "Name",
                                        label: "物件名"
                                    },
                                    {
                                        fieldName: "Address",
                                        label: "住所"
                                    },
                                    {
                                        fieldName: "Company",
                                        label: "オーナー企業名/氏名"
                                    },
                                    {
                                        fieldName: "Tel",
                                        label: "オーナー電話番号"
                                    }
                                ]
                            }
                        ]
                    }
                });

                graphicsLayer.graphics.add(pointGraphic);
                //console.log(record['緯度']['value']);
                //console.log(record['経度']['value']);
            }
            map.add(graphicsLayer);
        });
    });
})();

地図部分の実装に関しては前回紹介した内容と基本的には同じですので、前回の記事を参照してください。

不動産物件マスタ:編集機能

次は物件データの編集です。詳細画面からレコードの編集ボタンをクリックすることで、各項目が編集できるようになります。kintone では標準でこの機能を提供しており、編集画面に地図の機能を組み込むことで、地図上からユーザーが任意に場所を選択できるようにしました。また、任意の位置をクリックし、そのクリックした位置の住所を確定して物件の所在地として表示しました。これはリバースジオコーディングという機能で、緯度・経度から住所を検索する機能です。

edit.gif

レコードの編集イベントは “app.record.edit.show” で呼び出すことができます。イベントタイプやイベントが呼び出されるタイミングなど具体的な内容は kintone JavaScript API のレコードの編集イベントで確認できます。

全体のソースは以下の通りです。

(function () {
    "use strict";
    // API キー
    const api_key = '<API キー>';

    //レコード追加画面の表示
    kintone.events.on(['app.record.edit.show'], function (event) {
        
        // レコード情報を取得します
        let record = event.record;

        // 地図のスペースフィールドの要素を取得します
        let elAction = kintone.app.record.getSpaceElement('Map');

        // 地図を表示する div 要素を作成します
        let viewDiv = document.createElement('div');
        viewDiv.setAttribute('id', 'viewDiv');
        viewDiv.setAttribute('name', 'viewDiv');
        viewDiv.setAttribute('style', 'width: 800px; height: 450px; margin-right: 30px; border: solid 2px #c4b097');
        elAction.appendChild(viewDiv);

       //console.log(records);
       require([
            "esri/config",
            "esri/Map",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/Graphic",
            "esri/widgets/Legend",
            "esri/widgets/LayerList",
            "esri/rest/locator"
        ], function (esriConfig, Map, MapView, MapImageLayer, Graphic, Legend, LayerList, Locator) {
        
            // API キーの作成
            esriConfig.apiKey = api_key;
    
            const map = new Map({
                basemap: "osm-streets-relief" // Basemap layer
            });
        
            let lat = record['緯度']['value'];
            let lon = record['経度']['value'];
        
            const view = new MapView({
                map: map,
                center: [lon, lat],
                zoom: 17, // scale: 72223.819286
                container: "viewDiv",
                constraints: {
                    snapToZoom: false
                }
            });

            view.when(() => {
                // レイヤーリスト ウィジェット
                const layerList = new LayerList({
                    view: view
                });
                // ビューの右上にウィジェットを追加
                view.ui.add(layerList, "top-right");
            });

            // MapImageLayer の作成
            const mapImageLayer = new MapImageLayer({
                title: "浸水想定区域 ",
                url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/shinsuisoutei2020/MapServer",
                visible: false
            });
            
            map.add(mapImageLayer);

            // 凡例ウィジェット
            const legend = new Legend({
                view: view,
                layerInfos: [
                    {
                        layer: mapImageLayer
                    }
                ]
            });
        
            // ビューの右下に凡例ウィジェットを追加
            view.ui.add(legend, "bottom-left");
            
            const simpleMarkerSymbol = {
                type: "simple-marker",
                color: [0, 0, 205],  // blue
                outline: {
                    color: [255, 255, 255], // White
                    width: 1
                }
            };

            // point の作成
            let point = {
                type: "point",
                longitude: lon,
                latitude: lat
            };

            // Graphic オブジェクト
            let pointGraphic = new Graphic({
                geometry: point,
                symbol: simpleMarkerSymbol
            });

            view.graphics.add(pointGraphic);

            // 地図 クリック時
            view.on("click", (event) => {

                view.graphics.removeAll();

                if (event.mapPoint) {
                    // クリック時の緯度経度を kintone のレコードに設定 
                    const record = kintone.app.record.get();

                    record.record['緯度'].value = event.mapPoint.latitude;
                    record.record['経度'].value = event.mapPoint.longitude;
                    kintone.app.record.set(record);
                    
                    // クリック時の緯度経度をマップにマップに表示
                    let point = {
                        type: "point",
                        longitude: event.mapPoint.longitude,
                        latitude: event.mapPoint.latitude
                    };

                    let pointGraphic = new Graphic({
                        geometry: point,
                        symbol: simpleMarkerSymbol
                    });
                    
                    view.graphics.add(pointGraphic);

                    // サービス URL
                    let serviceUrl = "http://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer";

                    // リバース ジオコーディング
                    // 物件の所在地を取得
                    Locator.locationToAddress(serviceUrl , {
                        location: point
                    }).then((result) => {
                        if (result.address) {
                            record.record['物件所在地'].value = result.address;
                        } else {
                            record.record['物件所在地'].value = "";
                        }
                        kintone.app.record.set(record);
                    });

                }
            });
        });
    });
    
})();

リバースジオコーディングですが、ArcGIS Platform が提供しているロケーションサービスのリバースジオコーディングを使用しています。

ArcGIS API for JavaScript の locater モジュールの locationToAddress() メソッドを使用しています。引数にサービス URL と location プロパティに地図上でクリックした緯度・経度を指定します。ステップごとに確認したい方は、ArcGIS API for JavaScript のチュートリアル(Reverse geocode)をご参照ください。

// クリック時の緯度経度をマップにマップに表示
let point = {
    type: "point",
    longitude: event.mapPoint.longitude,
    latitude: event.mapPoint.latitude
};

// サービス URL
let serviceUrl = "http://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer";

// リバース ジオコーディング
// 物件の所在地を取得
Locator.locationToAddress(serviceUrl , {
    location: point
}).then((result) => {
    if (result.address) {
        record.record['物件所在地'].value = result.address;
    } else {
        record.record['物件所在地'].value = "";
    }
    kintone.app.record.set(record);
});

不動産物件マスタ:追加機能

最後に物件データの追加です。詳細画面からレコードを追加するボタンをクリックすることで、各項目が追加できます。kintone では標準でこの機能を提供しており、編集機能と同様に地図の機能を組み込みました。リバースジオコーディングの機能と Search ウィジェット呼ばれるマップ内の目標物や場所を検索するためのウィジェットを追加しました。ArcGIS API for JavaScript では多くの便利なウィジェットを提供しており、簡単に組み込むことができます。詳細については、以前にこちらのブログ「続編 はじめてのWeb マッピングアプリケーション開発 : ウィジェット編」でも紹介しているのでご参照ください。

create.gif

レコードの追加イベントは “app.record.create.show” で呼び出すことができます。イベントタイプやイベントが呼び出されるタイミングなど具体的な内容は kintone JavaScript API のレコードの追加イベントで確認できます。

全体のソースは以下の通りです。

(function () {
    "use strict";
    // API キー
    const api_key = '<API キー>';

    //レコード追加画面の表示
    kintone.events.on('app.record.create.show', function (event) {
        
        // レコード情報を取得します
        let record = event.record;

        // 地図のスペースフィールドの要素を取得します
        let elAction = kintone.app.record.getSpaceElement('Map');

        // 地図を表示する div 要素を作成します
        let viewDiv = document.createElement('div');
        viewDiv.setAttribute('id', 'viewDiv');
        viewDiv.setAttribute('name', 'viewDiv');
        viewDiv.setAttribute('style', 'width: 800px; height: 450px; margin-right: 30px; border: solid 2px #c4b097');
        elAction.appendChild(viewDiv);

       //console.log(records);
       require([
            "esri/config",
            "esri/Map",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/Graphic",
            "esri/layers/GraphicsLayer",
            "esri/widgets/Legend",
            "esri/widgets/LayerList",
            "esri/rest/locator",
            "esri/widgets/Search"
        ], function (esriConfig, Map, MapView, MapImageLayer, Graphic, GraphicsLayer, Legend, LayerList, Locator, Search) {
        
            // API キーの作成
            esriConfig.apiKey = api_key;
    
            const map = new Map({
                basemap: "osm-streets-relief" // Basemap layer
            });
            
            // デフォルトの値を設定
            let lat = "35.68944";
            let lon = "139.69167";
        
            if (record['緯度']['value'] && record['経度']['value']) {
              lat = record['緯度']['value'];
              lon = record['経度']['value'];
            }
            
            const view = new MapView({
                map: map,
                center: [lon, lat],
                zoom: 17, // scale: 72223.819286
                container: "viewDiv",
                constraints: {
                    snapToZoom: false
                }
            });

            view.when(() => {
                // レイヤーリスト ウィジェット
                const layerList = new LayerList({
                    view: view
                });
                // ビューの右上にウィジェットを追加
                view.ui.add(layerList, {
                    position: "top-right"
                });
            
            });

            // MapImageLayer の作成
            const mapImageLayer = new MapImageLayer({
                title: "浸水想定区域 ",
                url: "https://content.esrij.com/arcgis/rest/services/Dosyasaigai/shinsuisoutei2020/MapServer",
                visible: false
            });
            
            map.add(mapImageLayer);

            // 凡例ウィジェット
            const legend = new Legend({
                view: view,
                layerInfos: [
                    {
                        layer: mapImageLayer
                    }
                ]
            });
        
            // ビューの右下にウィジェットを追加
            view.ui.add(legend, "bottom-left");
            
            // Search ウィジェット                            
            const search = new Search({
                view: view,
                popupEnabled: false
            });
            
            view.ui.add(search, {
                position: "top-right"
            });

            const simpleMarkerSymbol = {
                type: "simple-marker",
                color: [0, 0, 205],  // blue
                outline: {
                    color: [255, 255, 255], // White
                    width: 1
                }
            };

            // point の作成
            let point = {
                type: "point",
                longitude: record['経度']['value'],
                latitude: record['緯度']['value']
            };
            // Graphic オブジェクト
            let pointGraphic = new Graphic({
                geometry: point,
                symbol: simpleMarkerSymbol
            });
            view.graphics.add(pointGraphic);

            // マップクリック時
            view.on("click", (event) => {

                view.graphics.removeAll();
                if (event.mapPoint) {
                    // クリック時の緯度経度を kintone のレコードに設定 
                    const record = kintone.app.record.get();

                    record.record['緯度'].value = event.mapPoint.latitude;
                    record.record['経度'].value = event.mapPoint.longitude;
                    kintone.app.record.set(record);
                    
                    // クリック時の緯度経度をマップにマップに表示
                    let point = {
                        type: "point",
                        longitude: event.mapPoint.longitude,
                        latitude: event.mapPoint.latitude
                    };
                    
                    let pointGraphic = new Graphic({
                        geometry: point,
                        symbol: simpleMarkerSymbol
                    });
                    
                    view.graphics.add(pointGraphic);

                    // サービス URL
                    let serviceUrl = "http://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer";

                    // リバース ジオコーディング
                    // 物件の所在地を取得
                    Locator.locationToAddress(serviceUrl , {
                        location: point
                    }).then((result) => {
                        if (result.address) {
                            record.record['物件所在地'].value = result.address;
                        } else {
                            record.record['物件所在地'].value = "";
                        }
                        kintone.app.record.set(record);
                    });
                }
            });

            // 検索が確定した場合
            search.on("select-result", (event) => {
                view.graphics.removeAll();
                
                if (event.result) {
                   // 検索結果をkintone のレコードに設定 
                    const record = kintone.app.record.get();
                    record.record['緯度'].value = event.result.feature.geometry.latitude;
                    record.record['経度'].value = event.result.feature.geometry.longitude;
                    record.record['物件所在地'].value = event.result.name;
                    kintone.app.record.set(record);
                    
                    // 検索結果の緯度経度をマップに表示
                    let point = {
                        type: "point",
                        longitude: event.result.feature.geometry.longitude,
                        latitude: event.result.feature.geometry.latitude
                    };
                    
                    let pointGraphic = new Graphic({
                        geometry: point,
                        symbol: simpleMarkerSymbol
                    });
                    
                    view.graphics.add(pointGraphic);
                }
            });
        });
    });
    
})();

Search ウィジェットですが、イベントに応じてその後の処理を実装することができます。今回は select-result で検索が確定した場合に確定した情報の緯度・経度、物件の所在地を kintone 側に設定するようにしました。同時にその場所も地図上に表示しています。

// 検索が確定した場合
search.on("select-result", (event) => {
    view.graphics.removeAll();
    
    if (event.result) {
       // 検索結果をkintone のレコードに設定 
        const record = kintone.app.record.get();
        record.record['緯度'].value = event.result.feature.geometry.latitude;
        record.record['経度'].value = event.result.feature.geometry.longitude;
        record.record['物件所在地'].value = event.result.name;
        kintone.app.record.set(record);
        
        // 検索結果の緯度経度をマップに表示
        let point = {
            type: "point",
            longitude: event.result.feature.geometry.longitude,
            latitude: event.result.feature.geometry.latitude
        };
        
        let pointGraphic = new Graphic({
            geometry: point,
            symbol: simpleMarkerSymbol
        });
        
        view.graphics.add(pointGraphic);
    }
});

アプリのデプロイ

アプリのデプロイは、前回も紹介しましたが、不動産物件マスタ > アプリの設定 > JavaScript/CSS でカスタマイズの画面で JavaScript や CSS ファイルを適用することで反映することができます。今回は、機能ごとにソースファイル(index.js、detail.js、edit.js、create.js)を作成して適用しました。ソースファイルも分割して適用することができるので保守も楽になるかと思います。

mete.png

詳しくは、kintone ヘルプの「JavaScript やCSS でアプリをカスタマイズする」をご確認ください。

まとめ

今回は前回紹介した不動産物件マスタのアプリを拡張した部分を紹介しました。ArcGIS API for JavaScript の Search ウィジェットやリバースジオコーディングの機能も紹介しました。今回紹介した以外にも ArcGIS API for JavaScript には多くの機能が提供されています。サンプルコードや API リファレンスも充実していますので、ぜひ参照してください。

また、今回連携した方法以外にも ArcGIS には便利な機能やサービスがあるので、kintone とさまざまな連携サービスが実現できるのではないかと思います。ぜひ、これを機会に kintone と ArcGIS の連携をお試しいただければと思います。

■関連リンク