diff --git a/web/src/beta/lib/core/engines/Cesium/core/Imagery.test.ts b/web/src/beta/lib/core/engines/Cesium/core/Imagery.test.ts index 60f67528b1..6e6efeed3a 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Imagery.test.ts +++ b/web/src/beta/lib/core/engines/Cesium/core/Imagery.test.ts @@ -1,103 +1,117 @@ -import { renderHook } from "@testing-library/react"; +import { renderHook, act } from "@testing-library/react"; import { expect, test, vi } from "vitest"; import { type Tile, useImageryProviders } from "./Imagery"; -test("useImageryProviders", () => { +test("useImageryProviders", async () => { const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url })); const provider2 = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge2: url })); const presets = { default: provider, foobar: provider2 }; - const { result, rerender } = renderHook( - ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => - useImageryProviders({ - tiles, - presets, - cesiumIonAccessToken, - }), - { initialProps: { tiles: [{ id: "1", tile_type: "default" }] } }, - ); + let result: any; + let rerender: any; + + await act(async () => { + const renderResult = renderHook( + ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => + useImageryProviders({ + tiles, + presets, + cesiumIonAccessToken, + }), + { initialProps: { tiles: [{ id: "1", tile_type: "default" }] } }, + ); + result = renderResult.result; + rerender = renderResult.rerender; + }); expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); - expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(1); const prevImageryProvider = result.current.providers["1"][2]; // re-render with same tiles - rerender({ tiles: [{ id: "1", tile_type: "default" }] }); + await act(async () => { + rerender({ tiles: [{ id: "1", tile_type: "default" }] }); + }); expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); - expect(result.current.updated).toBe(false); expect(result.current.providers["1"][2]).toBe(prevImageryProvider); // 1's provider should be reused expect(provider).toBeCalledTimes(1); // update a tile URL - rerender({ tiles: [{ id: "1", tile_type: "default", tile_url: "a" }] }); + await act(async () => { + rerender({ tiles: [{ id: "1", tile_type: "default", tile_url: "a" }] }); + }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }] }); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider); - expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(2); expect(provider).toBeCalledWith({ url: "a" }); const prevImageryProvider2 = result.current.providers["1"][2]; // add a tile with URL - rerender({ - tiles: [ - { id: "2", tile_type: "default" }, - { id: "1", tile_type: "default", tile_url: "a" }, - ], + await act(async () => { + rerender({ + tiles: [ + { id: "2", tile_type: "default" }, + { id: "1", tile_type: "default", tile_url: "a" }, + ], + }); }); expect(result.current.providers).toEqual({ "2": ["default", undefined, { hoge: undefined }], "1": ["default", "a", { hoge: "a" }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused - expect(provider).toBeCalledTimes(3); + expect(provider).toBeCalledTimes(2); // sort tiles - rerender({ - tiles: [ - { id: "1", tile_type: "default", tile_url: "a" }, - { id: "2", tile_type: "default" }, - ], + await act(async () => { + rerender({ + tiles: [ + { id: "1", tile_type: "default", tile_url: "a" }, + { id: "2", tile_type: "default" }, + ], + }); }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }], "2": ["default", undefined, { hoge: undefined }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused - expect(provider).toBeCalledTimes(3); + expect(provider).toBeCalledTimes(2); // delete a tile - rerender({ - tiles: [{ id: "1", tile_type: "default", tile_url: "a" }], - cesiumIonAccessToken: "a", + await act(async () => { + rerender({ + tiles: [{ id: "1", tile_type: "default", tile_url: "a" }], + cesiumIonAccessToken: "a", + }); }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2); - expect(provider).toBeCalledTimes(4); + expect(provider).toBeCalledTimes(3); // update a tile type - rerender({ - tiles: [{ id: "1", tile_type: "foobar", tile_url: "u" }], - cesiumIonAccessToken: "a", + await act(async () => { + rerender({ + tiles: [{ id: "1", tile_type: "foobar", tile_url: "u" }], + cesiumIonAccessToken: "a", + }); }); expect(result.current.providers).toEqual({ "1": ["foobar", "u", { hoge2: "u" }], }); - expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(4); + expect(provider).toBeCalledTimes(3); expect(provider2).toBeCalledTimes(1); - rerender({ tiles: [] }); + await act(async () => { + rerender({ tiles: [] }); + }); expect(result.current.providers).toEqual({}); }); diff --git a/web/src/beta/lib/core/engines/Cesium/core/Imagery.tsx b/web/src/beta/lib/core/engines/Cesium/core/Imagery.tsx index d504f98fe8..62dcc2baee 100644 --- a/web/src/beta/lib/core/engines/Cesium/core/Imagery.tsx +++ b/web/src/beta/lib/core/engines/Cesium/core/Imagery.tsx @@ -1,6 +1,5 @@ import { ImageryProvider } from "cesium"; -import { isEqual } from "lodash-es"; -import { useCallback, useMemo, useRef, useLayoutEffect } from "react"; +import { useMemo, useState, useEffect, useCallback, useRef } from "react"; import { ImageryLayer } from "resium"; import { tiles as tilePresets } from "./presets"; @@ -28,45 +27,44 @@ export type Props = { }; export default function ImageryLayers({ tiles, cesiumIonAccessToken }: Props) { - const { providers, updated } = useImageryProviders({ + const { providers } = useImageryProviders({ tiles, cesiumIonAccessToken, presets: tilePresets, }); - // force rerendering all layers when any provider is updated - // since Resium does not sort layers according to ImageryLayer component order - const counter = useRef(0); - useLayoutEffect(() => { - if (updated) counter.current++; - }, [providers, updated]); + const memoTiles = useMemo( + () => + tiles + ?.map(({ id, ...tile }) => ({ ...tile, id, provider: providers[id]?.[2] })) + .filter(({ provider }) => !!provider) ?? [], + [tiles, providers], + ); return ( <> - {tiles - ?.map(({ id, ...tile }) => ({ ...tile, id, provider: providers[id]?.[2] })) - .map(({ id, tile_opacity: opacity, tile_minLevel: min, tile_maxLevel: max, provider }, i) => - provider ? ( - - ) : null, - )} + {memoTiles.map( + ({ id, tile_opacity: opacity, tile_minLevel: min, tile_maxLevel: max, provider }, i) => ( + + ), + )} ); } type Providers = { - [id: string]: [ - string | undefined, - string | undefined, - Promise | ImageryProvider, - ]; + [id: string]: [string | undefined, string | undefined, ImageryProvider]; +}; + +type ResolvedProviders = { + [id: string]: ImageryProvider; }; export function useImageryProviders({ @@ -82,92 +80,41 @@ export function useImageryProviders({ cesiumIonAccessToken?: string; }) => Promise | ImageryProvider | null; }; -}): { providers: Providers; updated: boolean } { - const newTile = useCallback( - (t: Tile, ciat?: string) => - presets[t.tile_type || "default"]({ url: t.tile_url, cesiumIonAccessToken: ciat }), - [presets], +}): { + providers: Providers; +} { + const resolvedPresetProviders = useRef({}); + const [providers, setProviders] = useState({}); + + const providerKey = useCallback( + (t: Omit) => `${t.tile_type || "default"}_${t.tile_url}_${cesiumIonAccessToken}`, + [cesiumIonAccessToken], ); - const prevCesiumIonAccessToken = useRef(cesiumIonAccessToken); - const tileKeys = tiles.map(t => t.id).join(","); - const prevTileKeys = useRef(tileKeys); - const prevProviders = useRef({}); - - // Manage TileProviders so that TileProvider does not need to be recreated each time tiles are updated. - const { providers, updated } = useMemo(() => { - const isCesiumAccessTokenUpdated = prevCesiumIonAccessToken.current !== cesiumIonAccessToken; - const prevProvidersKeys = Object.keys(prevProviders.current); - const added = tiles.map(t => t.id).filter(t => t && !prevProvidersKeys.includes(t)); - - const rawProviders = [ - ...Object.entries(prevProviders.current), - ...added.map(a => [a, undefined] as const), - ].map(([k, v]) => ({ - key: k, - added: added.includes(k), - prevType: v?.[0], - prevUrl: v?.[1], - prevProvider: v?.[2], - tile: tiles.find(t => t.id === k), - })); - - const providers = Object.fromEntries( - rawProviders - .map( - ({ - key, - added, - prevType, - prevUrl, - prevProvider, - tile, - }): - | [ - string, - [ - string | undefined, - string | undefined, - Promise | ImageryProvider | null | undefined, - ], - ] - | null => - !tile - ? null - : [ - key, - added || - prevType !== tile.tile_type || - prevUrl !== tile.tile_url || - (isCesiumAccessTokenUpdated && (!tile.tile_type || tile.tile_type === "default")) - ? [tile.tile_type, tile.tile_url, newTile(tile, cesiumIonAccessToken)] - : [prevType, prevUrl, prevProvider], - ], - ) - .filter( - ( - e, - ): e is [ - string, - [string | undefined, string | undefined, Promise | ImageryProvider], - ] => !!e?.[1][2], + useEffect(() => { + Promise.all( + tiles.map(async t => { + if (!Object.keys(resolvedPresetProviders.current).includes(providerKey(t))) { + const newProvider = await presets[t.tile_type || "default"]({ + url: t.tile_url, + cesiumIonAccessToken, + }); + if (newProvider) { + resolvedPresetProviders.current[providerKey(t)] = newProvider; + } + } + }), + ).then(() => { + setProviders( + Object.fromEntries( + tiles.map(({ id, ...t }) => [ + id, + [t.tile_type, t.tile_url, resolvedPresetProviders.current[providerKey(t)]], + ]), ), - ); - - const updated = - !!added.length || - !!isCesiumAccessTokenUpdated || - !isEqual(prevTileKeys.current, tileKeys) || - rawProviders.some( - p => p.tile && (p.prevType !== p.tile.tile_type || p.prevUrl !== p.tile.tile_url), ); + }); + }, [tiles, cesiumIonAccessToken, presets, resolvedPresetProviders, providerKey]); - prevTileKeys.current = tileKeys; - prevCesiumIonAccessToken.current = cesiumIonAccessToken; - - return { providers, updated }; - }, [cesiumIonAccessToken, tiles, tileKeys, newTile]); - - prevProviders.current = providers; - return { providers, updated }; + return { providers }; } diff --git a/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.test.ts b/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.test.ts index 60f67528b1..6e6efeed3a 100644 --- a/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.test.ts +++ b/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.test.ts @@ -1,103 +1,117 @@ -import { renderHook } from "@testing-library/react"; +import { renderHook, act } from "@testing-library/react"; import { expect, test, vi } from "vitest"; import { type Tile, useImageryProviders } from "./Imagery"; -test("useImageryProviders", () => { +test("useImageryProviders", async () => { const provider = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge: url })); const provider2 = vi.fn(({ url }: { url?: string } = {}): any => ({ hoge2: url })); const presets = { default: provider, foobar: provider2 }; - const { result, rerender } = renderHook( - ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => - useImageryProviders({ - tiles, - presets, - cesiumIonAccessToken, - }), - { initialProps: { tiles: [{ id: "1", tile_type: "default" }] } }, - ); + let result: any; + let rerender: any; + + await act(async () => { + const renderResult = renderHook( + ({ tiles, cesiumIonAccessToken }: { tiles: Tile[]; cesiumIonAccessToken?: string }) => + useImageryProviders({ + tiles, + presets, + cesiumIonAccessToken, + }), + { initialProps: { tiles: [{ id: "1", tile_type: "default" }] } }, + ); + result = renderResult.result; + rerender = renderResult.rerender; + }); expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); - expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(1); const prevImageryProvider = result.current.providers["1"][2]; // re-render with same tiles - rerender({ tiles: [{ id: "1", tile_type: "default" }] }); + await act(async () => { + rerender({ tiles: [{ id: "1", tile_type: "default" }] }); + }); expect(result.current.providers).toEqual({ "1": ["default", undefined, { hoge: undefined }] }); - expect(result.current.updated).toBe(false); expect(result.current.providers["1"][2]).toBe(prevImageryProvider); // 1's provider should be reused expect(provider).toBeCalledTimes(1); // update a tile URL - rerender({ tiles: [{ id: "1", tile_type: "default", tile_url: "a" }] }); + await act(async () => { + rerender({ tiles: [{ id: "1", tile_type: "default", tile_url: "a" }] }); + }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }] }); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider); - expect(result.current.updated).toBe(true); expect(provider).toBeCalledTimes(2); expect(provider).toBeCalledWith({ url: "a" }); const prevImageryProvider2 = result.current.providers["1"][2]; // add a tile with URL - rerender({ - tiles: [ - { id: "2", tile_type: "default" }, - { id: "1", tile_type: "default", tile_url: "a" }, - ], + await act(async () => { + rerender({ + tiles: [ + { id: "2", tile_type: "default" }, + { id: "1", tile_type: "default", tile_url: "a" }, + ], + }); }); expect(result.current.providers).toEqual({ "2": ["default", undefined, { hoge: undefined }], "1": ["default", "a", { hoge: "a" }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused - expect(provider).toBeCalledTimes(3); + expect(provider).toBeCalledTimes(2); // sort tiles - rerender({ - tiles: [ - { id: "1", tile_type: "default", tile_url: "a" }, - { id: "2", tile_type: "default" }, - ], + await act(async () => { + rerender({ + tiles: [ + { id: "1", tile_type: "default", tile_url: "a" }, + { id: "2", tile_type: "default" }, + ], + }); }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }], "2": ["default", undefined, { hoge: undefined }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).toBe(prevImageryProvider2); // 1's provider should be reused - expect(provider).toBeCalledTimes(3); + expect(provider).toBeCalledTimes(2); // delete a tile - rerender({ - tiles: [{ id: "1", tile_type: "default", tile_url: "a" }], - cesiumIonAccessToken: "a", + await act(async () => { + rerender({ + tiles: [{ id: "1", tile_type: "default", tile_url: "a" }], + cesiumIonAccessToken: "a", + }); }); expect(result.current.providers).toEqual({ "1": ["default", "a", { hoge: "a" }], }); - expect(result.current.updated).toBe(true); expect(result.current.providers["1"][2]).not.toBe(prevImageryProvider2); - expect(provider).toBeCalledTimes(4); + expect(provider).toBeCalledTimes(3); // update a tile type - rerender({ - tiles: [{ id: "1", tile_type: "foobar", tile_url: "u" }], - cesiumIonAccessToken: "a", + await act(async () => { + rerender({ + tiles: [{ id: "1", tile_type: "foobar", tile_url: "u" }], + cesiumIonAccessToken: "a", + }); }); expect(result.current.providers).toEqual({ "1": ["foobar", "u", { hoge2: "u" }], }); - expect(result.current.updated).toBe(true); - expect(provider).toBeCalledTimes(4); + expect(provider).toBeCalledTimes(3); expect(provider2).toBeCalledTimes(1); - rerender({ tiles: [] }); + await act(async () => { + rerender({ tiles: [] }); + }); expect(result.current.providers).toEqual({}); }); diff --git a/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.tsx b/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.tsx index 2f5797e99b..62dcc2baee 100644 --- a/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.tsx +++ b/web/src/classic/components/molecules/Visualizer/Engine/Cesium/core/Imagery.tsx @@ -1,6 +1,5 @@ import { ImageryProvider } from "cesium"; -import { isEqual } from "lodash-es"; -import { useCallback, useMemo, useRef, useLayoutEffect } from "react"; +import { useMemo, useState, useEffect, useCallback, useRef } from "react"; import { ImageryLayer } from "resium"; import { tiles as tilePresets } from "./presets"; @@ -28,44 +27,44 @@ export type Props = { }; export default function ImageryLayers({ tiles, cesiumIonAccessToken }: Props) { - const { providers, updated } = useImageryProviders({ + const { providers } = useImageryProviders({ tiles, cesiumIonAccessToken, presets: tilePresets, }); - // force rerendering all layers when any provider is updated - // since Resium does not sort layers according to ImageryLayer component order - const counter = useRef(0); - useLayoutEffect(() => { - if (updated) counter.current++; - }, [providers, updated]); + const memoTiles = useMemo( + () => + tiles + ?.map(({ id, ...tile }) => ({ ...tile, id, provider: providers[id]?.[2] })) + .filter(({ provider }) => !!provider) ?? [], + [tiles, providers], + ); return ( <> - {tiles - ?.map(({ id, ...tile }) => ({ ...tile, id, provider: providers[id]?.[2] })) - .map(({ id, tile_opacity: opacity, tile_minLevel: min, tile_maxLevel: max, provider }, i) => - provider ? ( - - ) : null, - )} + {memoTiles.map( + ({ id, tile_opacity: opacity, tile_minLevel: min, tile_maxLevel: max, provider }, i) => ( + + ), + )} ); } type Providers = { - [id: string]: [ - string | undefined, - string | undefined, - Promise | ImageryProvider, - ]; + [id: string]: [string | undefined, string | undefined, ImageryProvider]; +}; + +type ResolvedProviders = { + [id: string]: ImageryProvider; }; export function useImageryProviders({ @@ -81,92 +80,41 @@ export function useImageryProviders({ cesiumIonAccessToken?: string; }) => Promise | ImageryProvider | null; }; -}): { providers: Providers; updated: boolean } { - const newTile = useCallback( - (t: Tile, ciat?: string) => - presets[t.tile_type || "default"]({ url: t.tile_url, cesiumIonAccessToken: ciat }), - [presets], +}): { + providers: Providers; +} { + const resolvedPresetProviders = useRef({}); + const [providers, setProviders] = useState({}); + + const providerKey = useCallback( + (t: Omit) => `${t.tile_type || "default"}_${t.tile_url}_${cesiumIonAccessToken}`, + [cesiumIonAccessToken], ); - const prevCesiumIonAccessToken = useRef(cesiumIonAccessToken); - const tileKeys = tiles.map(t => t.id).join(","); - const prevTileKeys = useRef(tileKeys); - const prevProviders = useRef({}); - - // Manage TileProviders so that TileProvider does not need to be recreated each time tiles are updated. - const { providers, updated } = useMemo(() => { - const isCesiumAccessTokenUpdated = prevCesiumIonAccessToken.current !== cesiumIonAccessToken; - const prevProvidersKeys = Object.keys(prevProviders.current); - const added = tiles.map(t => t.id).filter(t => t && !prevProvidersKeys.includes(t)); - - const rawProviders = [ - ...Object.entries(prevProviders.current), - ...added.map(a => [a, undefined] as const), - ].map(([k, v]) => ({ - key: k, - added: added.includes(k), - prevType: v?.[0], - prevUrl: v?.[1], - prevProvider: v?.[2], - tile: tiles.find(t => t.id === k), - })); - - const providers = Object.fromEntries( - rawProviders - .map( - ({ - key, - added, - prevType, - prevUrl, - prevProvider, - tile, - }): - | [ - string, - [ - string | undefined, - string | undefined, - Promise | ImageryProvider | null | undefined, - ], - ] - | null => - !tile - ? null - : [ - key, - added || - prevType !== tile.tile_type || - prevUrl !== tile.tile_url || - (isCesiumAccessTokenUpdated && (!tile.tile_type || tile.tile_type === "default")) - ? [tile.tile_type, tile.tile_url, newTile(tile, cesiumIonAccessToken)] - : [prevType, prevUrl, prevProvider], - ], - ) - .filter( - ( - e, - ): e is [ - string, - [string | undefined, string | undefined, Promise | ImageryProvider], - ] => !!e?.[1][2], + useEffect(() => { + Promise.all( + tiles.map(async t => { + if (!Object.keys(resolvedPresetProviders.current).includes(providerKey(t))) { + const newProvider = await presets[t.tile_type || "default"]({ + url: t.tile_url, + cesiumIonAccessToken, + }); + if (newProvider) { + resolvedPresetProviders.current[providerKey(t)] = newProvider; + } + } + }), + ).then(() => { + setProviders( + Object.fromEntries( + tiles.map(({ id, ...t }) => [ + id, + [t.tile_type, t.tile_url, resolvedPresetProviders.current[providerKey(t)]], + ]), ), - ); - - const updated = - !!added.length || - !!isCesiumAccessTokenUpdated || - !isEqual(prevTileKeys.current, tileKeys) || - rawProviders.some( - p => p.tile && (p.prevType !== p.tile.tile_type || p.prevUrl !== p.tile.tile_url), ); + }); + }, [tiles, cesiumIonAccessToken, presets, resolvedPresetProviders, providerKey]); - prevTileKeys.current = tileKeys; - prevCesiumIonAccessToken.current = cesiumIonAccessToken; - - return { providers, updated }; - }, [cesiumIonAccessToken, tiles, tileKeys, newTile]); - - prevProviders.current = providers; - return { providers, updated }; + return { providers }; }