From fbae48b43b75d53ede4cc84c606a840c9054482c Mon Sep 17 00:00:00 2001 From: Sanjay Bhangar Date: Fri, 29 Mar 2024 19:41:06 -0700 Subject: [PATCH 1/2] sync view state, move over code from #113, refs #112 --- lonboard/_map.py | 7 ++++++- src/index.tsx | 30 ++++++++++++++++++++-------- src/state.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ src/util.ts | 11 +++++++++++ 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 src/state.ts diff --git a/lonboard/_map.py b/lonboard/_map.py index 47499a89..3d25a97b 100644 --- a/lonboard/_map.py +++ b/lonboard/_map.py @@ -123,6 +123,11 @@ def __init__( This API is not yet stabilized and may change in the future. """ + _view_state = traitlets.Any(allow_none=True).tag(sync=True) + """ + View state that is synced from the frontend + """ + _height = traitlets.Int(default_value=DEFAULT_HEIGHT, allow_none=True).tag( sync=True ) @@ -333,6 +338,6 @@ def to_html( drop_defaults=False, ) - @traitlets.default("_initial_view_state") + @traitlets.default("_view_state") def _default_initial_view_state(self): return compute_view(self.layers) diff --git a/src/index.tsx b/src/index.tsx index 113e0e4a..a5ab5bbc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,6 +13,8 @@ import { isDefined, loadChildModels } from "./util.js"; import { v4 as uuidv4 } from "uuid"; import { Message } from "./types.js"; import { flyTo } from "./actions/fly-to.js"; +import { useModelStateDebounced } from "./state"; + await initParquetWasm(); @@ -73,7 +75,10 @@ function App() { let [pickingRadius] = useModelState("picking_radius"); let [useDevicePixels] = useModelState("use_device_pixels"); let [parameters] = useModelState("parameters"); - + const [viewState, setViewState] = useModelStateDebounced( + "_view_state", + 300 + ); let [initialViewState, setInitialViewState] = useState( pythonInitialViewState, ); @@ -147,13 +152,13 @@ function App() { return (
- Object.keys(initialViewState).includes(key), - ) - ? initialViewState - : DEFAULT_INITIAL_VIEW_STATE - } + // initialViewState={ + // ["longitude", "latitude", "zoom"].every((key) => + // Object.keys(initialViewState).includes(key), + // ) + // ? initialViewState + // : DEFAULT_INITIAL_VIEW_STATE + // } controller={true} layers={layers} // @ts-expect-error @@ -165,6 +170,15 @@ function App() { overAlloc: 1, poolSize: 0, }} + viewState={ + Object.keys(viewState).length === 0 + ? DEFAULT_INITIAL_VIEW_STATE + : viewState + } + onViewStateChange={(event) => { + // @ts-expect-error here viewState is typed as Record + setViewState(event.viewState); + }} parameters={parameters || {}} > diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 00000000..a2c43508 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,51 @@ +import * as React from "react"; +import { useModel } from "@anywidget/react"; +import { debounce } from "./util"; + +const debouncedModelSaveViewState = debounce((model) => { + console.log("DEBOUNCED"); + const viewState = model.get("_view_state"); + + // transitionInterpolator is sometimes a key in the view state while panning + // This is a function object and so can't be serialized via JSON. + // + // In the future anywidget may support custom serializers for sending data + // back from JS to Python. Until then, we need to clean the object ourselves. + // Because this is in a debounce it shouldn't often mess with deck's internal + // transition state it expects, because hopefully the transition will have + // finished in the 300ms that the user has stopped panning. + if ("transitionInterpolator" in viewState) { + console.debug("Deleting transitionInterpolator!"); + delete viewState.transitionInterpolator; + model.set("_view_state", viewState); + } + + model.save_changes(); +}, 300); + +export function useModelStateDebounced( + key: string, + wait: number +): [T, (value: T) => void] { + let model = useModel(); + let [value, setValue] = React.useState(model.get(key)); + React.useEffect(() => { + let callback = () => { + console.log("callback"); + console.log(model.get(key)); + setValue(model.get(key)); + }; + model.on(`change:${key}`, callback); + console.log(`model on change view state`); + return () => model.off(`change:${key}`, callback); + }, [model, key]); + return [ + value, + (value) => { + model.set(key, value); + // Note: I think this has to be defined outside of this function so that + // you're calling debounce on the same function object? + debouncedModelSaveViewState(model); + }, + ]; +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index fab10996..69d2524d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -21,3 +21,14 @@ export async function loadChildModels( export function isDefined(value: T | undefined | null): value is T { return value !== undefined && value !== null; } + +// From https://www.joshwcomeau.com/snippets/javascript/debounce/ +export const debounce = (callback: (args) => void, wait: number) => { + let timeoutId = null; + return (...args) => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback.apply(null, args); + }, wait); + }; +}; From 2881864322e607f59a88b5d7bce18260374f6d46 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 1 Apr 2024 11:00:57 -0400 Subject: [PATCH 2/2] lint --- src/index.tsx | 9 ++------- src/state.ts | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index a5ab5bbc..b206bcb6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -15,7 +15,6 @@ import { Message } from "./types.js"; import { flyTo } from "./actions/fly-to.js"; import { useModelStateDebounced } from "./state"; - await initParquetWasm(); const DEFAULT_INITIAL_VIEW_STATE = { @@ -77,7 +76,7 @@ function App() { let [parameters] = useModelState("parameters"); const [viewState, setViewState] = useModelStateDebounced( "_view_state", - 300 + 300, ); let [initialViewState, setInitialViewState] = useState( pythonInitialViewState, @@ -170,11 +169,7 @@ function App() { overAlloc: 1, poolSize: 0, }} - viewState={ - Object.keys(viewState).length === 0 - ? DEFAULT_INITIAL_VIEW_STATE - : viewState - } + viewState={viewState} onViewStateChange={(event) => { // @ts-expect-error here viewState is typed as Record setViewState(event.viewState); diff --git a/src/state.ts b/src/state.ts index a2c43508..ab3c74f1 100644 --- a/src/state.ts +++ b/src/state.ts @@ -25,7 +25,7 @@ const debouncedModelSaveViewState = debounce((model) => { export function useModelStateDebounced( key: string, - wait: number + wait: number, ): [T, (value: T) => void] { let model = useModel(); let [value, setValue] = React.useState(model.get(key)); @@ -48,4 +48,4 @@ export function useModelStateDebounced( debouncedModelSaveViewState(model); }, ]; -} \ No newline at end of file +}