Re:Earth Visualizer の React・TypeScript プラグインテンプレートから始めます。ISS Tracker ウィジェットを設定し、ISS の現在位置データと接続して、Visualizer のカメラを操作します。最後に、プラグインをパッケージ化してプロジェクトにインストールします。各ステップで画面の結果を確認しながら進められます。
完成した ISS Tracker プラグインには、ISS の現在の経度、緯度、高度が表示されます。Update は最新の位置を取得し、Jump は表示中の位置へカメラを移動します。Follow は5秒ごとに ISS の位置を取得し、カメラを移動します。完成したプラグインを ZIP ファイルにパッケージ化し、Visualizer プロジェクトにウィジェットとしてインストールします。
作業中のプロジェクトは、完成版の ISS Tracker ソースコードと比較できます。
- Node.js 20.11.0 以降
- Yarn 4.5.1
- Git
- Visual Studio Code などのコードエディター
- React と TypeScript の基本的な知識
- 開発用プラグインを動かすローカルの Re:Earth Visualizer フロントエンド
ステップ1・開発環境を準備する
Section titled “ステップ1・開発環境を準備する”プラグインテンプレートをインストールする
Section titled “プラグインテンプレートをインストールする”ターミナルを開き、次のコマンドを実行してください。<plugin-parent-directory> は、プロジェクトを作成するディレクトリに置き換えます。
cd <plugin-parent-directory>git clone https://github.com/reearth-plugins/reearth-visualizer-plugin-shadcn-template.gitcd reearth-visualizer-plugin-shadcn-templatecorepack enableyarn installテンプレートは、package.json で指定されたバージョンの Yarn を使います。Corepack を有効にすると、互換性のないグローバル版ではなく、指定されたバージョンを使えます。

ローカルの Visualizer フロントエンドを設定する
Section titled “ローカルの Visualizer フロントエンドを設定する”ローカル開発では、http://localhost:5005 のプレビューサーバーからプラグインを読み込みます。
- ローカルの Re:Earth Visualizer リポジトリで
web/.envを開きます - 次の環境変数を追加します
REEARTH_WEB_DEV_PLUGIN_URLS='["http://localhost:5005"]'- ファイルを保存して、ローカルの Visualizer 開発サーバーを再起動します
プラグインマニフェストを設定する
Section titled “プラグインマニフェストを設定する”プラグインプロジェクトで public/reearth.yml を開き、ファイル全体を次の内容に置き換えてください。
id: iss-tracker-pluginname: ISS Tracker Pluginversion: 1.0.0extensions: - id: iss_tracker type: widget name: ISS Tracker拡張 ID、拡張ディレクトリ、エントリーファイル名、ビルドスクリプト名には、すべて iss_tracker を使います。
ビルドスクリプトを設定する
Section titled “ビルドスクリプトを設定する”プラグインプロジェクトで package.json を開き、既存の scripts オブジェクトだけを次の内容に置き換えてください。
"scripts": { "dev:iss_tracker:main": "cross-env EXTENSION_NAME=iss_tracker UI_NAME=main vite -c configs/ui.ts", "build:iss_tracker:main": "cross-env EXTENSION_NAME=iss_tracker UI_NAME=main vite build -c configs/ui.ts", "build:iss_tracker:extension": "cross-env EXTENSION_NAME=iss_tracker vite build -c configs/extension.ts", "build:iss_tracker": "run-s build:iss_tracker:main build:iss_tracker:extension", "build": "run-s build:iss_tracker zip", "preview": "vite preview --port 5005 --strict-port", "zip": "node ./scripts/zip.mjs", "manage": "node ./scripts/manage.mjs", "type": "tsc -b", "lint": "eslint .", "fix": "eslint --fix .", "format": "prettier --write .", "dev-build": "concurrently 'yarn dev:iss_tracker:main' 'yarn build:iss_tracker:main --watch' 'yarn build:iss_tracker:extension --watch' 'vite preview --port 5005 --strict-port'"}package.json のほかの部分は置き換えないでください。
拡張ファイルの名前を変更する
Section titled “拡張ファイルの名前を変更する”プラグインプロジェクトのルートで、テンプレートの拡張ディレクトリとエントリーファイルの名前を変更します。
mv src/extensions/demo src/extensions/iss_trackermv src/extensions/iss_tracker/demo.ts src/extensions/iss_tracker/iss_tracker.tssrc/extensions/iss_tracker/iss_tracker.ts を開き、HTML の import を次のように変更してください。
import html from "@distui/iss_tracker/main/index.html?raw";関連するソースファイルが次の構成になっていることを確認してください。
src/extensions/iss_tracker/├── iss_tracker.ts└── main/ ├── App.tsx ├── app.css ├── hooks.ts ├── index.html └── main.tsx
開発用プラグインを起動してインストールする
Section titled “開発用プラグインを起動してインストールする”必要な出力を作るためにプラグインを一度ビルドしてから、開発プロセスを起動します。
yarn buildyarn dev-buildhttp://localhost:3000/を開き、ローカルの Visualizer でプロジェクトを選択します- エディターのヘッダーで Install Dev Plugins と Reload Dev Plugin Extensions を確認します

