今回は ArcGIS API for JavaScript を利用して、以下のような地図アプリを作る方法を解説します。
使用するライブラリ・データ
アーキテクチャ
地図アプリ(GIS)は、地図情報(絵)に地理情報(データ)を加えて、操作や分析を可能にするものです。前提知識についてはArcGISの開発元であるEsri社のページが分かりやすくてオススメです。
ArcGIS API for JavaScript のアーキテクチャを簡単な図で表すと、以下のようになります。
Name | Description |
---|---|
Container | 地図を表示するHTMLの領域です。ViewをContainer要素に紐づけます。 |
View | 地図操作のベースコンポーネントです。表示座標やズーム倍率などを管理します。 |
Map | 地図情報のコンポーネントです。表示する地図と、地図に重ねる地理情報のレイヤを管理します。 |
Layer | 地理情報のコンポーネントです。地理情報にはベクタとラスタがありますが、ベクタでは座標から点・線・面のオブジェクトが表現され、それが何であるかの情報を持ちます。 |
UI | 地図に追加されるUIコンポーネントです。例えば、GPS情報から現在地に移動したり、地理情報を検索するUIを追加することができます。 |
ArcGISでは Map で利用する地図情報をマップサービス、 Layer で利用する地理情報をジオデータサービスとして公開することができます。 Layer はジオデータサービスの地理情報だけでなく、標準的なGeoJSON、KML、OGCなどのさまざまなフォーマットを扱うことが可能です。
地図アプリの実装
今回は、ArcGIS Onlineで無料公開されているデータだけを利用して、地図アプリを実装します。
ライブラリの読み込み
ArcGIS API for JavaScript を読み込みます。
<head>
<link rel="stylesheet" href="https://js.arcgis.com/4.22/esri/themes/light/main.css">
<script src="https://js.arcgis.com/4.22/"></script>
</head>
モジュールの追加
ArcGIS API for JavaScript はモジュール形式となっており、使用したいコンポーネントごとにモジュールを読み込みます。リファレンスに autocast
とある一部のコンポーネントは、自動的に読み込まれます。
次の例では、最低限の View、Map、Layer モジュールを読み込みます。
<head>
...
<script>
require([
"esri/config",
"esri/Map",
"esri/views/MapView",
"esri/layers/GeoJSONLayer",
], (esriConfig, Map, MapView, GeoJSONLayer) => {
// 実装する
});
</script>
</head>
require
か import
でモジュールを読み込み、アロー関数(または function
)の引数に同じ順序で追加します。
Layerの生成
ArcGIS標準のジオデータサービスを利用する場合は GeoJSONLayer ではなく FeatureLayer を利用します。実装方法は変わらないので、今回はArcGISユーザ以外にも分かりやすいGeoJSONをベースに解説します。
- 利用するコンポーネント:GeoJSONLayer
- 利用するデータ:都道府県のGeoJSON
ArcGIS Hubで公開されるGeoJSON形式の地理情報から Layer を生成します。
ファイル形式に対応したコンポーネントを使用することで、GeoJSON以外から Layer を生成することもできます。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
});
最小セットとして、GeoJSONを取得する url
と、レイヤを識別する id
を指定します。id
は自動付与されますが、解析しにくいので指定したほうが良いでしょう。
outFields
で、GeoJSONのプロパティをオブジェクトの属性値として持たせることができます。これにより、プログラムから地図上のオブジェクトが何であるかを読み取ることができるようになります。
Layer については、高度な使い方でさらに解説していきます。
Mapの生成
- 利用するコンポーネント:Map
- 利用する地図:オープンストリートマップ
ArcGIS Onlineで公開されるベースマップから Map を生成します。
Map は ベースマップに地理情報の Layer を重ねることができます。
const map = new Map({
basemap: "streets-vector",
layers: [prefLayer, ...]
});
ここでは、ベースマップとして無料公開されているオープンストリートマップを読み込みます。
Viewの生成とContainerの配置
- 利用するコンポーネント:MapView
地図を表示するための View を生成します。
const view = new MapView({
map: map,
container: "viewDiv",
center: [138.727363, 35.360626],
zoom: 6,
});
map
に生成した Map オブジェクトを指定し、container
に表示するHTMLの領域を指定します。
center
と zoom
で初期表示する座標と拡大率を指定することで、ユーザをエントリポイントに誘導できます。
<head>
...
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="viewDiv"></div>
</body>
最後に、HTMLボディに container
で指定した要素を配置すれば、地図アプリの完成です。
UIの実装
View、Map、Layer だけの地図アプリでは寂しいので、UIを追加していきます。
スケールの追加
- 利用するコンポーネント:ScaleBar
View にスケール(縮尺)を追加します。
ズームを多用する場合に役立ちます。
// require(["esri/widgets/ScaleBar"], (ScaleBar) => { ...
const scalebar = new ScaleBar({
view: view,
unit: "dual"
});
view.ui.add(scalebar, "bottom-left");
bottom-left
は UI を追加する地図上の位置を指します。
コンパスの追加
- 利用するコンポーネント:Compass
View にコンパス(方位磁針)を追加します。
タブレットやモバイルで地図を回転させる場合に役立ちます。
// require(["esri/widgets/Compass"], (Compass) => { ...
const compass = new Compass({
view: view,
});
view.ui.add(compass, "bottom-left");
現在地ボタンの追加
- 利用するコンポーネント:Locate
View に現在地ボタンを追加します。
現在地の周辺にある地理データを探す場合に役立ちます。
// require(["esri/widgets/Locate"], (Locate) => { ...
const locateBtn = new Locate({
view: view
});
view.ui.add(locateBtn, "top-left");
現在地ボタンはセキュリティ保護されていないオリジンではサポートされず、画面に表示されません。(地図アプリをHTTPSで公開する必要があります。)
検索ボックスの追加
- 利用するコンポーネント:Search
View に検索ボックスを追加します。
検索ボックスを使うと、 地理データを検索して瞬時に移動することができます。
// require(["esri/widgets/Search"], (Search) => { ...
const search = new Search({
view: view,
autoSelect: true,
minSuggestCharacters: 1,
includeDefaultSources: false,
sources: [
{ layer: prefLayer, name: "都道府県", zoomScale: 1000000 },
{ layer: cityLayer, name: "都市", zoomScale: 500000 }
]
});
view.ui.add(search, "top-right");
デフォルトでは ArcGIS World Geocoding Service を利用して任意の地理データを検索します。
地図アプリで扱う Layer の地理データを検索したい場合は sources
に Layer を指定し、必要に応じて includeDefaultSources
を指定して ArcGIS World Geocoding Service をOFFります。
検索ボックスに表示される Layer 名は name
で指定します。
zoomScale
の指定により、検索結果を選択したときのズーム倍率を制御できます。
拡大率の指定にはレベルとスケールの2つの方法があります。
レベルは拡大率を示し、数字が大きいほど拡大率が高いです。
スケールは縮尺を示し、その縮尺まで拡大するため数字が小さいほど拡大率が高いです。
Zoom levels and scale
検索ボックスでは入力から結果をサジェストしてくれますが、 minSuggestCharacters
の指定によりサジェストが発生する文字数を制御できます。デフォルトは3文字ですが、都道府県はほとんど3文字なのでこれではサジェストがうまく機能しません。
Layer にデータフィールドを複数持つ場合、
searchFields
で検索に利用するフィールドを指定することができます。また、searchFields
でサジェストに表示されるフィールドを指定することができます。
ベースマップを切り替える
- 利用するコンポーネント:BasemapGallery
Google Mapのようにベースマップを切り替えることもできます。
// require(["esri/widgets/BasemapGallery"], (BasemapGallery) => { ...
const basemapGallery = new BasemapGallery({
view: view
});
view.ui.add(basemapGallery, "bottom-right");
動作の制御
地図アプリにロジックを追加して、動作を制御します。
イベントハンドラの実装
View や一部の UI コンポーネントは、イベントをパブリッシュします。
on
メソッドでイベントハンドラを実装することで、イベントを補足してロジックを実行できます。
view.on("click", (event) => {
view.hitTest(event).then((response) => {
const graphic = response.results[0]?.graphic;
if (graphic?.layer === prefLayer) {
view.goTo({
center: graphic.geometry.centroid,
zoom: 10
});
}
});
});
ここでは、地図上でいずれかの都道府県をクリックしたときに、指定倍率までズームしています。
hitTest
メソッドにより、パブリッシュされたイベントからクリックされたオブジェクトを取得しています。hitTest
メソッドでは、もう少し複雑な指定によりイベントから特定の Layer の結果だけ受け取ったりもできますが、今のところあまりメリットを感じていません。
ウォッチャーの実装
(きっと)すべてのコンポーネントの祖先である Accessor
には watch
メソッドがあり、
プロパティの変更を監視することができます。
view.watch("zoom", (newValue, oldValue) => {
console.log("zoom level: " + oldValue + " to " + newValue);
});
ここでは、ズームレベルの変更を監視してログ出力しています。(意味のない処理ですが)
なお、孫プロパティを監視するときは abc.def
のように指定します。
watch
により従来のイベント駆動プログラミングではなく、リアクティブプログラミングに移行することが可能となります。
View 生成を保証する
一部の処理は非同期で実行されるため、プロミスを利用して完了を保証する必要があります。
以下では、 View の生成が完了した後にロジックが実行されます。
view.when(() => {
// ロジックを実装する
}, (error) => {
// エラーハンドリングを実装する
});
以下では、 特定の Layer を表示する LayerView の生成が完了した後にロジックが実行されます。
view.whenLayerView(prefLayer)
.then((layerView) => {
// ロジックを実装する
.catch((error) => {
// エラーハンドリングを実装する
});
初期化前にロジックが実行され不正な状態となってしまうことを防止できるので、イベントハンドラやウォッチャーは基本的に when
配下に実装すると良さそうです。
高度な使い方
ここからは、少し高度な使い方を解説します。
Layer をカスタマイズする
オブジェクトの表示を制御する
オブジェクトの描画
renderer
の指定により、オブジェクトの描画を変更することができます。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
renderer: {
type: "simple",
symbol: {
type: "simple-fill",
color: [128, 128, 128, 0.5],
outline: {
color: "white"
}
}
}
});
ここでは、オブジェクトの表示色を変更してグレーの透過色にしています。
例えば、面の地理データを点として表示することも可能です。
ラベルの表示
labelingInfo
の指定により、オブジェクトにラベルを表示することができます。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
labelingInfo: [{
labelExpressionInfo: { expression: "$feature.japan_pref_pref" },
labelPlacement: "above-center",
symbol: {
type: "text",
color: "black",
haloSize: 1,
haloColor: "white"
}
}]
});
labelExpressionInfo
の expression
でラベルの内容を指定します。
labelPlacement
でラベルの表示位置を変更することもできます。
ポインタイベントでオブジェクトをハイライトする
highlightOptions
の指定により、選択したオブジェクトをハイライトすることができます。
const view = new MapView({
map: map,
container: "viewDiv",
zoom: 6,
center: [138.727363, 35.360626],
highlightOptions: {
color: "red"
}
});
ハイライトするには、LayerViewの highlight
メソッドを呼び出す必要があります。
以下では、ポインタがオブジェクトに触れている間はハイライトしています。
let highlighted = null;
view.on("pointer-move", (event) => {
view.hitTest(event).then((response) => {
const graphic = response.results[0]?.graphic;
const objectId = graphic.attributes.__OBJECTID;
const layer = graphic?.layer;
if (map.layers.includes(layer)) {
// 何度もハイライトされるため、同一オブジェクトでは抑止
if (highlighted?.objectId !== objectId) {
highlighted?.highlight.remove();
view.whenLayerView(layer)
.then((layerView) => highlighted = {
objectId: objectId,
highlight: layerView.highlight(graphic)
});
}
} else {
highlighted?.highlight.remove();
}
});
});
graphic
(地理データオブジェクト)はユニークIDを持つため、これをキーに同一オブジェクトかどうかを判断しすることができます。なお、ユニークIDは Layer ごとに振られるため、複数 Layer をアクティブにしている場合は「Layer ID + Object ID」で判断する必要がありそうです。
後でハイライトを消すために、 highlight
メソッドの戻り値を保持しておく必要があります。
なお、ポップアップを表示する場合は自動的にハイライトが制御されるようです。
ポップアップを制御する
popupTemplate
の指定により、オブジェクトに表示されるポップアップの内容を変更することができます。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
popupTemplate: {
title: "{japan_pref_pref}",
content: "<a href='{opendata_pref_url1}'>ホームページ</a>",
overwriteActions: true,
actions: [
{
title: "ズームアウト",
id: "zoom-out",
className: "esri-icon-zoom-out-magnifying-glass"
},
]
},
popupEnabled: false
});
popupTemplate
の title
には、GeoJSONのプロパティからオブジェクトの表示名を指定します。
content
には、HTMLやデータを指定してコンテンツを生成することができます。詳細はContentを参照。
ポップアップにはデフォルトで「ズーム」というボタンが表示されますが、actions
の指定により任意のボタンを追加することができます。アクションの id
はイベント名となり、以下のように Popupの trigger-action
イベントハンドラで補足することができます。
デフォルトの「ズーム」ボタンからは
zoom-to-feature
イベントがパブリッシュされますが、これの挙動が気に入らない場合はoverwriteActions
で「ズーム」ボタンを消去することができます。
最後に、デフォルトではオブジェクトをクリックするとポップアップが表示されますが、 popupEnabled
の指定により無効化することができます。 click
イベントの実装に合わせて使うと良さそうです。
ポインタイベントでポップアップを制御する
ポインタがオブジェクトを指しているときに自動的にポップアップを表示する場合、少々厄介です。
ポインタの動作は pointer-move
イベントハンドラで捕捉するのですが、これはポインタが動くと常に動作するため、オブジェクト内でポインタを移動させたり、ポップアップのボタンをクリックしようとすると、何度もポップアップが再描画されてしまいます。
実に原始的ですが、以下ではポインタが同一オブジェクト内にいる場合はポップアップの再描画を抑止しています。
let popupedObjectId = null;
view.on("pointer-move", (event) => {
view.hitTest(event).then((response) => {
const graphic = response.results[0]?.graphic;
if (map.layers.includes(graphic?.layer)) {
// ポップアップが何度も再描画されるため、同一オブジェクトでは抑止
const objectId = graphic.attributes.__OBJECTID;
if (popupedObjectId != objectId) {
popupedObjectId = objectId;
view.popup.open({
location: graphic.geometry.centroid,
features: [graphic]
});
}
// ポインタイベントで表示したポップアップのみクローズ
} else if (popupedObjectId === view.popup.features[0]?.attributes.__OBJECTID) {
popupedObjectId = null;
view.popup.close();
}
});
});
graphic
(地理データオブジェクト)はユニークIDを持つため、これをキーに同一オブジェクトかどうかを判断しすることができます。なお、ユニークIDは Layer ごとに振られるため、複数 Layer をアクティブにしている場合は「Layer ID + Object ID」で判断する必要がありそうです。
Layer 以外に「ベースマップの機能」や「現在地ボタンで表示された現在地の座標」などでもポップアップが表示される可能性があります。このため、ポップアップをクローズする際には「閉じて良いポップアップか」を確認することをお勧めします。
複数の Layer を使用する
表示する Layer を切り替える
都道府県と市区町村のように包含関係にあたる地理データを扱う場合、ズームの拡大率によって表示をする地理情報を切り替えたくなると思います。
ここでは Layer を切り替える方法を2つ紹介します。(他にもあると思います。)
方法1:minScale
, maxScale
を利用する
Layer にはスケールによって表示/非表示を切り替える機能があります。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
maxScale: 750000
});
const cityLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/ed99f2e4eab9416993020c97a429459f_0.geojson",
id: "city",
outFields: ["USER_city_name", ...]
minScale: 750000
});
minScale
と maxScale
により、ある倍率からは表示される Layer が切り替わります。
方法2:visible
を利用する
visible
を指定することで、プログラマティックに表示/非表示を切り替えることができます。
const prefLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/dd492422cd2e4eb09eb7bb7aa2ca91b6_0.geojson",
id: "pref",
outFields: ["japan_pref_pref", ...]
});
const cityLayer = new GeoJSONLayer({
url: "https://opendata.arcgis.com/datasets/ed99f2e4eab9416993020c97a429459f_0.geojson",
id: "city",
outFields: ["USER_city_name", ...]
visible: false
});
方法1と同様に拡大率で切り替えるなら、ウォッチャーに切り替えロジックを実装すれば良いです。
view.watch("zoom", (newValue, oldValue) => {
// zoomの値により、cityLayer.visible = true;
});
すべての Layer の生成を保証する
複数の Layer にまたがるイベントを実装する場合は、念のためすべての Layer の生成を保証しましょう。
Promise.all([
view.whenLayerView(prefLayer),
view.whenLayerView(cityLayer)
]).then([prefLayerView, cityLayerView]) => {
// ロジックを実装する
});
Promise.all
により、複数の非同期処理の完了を保証することができます。
Layer のオブジェクトを検索する
プログラムで検索する
Layer のオブジェクトに対してクエリを発行することができます。
これを利用して、 プログラマティックに特定のオブジェクトに移動するような操作を実現できます。
以下では、都道府県から北海道を検索して移動します。
const queryParams = prefLayer.createQuery();
queryParams.where = "japan_pref_pref = '北海道'"
prefLayer.queryFeatures(queryParams)
.then((results) => {
if (results.features.length > 0) {
view.goTo(results.features[0]);
}
});
createQuery
メソッドで Layer に対するクエリを生成し、 where
で検索条件を指定します。
queryFeatures
メソッドでクエリを発行し、検索結果( features
)に対する処理を実装します。
クエリのWHERE句では、GeoJSONのプロパティを使用できます。(
outFields
に指定していないプロパティも使用できます。)
Layer の表示を制限する
Layer にクエリを直接適用することで、 Layerに表示するオブジェクトを制限することができます。
Layer のデータセットが非常に大きい場合や、ユーザに見せる必要のないオブジェクトを除外するときに利用します。
以下では、既に表示されている都市を北海道の都市のみに制限します。
cityLayer.definitionExpression = "USER_city_name LIKE '北海道%'";
definitionExpression
に検索条件を指定します。
最初から制限する場合は、Layer 定義時に指定します。上記のように後から指定した場合は、 表示を制限するよう View が自動的に更新されます。後者は特に、「選択した都道府県の都市を表示する」ように複数の Layer の状態を連動させる場合に効果を発揮します。
まとめ
はじめての ArcGIS でしたが、思ったより簡単に地図アプリを実装することができました。
オープンデータではなくプライベートなマップサービスを作るところまでいくと、難しさを感じてくるのだろうと思います。
公式リファレンスが充実しているので英語が苦手でなければ実装に困ることはないと思いますが、日本語で参考になる記事が少なかったので書いてみました。
参考