6
2

CesiumJS × Svelte4におけるカスタムストアの活用

Last updated at Posted at 2024-07-15

はじめに

3D地理空間データの可視化を提供するオープンソースJavaScriptライブラリであるCesiumJSと、高速で柔軟性の高いフレームワークであるSvelte(今回はv.4)を組み合わせて使用する際の、カスタムストアの利点と実装方法について解説する。

CesiumJSのViewerクラス

Viewer クラスはCesiumJSにおける言わば「核」となるクラスで、地形モデルの切り替え、レイヤー追加削除、エンティティ追加削除、カメラの制御などに用いられる。

Viewerクラスについてはこちら

ユーザーは主にこのViewer クラスを用いて地図を制御することとなる。

今回はこのViewer クラスに言及する。

コンポーネント間でのViewer操作

複数のコンポーネントからViewerを操作うる場合、通常は多くのコンポーネントを渡す「バケツリレー」を要する。

バケツリレーはデータフローの単純, 明確化できる一方で、保守性/可読性・パフォーマンスの低下などの問題がある。

※ バケツリレーは決して悪ではなく、時と場合によっては有効である。

例えば以下のようなケースを想定する。

  1. サイドバーで地形モデルを変更・レイヤーの追加削除を行う。
  2. 計測モーダルで指定したポイントの標高やポイント間の標高を計測する。

この場合、Svelteでは以下のフォルダ構成とする。

src/
├── route
   └── +page.svelte                  // ルートコンポーネント
└── components
    ├── MapPane.svelte                // 地図描画用コンポーネント
    ├── Sidebar.svelte                // サイドバー用コンポーネント
    └── measurement
        ├── MeasurementModal.svelte   // 計測モーダル 
        ├── Distance.svelte           // 距離測定用コンポーネント
        └── Elevation.svelte          // 標高取得用コンポーネント

※ 説明で必要なコンポーネントのみ列挙

仮に各コンポーネントでViewerを制御しようとした場合、

MapPane.svelteで定義したViewerは以下のように各コンポーネントに渡す必要がある。

Frame 8.png

※Viewerを使用するのは赤いコンポーネントのみ

仮にViewer制御の責務をMapPane.svelteに限定した場合、「バケツリレー」はさらに複雑化するだろう。

そこで、Viewerの制御をストアに持たせるようにしてみる。

カスタムストアについて

上記でViewerの制御をストアに持たせると表現したが、正しくは「ストアで保持するViewerの制御を合わせてストア(カスタムストア)で行う」である。

Svelteにはビルドインでストア機能 svelte/store をサポートしている。特にsvelte/storeにはカスタムストアなる強力な機能が存在する。

状態の更新時に任意のロジックを介入することができる。これにより単純な値の保持だけでなく、より複雑な状態の管理や副作用の制御を行うことができる。

気になった人は是非、公式チュートリアルを試してほしい。

このカスタムストアを活用することで、Viewerの制御を全てストア(以下viewerStore)に持たせることができる。
こうすることで各コンポーネントは必要な時にviewerStoreを呼び出せば良いだけなので、関心, 責務の分離、保守性/可読性の向上などのメリットを享受できる。

viewerStore移行後のフローは以下の通り

Frame 9.png

必要なコンポーネントはviewerStoreからViewerを制御し、MapPane.svelteではViewerをsubscribe(常に最新の状態を読み込む)すれば良くなる。

最初にMapPane.svelteからviewerStoreの初期化を行う必要があるが、グラフでは省略している。

カスタムストア「viewStore」の実装

さて今回はviewStoreに、以下の関数を用意する。

  • Viewerの初期化
  • カメラのビューを設定
  • 地形データの変更
import { writable } from "svelte/store";
import {
  Viewer,
  Cartesian3,
  CesiumTerrainProvider,
  IonResource,
  Entity
} from "cesium";

