Skip to content

Commit 2ac3d52

Browse files
authored
fix: Clean up view state handling between frontend and backend (#1019)
Ensure that no Python warnings are produced by the Python models being unable to handle data from JS
1 parent 1548ee5 commit 2ac3d52

File tree

4 files changed

+48
-30
lines changed

4 files changed

+48
-30
lines changed

lonboard/traits/_map.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from typing import TYPE_CHECKING, Any
45
from urllib.parse import urlparse
56

@@ -92,4 +93,12 @@ def validate(self, obj: Map, value: Any) -> None | BaseViewState:
9293
# Otherwise dict input
9394
view = obj.view
9495
validator = view._view_state_type if view is not None else MapViewState # noqa: SLF001
95-
return validator(**value) # type: ignore
96+
97+
# The frontend currently sends back data in camelCase
98+
snake_case_kwargs = {_camel_to_snake(k): v for k, v in value.items()}
99+
return validator(**snake_case_kwargs) # type: ignore
100+
101+
102+
def _camel_to_snake(name: str) -> str:
103+
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
104+
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()

src/index.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { useViewStateDebounced } from "./state";
3030
import Toolbar from "./toolbar.js";
3131
import { getTooltip } from "./tooltip/index.js";
3232
import { Message } from "./types.js";
33-
import { isDefined, isGlobeView } from "./util.js";
33+
import { isDefined, isGlobeView, sanitizeViewState } from "./util.js";
3434
import { MachineContext, MachineProvider } from "./xstate";
3535
import * as selectors from "./xstate/selectors";
3636

@@ -194,14 +194,7 @@ function App() {
194194
onHover: onMapHoverHandler,
195195
...(isDefined(useDevicePixels) && { useDevicePixels }),
196196
onViewStateChange: (event) => {
197-
const { viewState } = event;
198-
199-
// This condition is necessary to confirm that the viewState is
200-
// of type MapViewState.
201-
if (viewState && "latitude" in viewState) {
202-
// TODO: ensure all view state types get updated on the JS side
203-
setViewState(viewState);
204-
}
197+
setViewState(sanitizeViewState(views, event.viewState));
205198
},
206199
parameters: parameters || {},
207200
views,

src/state.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,7 @@ import type { AnyModel } from "@anywidget/types";
33
import debounce from "lodash.debounce";
44
import * as React from "react";
55

6-
const debouncedModelSaveViewState = debounce((model: AnyModel) => {
7-
// TODO: this and below is hard-coded to the view_state model property!
8-
const viewState = model.get("view_state");
9-
10-
// transitionInterpolator is sometimes a key in the view state while panning
11-
// This is a function object and so can't be serialized via JSON.
12-
//
13-
// In the future anywidget may support custom serializers for sending data
14-
// back from JS to Python. Until then, we need to clean the object ourselves.
15-
// Because this is in a debounce it shouldn't often mess with deck's internal
16-
// transition state it expects, because hopefully the transition will have
17-
// finished in the 300ms that the user has stopped panning.
18-
if ("transitionInterpolator" in viewState) {
19-
console.debug("Deleting transitionInterpolator!");
20-
delete viewState.transitionInterpolator;
21-
model.set("view_state", viewState);
22-
}
23-
6+
const debouncedModelSaveChanges = debounce((model: AnyModel) => {
247
model.save_changes();
258
}, 300);
269

@@ -41,7 +24,7 @@ export function useViewStateDebounced<T>(key: string): [T, (value: T) => void] {
4124
model.set(key, value);
4225
// Note: I think this has to be defined outside of this function so that
4326
// you're calling debounce on the same function object?
44-
debouncedModelSaveViewState(model);
27+
debouncedModelSaveChanges(model);
4528
},
4629
];
4730
}

src/util.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/** Check for null and undefined */
22

3-
import { _GlobeView as GlobeView } from "@deck.gl/core";
3+
import {
4+
_GlobeView as GlobeView,
5+
GlobeViewState,
6+
MapView,
7+
MapViewState,
8+
} from "@deck.gl/core";
49

510
import { MapRendererProps } from "./renderers";
611

@@ -17,3 +22,31 @@ export function isGlobeView(views: MapRendererProps["views"]) {
1722
const firstView = Array.isArray(views) ? views[0] : views;
1823
return firstView instanceof GlobeView;
1924
}
25+
26+
export function isMapView(views: MapRendererProps["views"]) {
27+
const firstView = Array.isArray(views) ? views[0] : views;
28+
return firstView instanceof MapView;
29+
}
30+
31+
export function sanitizeViewState(
32+
views: MapRendererProps["views"],
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
viewState: (MapViewState | GlobeViewState) & Record<string, any>,
35+
): MapViewState | GlobeViewState {
36+
const sanitized: MapViewState | GlobeViewState = {
37+
longitude: Number.isFinite(viewState.longitude) ? viewState.longitude : 0,
38+
latitude: Number.isFinite(viewState.latitude) ? viewState.latitude : 0,
39+
zoom: Number.isFinite(viewState.zoom) ? viewState.zoom : 0,
40+
...(Number.isFinite(viewState.minZoom)
41+
? {
42+
minZoom: viewState.minZoom,
43+
}
44+
: 0),
45+
...(Number.isFinite(viewState.maxZoom)
46+
? {
47+
maxZoom: viewState.maxZoom,
48+
}
49+
: 0),
50+
};
51+
return sanitized;
52+
}

0 commit comments

Comments
 (0)