- Install Dev Plugins を選択します。初めてプラグインをインストールするときと、
reearth.ymlを変更したあとに使います

- 拡張または UI のコードを変更したら、Reload Dev Plugin Extensions を選択して最新のビルドを読み込みます

- 開発用プラグインがインストールされたことを確認します

- Widgets タブを開き、シーンに ISS Tracker を追加します

変更前のテンプレートウィジェットには、サンプルの UI が表示されます。次のステップでこの UI を置き換えます。

ステップ2・ISS Tracker を作成する
Section titled “ステップ2・ISS Tracker を作成する”完成したウィジェットには、次の3つの操作があります。
- Update は ISS の現在位置を取得します
- Jump は表示中の ISS の位置へ Visualizer のカメラを移動します
- Follow は5秒ごとに位置を取得してカメラを移動します

プロジェクト構成を確認する
Section titled “プロジェクト構成を確認する”主に src ディレクトリ内のファイルを編集します。
iss-tracker-plugin/├── node_modules/├── public/│ └── reearth.yml # Plugin definition├── src/│ ├── extensions/│ │ └── iss_tracker/ # Extension directory named after its ID│ │ ├── main/ # UI project for the main view│ │ └── iss_tracker.ts # Extension script│ └── shared/│ ├── components/ # Shared shadcn/ui components│ ├── lib/ # Shared shadcn/ui utilities│ ├── reearthTypes/ # Visualizer Plugin API types│ ├── global.css # Shared Tailwind CSS│ └── utils.ts├── dist/ # Plugin build output├── dist-ui/ # UI build output├── package/ # Installable ZIP output├── configs/ # Vite configurations├── scripts/├── package.json└── README.mdテンプレートの UI を変更する
Section titled “テンプレートの UI を変更する”src/extensions/iss_tracker/main/App.tsx を開きます。
既存の CardHeader を次の内容に置き換えてください。
<CardHeader> <CardTitle>ISS Tracker</CardTitle> <CardDescription> View, move, and update the current location of the ISS </CardDescription></CardHeader>テーブル本体で、行ラベルを Mouse から ISS に変更します。
<TableCell className="font-semibold">ISS</TableCell>既存の CardFooter 全体を、次の3つの仮の操作に置き換えてください。
<CardFooter className="justify-center gap-3 p-4 border-t"> <Button size="sm" className="gap-1" onClick={handleFlyToTokyo}> Update </Button> <Button size="sm" className="gap-1"> Jump </Button> <Button size="sm" className="gap-1"> Follow </Button></CardFooter>この時点では、すべてのボタンが動作するわけではありません。新しい動作を実装する間も既存のハンドラーが参照されるように、handleFlyToTokyo を一時的に Update に設定します。
Visualizer で Reload Dev Plugin Extensions を選択します。UI 開発サーバーの起動中に http://localhost:5173/ を開くと、UI の変更がホットリロードされます。

