コンテンツにスキップ
JP

経路検索プラグインを作成する

更新日

プラグインプレイグラウンドを使って、2 地点間の経路を検索して地球儀上に描画するプラグインを作成します。ブラウザ上で完結するため、ローカル環境を用意する必要はありません。

経路検索ウィジェットは、2 地点間の経路を地球儀上に描画します。開始地点と終了地点は、座標を直接入力するか地図をクリックして指定します。徒歩・自転車・車のいずれかの移動手段を選ぶと、ウィジェットは外部の経路検索サービスを呼び出して経路を取得します。取得した経路は、クリックすると距離と所要時間を表示するクリック可能な線として描画されます。両端点にはマーカーが表示され、ボタンひとつですべてをクリアできます。

完成した経路検索ウィジェットと、東京の 2 地点間に青で描画された徒歩の経路。両端にマーカーが表示されている

このウィジェットは、プラグインAPI の次の 4 つの部分を使用します。

  • UI と Visualizer 間の通信 — reearth.ui と reearth.extension を通じて、ウィジェットの iframe とプラグインロジックの間で双方向にデータをやり取りします
  • 外部 API との連携 — fetch で経路検索サービスを呼び出し、レスポンスを GeoJSON に変換します
  • レイヤAPI — 実行時にレイヤを追加・更新・検索・削除します
  • カメラAPI — シーンの初期表示位置を設定します
  • Re:Earth Visualizer のプロジェクト
  • ブラウザのタブで開いたプラグインプレイグラウンド
  • HTML・CSS・JavaScript の基礎知識。このガイドは、一般的な Web 開発ではなく Re:Earth プラグイン API について説明します。

はじめてプラグインを作成する場合は、先に「プラグインプレイグラウンドで動かす」「UI を表示する」「プラグインと Re:Earth の間で通信する」に取り組んでください。これらのガイドでは、プレイグラウンドの画面構成、reearth.yml マニフェスト、エクスポートとインストールの手順、そしてこのガイドでも使うメッセージのやり取りの方法を扱います。

ステップ 1 — プラグインのファイルを開く

Section titled “ステップ 1 — プラグインのファイルを開く”

プレイグラウンドのファイルツリーの Custom セクションで、My Plugin を選択します。すでに reearth.yml マニフェストと demo-widget ファイルが含まれており、この 2 つのファイルを編集します。

ステップ 2 — マニフェストを編集する

Section titled “ステップ 2 — マニフェストを編集する”

reearth.yml を開き、内容を 1 つのウィジェット拡張に置き換えます。

プレイグラウンドのコードパネルに reearth.yml マニフェストが開かれ、ファイルツリーで My Plugin が選択されている

id: route-search-plugin
name: Route Search
version: 1.0.0
extensions:
- id: demo-widget
type: widget
name: Route Search
description: Search and draw a route between two points
widgetLayout:
defaultLocation:
zone: outer
section: left
area: top

拡張の id は demo-widget のまま、既存の demo-widget ファイルと一致させます。拡張の id とファイル名は一致していなければなりません。プラグインの id には英小文字・数字・_・- を使用でき、スペースは使用できず、reearth は指定できません。同じ id のプラグインがすでにプロジェクトにあると、後のインストールが失敗するため、他と重複しない id を選んでください。

ステップ 3 — ウィジェットの UI を作成する

Section titled “ステップ 3 — ウィジェットの UI を作成する”

demo-widget を選択し、内容を次のコードに置き換えます。reearth.ui.show は HTML 文字列を受け取り、iframe 内にウィジェットとして描画します。

プレイグラウンドのファイルツリーで demo-widget ファイルが選択され、コードパネルに初期状態のコードが表示されている

