tpc19-3

TOPIC 19|TerriaJSでバリアフリールート検索ウェブアプリを開発する[3/3]|バリアフリールート情報を探索する

データカタログとして、歩行ルートを表示できるようになりました。最後に、「バリアフリールート情報の探索」の機能を追加します。

Share

データカタログとして、歩行ルートを表示できるようになりました。最後に、「バリアフリールート情報の探索」の機能を追加します。

【目次】

19.5   バリアフリールート情報の探索

 19.5.1  経路探索APIを作る

 19.5.2  PLATEAU VIEW 1.1に探索UIを実装する

19.6   まとめ

19.5 _ バリアフリールート情報の探索

続いて、バリアフリールート情報を探索できるようにします。

19.5.1 _ 経路探索APIを作る

まずは、始点と終点、そして、「健常者向け」「車いす利用者向け」などの検索条件を指定すると、その条件に合うバリアフリールートを返す、経路探索APIを作ります。

■APIのビルド

このAPIは、本トピックで提供しているサンプルのSRC¥routesearchapiフォルダに含まれています。このフォルダの内容を、適当な作業用フォルダ(以下ではhands-on2023フォルダとします)にコピーして、ビルドします。

JavaフレームワークのSpring Bootで実装しており、Spring Tools 4を用いて、次のようにビルドします。

【手順】経路探索APIをビルドする

[1]作業フォルダをラウンチする

Spring Tools 4を起動し、ファイルをコピーしたフォルダ(hands-on2023)を実行します。

[2]プロジェクトのインポート

[Project Explorer]から、[Import projects]―[Maven]―[Existing Maven Project]をクリックします。インポートウィンドウが表示されたら、「/pom.xml」という、routesearchapiのSNAPSHOT.warに関連のあるプロジェクトを指定し、[Finish]をクリックします。

図19-49 プロジェクトのインポート
図19-50 インポートするプロジェクトの選択

[3]Mavenを更新する

プロジェクトを右クリックし、[Maven]―[Update Project]を選択し、Maven を更新します。

図19-51 Maven を更新する

[4]warファイルを作成する

プロジェクトを右クリックし、[Run As]―[Maven build]を選択します。ビルドの設定画面が表示されたら、[Goals]に「package」と入力して、[Run]をクリックします。

図19-52 Marven build を実行する
図19-53 Goals に packageを設定して実行する

[5]ビルドの確認

コンソールに「BUILD SUCCESS」と表示され、targetディレクトリに「routesearchapi-0.0.1-SNAPSHOT.war」が作られます。

コラム:環境設定の変更

提供しているソースファイルは、Dockerコンテナの構成で動くように設定されています。接続先のデータベースなどをカスタマイズしたいときは、src/main/resources/application.propertiesを変更してください。

#DATABASE ⇒データベースの接続設定
spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://localhost:5432/devps_db
spring.datasource.username=devps
spring.datasource.password=handsonp2023
spring.jpa.show-sql=true

#LOG ⇒ログの出力先とログレベル
logging.file.name=/var/lib/tomcat9/logs/routesearchapi.log
logging.level.org.springframework.web=INFO
logging.level.view3d=DEBUG

#PORT ⇒ ポート番号※ 開発環境で実行する時のみ適用されます。war ファイルには反映されません。
server.port=8082
#ROUTE SEARCH EPSG ⇒ 表示時の座標系(view)と元データの座標系(data)
# ※元データの座標系は平面直角座標系を使用してください。
route.epsg.view=4326
route.epsg.data=6671
#etc
spring.mvc.pathmatch.matching-strategy = ANT_PATH_MATCHER

■APIのデプロイ

このAPIをDockerコンテナにデプロイします。

ビルドした「routesearchapi-0.0.1-SNAPSHOT.war」をDockerコンテナの作業用フォルダ(Settings¥docker)の下のwebappsフォルダにコピーし、名前を「routesearchapi.war」に変更します。

そして、一度、Dockerコンテナを停止して、開始し直します。

本トピックで配布しているサンプルでは、webappsフォルダは、Dockerコンテナ内の/opt/apache-tomcat-9.0.75/webapps/にマウントするように構成しています。コンテナを次回起動したときは、このフォルダに置いたファイルは、Tomcatによって自動デプロイされます。

■APIの解説