Update を実装する
Section titled “Update を実装する”src/extensions/iss_tracker/main/hooks.ts を開き、ファイル全体を次の内容に置き換えてください。
import { useState } from "react";
type IssPosition = { lat: number; lon: number; height: number;};
export default () => { const [issPosition, setIssPosition] = useState<IssPosition | null>(null);
const handleUpdate = async () => { const position = await fetchIssLocation(); setIssPosition(position); };
return { issPosition, handleUpdate };};
const fetchIssLocation = async (): Promise<IssPosition | null> => { try { const response = await fetch( "https://api.wheretheiss.at/v1/satellites/25544", ); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
const data = await response.json(); return { lat: data.latitude, lon: data.longitude, height: data.altitude * 1000, }; } catch { return null; }};このフックは、ISS の位置または null を保持します。fetchIssLocation() は現在の座標を取得し、高度を km から m に変換します。handleUpdate() は結果を React の状態に保存します。
次に src/extensions/iss_tracker/main/App.tsx を開き、既存の useHooks() 呼びだしを次の内容に置き換えてください。
const { issPosition, handleUpdate } = useHooks();Update が新しいハンドラーを呼びだすように、CardFooter 全体を次の内容に置き換えてください。
<CardFooter className="justify-center gap-3 p-4 border-t"> <Button size="sm" className="gap-1" onClick={handleUpdate}> Update </Button> <Button size="sm" className="gap-1"> Jump </Button> <Button size="sm" className="gap-1"> Follow </Button></CardFooter>テーブル行の3つの座標セルを次の内容に置き換えてください。
<TableCell> <Label htmlFor="iss-lng" className="sr-only"> Longitude </Label> <Input id="iss-lng" type="number" disabled value={issPosition?.lon ?? ""} /></TableCell><TableCell> <Label htmlFor="iss-lat" className="sr-only"> Latitude </Label> <Input id="iss-lat" type="number" disabled value={issPosition?.lat ?? ""} /></TableCell><TableCell> <Label htmlFor="iss-height" className="sr-only"> Height </Label> <Input id="iss-height" type="number" disabled value={issPosition?.height ?? ""} /></TableCell>プラグインを再読み込みして、Update を選択します。ウィジェットに現在の経度、緯度、高度が表示されることを確認してください。

