📍

kintone 緯度経度プラグイン

2022/09/08に公開

kintoneに登録されている不動産の所在地がどこなのか地図上で知りたいので、スクレイピングで緯度経度が取得できなかったら手動で登録していますが、少しでも便利になるように色々とカスタマイズしてみました。


アプリイメージ

機能

・レコード詳細画面へのマップの埋め込み
・住所を変更したら緯度経度を取得して出力
・マップURLを自動出力
・位置精度に何か入ったら緯度経度、マップURLの入力制御
・yahooのクレジット表示

等々・・
他のアプリに使いまわせるような汎用性はほぼないに等しく、このアプリ専用のプラグインになっています。

マニフェストファイル

manifest.json
{
    "manifest_version": 1,
    "version": 1,
    "type": "APP",
    "name": {
        "ja": "緯度経度取得",
        "en": "get longitude latitude Plug-in",
        "zh": "get longitude latitude Plug-in"
    },
    "description": {
        "ja": "住所から緯度経度を出力するプラグインです",
        "en": "Only for new collection apps.",
        "zh": "Only for new collection apps."
    },
    "icon": "image/pin.png",
    "desktop": {
        "js": [
            "https://js.cybozu.com/jquery/jquery-1.11.0.min.js",
            "js/vote.js"
        ]
    },
    "config": {
        "html": "html/config.html",
        "js": [
            "https://js.cybozu.com/jquery/jquery-1.11.0.min.js",
            "js/config.js"
        ],
        "css": [
            "css/51-modern-default.css",
            "css/config.css"
        ],
        "required_params": ["id","address","LongitudeLatitude","map","accuracy"]
    }
}

設定画面

config.htlm
<div id="body">
       <div id="header">
              <h2>住所から緯度経度を取得して出力するプラグインです</h2>
       </div>
       <div id="contents">
              <div class="parts">
                     <h3>使用するフィールドとフィールドコード</h3>
                     <li>住所(文字列1)</li>
                     <li>緯度経度(文字列1)</li>
                     <li>位置精度(チェックボックス)</li>
                     <li>スペースID「yahoo」のスペースフィールドを用意してください(クレジット表示)</li>
                     <li>スペースIDMAP」のスペースフィールドを用意してください(地図埋め込み)</li>
                    
              </div>
              <div class="parts">
                     <div  class="kintoneplugin-label">
                            <label>APIトークン</label>
                     </div>
                     <div class="kintoneplugin-input-outer">
                            <input class="kintoneplugin-input-text" type="text" id="id_field">
                     </div>
                     <div  class="kintoneplugin-label">
                            <label>住所フィールド</label>
                     </div>
                     <div class="kintoneplugin-select-outer">
                            <div class="kintoneplugin-select">
                                   <select id="address_field">
                                   </select>
                            </div>
                     </div>
                     <div  class="kintoneplugin-label">
                            <label>緯度経度フィールド</label>
                     </div>
                     <div class="kintoneplugin-select-outer">
                            <div class="kintoneplugin-select">
                                   <select id="LongitudeLatitude_field">
                                   </select>
                            </div>
                     </div>
                     <div  class="kintoneplugin-label">
                            <label>位置精度フィールド</label>
                     </div>
                     <div class="kintoneplugin-select-outer">
                            <div class="kintoneplugin-select">
                                   <select id="accuracy_field">
                                   </select>
                            </div>
                     </div>
                     <div  class="kintoneplugin-label">
                            <label>MapURLフィールド</label>
                     </div>
                     <div class="kintoneplugin-select-outer">
                            <div class="kintoneplugin-select">
                                   <select id="map_field">
                                   </select>
                            </div>
                     </div>
              </div>
              
              <div class="parts">     
                     <button type="button" id="check-plugin-submit" class="kintoneplugin-button-dialog-ok"> 保存する </button>
                     <button type="button" id="check-plugin-cancel" class="kintoneplugin-button-dialog-cancel"> キャンセル </button>
              </div>
       </div>
</div>
config.js
/*
    kintone plugin
 */
