Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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)
25 changes: 17 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -73,7 +74,10 @@ function App() {
let [pickingRadius] = useModelState<number>("picking_radius");
let [useDevicePixels] = useModelState<number | boolean>("use_device_pixels");
let [parameters] = useModelState<object>("parameters");

const [viewState, setViewState] = useModelStateDebounced<MapViewState>(
"_view_state",
300,
);
let [initialViewState, setInitialViewState] = useState(
pythonInitialViewState,
);
Expand Down Expand Up @@ -147,13 +151,13 @@ function App() {
return (
<div id={`map-${mapId}`} style={{ height: mapHeight || "100%" }}>
<DeckGL
initialViewState={
["longitude", "latitude", "zoom"].every((key) =>
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
Expand All @@ -165,6 +169,11 @@ function App() {
overAlloc: 1,
poolSize: 0,
}}
viewState={viewState}
onViewStateChange={(event) => {
// @ts-expect-error here viewState is typed as Record<string, any>
setViewState(event.viewState);
}}
parameters={parameters || {}}
>
<Map mapStyle={mapStyle || DEFAULT_MAP_STYLE} />
Expand Down
51 changes: 51 additions & 0 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
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);
},
];
}
11 changes: 11 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,14 @@ export async function loadChildModels(
export function isDefined<T>(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);
};
};