Web Performer V2.4の新機能「カスタム部品」を使って地図を表示してみる

2020-07-15

はじめに

はじめまして、エヌデーデーの関口です。当社ではキヤノンITソリューションズから販売されているWebアプリケーション超高速開発ツールである「Web Performer」を使った開発を行っています。
この記事では2020年5月にリリースされた Web Performer V2.4 の新機能である「カスタム部品」について、実装例を踏まえてを紹介したいと思います。

想定される読者

  • Web Performerは導入済み
  • Web Performer V2.4 での開発は未経験
  • Web Performer開発でUI部分に課題を抱えている

カスタム部品とは

「カスタム部品」とは、ユーザーが作成した JavaScript や、広く公開されている JavaScript ライブラリを利用して、Web Performer で作成した画面に、従来では表現できない表現や動きを実現させるというものです。
Web Performer で開発をした経験がある人ならば、「ん?JavaScript を使った画面の開発ならこれまでも出来ていたんじゃない?」と思いますよね。

カスタム部品の機能を使うと、これまでは生成されたHTML要素を jQuery などを駆使して無理矢理画面に割り込ませていたところを、入出力項目に定義をしていくという、Web Performer 開発としては自然な形で、画面に溶け込ませる事ができます。また、カスタム部品を再利用することで、複数の画面で同じような制御が行えるようになります。

今回はその一例として、Web Performer で作成した画面に次の画像のように地図を表示し、さらに検索が出来るようにした実装例を紹介します。

地図を利用するにあたり

今回地図をWeb Performer で利用するにあたっては、最近利用者が増えている Mapbox の API を利用することにします。Mapbox は OpenStreetMap をベースにして、JavaScript をはじめ様々なプラットフォーム向けの API を公開しています。
無償プラン、有償プラン、商用利用プランがありますが、個人での利用開始も簡単なので、地図のアプリを作ってみたいという人は登録しておくといいかもしれません。

MapboxのJSライブラリの利用まで

Mapbox へログインして、 Account ページからJS Web をクリックします。

次のような画面になります。今回は Web 上で利用するので、CDN を選択しましょう。

次に HTML に記述する CDN の記述例が出ますので、控えておきます。(画像が小さいので、コードを貼り付けておきます)

<script src='https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.css' rel='stylesheet' />

「Next」を押すと、HTML に埋め込むサンプルスクリプトの例が表示されます。そのままではないですが、このコードは後で利用するので、控えておきましょう。(黄色の部分はトークンです)

<div id='map' style='width: 400px; height: 300px;'></div>
<script>
    mapboxgl.accessToken = '{あなたのアクセストークン}';
    var map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/streets-v11'
    });
</script>

この例では、HTML の <div id='map' style='width: 400px; height: 300px;'></div> と記述したタグ部分に、地図が表示される例になります。スクリプトの container: 'map' という箇所で、地図を表示するエリアのHTMLタグを示しています。

Web Performerのアプリケーションを作成する

いよいよ Web Performer のアプリケーションの作成です。

プロジェクトとアプリケーションを作成する

ここでは適当に、プロジェクトを sample 、アプリケーションを CUSTOM_DEMO としておきます。

地図情報をデータモデルとして定義する

今回は地図上に、検索した結果のマーカーを表示しようと思うので、検索対象となるものの属性情報をデータモデルとして定義し、データベースからそれらの情報にアクセス出来るようにしておきましょう。
データモデル MAPINFO の構成例を示します。

項目コード名前キーグループ桁数小数桁データタイプ
IDID120NUM
CATEGORYカテゴリ040TEXT
SHOP_NAME店名0110 TEXT
LAT緯度01715NUM
LNG経度01815NUM

データベースにも、MAPINFO というテーブルがあり、登録されているデータの例は次のようなものです。(データベース接続部分の説明は省略します)

IDCATEGORYSHOP_NAMELATLNG
1ラーメンみなもと35.6945395139.6829662
2焼き肉さかむら35.695332139.682837
3コンビニラーソン中野坂上駅前店35.69687139.682209

カスタム部品を表示する入出力を作成

