フリーダムの日記

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

React を利用したローコードツールによるアプリの開発

はじめに

本エントリーは React Advent Calendar 2021 11日目の記事です。

qiita.com

昨年は「地図アプリ開発で React Hooks を利用して React コンポーネントを作成する」内容の記事を紹介しました。 freedom-tech.hatenablog.com

そのなかで、地図ライブラリとして、ArcGIS API for JavaScript を紹介しました。ArcGIS API for JavaScript では、ES モジュールが提供されていますので、React などのフレームワークを利用した地図アプリ開発も容易に行うことができるようになっています。

github.com

今回は ArcGIS API for JavaScript も利用できるモダンな Web アプリ用の開発キット、ローコードツールとして人気がある ArcGIS Experience Builder という Web アプリの開発ツールについて紹介していきたいと思います。

ArcGIS Experience Builder とは

ArcGIS Experience Builder とは、2D および 3D データと連動する柔軟なレイアウトやコンテンツ、ウィジェットを使用して、独自の Web Experience を作成することができる Web アプリのローコードツールです。テンプレートを使用して、モバイルに適応したアプリを作成したり、テンプレートのレイアウトを変更して異なるスクリーンサイズに合わせてデザインを作成したりと、コードを書くことなく Web アプリを開発することができます。

developers.arcgis.com

ArcGIS Experience Builder は、ローコードツールでもあるため、ノーコードでアプリを開発することができますが、機能を拡張することができるようなっています。 ArcGIS Experience Builder では、ウィジェットと呼ばれる機能の塊が提供されており、そのウィジェットを拡張することができるようになっています。ウィジェット開発では、React のフレームワークを利用します。今回は、簡単なウィジェット開発について紹介したいと思います。

developers.arcgis.com

ArcGIS Experience Builder を利用した多くのアプリが公開されています。
参考までに公開されているアプリを紹介します。

軽石の漂着状況を可視化した地図システム「沖縄軽石マップ」

www.okicom.co.jp

アプリの例

アプリの例

ArcGIS Experience Builder の利用

ArcGIS Experience Builder を利用するにあたり、まずはインストールが必要になってきます。インストールに関しては本家のサイトに加えて、日本語向けのドキュメントも提供されているようですので、以下を参考にしていただければと思います。

developers.arcgis.com

esrijapan.github.io

ローコードツールなので、以下のようにノーコードでアプリを作成する場合は、 UI 操作だけで作成することが可能です。また、テンプレートなども豊富に用意されていますので、デザインができない方でもリッチなアプリやサイトを構築することができます。そのため、非エンジニアなどのデザイナーでも Web サイトを構築することができます。

ノーコード、ローコード

ノーコード、ローコード

ノーコード、ローコード

ノーコード、ローコード

React を利用したウィジェット開発

ArcGIS Experience Builder で機能を拡張したい場合は、ウィジェットと呼ばれる機能の塊があり、独自のウィジェットを開発することができます。基本的なウィジェット開発については以下を参考にしていただければと思います。

developers.arcgis.com

developers.arcgis.com

今回はクラスタリングの機能を追加しました。 機能としては、以下の画面のとおりで、複数のポイントをグループ単位に集約することができる機能です。縮尺ごとに動的に適用されます。縮小表示するにつれて、グループごとに集約されるポイント数が増えてグループの数が減少し、拡大表示するにつれて、クラスターグループの数が増加します

ノーコード、ローコード

ノーコード、ローコード

こちらのサンプルプログラムは、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
  })

 }
} 

widget.tsx の全体のソースは以下になります。

/** @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>
  }
}

作成したアプリは以下のようにプレビュー表示として完成形のイメージを確認することができます。

youtu.be

作成したアプリのデプロイや運用については以下をご確認ください。アプリのデプロイは簡単で ZIP 形式でダウンロードできるため、それを展開して Web サーバーなどに配置します。

developers.arcgis.com

さいごに

今回は、React を利用したローコードアプリについて紹介しました。最近はフルスクラッチだけでなく、ローコードアプリやノーコードアプリなども DX の影響かアプリ開発のスピードも重要な要素となっています。このようなローコードツールは開発工数も削減できるなどのメリットは大きいですね。開発者としては、バリバリコードを書きたいので寂しい気もしますが、世の中の流れやユーザーの要望にあったサービスを提供していくのは大事な要素なので、今後もこのあたりのスキルもキャッチアップしていきたいと思います。また、次回も React をネタに何か紹介できればと思います。