reearth.ui.show(`
<style>
@import url("https://reearth.github.io/visualizer-plugin-sample-data/public/css/preset-ui.css");
</style>
<div class="rounded-sm secondary-background" style="width: 290px">
<div class="display-flex flex-between p-16" style="height: 48px">
<h2 class="text-md">経路検索</h2>
</div>
<div class="flex-column p-8">
<div class="flex-column gap-8 p-16">
<div class="flex-between" style="gap: 0">
<label class="font-bold" for="start-point">開始地点</label>
<div class="display-flex" style="width: 180px; height: 30px; border-radius: 4px; border: 1px solid #8b8b8b">
<input id="start-point" type="text" placeholder="lng,lat" style="border: none; outline: 0" />
<button class="icon-btn" onclick="handleIconClick('start', this)">📍</button>
</div>
</div>
<div class="flex-between" style="gap: 0">
<label class="font-bold" for="end-point">終了地点</label>
<div class="display-flex" style="width: 180px; height: 30px; border-radius: 4px; border: 1px solid #8b8b8b">
<input id="end-point" type="text" placeholder="lng,lat" style="border: none; outline: 0" />
<button class="icon-btn" onclick="handleIconClick('end', this)">📍</button>
</div>
</div>
</div>
<div class="display-flex p-16" style="align-items: flex-start; gap: 43px">
<label class="font-bold">移動手段</label>
<div class="flex-column gap-4">
<label><input type="radio" name="transport" value="foot" checked /> 徒歩</label>
<label><input type="radio" name="transport" value="bike" /> 自転車</label>
<label><input type="radio" name="transport" value="car" /> 車</label>
</div>
</div>
<div class="display-flex gap-8 p-8">
<button class="btn-primary button-padding w-full text-sm" onclick="searchRoute()">検索</button>
<button class="btn-neutral button-padding w-full text-sm" onclick="deleteRouteAndMarker()">クリア</button>
</div>
<p id="status" class="text-sm p-8" style="margin: 0; min-height: 18px"></p>
</div>
</div>
`);

入力欄のプレースホルダーに表示されている lng,lat は経度・緯度を指します。以降も、コードに合わせて経度・緯度を lng,lat と表記します。

ウィジェットの動作は、この後の手順で <script> ブロックとして追加します。そのため、現時点ではボタンをクリックしても何も起こりません。

ステップ 4 — 背景地図を OpenStreetMap に切り替える

Section titled “ステップ 4 — 背景地図を OpenStreetMap に切り替える”

経路は OpenStreetMap のデータから取得しているため、地図をクリックし始める前に、背景地図もそれに合わせて切り替えておきます。reearth.ui.show(...) の下に、次のコードを追加します。

reearth.viewer.overrideProperty({
tiles: [
{
id: "osm",
type: "open_street_map",
},
],
});

open_street_map は Re:Earth Visualizer の組み込みの背景地図の 1 つで、プロジェクト自体の背景地図の一部の選択肢とは異なり、Cesium Ion のアクセストークンを必要としません。他の組み込みの背景地図やカスタムタイルサーバーの使い方については、背景地図を変更するを参照してください。

reearth.viewer.overrideProperty の呼び出しがハイライトされたプレイグラウンドのコードパネルと、OpenStreetMap のタイルに切り替わった地球儀

ステップ 5 — 地図上で地点を選択する

Section titled “ステップ 5 — 地図上で地点を選択する”

📍 アイコンをクリックするとピックモードに入り、続けて地図をクリックすると対応する入力欄に座標が入ります。この動作には、双方向のメッセージのやり取りが必要です。

reearth.ui.show(...) の下に、地図のクリックを監視するリスナーを追加します。

reearth.viewer.on("click", (event) => {
const { lat, lng } = event;
if (lat === undefined || lng === undefined) return; // 空や何もない場所をクリックした場合
reearth.ui.postMessage({ type: "position", lat, lng });
});

reearth.viewer.on("click", ...) は地図がクリックされるたびに発火し、reearth.ui.postMessage は Visualizer からウィジェットの iframe へデータを送ります。地球儀を外れたクリックには座標が伴わないため、ハンドラはここで処理を打ち切ります。undefined の位置情報を送信すると、ウィジェット側で .toFixed() の呼び出し先がなくなり、目に見える原因のないままピックモードが固まってしまいます。

ウィジェット側で座標を受け取る

Section titled “ウィジェット側で座標を受け取る”

