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..b206bcb6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,6 +13,7 @@ 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 +74,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 +151,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 +169,11 @@ function App() { overAlloc: 1, poolSize: 0, }} + viewState={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..ab3c74f1 --- /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); + }, + ]; +} 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); + }; +};