Jump を実装する
Section titled “Jump を実装する”src/extensions/iss_tracker/main/hooks.ts を開き、ファイル全体を次の内容に置き換えてください。
import { useState } from "react";
import { postMsg } from "@/shared/utils";
type IssPosition = { lat: number; lon: number; height: number;};
export default () => { const [issPosition, setIssPosition] = useState<IssPosition | null>(null);
const handleUpdate = async () => { const position = await fetchIssLocation(); setIssPosition(position); };
const handleJump = () => { flyTo(issPosition); };
return { issPosition, handleUpdate, handleJump };};
const fetchIssLocation = async (): Promise<IssPosition | null> => { try { const response = await fetch( "https://api.wheretheiss.at/v1/satellites/25544", ); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); }
const data = await response.json(); return { lat: data.latitude, lon: data.longitude, height: data.altitude * 1000, }; } catch { return null; }};
const flyTo = (position: IssPosition | null): void => { if (!position) { console.warn("No position available to fly to."); return; } postMsg("flyTo", position);};flyTo() は位置があることを確認し、postMsg() を使ってウィジェットの iframe から拡張へ位置を送ります。
src/extensions/iss_tracker/iss_tracker.ts を開き、ファイル全体を次の内容に置き換えてください。
import html from "@distui/iss_tracker/main/index.html?raw";
import { GlobalThis } from "@/shared/reearthTypes";
type IssPosition = { lat: number; lon: number; height: number;};
type FlyToMessage = { action: "flyTo"; payload: IssPosition;};
const isFlyToMessage = (message: unknown): message is FlyToMessage => { if (!message || typeof message !== "object") return false;
const candidate = message as Partial<FlyToMessage>; const position = candidate.payload; return ( candidate.action === "flyTo" && !!position && Number.isFinite(position.lat) && Number.isFinite(position.lon) && Number.isFinite(position.height) );};
const reearth = (globalThis as unknown as GlobalThis).reearth;reearth.ui.show(html);
reearth.extension.on("message", (message: unknown) => { if (!isFlyToMessage(message)) return;
reearth.camera.flyTo( { lat: message.payload.lat, lng: message.payload.lon, height: message.payload.height, }, { duration: 1 }, );});拡張はメッセージを検証し、reearth.camera.flyTo() を呼びだします。ウィジェットでは lon を使いますが、カメラ API では lng が必要です。
最後に src/extensions/iss_tracker/main/App.tsx を開き、フックの呼びだしに handleJump を含めてください。
const { issPosition, handleUpdate, handleJump } = useHooks();CardFooter 全体を次の内容に置き換えてください。
<CardFooter className="justify-center gap-3 p-4 border-t"> <Button size="sm" className="gap-1" onClick={handleUpdate}> Update </Button> <Button size="sm" className="gap-1" onClick={handleJump}> Jump </Button> <Button size="sm" className="gap-1"> Follow </Button></CardFooter>プラグインを再読み込みし、Update、Jump の順に選択します。表示中の ISS の位置へカメラが移動することを確認してください。
Follow を実装する
Section titled “Follow を実装する”src/extensions/iss_tracker/main/hooks.ts を開き、ファイル全体を次の完成版フックに置き換えてください。
import { useCallback, useEffect, useRef, useState } from "react";
import { postMsg } from "@/shared/utils";
type IssPosition = { lat: number; lon: number; height: number;};
const ISS_API_URL = "https://api.wheretheiss.at/v1/satellites/25544";
const isIssApiResponse = ( value: unknown,): value is { latitude: number; longitude: number; altitude: number } => { if (!value || typeof value !== "object") return false;
const data = value as Record<string, unknown>; return ( Number.isFinite(data.latitude) && Number.isFinite(data.longitude) && Number.isFinite(data.altitude) );};
const fetchIssLocation = async (signal: AbortSignal): Promise<IssPosition> => { const response = await fetch(ISS_API_URL, { signal }); if (!response.ok) { throw new Error(`ISS API request failed (${response.status}).`); }
const data: unknown = await response.json(); if (!isIssApiResponse(data)) { throw new Error("The ISS API returned an unexpected response."); }
return { lat: data.latitude, lon: data.longitude, height: data.altitude * 1000, };};
const flyTo = (position: IssPosition) => { postMsg("flyTo", position);};
export default () => { const [issPosition, setIssPosition] = useState<IssPosition | null>(null); const [isFollowing, setIsFollowing] = useState(false); const [error, setError] = useState<string | null>(null); const activeRequest = useRef<AbortController | null>(null);
const loadPosition = useCallback(async (): Promise<IssPosition | null> => { activeRequest.current?.abort(); const controller = new AbortController(); activeRequest.current = controller; setError(null);
try { const position = await fetchIssLocation(controller.signal); setIssPosition(position); return position; } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { return null; } setError( error instanceof Error ? error.message : "Unable to load the ISS position.", ); return null; } finally { if (activeRequest.current === controller) activeRequest.current = null; } }, []);
const handleUpdate = useCallback(() => { void loadPosition(); }, [loadPosition]);
const handleJump = useCallback(() => { if (issPosition) flyTo(issPosition); }, [issPosition]);
useEffect(() => { if (!isFollowing) return;
let stopped = false; let timeoutId: ReturnType<typeof setTimeout> | undefined;
const follow = async () => { const position = await loadPosition(); if (position) flyTo(position); if (!stopped) timeoutId = setTimeout(follow, 5000); };
void follow();
return () => { stopped = true; if (timeoutId) clearTimeout(timeoutId); activeRequest.current?.abort(); }; }, [isFollowing, loadPosition]);
const toggleFollow = useCallback(() => { setIsFollowing((current) => !current); }, []);
return { issPosition, isFollowing, error, handleUpdate, handleJump, toggleFollow, };};完成版のフックは API のレスポンスを検証します。新しいリクエストが失敗しても、最後に取得できた位置を表示したままにします。また、リクエストの重複を防ぎ、追跡モードの停止時にタイマーと実行中のリクエストを取り消します。
src/extensions/iss_tracker/main/App.tsx を開き、ファイル全体を次の完成版 UI に置き換えてください。
import useHooks from "./hooks";
import { Button } from "@/shared/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/shared/components/ui/card";import { Input } from "@/shared/components/ui/input";import { Label } from "@/shared/components/ui/label";import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/shared/components/ui/table";
function App() { const { issPosition, isFollowing, error, handleUpdate, handleJump, toggleFollow, } = useHooks();
return ( <Card> <CardHeader> <CardTitle>ISS Tracker</CardTitle> <CardDescription> View, move, and update the current location of the ISS </CardDescription> </CardHeader> <CardContent> <Table> <TableHeader> <TableRow> <TableHead className="w-[100px]" /> <TableHead>Longitude</TableHead> <TableHead>Latitude</TableHead> <TableHead>Height (m)</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow> <TableCell className="font-semibold">ISS</TableCell> <TableCell> <Label htmlFor="iss-lng" className="sr-only"> Longitude </Label> <Input id="iss-lng" disabled value={issPosition?.lon ?? ""} /> </TableCell> <TableCell> <Label htmlFor="iss-lat" className="sr-only"> Latitude </Label> <Input id="iss-lat" disabled value={issPosition?.lat ?? ""} /> </TableCell> <TableCell> <Label htmlFor="iss-height" className="sr-only"> Height in meters </Label> <Input id="iss-height" disabled value={issPosition ? Math.round(issPosition.height) : ""} /> </TableCell> </TableRow> </TableBody> </Table> {error && ( <p role="alert" className="mt-3 text-sm text-destructive"> {error} </p> )} </CardContent> <CardFooter className="justify-center gap-3 p-4 border-t"> <Button size="sm" className="gap-1" onClick={handleUpdate}> Update </Button> <Button size="sm" className="gap-1" onClick={handleJump}> Jump </Button> <Button size="sm" className="gap-1" onClick={toggleFollow}> {isFollowing ? "Unfollow" : "Follow"} </Button> </CardFooter> </Card> );}
export default App;プラグインを再読み込みして、Follow を選択します。ボタンが Unfollow に変わり、5秒ごとに ISS の最新位置へカメラが移動することを確認してください。