reearth.ui.show に渡した HTML の内側、閉じるバッククォートの直前に <script> ブロックを追加します。

<script>
let selecting = null;
function handleIconClick(type, btn) {
if (selecting === type) {
selecting = null;
resetIconColors();
} else {
selecting = type;
resetIconColors();
btn.style.background = "#ffcccc";
}
}
function resetIconColors() {
document.querySelectorAll(".icon-btn").forEach((b) => (b.style.background = ""));
}
window.addEventListener("message", (e) => {
const msg = e.data;
if (msg.type === "position" && selecting) {
const coordinates = msg.lng.toFixed(6) + "," + msg.lat.toFixed(6);
document.getElementById(selecting + "-point").value = coordinates;
selecting = null;
resetIconColors();
}
});
</script>

iframe 内では、メッセージは window.addEventListener("message", ...) を通じて届きます。ピックモードが有効な間、座標は開始地点または終了地点の入力欄に書き込まれます。座標の順序は lng,lat で、これは経路検索サービスが想定する順序です。

📍 アイコンをクリックしてから地図をクリックすると、クリックした地点の座標が入力欄に表示されます。

地図をクリックした後、開始地点と終了地点の入力欄に座標が入力された経路検索ウィジェットと、ハイライトされたメッセージリスナーのコード

ステップ 6 — 選択した地点にマーカーを表示する

Section titled “ステップ 6 — 選択した地点にマーカーを表示する”

選択した各地点のマーカーは、レイヤAPI で表示します。

ウィジェットからマーカーをリクエストする

Section titled “ウィジェットからマーカーをリクエストする”

<script> の中に、ヘルパー関数を追加します。

function addMarkerLayer(lat, lng, pointName) {
parent.postMessage({ action: "addMarkerLayer", lat, lng, pointName }, "*");
}

続けて、message リスナー内の selecting = null; の直前に addMarkerLayer(msg.lat, msg.lng, selecting); を追加して呼び出します。

parent.postMessage は、ウィジェットの iframe から Visualizer へデータを送り返します。前の手順の reearth.ui.postMessage と合わせて、ほとんどのインタラクティブなプラグインを支える双方向のチャンネルを構成します。

Visualizer 側でマーカーを追加する

Section titled “Visualizer 側でマーカーを追加する”

reearth.viewer.on("click", ...) ブロックの下に、このプラグイン自身のレイヤを追跡する id 用の変数と、ウィジェットから届くメッセージを処理するハンドラを追加します。

let startMarkerId = null;
let endMarkerId = null;
reearth.extension.on("message", (msg) => {
if (msg.action === "addMarkerLayer") {
const value = {
type: "FeatureCollection",
features: [
{ type: "Feature", properties: {}, geometry: { type: "Point", coordinates: [msg.lng, msg.lat] } },
],
};
const markerId = msg.pointName === "start" ? startMarkerId : endMarkerId;
if (markerId) {
reearth.layers.override(markerId, { data: { type: "geojson", value } });
} else {
const newId = reearth.layers.add({
type: "simple",
title: msg.pointName,
data: { type: "geojson", value },
marker: { pointColor: "blue", pointSize: 12, style: "point" },
});
if (msg.pointName === "start") startMarkerId = newId;
else endMarkerId = newId;
}
}
});

reearth.extension.on("message", ...) はウィジェットからの送信内容を受け取り、ハンドラは msg.action によって処理を分岐します。startMarkerId と endMarkerId は、それぞれのマーカーについて reearth.layers.add が返した id を保持します。reearth.layers.find で start や end というタイトルのレイヤを探すのではなく id で参照することで、reearth.layers.override が、同じタイトルを偶然持つ別のレイヤ — プロジェクトに既存のレイヤや、別のプラグインが追加した一時的なレイヤ — を書き換えてしまうことを防いでいます。

クリックした地点に青いマーカーが表示されるようになります。新しい地点を選び直すと、軌跡を残さずマーカーが移動します。

選択した開始地点と終了地点に表示された 2 つの青いマーカーと、ハイライトされた addMarkerLayer ハンドラのコード