// カスタムストア作成
function createViewerStore() {
  let viewer: Viewer | null = null;

  const { subscribe, set } = writable({
    viewer: null as Viewer | null,
    attributions: [] as string[]
  });

  // Viewerの初期化
  function init(containerId: string, options?: any) {
    (window as any).CESIUM_BASE_URL = '/node_modules/cesium/Build/Cesium/';
    viewer = new Viewer(containerId, options);
    set({ viewer, attributions: [] });
  }

  // カメラのビューを設定
  function setView(latitude: number, longitude: number, height: number) {
    if (!viewer) return;
    viewer.camera.setView({
      destination: Cartesian3.fromDegrees(longitude, latitude, height),
      orientation: {
        heading: 0,
        pitch: -0.5,
        roll: 0
      }
    });
  }

  // 地形データの変更(Cesium ion使用例)
  async function changeTerrain(assetId: number) {
    if (!viewer) return;
		viewer.terrainProvider = await CesiumTerrainProvider.fromUrl(
			IonResource.fromAssetId(assetId)
		);
  }

  // エンティティの追加
  function addEntity(entity: Entity.ConstructorOptions) {
    if (!viewer) return;
    viewer.entities.add(entity);
  }

  // エンティティの削除
  function removeEntityById(entityId: string) {
    if (!viewer) return;
    const existingEntity = viewer.entities.getById(entityId);
    if (existingEntity) {
      viewer.entities.remove(existingEntity);
    }
  }

  return {
    subscribe,
    init,
    setView,
    changeTerrain,
    addEntity,
    removeEntityById
  };
}

export const viewerStore = createViewerStore();

使用例

  • Viewerの初期化、初期カメラ位置の制御、Viewer・attributeの同期

    viewerStoreをsubscribeすることで、他のコンポーネントからの変更内容をリアクティブに反映することができる。

    <script lang="ts">
    	import { onMount } from 'svelte';
    	import { Viewer } from 'cesium';
    	import 'cesium/Build/Cesium/Widgets/widgets.css';
    
    	import { viewerStore } from '$lib/store/viewer';
    
    	let viewer: Viewer | null = null;
    	let attributions: string[] = [];
    
    	// Viewer・attributeの同期
    	$: viewerStore.subscribe((state) => {
    		viewer = state.viewer;
    		attributions = state.attributions;
    	});
    
    	$: attributionHtml = attributions.join(' | ');
    	onMount(async () => {
    		// Viewerの初期化
    		viewerStore.init('cesiumContainer');
    		// 初期カメラ位置の制御
    		viewerStore.setView(35.66, 139.76, 600);
    	});
    </script>
    
    <div class="absolute w-full h-full">
    	<div id="cesiumContainer" class="absolute cesiumContainer w-full h-full"></div>
    	<div id="attribution">{@html attributionHtml}</div>
    </div>
    

    ※ viewerStoreを扱う最低限のコードに限定

  • 地形データの変更
    viewerStoreのchangeTerrainを呼び出すことで、ストアのViewを更新している。

    <script lang="ts">
    	import type { CesiumTerrain } from '$lib/types/terrains';
    	import { viewerStore } from '$lib/store/viewer';
    	import { terrainStore } from '$lib/store/terrain';
    	
    	$: terrains = terrainStore;
    
    	function switchTerrain(terrain: CesiumTerrain) {
    		viewerStore.changeTerrain(terrain.assetId);
    	}
    </script>
    
    <div>
    	{#each terrains as terrain}
    		<div class="flex items-center px-2 h-10 border border-b-slate-400">
    			<input
    				type="radio"
    				name="terrain"
    				class="mr-2"
    				checked={activeTerrain === terrain}
    				on:change={() => {
    					switchTerrain(terrain);
    				}}
    			/>
    			<div class="overflow-hidden whitespace-nowrap">
    				{terrain.name}
    			</div>
    		</div>
    	{/each}
    </div>
    

    ※ viewerStoreを扱う最低限のコードに限定

  • エンティティ(ポリゴン)の追加

    import { viewerStore } from '$lib/store/viewer';
    
    viewerStore.addEntity({
        id: 'polygon',
        polygon: {
            hierarchy: new CallbackProperty(() => {
                return new PolygonHierarchy(Object.values(points));
            }, false),
            material: Color.RED.withAlpha(0.2)
        }
    });
    
  • エンティティ(ポリゴン)の削除

    import { viewerStore } from '$lib/store/viewer';
    
    viewerStore.removeEntityById('polygon');
    
  • その他: 各コンポーネントで直接Viewerを制御

    import { viewerStore } from '$lib/store/viewer';
    import { get } from 'svelte/store';
    
    const viewer = get(viewerStore).viewer;
    
    // イベントハンドラーを定義
    const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
    

まとめ

高速で柔軟性がありローコードを達成できるSvelteを用いることで、CesiumJSはさらに使いやすいライブラリへと進化するでしょう。ぜひお試しを。

CesiumJSを用いた計測系などの記事も書く予定なのでお楽しみに。

では良いCesiumライフを

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2