ステップ3・プラグインをパッケージ化してインストールする
Section titled “ステップ3・プラグインをパッケージ化してインストールする”プラグインを検証してビルドする
Section titled “プラグインを検証してビルドする”プラグインプロジェクトのルートで、次のコマンドを実行してください。
yarn typeyarn lintyarn build最後のコマンドは現在のソースをビルドし、package/iss-tracker-plugin-1.0.0.zip を作成します。

ZIP ファイルを開き、reearth.yml や iss_tracker.js などのインストール用ファイルが別のディレクトリ内ではなく、ZIP のルートにあることを確認してください。

ローカル開発モードを終了する
Section titled “ローカル開発モードを終了する”ローカルの Re:Earth Visualizer リポジトリで web/.env を開きます。REEARTH_WEB_DEV_PLUGIN_URLS を削除するか、次のようにコメントアウトしてください。
# REEARTH_WEB_DEV_PLUGIN_URLS='["http://localhost:5005"]'環境ファイルを変更したら、Visualizer 開発サーバーを再起動してください。
ZIP ファイルをインストールする
Section titled “ZIP ファイルをインストールする”- Visualizer プロジェクトを作成するか、既存のプロジェクトを開きます
- プロジェクト名を選択し、Plugins を選択します

- Personally Installed を開き、Upload ZIP File from PC を選択します

- プラグインプロジェクトの
packageディレクトリからiss-tracker-plugin-1.0.0.zipをアップロードします - インストール済みのプラグインに ISS Tracker Plugin が表示されることを確認します

- エディターに戻ります

- Widgets タブを開き、シーンに ISS Tracker を追加します


完成したプラグインを確認する
Section titled “完成したプラグインを確認する”次の項目をすべて確認できたら、プラグインは完成です。
- インストール済みのプラグインに ISS Tracker Plugin が表示される
- Widgets タブから ISS Tracker ウィジェットを追加できる
- Update を選択すると、ISS の最新の座標が表示される
- Jump を選択すると、表示中の位置へカメラが移動する
- Follow が Unfollow に変わり、ISS の位置が更新されるたびにカメラが移動する