経路は、オープンソースの経路検索エンジンである OSRM(Open Source Routing Machine)の公開デモサーバーから取得します。<script> の中に、次の関数を追加します。

// デモサーバーは、移動手段ごとに異なるホストで提供されています。
const OSRM_HOSTS = {
foot: "https://routing.openstreetmap.de/routed-foot",
bike: "https://routing.openstreetmap.de/routed-bike",
car: "https://routing.openstreetmap.de/routed-car",
};
function setStatus(text) {
document.getElementById("status").textContent = text || "";
}
async function getRoute(start, end, mode) {
const url =
OSRM_HOSTS[mode] +
"/route/v1/driving/" + start + ";" + end +
"?overview=full&geometries=geojson";
const res = await fetch(url);
const data = await res.json();
if (!res.ok || data.code !== "Ok") throw new Error(data.message || "経路が見つかりませんでした");
const route = data.routes[0];
return {
type: "Feature",
geometry: route.geometry,
properties: { distance: route.distance, duration: route.duration },
};
}
function addRouteLayer(geojson) {
parent.postMessage({ action: "addRouteLayer", geojson }, "*");
}
async function searchRoute() {
const start = document.getElementById("start-point").value.trim();
const end = document.getElementById("end-point").value.trim();
if (!start || !end) {
setStatus("開始地点と終了地点の両方を設定してください。");
return;
}
const mode = document.querySelector('input[name="transport"]:checked').value;
setStatus("検索中...");
try {
const [startLng, startLat] = start.split(",").map(Number);
const [endLng, endLat] = end.split(",").map(Number);
if ([startLng, startLat, endLng, endLat].some(Number.isNaN)) {
throw new Error("lng,lat の形式で座標を入力してください");
}
addMarkerLayer(startLat, startLng, "start");
addMarkerLayer(endLat, endLng, "end");
const geojson = await getRoute(start, end, mode);
addRouteLayer(geojson);
setStatus("");
} catch (err) {
console.error("経路の取得に失敗しました:", err);
setStatus("経路の検索に失敗しました。");
}
}

getRoute は fetch で経路検索サービスを呼び出し、レスポンスを確認したうえで、distance(メートル単位)と duration(秒単位)を持つ GeoJSON の Feature に変換します。searchRoute は 2 つの入力値を検証し、それぞれを lng と lat に分解したうえで、getRoute を呼びだす前に両方の地点に対して addMarkerLayer を呼び出します。これにより、地図をクリックして選んだ地点だけでなく、入力欄に直接座標を入力した地点にもマーカーが表示されるようになります — これまでは、座標を入力すると経路は動く一方、マーカーは古い位置に残るか、そもそも表示されませんでした。空欄・座標として解釈できない文字列・経路の取得失敗のいずれであっても、失敗はすべてボタンの下のステータス行に表示されるため、何も起こらないということはなく、失敗したことがそのまま伝わります。

移動手段ごとに異なるホストが用意されているため、OSRM_HOSTS はラジオボタンの値をホストに対応させ、パス自体は /driving/ のまま使います。公開デモサーバーでは、OSRM の URL に含まれるプロファイル部分は移動手段の選択に反映されません。たとえば router.project-osrm.org/route/v1/foot/... へのリクエストは受け付けられますが、返ってくるのは車の経路のままです。これでは、徒歩・自転車・車のオプションを選んでも結果が変わらなくなってしまいます。

ステップ 8 — 経路レイヤを描画する

Section titled “ステップ 8 — 経路レイヤを描画する”

Visualizer 側で addRouteLayer アクションを処理します。これは、マーカーの手順で作成した reearth.extension.on("message", ...) ハンドラに追加する 2 つ目の分岐です。先頭の } は addMarkerLayer 分岐を閉じるものなので、ハンドラ全体の後ろに貼り付けるのではなく、その分岐の閉じ括弧を次のブロックで置き換えてください。あわせて、startMarkerId ・endMarkerId の隣に 3 つ目の id 用の変数 let routeLayerId = null; を追加します。

} else if (msg.action === "addRouteLayer") {
// 検索するたびに経路が積み重ならないよう、直前の経路を削除する
if (routeLayerId) reearth.layers.delete(routeLayerId);
routeLayerId = reearth.layers.add({
type: "simple",
title: "route",
data: { type: "geojson", value: msg.geojson },
infobox: {
blocks: [{ pluginId: "reearth", extensionId: "propertyInfoboxBetaBlock" }],
},
polyline: { strokeColor: "blue", strokeWidth: 2 },
});
}