jQuery.noConflict();
(function($, PLUGIN_ID) {
  'use strict';
  // プラグインIDの設定
  const config = kintone.plugin.app.getConfig(PLUGIN_ID);

  // 既に値が設定されている場合はフィールドに値を設定する
  /*設定対象のフィールドタイプ */
  const aryType = ["SINGLE_LINE_TEXT","LINK","CHECK_BOX"];

  /*設定項目オブジェクト*/
  const vals = [
    {
      elm:"id_field",
      feild:"id",
      type:"SINGLE_LINE_TEXT"
    },
    {
      elm:"LongitudeLatitude_field",
      feild:"LongitudeLatitude",
      type:"SINGLE_LINE_TEXT"
    },
    {
      elm:"address_field",
      feild:"address",
      type:"SINGLE_LINE_TEXT"
    },
    {
      elm:"map_field",
      feild:"map",
      type:"LINK"
    },
    {
      elm:"accuracy_field",
      feild:"accuracy",
      type:"CHECK_BOX"
    }
];
  const SettingVal = function(elm, feild){
    this.elm = elm;
    this.feild = feild;
    this.Setting = function(){
      if (config[this.feild]) { 
        $("#"+this.elm).val(config[this.feild]);
      }
    }
  }
  vals.forEach(function(obj){
      new SettingVal(obj.elm,obj.feild);
  });

  async function setDropDownForDate() {
    try {
      const resp = await kintone.api('/k/v1/app/form/fields.json','GET',{'app': kintone.app.getId()});
      
      for(var key in resp.properties){
        if(aryType.indexOf(resp.properties[key].type) <= -1){
            //配列aryTypeにないフィールドタイプならコンティニューする
            continue;
        }
        let $option;
        vals.forEach(function(obj){
            if(obj.type === resp.properties[key].type){
                $option = $('<option></option>');
                $option.attr('value', resp.properties[key].code);
                $option.text(resp.properties[key].label);
                $('#'+obj.elm).append($option.clone());
                $('#'+obj.elm).val(config[obj.feild]);
            }
        });
      }

    }catch(error) {
      console.log(error);
      alert("フィールド情報の取得中にエラーが発生しました。");
    }
  }
  setDropDownForDate();
  
  // 「保存する」ボタン押下時に入力情報を設定する
  $('#check-plugin-submit').click(function() {
    kintone.plugin.app.setConfig({id:$("#id_field").val(),address:$("#address_field").val(),LongitudeLatitude:$("#LongitudeLatitude_field").val(),map:$("#map_field").val(),accuracy:$("#accuracy_field").val()});
  });

  // 「キャンセル」ボタン押下時の処理
  $('#check-plugin-cancel').click(function() {
    history.back();
  });

})(jQuery, kintone.$PLUGIN_ID);
config.css
@charset "UTF-8";

#header {
    padding: 0.5em 1em;
    margin: 2em 0;
}
.parts {
    margin: 2em;
}

config.jsの工夫した点は前回の記事で投稿済です。

https://zenn.dev/akaneiy/articles/c50b0d335004ca

メイン機能