本サンプルで作成しているAPIは、Spring Bootで実装されています。

標準的なWebアプリケーションの3層構造に則り、外部からの入力を受け付けるController、業務ロジックを実装するService、データベースにアクセスするDaoの3階層に分けてアプリケーションを構成しています。以下では、3層構造に基づいて、サンプルソースの内部ロジックを説明します。

【参考】

https://penguinlabo.hatenablog.com/entry/spring/web-layered

①Controllerクラス

[/routesearchapi/src/main/java/view3d/controller/RouteSearchApiController.java]APIの処理全体を、routeSearchメソッドで実装しています。

APIのパスやメソッドを@RequestMapping、レスポンス@ApiResponsesアノテーションで定義しています。

メソッドの引数に、@RequestParamアノテーションでAPIのリクエストパラメータを定義しています。以下の3種類をクエリパラメータで定義しています。

・start: 経路探索開始地点緯度経度(カンマ区切り文字列)

・end: 経路探索終了地点緯度経度(カンマ区切り文字列)

・condition: 検索条件タイプ(1:健常者向け,2:車いす利用者向け,3:高齢者・乳幼児連れ向け,4: 視覚障害者向け)

@RequestMapping(value = "/search", method = RequestMethod.GET)
@ApiOperation(value = "条件付き経路探索を実施します", notes = "条件付き経路探索を実施")
@ResponseBody
@ApiResponses(value ={
@ApiResponse(code = 400, message = "リクエストパラメータが不正の場合.または開始・終了ノードがリクエスト地点から一定範囲内に存在しない場合.", response = ResponseError.class),
@ApiResponse(code = 404, message = "経路が見つからなかった場合", response = ResponseError.class),
@ApiResponse(code = 500, message = "処理時にエラーが発生した場合", response = ResponseError.class) })
public RouteSearchResultForm routeSearch(@RequestParam("start") String start,@RequestParam("end") String end, @RequestParam("condition") Integer condition) {

Controllerでは、リクエストデータのチェックを実装しています(42〜62行目)。パラメータのフォーマット不正や、緯度経度の範囲不正はリクエスト不正としてレスポンスを返すように実装しています。
64行目でServiceの処理を呼び出します。処理結果を受取り、レスポンスとして返します。

②Serviceクラス

[/routesearchapi/src/main/java/view3d/service/RouteSearchService.java]

経路探索の内部処理をrouteSearchサービスで実装しています。
内部処理の全体は下図のとおりです(ピンク色の項目のDBアクセス処理はDaoクラスで実施)。

図19-54 Serviceクラスの構成図

63〜67行目でNodeDao.getNearNodeメソッドから開始地点、終了地点から近い順にノードを5件ずつ取得しています。

開始終了地点から150m以内でノードを得られなかった場合、取得不正として結果を返します(105〜108行目)。

ノードが得られた場合、近い順に開始ノードから終了ノードまでの経路探索を実施します(69〜100行目)。経路探索処理は87行目にあるRouteSearchDao.searchRouteWithCostメソッドの呼び出しです。経路探索結果が得られた場合は、この結果を返します。RouteSearchDao.searchRouteWithCostメソッドの呼び出しです。経路探索結果が得られた場合は、この結果を返します。

結果が得られなかった場合、2番目、3番目……と近いノードを順に使用して経路探索を実施し、結果を得次第結果を返します。結果が得られない場合、エラーを返します。

③Daoクラス(NodeDao)

[/routesearchapi/src/main/java/view3d/dao/NodeDao.java]

getNearNodeメソッドで、最も近いノードを順に取得します。

引数として以下を受け付け、SQL文に埋め込むことで検索します。

wkt開始(終了)地点の座標(Well-known text形式)
limit取得件数
viewEPSG画面表示で用いる座標系
dataEPSGデータの座標系

元のSQL文は以下のとおりです(:(コロン)+引数名の箇所は引数の値で置換されます)。PostGISのST_Distance関数を用いることで、指定した座標からノードの距離を求め、距離が近い順の取得を実現しています。

そのほかPostGISの関数として、ST_GeomFromText関数をwktからジオメトリ型への変換に、ST_Transform関数を座標系の変換に使用しています。

SELECT
CAST(node_id AS integer) AS node_id
, ST_Distance(
t1.geom
, ST_Transform(ST_GeomFromText(:wkt, :viewEPSG), :dataEPSG)) AS distance
FROM
node_3d AS t1
ORDER BY distance asc Limit :limit

④Daoクラス(RouteSearchDao)

[/routesearchapi/src/main/java/view3d/dao/RouteSearchDao.java]

searchRouteWithCostメソッドで経路探索を実施します。
引数として以下を受け付け、SQL文に埋め込むことで検索しています。

startNodeId経路探索を開始するノードID
endNodeId経路探索を終了するノードID
condition検索条件タイプ(1:健常者向け,2:車いす利用者向け,3:高齢者・乳幼児連れ向け,4: 視覚障害者向け)
resSRIDレスポンス(画面表示で用いる)座標系

79〜92行目で、検索条件タイプに応じてエッジ(経路探索対象の経路一覧)を取得する SQLを組み立てています。エッジを取得するSQLの基本形(34行目の変数route_seach_edggeの内容)は以下のとおりです。

SELECT id AS seq, cast(link_id AS integer) AS id, cast(start_id AS integer) AS source,
cast(end_id AS integer) AS target, $edge_column AS cost
FROM link_3d WHERE link_id is not NULL

後述のpgr_dijksta関数で用いるため、以下のカラムを含めます。

seqシーケンス
id 一意のID
source リンクの開始地点ID
target リンクの終了地点ID
cost経路算出に用いるコスト。コストの値は検索条件別に異なるカラムに格納しているので、動的に取得先のカラムを変えるようにします。車いすや視覚障害者向けの検索の場合、段差や踏切などの経路を絞るため条件を追加しています(36、38行目の変数condition_wheelchair、condition_brail)。

引数の条件と、組み立ててエッジSQLを埋め込んで、経路探索SQLを実行します。
実行するSQLは以下のとおりです。流れを追って説明します。

SELECT ROW_NUMBER() OVER(ORDER BY united.geom ASC) AS result_id, :priority AS priority,
-- (3) (2)のジオメトリを GeoJSON 形式に変換し、総距離を求める
ST_AsGeoJSON(ST_Transform(united.geom, :srid)) AS geojson, ST_Length(united.geom) AS distance FROM
(
-- (2) (1)で取得したジオメトリを 1 つのジオメトリにまとめる
SELECT
ST_LineMerge(ST_UNION(res.geom)) AS geom
FROM
(
--- (1) pgr_dijkstra 関数で最短経路となるリンクの組み合わせを取得
SELECT
t1.seq
, t1
, edge
, t2.geom as geom
FROM
pgr_dijkstra(
:edge_sql
, :start_node_id
, :end_node_id
, directed ¥¥:= false
) AS t1
INNER JOIN link_3d AS t2
ON t1.edge = cast(t2.link_id AS integer)
) as res
) AS united;

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

(1)まず、pgr_dijkstra関数で最短経路となるリンクの組み合わせを取得します。pgr_dijkstra関数の引数にはエッジ取得SQLと、開始ノードID、終了ノードID、データの有向性の有無を指定しています。また、pgr_dijkstra関数の取得結果は最短となるリンクID(edge)の一覧となるため、内部結合でリンクのジオメトリも取得するようにします。

(2)(1)で取得したジオメトリはリンクごとに分かれているので1つのジオメトリにまとめます。PostGISのST_Union関数とST_LineMerge関数を使用します。

(3)(2)で求めたジオメトリをGeoJSON形式にまとめて、総距離を求めます。GeoJSONへの変換はST_AsGeoJSON関数で行っており、距離の算出はST_Length関数で行っています

19.5.2 _ PLATEAU VIEW 1.1に探索UIを実装する

こうして作成した経路探索APIを、PLATEAU VIEW 1.1から呼び出せるようにするUIを作ります。

■連携するためのカスタマイズとソースの構造

PLATEAU VIEW 1.1のソースコードのパッケージ構成は、図のとおりです。本トピックで修正するファイルは☆を、追加するファイルには★を付けています。PLATEAU VIEW 1.1は、JavaScriptのフレームワークであるReactを採用しているため、カスタマイズ箇所も、Reactで実装します。

【メモ】

ソースコードの配置やビルド手順の詳細については、PLATEAU VIEW1.1のREADMEを参照してください。

図19-55 PLATEAU VIEW 1.1のソースの構造と、本トピックにおいてカスタマイズ対象のファイル

■変更の適用とビルド

★や☆で示したファイルなど、今回、カスタマイズするファイル一式は、提供しているサンプルのSRC¥terriajsフォルダおよびSRC¥TerriaMapフォルダに格納してあります。これらを次の手順で、Dockerコンテナに適用し、ビルドします。

【手順】Dockerコンテナにカスタマイズしたファイルを適用する

[1]SRCをカレントフォルダにする

コマンドプロンプトを開き、SRCをカレントフォルダにします(「・・・略・・・」の部分は、ご自身の環境に合わせてください)。

cd ・・・略・・・¥SRC

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

[2]ファイルをコピーする

terriajsフォルダおよびTerriaMapフォルダの内容をすべてコピーします。

docker cp TerriaMap plateau_demo:/usr/local/work
docker cp terriajs plateau_demo:/usr/local/work/TerriaMap/packages/

[3]ビルドする

Dockerコンテナに、シェルでログインします。

docker exec -it plateau_demo /bin/bash

次のコマンドを順に実行して、ビルドします。

cd /usr/local/work/TerriaMap
yarn gulp
yarn start
コラム:うまく動かないときは

うまく動かないときは、次の点を確認してください。

①PLATEAU VIEW 1.1の動作が重い場合

NODE_OPTIONSの環境変数が適切に設定されているかを、Dockerのターミナルから確認してください。設定されていない場合は、適切に設定し、再度yarn gulpから実行してください。

【確認】

export -p

【設定】

export NODE_OPTIONS=--max_old_space_size=4096

②初期画面からのロードが進まない場合

PLATEAU VIEW 1.1の初期画面を開いたままロードが進まない場合、Tomcatに割り当てられたJVMメモリサイズが足りていないためGeoServerが正常に動作しておらず、初期読み込みのレイヤが読み込めていないことが影響している場合があります。その場合はDockerのターミナルに入りJVMメモリサイズを引き上げてください。
まずは、viエディタで、設定ファイルのsetenv.shを開きます。

vi $CATALINA_HOME/bin/setenv.sh

viエディタで、以下の内容を記述して保存します。

export CATALINA_OPTS='-Xms512m -Xmx1024m -Xss1024k -XX:PermSize=512m -XX:MaxPermSize=1024m -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled'

次のコマンドを入力して、Tomcatを再起動します。

exec $CATALINA_HOME/bin/catalina.sh run

■動作の確認

ブラウザで「http://localhost:8080/plateau/」を開き、変更点を確認します。
開くと、上部メニューに[経路]ボタンが追加されたのがわかります。クリックすると、「開始地点」「終了地点」「条件」を選択できます。

「開始地点」や「終了地点」は、マウスで地図上をクリックすることで検索できます。検索すると、その検索ルートが表示されます。

図19-56 条件に応じたルートを検索できる

■UI実装の解説

以下では、ソースコードの修正箇所と、その処理内容について、説明します。

●画面用コンポーネント

まずは、経路探索の入力を受け付ける画面用のコンポーネントから説明します。次のファイルです。

TerriaMap¥packages¥terriajslib¥ReactViews¥Map¥Panels¥RoutePanel¥RoutePanel.jsx

このファイルでは、次の関数が定義されています。★をつけたものについて、順に説明します。

関数名処理概要
componentDidMountコンポーネント読み込み時処理
★createCZMLTextAndWorkbenchAddGeoJSON形式のデータからczml形式のデータを作成し、ワークベンチに追加(=検索結果地物を表示)する。
★render画面描画処理
clearクリアボタン押下時イベント。検索条件と検索結果をリセットする。
★search検索ボタン押下時イベント。入力した条件でAPIリクエストし、結果を処理する。
表19-7 画面用コンポーネント

【メモ】

Panelsフォルダ内には、ヘルプパネルや共有パネルなどのパネルのjsxファイルも格納されているので、こ
れらも作成する必要があります。

(1)コンポーネントが読み込まれたときの処理

RoutePanel.jsxの36〜51行目のconstructorでは、コンポーネントが読み込まれたときの処理を定義しています。親コンポーネントからのpropsの引継ぎと、自コンポーネント内のstateの初期化をしています。

constructor(props) {
super(props);
this.state = {
viewState: props.viewState,
terria: props.terria,
isAnimatingOpen: true,
routeStart: props.terria.routeStart,
routeEnd: props.terria.routeEnd,
routeItems: [
{ title: "最短のルート(健常者向け)" },
{ title: "階段・段差が少ないルート(車いす/杖・義手義足利用者向け)" },
{ title: "段差・勾配が少ないルート(高齢者・乳幼児むけ)" },
{ title: "点字ブロックを優先したルート(視覚障害者)" }
]
};
}

【参考】

React.Component – React (reactjs.org)
propsとstateのイメージをつかむ【はじめての React】 - Qiita

viewState、terria、routeStart、routeEndはpropsから受け取っています。viewStateは画面の表示状態を共通管理するオブジェクト、terriaはPlateauView(TerriaMapベース)の諸々の機能を司るオブジェクトです。routeStartとrouteEndは、それぞれ経路探索の開始地点・終了地点の座標を保持します。地図上でクリックした際に取得した座標を受け渡すためにpropsを用います。

(2)画面の描画処理

renderでは、画面の描画処理を定義します。パネル全体の構成や、入力欄、検索・クリアボタンなどの要素、イベント処理を定義します。

(3)ボタンを押したときのイベント処理

Searchでは、検索ボタン押下時のイベントを定義します。経緯度の値をチェックしたあとで、fetch関数を用いて、経路探索APIを呼び出します(324行目)。結果が正常であれば、createCZMLTextAndWorkbenchAdd関数を呼び出します。結果が異常の場合は、ステータスコードによってダイアログメッセージを出し分けます。

なお、fetch関数に渡すapiUrlのうち、プロトコルやドメインを指定する箇所(/route以前の部分)は環境によって設定を変える必要があるため、cutomconfig.jsonで設定した値を参照するようにします。

cutomconfig.jsonで設定した値はimport文を使ってimportし、Configから取得します。

※cutomconfig.jsonの設定内容は後述

import Config from "../../../../../customconfig.json";
・・・中略・・・
const routeStartArray = document.getElementById("routeStart").value.split(',');
const routeEndArray = document.getElementById("routeEnd").value.split(',');
const searchType = document.getElementById("searchType").value;
const viewparams = "?start=" + routeStartArray[1] + "," + routeStartArray[0] + "&end="routeEndArray[1] + "," + routeEndArray[0] + "&condition=" + searchType;
const apiUrl = Config.config.apiUrl + "/route/search" + viewparams;
const reg = /^¥d+.{1}¥d+/;
if (!reg.exec(routeStartArray[0]) || !reg.exec(routeStartArray[1])) {
alert("開始地点の経緯度を正しく指定してください");
return false;
}
if (!reg.exec(routeEndArray[0]) || !reg.exec(routeEndArray[1])) {
alert("終了地点の経緯度を正しく指定してください");
return false;
}
fetch(apiUrl)
.then(res => res.json())
.then(res => {
if (res.result && JSON.parse(res.result) && JSON.parse(res.result).coordinates) {
console.log(res);
this.createCZMLTextAndWorkbenchAdd(JSON.parse(res.result).coordinates,JSON.parse(res.result).type);
} else {
if (res.status) {
if (res.status == 400) {
alert("指定した開始・終了地点の近辺に経路が見つかりませんでした。");
} else if (res.status == 404) {
alert("条件に合致した経路が見つかりませんでした。");
} else {
alert(res.status + "エラー 経路情報の取得に失敗しました");
}
} else {
alert("経路情報の取得に失敗しました");
}
}
}).catch(error => {
console.error('経路探索処理に失敗しました', error);
alert('経路探索処理に失敗しました');
});

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

(4)経路検索結果の地物表示

createCZMLTextAndWorkbenchAddでは、経路探索結果として受け取った地物の表示処理をします。

TerriaMapでは、3DのGISデータを3D表示するためには、データがCZML形式である必要があります。経路探索APIで受け取ったデータはGeoJSON形式であるため、CZML形式に変換します(67〜149行目)。ここでは座標情報のセットと表示のスタイル定義を行っています。

[参考]

CZML·GIS実習オープン教材(gis-oer.github.io)

let czmlObj = [];
let positionsArrayArray = [];
const czmlTop = {
id: "document",
name: "line",
version: "1.0"
};
czmlObj.push(czmlTop);
if (type == "MultiLineString") {
// マルチラインの場合
for (let i = 0; i < coordinates.length; i++) {
let positionsArray = [];
let coordArray = [];
for (let j = 0; j < coordinates[i].length; j++) {
coordArray.push(coordinates[i][j][0]);
coordArray.push(coordinates[i][j][1]);
positionsArray.push(
Cartographic.fromDegrees(coordinates[i][j][0], coordinates[i][j][1]));
//2D の JSON が来た場合高さ 0 でセットする。
(coordinates[i][j].length == 3) ?
coordArray.push(parseFloat(coordinates[i][j][2])) : coordArray.push(0);
}
let featureObj = {
id: i + 1,
name: "routeSearchResult",
polyline: {
"positions": {
"cartographicDegrees": coordArray,
},
"material": {
"polylineOutline": {
"color": {
"rgba": [255, 165, 0, 255],
},
"outlineColor": {
"rgba": [0, 0, 0, 255],
},
"outlineWidth": 2,
},
},
"width": 5.0,
}
}
czmlObj.push(featureObj);
positionsArrayArray.push(positionsArray);
}
} else {
// シングルラインの場合
・・・中略・・・
}

結果によって、ポリラインがマルチポリラインの場合とシングルポリラインの場合とがあるので、条件を分けて処理します。

【メモ】

ポリラインとは、複数の線分や円弧、曲線、矩形などを組み合わせた1つのオブジェクトのことです。

CZMLデータを生成したら、高さを調整します(155〜165行目)。これは、データの持つ高さ情報がCesiumの地形情報と合っていないため、必要な調整です。

【参考】

「Cesium」を利用して、任意の座標から高度を取得する(nnao-web.boo.jp)

sampleTerrainMostDetailed(terrainProvider, positions).then((updatedPositions) => {
try{
if (czmlObj[i + 1]?.polyline?.positions?.cartographicDegrees) {
for (let j = 2;
j < czmlObj[i + 1]?.polyline?.positions?.cartographicDegrees.length;
j = j + 3) {
let newHeight = parseFloat(updatedPositions[((j + 1) / 3) - 1].height);
czmlObj[i + 1].polyline.positions.cartographicDegrees[j] =
newHeight +
(parseFloat(czmlObj[i + 1].polyline.positions.cartographicDegrees[j]));
}
・・・中略・・・
}
}catch(error){
console.error('処理に失敗しました', error);
}
})

最後に生成したデータを地図に追加します(167〜176行目)。まずは、既存の経路探索結果表示物を消去します(167〜171行目)。その後、CzmlCatalogItemオブジェクトを生成し、czmlStringとして、生成したCZMLデータをセットします。item.loadMapItems()関数を呼び出してアイテムを追加し、terria.work bench.add(item)関数を呼び出して、ワークベンチ上に経路探索結果を追加します。

item.setTrait(CommonStrata.user, "name", "経路探索 " + routeItems[searchType - 1]?.title);
item.setTrait(CommonStrata.definition, "czmlString", JSON.stringify(czmlObj));
item.loadMapItems();
this.state.terria.workbench.add(item);

●経路検索画面を呼び出すボタン

経路検索画面を呼び出すボタンは、下記のファイルとして作成しています。このファイルを格納しているフォルダは、ヘルプボタンなど既存のボタンが格納されている場所です。

TerriaMap¥packages¥terriajs¥lib¥ReactViews¥Map¥RouteButton¥RouteButton.tsx

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

このファイルでは、要素の定義とクリックイベントを定義しています。
ボタンをパネルに追加するため、以下の場所のviewState.tsを修正しています。

TerriaMap¥packages¥terriajs¥lib¥ReactViewModels¥ViewState.ts

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

まずは、パネルの開閉状態を管理する、showRouteMenuとroutePanelExpandedをobsevableとして定義します(106〜108行目)。

// 経路探索
@observable showRouteMenu: boolean = false;
@observable routePanelExpanded: boolean = false;

パネルの表示・非表示を切り替える、「showRoutePanel」「clearRoutePanel」「hideRoutePanel」の3つ関数の定義を追加します(639〜667行目)。

//経路探索パネルの表示
@action
showRoutePanel() {
//this.terria.analytics?.logEvent(Category.route, CustomAction.panelOpened);
this.showRouteMenu = true;
this.routePanelExpanded = false;
this.terria.setMode(2);
this.terria.setRouteStart("");
this.terria.setRouteEnd("");
this.setTopElement("RoutePanel");
}
//経路探索パネルの初期化
@action
clearRoutePanel() {
this.terria.setMode(2);
this.terria.setRouteStart("");
this.terria.setRouteEnd("");
}
//経路探索パネルの非表示
@action
hideRoutePanel() {
this.terria.setMode(0);
this.terria.setRouteStart("");
this.terria.setRouteEnd("");
this.showRouteMenu = false;
this.setTopElement("");
}

●マップ上で地点をクリックできるようにする

マップ上で地点をクリックできるようにするため、まずは、操作モードや開始地点、終了地点、クリックしたときの経緯度を保存できる変数を作ります。
TerriaMap¥packages¥terriajs¥lib¥Models¥Terria.tsに、次のように、observableとsetterを定義します(553-608行目)。

・mode: 操作モード

・routeStart: 開始地点

・routeEnd: 終了地点

・clickLatLong: クリック地点の経緯度

/**
*経路検索におけるスタート地点の経緯度
*@type {string}
*/
@observable routeStart = "";
/**
*経路探索 開始地点の経緯度をセット
*@param {string}
*/
@action
setRouteStart(routeStart: string) {
this.routeStart = routeStart;
}

そして、TerriaMap¥packages¥terriajs¥lib¥Models¥Cesium.tsにある、地図画面左クリックイベント定義を修正します(307-381行目)。

inputHandler.setInputAction(e => {
// ここにイベント処理を実装(後述)
}, ScreenSpaceEventType.LEFT_CLICK);

なお、上記の修正に加え、冒頭で、以下のimportを追加しています。

import CommonStrata from "./Definition/CommonStrata";
import GeoJsonCatalogItem from "./Catalog/CatalogItems/GeoJsonCatalogItem";

イベント処理の実装内容は、以下のとおりです。

(1)経緯度の算出

308〜314行目では、クリックしたピクセル位置から経緯度を求めています。

const pickRay = this.scene.camera.getPickRay(e.position);
const pickPosition = this.scene.globe.pick(pickRay, this.scene);
const pickPositionCartographic =
pickPosition && Ellipsoid.WGS84.cartesianToCartographic(pickPosition);
if (pickPositionCartographic) {
const latitude = CesiumMath.toDegrees(pickPositionCartographic.latitude);
const longitude = CesiumMath.toDegrees(pickPositionCartographic.longitude);

terria.modeの値によって処理が分岐するのですが、ここではmode=2で経路探索の開始地点を追加するコードを例に説明します(321-348行目)。その処理では、経緯度の値をrouteStartにセットし、開始地点の入力部に表示します。またこのとき、既存の開始地点と終了地点のレイヤをworkbenchから削除します。

//mode=2 の時は経路探索の開始地点を追加
} else if (this.terria.mode === 2) {
this.terria.setRouteStart(latitude + "," + longitude);
this.terria.setMode(3);
const routeStart = document.getElementById('routeStart') as HTMLInputElement;
routeStart.value = this.terria.routeStart;
const items = this.terria.workbench.items;
for (const aItem of items) {
if (aItem.uniqueId === '開始地点' || aItem.uniqueId === '終了地点') {
this.terria.workbench.remove(aItem);
}
}

次に、開始地点のポイントフィーチャレイヤを作成し、workbenchに追加します。

 const pointItem = new GeoJsonCatalogItem("開始地点" , this.terria);
pointItem.setTrait(CommonStrata.user, "geoJsonData", {
type: "Feature",
properties: {
"marker-color": "#ffff00",
"searchMode":"on",
"経度":longitude,
"緯度":latitude,
},
geometry: {
type: "Point",
coordinates: [longitude, latitude]
}
});
pointItem.setTrait(CommonStrata.user, "idealZoom",{targetLongitude:longitude,targetLatitude:latitude,targetHeight:100,heading:0,pitch:30,range:500});
pointItem.loadMapItems();
this.terria.workbench.add(pointItem);

●親コンポーネントに追加する

これまで説明してきた追加したコンポーネントを、既存の親コンポーネントに追加します。

(1)UIの追加

UIのファイルであるTerriaMap¥packages¥terriajs¥lib¥ReactViews¥StandardUserInterface¥StandardUser Interface.jsxには、次の変更を加えています。

・RoutePanel.jsxをimportする

import RoutePanel from "../Map/Panels/RoutePanel/RoutePanel";

・HelpPanelを読み込んでいる箇所の直下にRoutePanelのコンポーネントを追加(479~481行目

{this.props.viewState.showHelpMenu &&
this.props.viewState.topElement === "HelpPanel" &&
<HelpPanel terra={terria} viewState={this.props.viewState} />
)}
{this.props.viewState.showHelpMenu &&(
<RoutePanel terria={terria} viewState={this.props.viewState} />

)}

(2)メニューの追加

メニューのファイルであるTerriaMap¥packages¥terriajs¥lib¥ReactViews¥Map¥MenuBar.jsxには、次の
変更を加えています。

・RoutePanel.jsxをimportする

import RoutePanel from "../Map/Panels/RoutePanel/RoutePanel";

・RouteButtonコンポーネントを追加する

<ul className={classNames(Styles.menu)}>
<li className={Styles.menuItem}>
<RouteButton
viewState={props.viewState}
/>
</li>
</ul>

●カスタム機能向けの追加

カスタム機能向けに、以下の設定ファイルを追加しています。

TerriaMap¥packages¥terriajs¥customconfig.json

※上記リスト内、「¥」表記の箇所はフォントの設定によっては半角バックスラッシュとなります。なお、文字コードとしては同じですので、リストからコピーのうえ使っていただいて構いません。

内容は、以下のとおりです。Config.apiUrlは、APIのトップURLです。環境に合わせて修正してください。

{
"config": {
"apiUrl":"http://localhost/api"
}
}

■UIの完成

以上のようにして、PLATEAU VIEW 1.1に独自のUIを付けることができました。

図19-57 経路探索でUIを追加した例

19.6 _ まとめ

このトピックでは、バリアフリールートの探索を題材に、PLATEAU VIEW 1.1に機能を追加する2つの方法を解説しました。

ひとつはPLATEAU VIEW 1.1のデータカタログとして追加できるようにする方法です。GeoServerで、さまざまな地理空間情報を配信することで、表示する地図をカスタマイズできます。このトピックで、バリアフリーのルートを重ねましたが、バスの路線ルートや道路の埋設管などを可視化することも同様の手法で行えます。

もうひとつは、PLATEAU VIEW 1.1のUI自体に手を加える方法です。PLATEAU VIEW 1.1にボタンを付けるのは比較的簡単ですが、地図上でクリックしたときに、その緯度経度を取得したり、地図上に何かオブジェクトを重ねたりするのは少し複雑で、このトピックで解説したようなコードが必要です。

このトピックでは、バリアフリールートの情報検索を題材としましたが、リアルタイムで何かオブジェクトを重ねて表示したり、地図上のオブジェクトをクリックして選択したりするなどできるUIを実装したい場面などにも応用できるでしょう。

●参考リンク

【歩行空間ネットワーク】

歩行空間ネットワークデータの概要
歩行者移動支援サービスに関するデータサイト
歩行空間ネットワークデータ等整備仕様
歩行空間ネットワークデータ整備ツール
G空間情報センターの配布ページ
エリアマネジメント・ダッシュボード構築 技術検証レポート

【各種ツール/ソフトウェア】

Docker for Windows
QGIS
Anaconda
Spring Tools 4
Spring Boot
GeoServer
GeoServerのSQL Views
GeoServerのコンテナ設定

【PLATEAU VIEW 1.1/TerriaJS/Cesium/React】

稼動環境構築手順書
PLATEAU VIEWのカスタマイズ
PLATEAU-Terria-web-app
TerriaJS―Catalog Items
「Cesium」を利用して、任意の座標から高度を取得する(nnao-web.boo.jp)
React.Component
propsとstateのイメージをつかむ【はじめてのReact】-Qiita

【その他】

わかりやすい平面直角座標
CZML·GIS実習オープン教材(gis-oer.github.io)
3層構造

【文】

守屋 三登志(アジア航測株式会社)、大澤文孝