カスタム部品を表示する入出力は、CUSTOM_IO (カスタム入出力) としておきましょう。
この入出力は、MAPINFO を対象データモデルとした一覧形式のIOとします。また、検索が出来るように入出力項目を追加しておきます。定義の抜粋は次のようになります。

また、カスタム部品を設定していない状態のプレビューは次のような画面です。

カスタム部品の設定を入出力項目に定義する

次にカスタム部品である地図を表示させるための設定を行います。
今回は、グループ(一覧)の情報を元に地図を表示させるので、グループ(G)の入出力項目にカスタム部品の定義をします。

まず、カスタム部品の名前を決めます。これは後でカスタム部品の実装である JavaScript を格納するフォルダの名称になります。今回は mapView というカスタム部品の名称にして進めます。

入出力項目一覧のグループ( MAPINFO )を選択して、入出力項目プロパティに次の値を設定します。
compParam にはカスタム部品を初期化する際に利用出来るパラメータをJSONで表記します。 ここに示した JSON は、当社の緯度経度と、地図を初期表示したときのズームの値を表現しています。

キー
compName カスタム部品mapView
compParam カスタム部品パラメータ{“init_lat":35.696174, “init_lng": 139.682268, “init_zoom": 17}

カスタム部品を作成

先ほど、compName を mapView と定義しました。この名前に基づいて、カスタム部品に必要なファイルを用意します。
ファイルの作成場所は次のようになります。

パターンファイル作成場所
プロジェクト共通の場合/JavaWebApp/@COMMON/components/compName/
アプリケーション固有の場合/JavaWebApp/アプリ名/components/compName/

今回は、mapView という名称をアプリケーション固有で作成してみるので、/JavaWebApp/CUSTOM_DEMO/components/mapView/ というフォルダにファイルを作成します。
また、Web Performer で規定されている最低限必要なファイルは、次の3つです。

  • load.jsp
  • snippet.jsp
  • glue.js

これらを次のように配置します。

/JavaWebApp/CUSTOM_DEMO/components/mapView/
 ├load.jsp
 ├snippet.jsp
 └glue.js

さらに、カスタム部品固有の JavaScript やスタイルシートを作成したい場合は、フォルダを作成して管理するとよいでしょう。
ここでは、mapmodule.js と mapmodule.css というファイルを作成して、次のように配置することにします。

/JavaWebApp/CUSTOM_DEMO/components/mapView/
 ├script
 │  └mapmodule.js
 ├style
 │  └mapmodule.css
 ├load.jsp
 ├snippet.jsp
 └glue.js

load.jsp

load.jsp は、カスタム部品を定義した入出力項目を HTML として表示したときに、HTML 下部に出力されるタグ等になります。
用途としては、カスタムで作成したライブラリやスタイルシートを読み込む事に利用します。
ここでは、作成した JavaScript のファイルとスタイルシート、および先ほど Mapbox のサイトで確認した CDN を読み込む事にしましょう。

<%@ page pageEncoding="UTF-8" %>
<%@ page import="jp.co.canon_soft.wp.runtime.AppContext"%>

<!-- MapboxのAPIを読み込む(必ず先に読み込みましょう) -->
<script src='https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.css' rel='stylesheet' />

<div id='map' style='width: 400px; height: 300px;'></div>
<!-- 自作したスタイルシート、スクリプトを読み込む -->
<link rel="stylesheet" href="<%= AppContext.getContextPath() %>/components/mapView/style/mapmodule.css" />
<script src="<%= AppContext.getContextPath() %>/components/mapView/script/mapmodule.js" type="text/javascript"></script>

snippet.jsp

snippet.jsp は、カスタム部品の内部に表現したいHTML要素を記述します。ただし、formタグは利用出来ません。用途としては、カスタム部品の説明など、静的な情報を表示するのに利用すると考えた方がいいでしょう。
ここでは、地図の説明を表示させてみます。

<%@ page pageEncoding="UTF-8" %>
<div name="Description" class="maparea"></div>
<h1>お店を地図上でマーカー表示します。</h1>

glue.js

glue.js(jspではなく、jsです)は、その名の通り、カスタム部品を定義した入出力項目とカスタム部品を糊付けするためのコードです。
カスタム部品は、一つの入出力だけでは無く、いろいろな入出力で利用することが想定されますので、入出力項目の名称が様々に定義される可能性があります。そのため、入出力項目の名前が変わったとしても、動作に影響がないようにするための JavaScript 部品を、この glue.js で利用することができます。

なお、glue.js には、必ず init_カスタム部品名 という初期化関数を定義する必要があります。これが定義されていないと、JavaScript エラーが発生します。初期化関数の引数は次の通り。

パラメータ説明
itemElemDOMカスタム部品を定義した入出力項目の要素
itemValue右記に準ずるカスタム部品を定義した入出力項目の値
itemCode文字列カスタム部品を定義した入出力項目の項目コード
compParamJSONカスタム部品を定義した入出力項目に設定した、入出力項目プロパティ「compParam カスタム部品パラメータ」の値からJSONパースされたオブジェクト
※入出力項目プロパティ「compParam カスタム部品」を設定していない場合、空オブジェクト
isGroupItem真偽値カスタム部品を定義した入出力項目がレベル2であるかどうか
index数値カスタム部品を定義した入出力項目がレベル2の時の行数
※レベル1の場合、0

実際のところ、glue.js 自身をカスタム部品としてゴリゴリ記述するのもいいのでしょうが、私が実装する場合は、glue.js には入出力項目の値や DOM を、独自で作成した JavaScriptファイル(今回であれば、script/mapmodule.js)にどうやって受け渡すか、あるいはその戻り値をどうやって、入出力項目に反映するかを記述するほうが役割分担としては綺麗かと思います。

const init_mapView = (itemElem, itemValue, itemCode, compParam, isGroupItem, index) => {
	const raw =  JSON.parse(disp.get(itemCode))[itemCode];
	
	const records = raw.reduce( (result, current) => {
		/*
		 * データモデルで取得した項目の値は、全てテキスト型として、
		 * normalizedvalueという属性に値が登録されているので、
		 * 必要に応じて型変換を行う必要がある。
		 */
		const obj = {
				category: current.CATEGORY.normalizedvalue,
				shop_name: current.SHOP_NAME.normalizedvalue,
				lat: Number(current.LAT.normalizedvalue),
				lng: Number(current.LNG.normalizedvalue)
		};
		result.push(obj)
		return result;
	},[])

	const param = {
			lat: compParam.init_lat,
			lng: compParam.init_lng,
			zoom: compParam.init_zoom
	};
	init_map(itemElem, param, records);
}

コードの解説です。
2行目の const records = JSON.parse(disp.get(itemCode))[itemCode]; では一覧表示されたデータモデルのレコードをJSON形式の配列として変換しています。この書き方は、一覧形式にカスタム部品を定義したときは定番の記述方法となるので、覚えておく必要がありそうです。

4行目から18行目では、カスタム部品独特の使用で、取得した入出力項目の値が、必ず normalizedvalue という属性に文字列として格納されるため、それを後続の処理で必要な型に変換し直しています。

20行目から24行目では、compParamから値を取得して、地図表示の初期値となるパラメータを生成しています。(そのままcompParamを次の関数に引き渡してもいいですが、今回はあえてこういう使い方ですという意味で示しています)

25行目で、実際に実行する init_map(itemElem, param, records) の呼び出しを記述しています。関数の中身は後述します。

今回は初期座標として、compParam を定義するという利用方法にしましたが、compParam の他の使いどころとしては、例えばカスタム部品を定義した入出力項目以外に、その画面で参照したり、影響を及ぼしたりする 入出力 項目の名前などを渡しておくといいのかもしれません。そうすることで、画面の入出力項目の名称の差を吸収することができます。以下にその一例を挙げます。

/*
カスタム部品のIO項目の名称が、
①画面上ではEMP_IDで、影響を与えるIO項目が氏名(NAME)と部署(DEPT)である場合。
②画面上ではI_EMP_IDで、影響を与えるIO項目が氏名(I_NAME)と部署(I_DEPT)である場合。

①の場合、compParamには {empName: 'NAME', deptCd: 'DEPT'}と書いておく。
②の場合、compParamには {empName: 'I_NAME', deptCd: 'I_DEPT'}と書いておく。
*/
const init_mapView = (itemElem, itemValue, itemCode, compParam, isGroupItem, index) => {
    let emp_id = disp.get(itemCode); //これで、IO項目の名称を意識せずに取れる。
    let name = disp.get(compParam.empName); //これで、NAMEでもI_NAMEでも値が取れる。
    let dept = disp.get(compParam.deptCd);  //これで、DEPTでもI_DEPTでも値が取れる。
    // do something...
 }   

mapmodule.js

呼び出される側のソースです。呼び出し関数(init_map)を作り、内部で Mapbox の API で確認したソースの他、MAPINFO のデータを元に、マーカーなどを設定するコードを追加しています。

mapboxgl.accessToken = '{あなたのアクセストークン}';

const init_map = (elem, param, records) => {
    elem.classList.add('mymap'); // 表示領域をスタイルシートで定義

    const map = new mapboxgl.Map({
        container: elem,  // 引数で渡ってきたDOM(IO項目)に地図を表示する
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [param.lng, param.lat],
        zoom: param.zoom
    }); 

    // 一覧の情報を元にマーカーとポップアップを生成する
    records.forEach(data => {
        const popup = new mapboxgl.Popup({offset: 25})
        	.setHTML(`<h2>${data.shop_name}</h2>${data.category}`);

        new mapboxgl.Marker()
        	.setLngLat([data.lng, data.lat])
        	.setPopup(popup)
        	.addTo(map);
    })
}

コードの解説です。
6行目から11行目で Mapbox の API を利用して地図を初期化します。container には、Web Performer の入出力項目で生成されるHTML要素を渡しています。center 、zoom には、compParam で設定した初期値を設定しています。

14行目から22行目では入出力項目の一覧から取得した座標や、店名を使って、地図上にマーカーやマーカーをクリックしたときのポップアップなどを生成しています。

mapmodule.css

スタイルシートはシンプルです。 先ほど mapmodule.js で指定したクラスの高さと幅を指定してあげましょう。
mapboxgl-popup-close-button のスタイルは、mapbox の提供する部品のスタイルが、Web Performer の提供するスタイルで上書きされないための設定です。

.mymap {
    height: 750px;
    width: 1500px;
}

.mapboxgl-popup-close-button {
    color: black;
    background-image: none;
    box-shadow: none;
}

アプリケーションを自動生成する

ここまで設定できたら、Web Performer でアプリの自動生成を行ってみましょう。
メニューバーから、ダイヤのマークをクリック

表示されたダイアログでは、HTML を HTML5 としてください。HTML4 にしてしまうとカスタム部品が動きません。

アプリケーションを実行してみる

いよいよアプリケーションを実行します。アプリケーションサーバー(Tomcat)を起動して、 http://localhost:8080/CUSTOM_DEMO/ にアクセスしてください。

今回はログインの制御は特にしていませんので、適当な文字を入力してログインしてください。

テスト用の画面一覧が表示されるので、「カスタム入出力」をクリックします。

さあ・・・どうでしょうか??

作成した画面

ごらんのように、検索に応じてマーカーが増減するのがわかると思います。これで、当社近辺のランチマップのできあがりですw

今回は検索だけになっていますが、Web Performer の処理を使ってデータの更新などもできそうです。

まとめ

Web Performer V2.4 の新機能であるカスタム部品を使った例を紹介しました。今回は地図データと一覧のデータを連係させるケースの紹介でした。地図以外にも、Web Performer 標準のグラフではなく、外部のグラフライブラリを使ってよりリッチなグラフを表現したり、一覧形式ではない特定の入出力項目に対してカスタム部品を用いて、標準では行えないような表示や動作ができますので、次の機会に紹介したいと思います。
Web Performer の標準機能だけでは少し物足りないと感じている方も、V2.4の新機能、カスタム部品を活用してみてはいかがでしょうか。