(function($,PLUGIN_ID) {
    "use strict";
    /*設定画面で設定した内容を取得しておく*/
    const config = kintone.plugin.app.getConfig(PLUGIN_ID);
    /*取得していた内容を取り出して変数にそれぞれ代入する*/
    const LonLat = config.LongitudeLatitude;
    const address = config.address;
    const id = config.id;
    const map = config.map;
    const accuracy = config.accuracy;

    /*位置精度が何かしら選択されていたらMAPURLと緯度経度はロックかける*/
    function Lock(event){
        const record = event.record;
        if(record[accuracy].value.length !== 0) {
            record[LonLat].disabled = true;
            record[map].disabled = true;
        }else {
            record[LonLat].disabled = false;
            record[map].disabled = false;
        }
        return event;
    }

    /*クレジット表示*/
    function credit(){
        const html = '<!-- Begin Yahoo! JAPAN Web Services Attribution Snippet -->'+
                    '<a href="https://developer.yahoo.co.jp/sitemap/">'+
                    '<img src="https://s.yimg.jp/images/yjdn/yjdn_attbtn2_105_17.gif" width="105" height="17" title="Webサービス by Yahoo! JAPAN" alt="Webサービス by Yahoo! JAPAN" border="0" style="margin:15px 15px 15px 15px"></a>'+
                    '<!-- End Yahoo! JAPAN Web Services Attribution Snippet -->';
        const elm = kintone.app.record.getSpaceElement('yahoo');
        elm.innerHTML = html;
    }

    /*マップを埋め込む*/
    function mapView(event){
        const ifrMaphtml = '<iframe src="https://maps.google.co.jp/maps?output=embed&t=m&hl=ja&z=18&q='+
                        event.record[LonLat].value +
                        '" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy"></iframe>"';
        const ifrMapElm = kintone.app.record.getSpaceElement('MAP');
        ifrMapElm.innerHTML = ifrMaphtml;
    }

    /*住所が変更された時の処理
    ①yahooAPI叩いて緯度経度を取得
    ②マップURLの変更*/
    function getLonLat(event) {
        const record = event.record;
        //位置精度になにか入っていたら処理終了
        if(record[accuracy].value.length !== 0) {
            return event;
        }
        const syozai = record[address].value;
        const APPID = id;
        const add = encodeURI(syozai);
        const URL = "https://map.yahooapis.jp/geocode/V1/geoCoder?appid="+APPID+"&query="+add+"&output=json";
        //成功した時の処理
        function SucProcess(response){
            if(response.ResultInfo.Count === 0){
                return event;
            }
            let goordinates = response.Feature[0].Geometry.Coordinates;
            const ArrayGoordinates = goordinates.split(',');
            goordinates = ArrayGoordinates[1]+","+ArrayGoordinates[0];
            const count = response.Feature.length;
            const rec = kintone.app.record.get();
            if(count >=2) {
                let message = "";
                for (const key in response.Feature){
                    message += response.Feature[key].Name+"  : "+response.Feature[key].Geometry.Coordinates +"\n";
                }
                rec.record[LonLat].value = goordinates;
            }
            rec.record[LonLat].value = goordinates;
            rec.record[map].value = "https://www.google.co.jp/maps?q="+goordinates;
            kintone.app.record.set(rec);
        }
        /*yahooのAPIを叩く*/
        $.ajax({
            url: URL,
            async: false,
            dataType: 'jsonp',
            //成功したら上の関数の処理を行なう
            success:  (response)=> {SucProcess(response)},
            error: (response)=> {
                alert("取得に失敗しました。");
                console.log(response);
            }
        });
        return event;
    }

    /*詳細画面で動作させるクレジット表示とマップ埋め込み処理*/
    function dedail(event){
        credit();
        mapView(event);
        return event;
    }

    /*緯度経度を変更した時 */
    function changeLonLat(event){
        const record = event.record;
        /*手動でコピペした緯度経度はスペースが含まれていたので置換する*/
        const LongitudeLatitude = record[LonLat].value.replace(" ","");
        record[LonLat].value = LongitudeLatitude;
        record[map].value = "https://www.google.co.jp/maps?q="+LongitudeLatitude;
        //MapLink();
        mapView(event);
        credit();
        return event;
    }

    /*編集時はロックかけてクレジット表示する */
    function editShow(event){
        credit();
        event = Lock(event);
        mapView(event);
        return event;
    }

    //イベント定義
    /*住所フィールドの変更時でイベント定義*/
    const adressEvents = ['app.record.create.change.'+address,'app.record.edit.change.'+address];
    /*位置精度フィールドの変更時でイベント定義 */
    const accuracyEvents = ['app.record.create.change.'+accuracy,'app.record.edit.change.'+accuracy];
    /*緯度経度を手動で変更した時のイベント定義*/
    const LonLatEvents = ["app.record.edit.change."+LonLat,"app.record.create.change."+LonLat];
    /*編集イベント定義*/
    const otherEvents = ["app.record.create.show","app.record.index.edit.show","app.record.edit.show"];
    /*詳細イベント定義*/
    const detailEvent = "app.record.detail.show";

        
    /*イベント毎に関数を呼び出していく*/
    //関数()だとindex.show時点で動作してエラーがでる挙動になる
    kintone.events.on(detailEvent,dedail);
    kintone.events.on(adressEvents,getLonLat);
    kintone.events.on(accuracyEvents,Lock);
    kintone.events.on(otherEvents,editShow);
    kintone.events.on(LonLatEvents,changeLonLat);

})(jQuery,kintone.$PLUGIN_ID);

イベントごとに処理をまとめた関数を呼び出していくようにしています。
最初はべたべた処理を書いていましたが、同じ処理を何度も書くことになってしまっていたので、処理毎に分けてイベントごとに必要な処理だけできるようにしました。

return eventするタイミングについて

検証しながら進めました。
eventオブジェクトを書き換えた後に再度別の処理で変更後のeventオブジェクトの値を使用する場合です。

function editShow(event){
        credit();
        event = Lock(event); //関数Lockはeventをreturnするためeventで受け取る
        mapView(event);
        return event;
}

関数Lockでeventオブジェクトを書き換えて、一応returnはしています。
そして、関数mapViewでもeventオブジェクトを使用しますが、値の書き換えはしないので値は受けとる必要がありません。

すごく当たり前のことですが、関数Lockの戻り値をちゃんと受け取らないとeventオブジェクトは関数editShowが呼び出された時点のeventオブジェクトを関数mapViewに渡してしまいます。

下記、悪い例です。挙動が変わります。

function editShow(event){
        credit();
        Lock(event);
        mapView(event);
        return event;
}

イベントハンドラの記述でハマった

https://developer.cybozu.io/hc/ja/articles/201941954

kintone.events.on(type, handler)

いつもこのハンドラーは直接関数の記述をしていますが、今回は関数を別で記述して関数名を指定しました。

その時しばらく挙動が思うようにならずエラーが出て困ったので記録しておきます。

正解
kintone.events.on(detailEvent,dedail);
誤り
kintone.events.on(detailEvent,dedail());

イベントに設定していない一覧画面で動いてしまいeventオブジェクトにそんなのない!と怒られました。
ハンドラーはそこで関数を呼び出しているというよりは、呼び出す関数を指定していると考えたら良いのかな?と思いました。

所感

イベントごとにやりたい処理が様々なので情報の整理が大変でした。
基本的なことこそしっかり理解しておくことが大事だと改めて感じました。

Discussion