経路は、経路検索サービスのレスポンスから得た GeoJSON をそのまま使う simple レイヤです。polyline で線のスタイルを指定します。infobox は、線をクリックしたときに経路の distance と duration を表示します。routeLayerId は、startMarkerId ・endMarkerId がマーカーを追跡するのと同じ方法でこのレイヤを追跡します。そのため、新しい経路を追加する前に直前の経路を削除しても、このプラグインが作成したレイヤだけを操作します。

両方の地点を設定し、移動手段を選んで 検索 を選択すると、経路に沿って青い線が描画されます。線をクリックするとインフォボックスが開きます。

2 つのマーカーを結ぶ青い経路の線と、ハイライトされた addRouteLayer ハンドラのコード

ステップ 9 — 経路とマーカーをクリアする

Section titled “ステップ 9 — 経路とマーカーをクリアする”

<script> の中に、クリア ボタンの動作となる関数を追加します。

function deleteRouteAndMarker() {
document.getElementById("start-point").value = "";
document.getElementById("end-point").value = "";
selecting = null;
resetIconColors();
setStatus("");
parent.postMessage({ action: "deleteRouteAndMarkerLayer" }, "*");
}

Visualizer 側では、経路の分岐を追加したときと同じ要領で、同じハンドラに 3 つ目の分岐を連結します。

} else if (msg.action === "deleteRouteAndMarkerLayer") {
if (routeLayerId) {
reearth.layers.delete(routeLayerId);
routeLayerId = null;
}
if (startMarkerId) {
reearth.layers.delete(startMarkerId);
startMarkerId = null;
}
if (endMarkerId) {
reearth.layers.delete(endMarkerId);
endMarkerId = null;
}
}

reearth.layers.delete は id を指定してレイヤを削除します — ここで使う 3 つの id は、他の 2 つの分岐が管理しているものと同じです。削除したあとにそれぞれを null に戻しておくことで、次の検索でもう存在しないレイヤを上書きしようとせず、新しいマーカーや経路レイヤを作成できます。

ステップ 10 — カメラの初期表示位置を設定する

Section titled “ステップ 10 — カメラの初期表示位置を設定する”

シーンに初期表示位置を設定します。reearth.ui.show(...) の後に、次のコードを 1 回だけ追加します。

reearth.camera.setView({
lat: 35.68426,
lng: 139.71043,
height: 2000,
heading: 0,
pitch: -0.785,
roll: 0,
});

setView は指定した位置に即座に移動します。アニメーションしながら移動したい場合は、代わりに reearth.camera.flyTo を使用します。

ステップ 11 — プラグインをエクスポートしてインストールする

Section titled “ステップ 11 — プラグインをエクスポートしてインストールする”

プレイグラウンドの Plugins ツールバー。エクスポートの操作が赤で、インポートの操作が青で示されている

  1. プレイグラウンドで Export plugin を選択し、.zip をダウンロードします。
  2. Re:Earth Visualizer のプロジェクトでプラグインのインストールダイアログを開き、Zip file from PC を選択して、ダウンロードしたファイルを選びます。
  3. アップロードが成功したら、Widgets タブからウィジェットを追加します。

インストールに失敗する場合は、reearth.yml が正しい形式になっているか、同じ id のプラグインがすでにインストールされていないか、.zip の中身が reearth.yml をルートに持つ単一のフォルダになっているかを確認してください。圧縮ツールによっては、ダイアログが受け付けない形式の .zip を作成することがあるため、その場合は別のツールを試してください。

