Skip to content

Commit

Permalink
Capturing camera parameters within the URL (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkuznetsov-actionengine authored Dec 21, 2023
1 parent 7c9f7ab commit 459cf37
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 7 deletions.
6 changes: 5 additions & 1 deletion src/components/deck-gl-wrapper/deck-gl-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DeckGL from "@deck.gl/react";
import { LineLayer, ScatterplotLayer } from "@deck.gl/layers";
import { TerrainLayer, Tile3DLayer } from "@deck.gl/geo-layers";
import { MapController } from "@deck.gl/core";
import { MapController, InteractionState } from "@deck.gl/core";
import type { Tile3D, Tileset3D } from "@loaders.gl/tiles";
import { I3SLoader, SceneLayer3D } from "@loaders.gl/i3s";
import { CesiumIonLoader, Tiles3DLoader } from "@loaders.gl/3d-tiles";
Expand Down Expand Up @@ -161,6 +161,8 @@ type DeckGlI3sProps = {
onWebGLInitialized?: (gl: any) => void;
/** DeckGL after render callback */
onAfterRender?: () => void;
/** DeckGL onInteractionStateChange callback */
onInteractionStateChange?: (interactionState: InteractionState) => void;
/** DeckGL callback. On layer hover behavior */
getTooltip?: (info: { object: Tile3D; index: number; layer: any }) => void;
/** DeckGL callback. On layer click behavior */
Expand Down Expand Up @@ -203,6 +205,7 @@ export const DeckGlWrapper = ({
onViewStateChange,
onWebGLInitialized,
onAfterRender,
onInteractionStateChange,
getTooltip,
onClick,
onTilesetLoad,
Expand Down Expand Up @@ -777,6 +780,7 @@ export const DeckGlWrapper = ({
}}
onWebGLInitialized={onWebGLInitialized}
onAfterRender={onAfterRender}
onInteractionStateChange={onInteractionStateChange}
getTooltip={getTooltip}
onClick={onClick}
>
Expand Down
44 changes: 41 additions & 3 deletions src/pages/debug-app/debug-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { render } from "react-dom";
import { lumaStats } from "@luma.gl/core";
import { PickingInfo } from "@deck.gl/core";
import { PickingInfo, InteractionStateChange, ViewState } from "@deck.gl/core";

import { v4 as uuidv4 } from "uuid";
import { Stats } from "@probe.gl/stats";
Expand All @@ -38,7 +38,11 @@ import ColorMap, {
makeRGBObjectFromColor,
} from "../../utils/debug/colors-map";
import { initStats, sumTilesetsStats } from "../../utils/stats";
import { parseTilesetUrlParams } from "../../utils/url-utils";
import {
parseTilesetUrlParams,
urlParamsToViewState,
viewStateToUrlParams,
} from "../../utils/url-utils";
import { validateTile } from "../../utils/debug/tile-debug";
import {
BottomToolsPanelWrapper,
Expand Down Expand Up @@ -149,7 +153,8 @@ export const DebugApp = () => {
const [sublayers, setSublayers] = useState<ActiveSublayer[]>([]);
const [buildingExplorerOpened, setBuildingExplorerOpened] =
useState<boolean>(false);

const [stateUrlViewStateParams, setStateUrlViewStateParams] =
useState<ViewState>({});
const [, setSearchParams] = useSearchParams();
const dispatch = useAppDispatch();

Expand Down Expand Up @@ -187,6 +192,9 @@ export const DebugApp = () => {
dispatch(setColorsByAttrubute(null));
dispatch(setDragMode(DragMode.pan));
dispatch(setDebugOptions({ minimap: true }));

setStateUrlViewStateParams(urlParamsToViewState(viewState));

return () => {
dispatch(resetDebugOptions());
dispatch(setInitialBaseMaps());
Expand Down Expand Up @@ -278,6 +286,19 @@ export const DebugApp = () => {

tilesetRef.current = tileset;
setUpdateStatsNumber((prev) => prev + 1);
if (Object.keys(stateUrlViewStateParams).length > 0) {
const { longitude, latitude } = stateUrlViewStateParams;
setViewState({
...viewState,
main: { ...viewState.main, ...stateUrlViewStateParams },
minimap: {
...viewState.minimap,
longitude,
latitude,
},
});
setStateUrlViewStateParams({});
}
};

const handleValidateTile = (tile) => {
Expand Down Expand Up @@ -690,6 +711,22 @@ export const DebugApp = () => {
}));
}, []);

const onInteractionStateChange = (
interactionStateChange: InteractionStateChange
) => {
const { isDragging, inTransition, isZooming, isPanning, isRotating } =
interactionStateChange;
if (
!isDragging &&
!inTransition &&
!isZooming &&
!isPanning &&
!isRotating
) {
setSearchParams(viewStateToUrlParams(viewState), { replace: true });
}
};

return (
<MapArea>
{renderTilePanel()}
Expand Down Expand Up @@ -722,6 +759,7 @@ export const DebugApp = () => {
onTileLoad={onTileLoad}
onWebGLInitialized={onWebGLInitialized}
preventTransitions={preventTransitions}
onInteractionStateChange={onInteractionStateChange}
/>
{layout !== Layout.Mobile && (
<OnlyToolsPanelWrapper layout={layout}>
Expand Down
36 changes: 35 additions & 1 deletion src/pages/viewer-app/viewer-app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { render } from "react-dom";
import { InteractionStateChange, ViewState } from "@deck.gl/core";
import { lumaStats } from "@luma.gl/core";
import { loadFeatureAttributes, StatisticsInfo } from "@loaders.gl/i3s";
import { v4 as uuidv4 } from "uuid";
Expand All @@ -13,7 +14,11 @@ import { Tileset3D } from "@loaders.gl/tiles";
import { DeckGlWrapper } from "../../components/deck-gl-wrapper/deck-gl-wrapper";
import { AttributesPanel } from "../../components/attributes-panel/attributes-panel";
import { initStats, sumTilesetsStats } from "../../utils/stats";
import { parseTilesetUrlParams } from "../../utils/url-utils";
import {
parseTilesetUrlParams,
urlParamsToViewState,
viewStateToUrlParams,
} from "../../utils/url-utils";
import {
FeatureAttributes,
Sublayer,
Expand Down Expand Up @@ -122,6 +127,8 @@ export const ViewerApp = () => {
const [sublayers, setSublayers] = useState<ActiveSublayer[]>([]);
const [buildingExplorerOpened, setBuildingExplorerOpened] =
useState<boolean>(false);
const [stateUrlViewStateParams, setStateUrlViewStateParams] =
useState<ViewState>({});
const [, setSearchParams] = useSearchParams();
const dispatch = useAppDispatch();
const filtersByAttribute = useSelector((state: RootState) =>
Expand Down Expand Up @@ -154,6 +161,9 @@ export const ViewerApp = () => {
setActiveLayers([newActiveLayer]);
dispatch(setColorsByAttrubute(null));
dispatch(setDragMode(DragMode.pan));

setStateUrlViewStateParams(urlParamsToViewState(viewState));

return () => {
dispatch(setInitialBaseMaps());
};
Expand Down Expand Up @@ -228,6 +238,13 @@ export const ViewerApp = () => {
);
tilesetRef.current = tileset;
setUpdateStatsNumber((prev) => prev + 1);
if (Object.keys(stateUrlViewStateParams).length > 0) {
setViewState({
...viewState,
main: { ...viewState.main, ...stateUrlViewStateParams },
});
setStateUrlViewStateParams({});
}
};

const isLayerPickable = () => {
Expand Down Expand Up @@ -547,6 +564,22 @@ export const ViewerApp = () => {
}));
}, []);

const onInteractionStateChange = (
interactionStateChange: InteractionStateChange
) => {
const { isDragging, inTransition, isZooming, isPanning, isRotating } =
interactionStateChange;
if (
!isDragging &&
!inTransition &&
!isZooming &&
!isPanning &&
!isRotating
) {
setSearchParams(viewStateToUrlParams(viewState), { replace: true });
}
};

return (
<MapArea>
{selectedFeatureAttributes && renderAttributesPanel()}
Expand All @@ -573,6 +606,7 @@ export const ViewerApp = () => {
onTileLoad={onTileLoad}
onWebGLInitialized={onWebGLInitialized}
preventTransitions={preventTransitions}
onInteractionStateChange={onInteractionStateChange}
/>

{layout !== Layout.Mobile && (
Expand Down
45 changes: 45 additions & 0 deletions src/utils/url-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
getTilesetType,
parseTilesetFromUrl,
parseTilesetUrlParams,
urlParamsToViewState,
viewStateToUrlParams,
} from "./url-utils";

const mockResponse = jest.fn();
Expand Down Expand Up @@ -134,3 +136,46 @@ describe("Url Utils - getTilesetType", () => {
expect(resultEmptyUrl).toEqual(TilesetType.I3S);
});
});

describe("Url Utils - viewStateToUrlParams", () => {
test("Should generate updated url search params", () => {
Object.defineProperty(window, "location", {
value: {
search: "tileset=test-tileset-name&token=test-token",
},
writable: true,
});
const viewState = {
main: { zoom: 1, longitude: 2, latitude: 3, bearing: 4, pitch: 5 },
};
const result = viewStateToUrlParams(viewState);
expect(result).toEqual({
...viewState.main,
tileset: "test-tileset-name",
token: "test-token",
});
});
});

describe("Url Utils - urlParamsToViewState", () => {
test("Should generate updated url search params", () => {
Object.defineProperty(window, "location", {
value: {
search:
"tileset=test-tileset-name&zoom=6&longitude=7&latitude=8&bearing=9&pitch=10",
},
writable: true,
});
const viewState = {
main: { zoom: 1, longitude: 2, latitude: 3, bearing: 4, pitch: 5 },
};
const result = urlParamsToViewState(viewState);
expect(result).toEqual({
zoom: 6,
longitude: 7,
latitude: 8,
bearing: 9,
pitch: 10,
});
});
});
43 changes: 41 additions & 2 deletions src/utils/url-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TilesetType } from "../types";
import { TilesetType, ViewStateSet } from "../types";

export const parseTilesetFromUrl = () => {
const parsedUrl = new URL(window.location.href);
Expand All @@ -7,7 +7,7 @@ export const parseTilesetFromUrl = () => {

export const parseTilesetUrlParams = (url, options) => {
if (!url) {
return { ...options, tilesetUrl: '', token: '', metadataUrl: '' }
return { ...options, tilesetUrl: "", token: "", metadataUrl: "" };
}

const parsedUrl = new URL(url);
Expand Down Expand Up @@ -57,3 +57,42 @@ const prepareTilesetUrl = (parsedUrl) => {
.concat("layers/0");
return `${parsedUrl.origin}${replacedPathName}${parsedUrl.search}`;
};

/**
* Generate updated url search params according to the viewState
* @param viewState view state
* @returns updated url search params object
*/
export const viewStateToUrlParams = (viewState: ViewStateSet) => {
const search = Object.fromEntries(
new URLSearchParams(window.location.search)
);
const { longitude, latitude, pitch, bearing, zoom } = viewState.main;
return {
...search,
longitude,
latitude,
pitch,
bearing,
zoom,
};
};

/**
* Parse view state params from the url search params
* @param viewState view state
* @returns viewState params available in the url search params
*/
export const urlParamsToViewState = (viewState: ViewStateSet) => {
const search = new URLSearchParams(window.location.search);
const urlViewStateParams = {};
for (const viewStateParam of search) {
if (
Object.keys(viewState.main).includes(viewStateParam[0]) &&
!isNaN(parseFloat(viewStateParam[1]))
) {
urlViewStateParams[viewStateParam[0]] = parseFloat(viewStateParam[1]);
}
}
return urlViewStateParams;
};

0 comments on commit 459cf37

Please sign in to comment.