はじめに
本エントリーは React Advent Calendar 2021 11日目の記事です。
昨年は「地図アプリ開発で React Hooks を利用して React コンポーネントを作成する」内容の記事を紹介しました。 freedom-tech.hatenablog.com
そのなかで、地図ライブラリとして、ArcGIS API for JavaScript を紹介しました。ArcGIS API for JavaScript では、ES モジュールが提供されていますので、React などのフレームワークを利用した地図アプリ開発も容易に行うことができるようになっています。
今回は ArcGIS API for JavaScript も利用できるモダンな Web アプリ用の開発キット、ローコードツールとして人気がある ArcGIS Experience Builder という Web アプリの開発ツールについて紹介していきたいと思います。
ArcGIS Experience Builder とは
ArcGIS Experience Builder とは、2D および 3D データと連動する柔軟なレイアウトやコンテンツ、ウィジェットを使用して、独自の Web Experience を作成することができる Web アプリのローコードツールです。テンプレートを使用して、モバイルに適応したアプリを作成したり、テンプレートのレイアウトを変更して異なるスクリーンサイズに合わせてデザインを作成したりと、コードを書くことなく Web アプリを開発することができます。
ArcGIS Experience Builder は、ローコードツールでもあるため、ノーコードでアプリを開発することができますが、機能を拡張することができるようなっています。 ArcGIS Experience Builder では、ウィジェットと呼ばれる機能の塊が提供されており、そのウィジェットを拡張することができるようになっています。ウィジェット開発では、React のフレームワークを利用します。今回は、簡単なウィジェット開発について紹介したいと思います。
ArcGIS Experience Builder を利用した多くのアプリが公開されています。
参考までに公開されているアプリを紹介します。
ArcGIS Experience Builder の利用
ArcGIS Experience Builder を利用するにあたり、まずはインストールが必要になってきます。インストールに関しては本家のサイトに加えて、日本語向けのドキュメントも提供されているようですので、以下を参考にしていただければと思います。
ローコードツールなので、以下のようにノーコードでアプリを作成する場合は、 UI 操作だけで作成することが可能です。また、テンプレートなども豊富に用意されていますので、デザインができない方でもリッチなアプリやサイトを構築することができます。そのため、非エンジニアなどのデザイナーでも Web サイトを構築することができます。
React を利用したウィジェット開発
ArcGIS Experience Builder で機能を拡張したい場合は、ウィジェットと呼ばれる機能の塊があり、独自のウィジェットを開発することができます。基本的なウィジェット開発については以下を参考にしていただければと思います。
今回はクラスタリングの機能を追加しました。 機能としては、以下の画面のとおりで、複数のポイントをグループ単位に集約することができる機能です。縮尺ごとに動的に適用されます。縮小表示するにつれて、グループごとに集約されるポイント数が増えてグループの数が減少し、拡大表示するにつれて、クラスターグループの数が増加します
こちらのサンプルプログラムは、GitHub にも公開されています。まず、はじめにこのサンプルリポジトリをクローンし、このウィジェットのフォルダ(widgets内)をExperience Builderインストールの client/your-extensions/widgetsフォルダにコピーします。
そして、widget.tsx で、clusterConfig プロパティを定義します。機能を含むための各クラスタの領域を決定する clusterRadius などのクラスタのプロパティを定義します。
clusterConfig = { type: "cluster", clusterRadius: "100px", popupTemplate: { content: "This cluster represents <b>{cluster_count}</b> points.", fieldInfos: [{ fieldName: "cluster_count", format: { digitSeparator: true, places: 0 } }] }, clusterMinSize: "24px", clusterMaxSize: "60px", labelingInfo: [{ deconflictionStrategy: "none", labelExpressionInfo: { expression: "Text($feature.cluster_count, '#,###')" }, labelPlacement: "center-center", }] }
onCheckBoxChange 関数は、Checkbox コンポーネントのロジックを処理し、値に基づいて状態を設定し、これを clusterSwitch 関数に渡します。
onCheckBoxChange = (e) => { const valueState = e.target.value this.setState({ clusterStatus: valueState }, () => { this.clusterSwitch(this.state.clusterStatus); }) }
clusterSwitch 関数は、クラスタの表示を制御します。this.state.clusterStatus が true であれば、クラスタリングを有効にします。そうでない場合は、null を設定して、そのレイヤーのクラスタリングをオフにします。
clusterSwitch = (e) => { if (this.state.clusterStatus){ this.state.jimuMapView.jimuLayerViews[this.state.clusterLayer].layer.featureReduction = this.clusterConfig } if (!this.state.clusterStatus) this.state.jimuMapView.jimuLayerViews[this.state.clusterLayer].layer.featureReduction = this.state.clusterStatus ? this.clusterConfig : null }
activeViewChangeHandler 関数は、いくつかのことを処理します。JimuMapView があるかどうか、レイヤーが含まれているかどうかをチェックします。レイヤーが含まれている場合は、マップビューの状態、データソース、エラーチップを設定し、ウィジェットを表示します。レイヤーを含まない JimuMapView がある場合は、WidgetPlaceHolder コンポーネントを表示します。
activeViewChangeHandler = (jmv: JimuMapView) => { if (jmv) { const jimuLayerViews = jmv.jimuLayerViews; const jimuLayerViewIds = Object.keys(jimuLayerViews)[0]; const layerViewId = jimuLayerViewIds; if (layerViewId === undefined) { jmv = null; return } this.setState({ jimuMapView: jmv, clusterLayer: jimuLayerViewIds, errorTip: false }) } else { this.setState({ jimuMapView: undefined, clusterLayer: undefined, errorTip: true, clusterStatus: false }) } }
/** @jsx jsx */ /** Licensing Copyright 2021 Esri Licensed under the Apache License, Version 2.0 (the "License"); You may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. A copy of the license is available in the repository's LICENSE file. */ import { React, AllWidgetProps, jsx } from 'jimu-core'; import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis"; import { Label, Checkbox, WidgetPlaceholder } from 'jimu-ui'; interface State { jimuMapView: JimuMapView; clusterLayer: string; clusterStatus: boolean; errorTip: boolean; } export default class Widget extends React.PureComponent<AllWidgetProps<any>, State> { constructor(props) { super(props); this.state = { jimuMapView: undefined, clusterLayer: undefined, clusterStatus: false, errorTip: true }; } clusterConfig = { type: "cluster", clusterRadius: "100px", popupTemplate: { content: "This cluster represents <b>{cluster_count}</b> points.", fieldInfos: [{ fieldName: "cluster_count", format: { digitSeparator: true, places: 0 } }] }, clusterMinSize: "24px", clusterMaxSize: "60px", labelingInfo: [{ deconflictionStrategy: "none", labelExpressionInfo: { expression: "Text($feature.cluster_count, '#,###')" }, labelPlacement: "center-center", }] } onCheckBoxChange = (e) => { const valueState = e.target.value this.setState({ clusterStatus: valueState }, () => { this.clusterSwitch(this.state.clusterStatus); }) } clusterSwitch = (e) => { if (this.state.clusterStatus) { this.state.jimuMapView.jimuLayerViews[this.state.clusterLayer].layer.featureReduction = this.clusterConfig } if (!this.state.clusterStatus) this.state.jimuMapView.jimuLayerViews[this.state.clusterLayer].layer.featureReduction = this.state.clusterStatus ? this.clusterConfig : null } activeViewChangeHandler = (jmv: JimuMapView) => { if (jmv) { const jimuLayerViews = jmv.jimuLayerViews; const jimuLayerViewIds = Object.keys(jimuLayerViews)[0]; const layerViewId = jimuLayerViewIds; if (layerViewId === undefined) { jmv = null; return } this.setState({ jimuMapView: jmv, clusterLayer: jimuLayerViewIds, errorTip: false }) } else { this.setState({ jimuMapView: undefined, clusterLayer: undefined, errorTip: true, clusterStatus: false }) } } renderWidgetPlaceholder() { return <WidgetPlaceholder icon={require('./assets/cluster.svg')} widgetId={this.props.id} message={'Please select a web map with a feature layer to enable point clustering.'} />; } render() { const mapContent = <JimuMapViewComponent useMapWidgetId={this.props.useMapWidgetIds?.[0]} onActiveViewChange={this.activeViewChangeHandler} /> let clusterContent = null; if (this.state.errorTip || !(this.props.useMapWidgetIds && this.props.useMapWidgetIds.length > 0)) { clusterContent = this.renderWidgetPlaceholder(); } else { clusterContent = <Label style={{ cursor: "pointer" }} > <Checkbox checked={this.state.clusterStatus} onChange={(e) => { this.onCheckBoxChange({ target: { value: e.target.checked, }, }); }} /> Cluster points </Label> } return <div style={{ height: '100%' }} className="cluster-map"> {clusterContent} <div>{mapContent}</div> </div> } }
作成したアプリは以下のようにプレビュー表示として完成形のイメージを確認することができます。
作成したアプリのデプロイや運用については以下をご確認ください。アプリのデプロイは簡単で ZIP 形式でダウンロードできるため、それを展開して Web サーバーなどに配置します。
さいごに
今回は、React を利用したローコードアプリについて紹介しました。最近はフルスクラッチだけでなく、ローコードアプリやノーコードアプリなども DX の影響かアプリ開発のスピードも重要な要素となっています。このようなローコードツールは開発工数も削減できるなどのメリットは大きいですね。開発者としては、バリバリコードを書きたいので寂しい気もしますが、世の中の流れやユーザーの要望にあったサービスを提供していくのは大事な要素なので、今後もこのあたりのスキルもキャッチアップしていきたいと思います。また、次回も React をネタに何か紹介できればと思います。