完成した経路検索ウィジェットと、東京の 2 地点間に青で描画された徒歩の経路。両端にマーカーが表示されている

完成したウィジェットを、最初から最後まで確認します。

  1. 開始地点 の隣にある 📍 を選択してから地図をクリックします。入力欄に座標が入り、青いマーカーが表示されます。
  2. 終了地点 でも同様に操作します。
  3. 移動手段を選んで 検索 を選択します。経路の線が描画され、クリックすると距離と所要時間が表示されます。
  4. クリア を選択します。線と両方のマーカーが削除されます。
demo-widget の完全なコード
// ---------- プラグインコンテキスト: ウィジェット UI を作成して表示する ----------
// (以下の <script> 内のコードのみが、ウィジェットの iframe 内で実行されます)
reearth.ui.show(`
<style>
@import url("https://reearth.github.io/visualizer-plugin-sample-data/public/css/preset-ui.css");
</style>
<div class="rounded-sm secondary-background" style="width: 290px">
<div class="display-flex flex-between p-16" style="height: 48px">
<h2 class="text-md">経路検索</h2>
</div>
<div class="flex-column p-8">
<div class="flex-column gap-8 p-16">
<div class="flex-between" style="gap: 0">
<label class="font-bold" for="start-point">開始地点</label>
<div class="display-flex" style="width: 180px; height: 30px; border-radius: 4px; border: 1px solid #8b8b8b">
<input id="start-point" type="text" placeholder="lng,lat" style="border: none; outline: 0" />
<button class="icon-btn" onclick="handleIconClick('start', this)">📍</button>
</div>
</div>
<div class="flex-between" style="gap: 0">
<label class="font-bold" for="end-point">終了地点</label>
<div class="display-flex" style="width: 180px; height: 30px; border-radius: 4px; border: 1px solid #8b8b8b">
<input id="end-point" type="text" placeholder="lng,lat" style="border: none; outline: 0" />
<button class="icon-btn" onclick="handleIconClick('end', this)">📍</button>
</div>
</div>
</div>
<div class="display-flex p-16" style="align-items: flex-start; gap: 43px">
<label class="font-bold">移動手段</label>
<div class="flex-column gap-4">
<label><input type="radio" name="transport" value="foot" checked /> 徒歩</label>
<label><input type="radio" name="transport" value="bike" /> 自転車</label>
<label><input type="radio" name="transport" value="car" /> 車</label>
</div>
</div>
<div class="display-flex gap-8 p-8">
<button class="btn-primary button-padding w-full text-sm" onclick="searchRoute()">検索</button>
<button class="btn-neutral button-padding w-full text-sm" onclick="deleteRouteAndMarker()">クリア</button>
</div>
<p id="status" class="text-sm p-8" style="margin: 0; min-height: 18px"></p>
</div>
</div>
<script>
let selecting = null;
// デモサーバーは、移動手段ごとに異なるホストで提供されています。
const OSRM_HOSTS = {
foot: "https://routing.openstreetmap.de/routed-foot",
bike: "https://routing.openstreetmap.de/routed-bike",
car: "https://routing.openstreetmap.de/routed-car",
};
function setStatus(text) {
document.getElementById("status").textContent = text || "";
}
function resetIconColors() {
document.querySelectorAll(".icon-btn").forEach((b) => (b.style.background = ""));
}
function handleIconClick(type, btn) {
if (selecting === type) {
selecting = null;
resetIconColors();
} else {
selecting = type;
resetIconColors();
btn.style.background = "#ffcccc";
}
}
function addMarkerLayer(lat, lng, pointName) {
parent.postMessage({ action: "addMarkerLayer", lat, lng, pointName }, "*");
}
function addRouteLayer(geojson) {
parent.postMessage({ action: "addRouteLayer", geojson }, "*");
}
async function getRoute(start, end, mode) {
const url =
OSRM_HOSTS[mode] +
"/route/v1/driving/" + start + ";" + end +
"?overview=full&geometries=geojson";
const res = await fetch(url);
const data = await res.json();
if (!res.ok || data.code !== "Ok") throw new Error(data.message || "経路が見つかりませんでした");
const route = data.routes[0];
return {
type: "Feature",
geometry: route.geometry,
properties: { distance: route.distance, duration: route.duration },
};
}
async function searchRoute() {
const start = document.getElementById("start-point").value.trim();
const end = document.getElementById("end-point").value.trim();
if (!start || !end) {
setStatus("開始地点と終了地点の両方を設定してください。");
return;
}
const mode = document.querySelector('input[name="transport"]:checked').value;
setStatus("検索中...");
try {
const [startLng, startLat] = start.split(",").map(Number);
const [endLng, endLat] = end.split(",").map(Number);
if ([startLng, startLat, endLng, endLat].some(Number.isNaN)) {
throw new Error("lng,lat の形式で座標を入力してください");
}
addMarkerLayer(startLat, startLng, "start");
addMarkerLayer(endLat, endLng, "end");
const geojson = await getRoute(start, end, mode);
addRouteLayer(geojson);
setStatus("");
} catch (err) {
console.error("経路の取得に失敗しました:", err);
setStatus("経路の検索に失敗しました。");
}
}
function deleteRouteAndMarker() {
document.getElementById("start-point").value = "";
document.getElementById("end-point").value = "";
selecting = null;
resetIconColors();
setStatus("");
parent.postMessage({ action: "deleteRouteAndMarkerLayer" }, "*");
}
window.addEventListener("message", (e) => {
const msg = e.data;
if (msg.type === "position" && selecting) {
const target = selecting;
document.getElementById(target + "-point").value =
msg.lng.toFixed(6) + "," + msg.lat.toFixed(6);
addMarkerLayer(msg.lat, msg.lng, target);
selecting = null;
resetIconColors();
}
});
</script>
`);
// ---------- プラグインロジック(Visualizer コンテキストで実行される) ----------
reearth.viewer.overrideProperty({
tiles: [
{
id: "osm",
type: "open_street_map",
},
],
});
reearth.viewer.on("click", (event) => {
const { lat, lng } = event;
if (lat === undefined || lng === undefined) return; // 空や何もない場所をクリックした場合
reearth.ui.postMessage({ type: "position", lat, lng });
});
let startMarkerId = null;
let endMarkerId = null;
let routeLayerId = null;
reearth.extension.on("message", (msg) => {
if (msg.action === "addMarkerLayer") {
const value = {
type: "FeatureCollection",
features: [
{ type: "Feature", properties: {}, geometry: { type: "Point", coordinates: [msg.lng, msg.lat] } },
],
};
const markerId = msg.pointName === "start" ? startMarkerId : endMarkerId;
if (markerId) {
reearth.layers.override(markerId, { data: { type: "geojson", value } });
} else {
const newId = reearth.layers.add({
type: "simple",
title: msg.pointName,
data: { type: "geojson", value },
marker: { pointColor: "blue", pointSize: 12, style: "point" },
});
if (msg.pointName === "start") startMarkerId = newId;
else endMarkerId = newId;
}
} else if (msg.action === "addRouteLayer") {
// 検索するたびに経路が積み重ならないよう、直前の経路を削除する
if (routeLayerId) reearth.layers.delete(routeLayerId);
routeLayerId = reearth.layers.add({
type: "simple",
title: "route",
data: { type: "geojson", value: msg.geojson },
infobox: {
blocks: [{ pluginId: "reearth", extensionId: "propertyInfoboxBetaBlock" }],
},
polyline: { strokeColor: "blue", strokeWidth: 2 },
});
} else if (msg.action === "deleteRouteAndMarkerLayer") {
if (routeLayerId) {
reearth.layers.delete(routeLayerId);
routeLayerId = null;
}
if (startMarkerId) {
reearth.layers.delete(startMarkerId);
startMarkerId = null;
}
if (endMarkerId) {
reearth.layers.delete(endMarkerId);
endMarkerId = null;
}
}
});
reearth.camera.setView({
lat: 35.68426,
lng: 139.71043,
height: 2000,
heading: 0,
pitch: -0.785,
roll: 0,
});