Skip to content
Merged
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
11 changes: 10 additions & 1 deletion lonboard/traits/_map.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse

Expand Down Expand Up @@ -92,4 +93,12 @@ def validate(self, obj: Map, value: Any) -> None | BaseViewState:
# Otherwise dict input
view = obj.view
validator = view._view_state_type if view is not None else MapViewState # noqa: SLF001
return validator(**value) # type: ignore

# The frontend currently sends back data in camelCase
snake_case_kwargs = {_camel_to_snake(k): v for k, v in value.items()}
return validator(**snake_case_kwargs) # type: ignore


def _camel_to_snake(name: str) -> str:
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
11 changes: 2 additions & 9 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useViewStateDebounced } from "./state";
import Toolbar from "./toolbar.js";
import { getTooltip } from "./tooltip/index.js";
import { Message } from "./types.js";
import { isDefined, isGlobeView } from "./util.js";
import { isDefined, isGlobeView, sanitizeViewState } from "./util.js";
import { MachineContext, MachineProvider } from "./xstate";
import * as selectors from "./xstate/selectors";

Expand Down Expand Up @@ -194,14 +194,7 @@ function App() {
onHover: onMapHoverHandler,
...(isDefined(useDevicePixels) && { useDevicePixels }),
onViewStateChange: (event) => {
const { viewState } = event;

// This condition is necessary to confirm that the viewState is
// of type MapViewState.
if (viewState && "latitude" in viewState) {
// TODO: ensure all view state types get updated on the JS side
setViewState(viewState);
}
setViewState(sanitizeViewState(views, event.viewState));
},
parameters: parameters || {},
views,
Expand Down
21 changes: 2 additions & 19 deletions src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,7 @@ import type { AnyModel } from "@anywidget/types";
import debounce from "lodash.debounce";
import * as React from "react";

const debouncedModelSaveViewState = debounce((model: AnyModel) => {
// TODO: this and below is hard-coded to the view_state model property!
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);
}

const debouncedModelSaveChanges = debounce((model: AnyModel) => {
model.save_changes();
}, 300);

Expand All @@ -41,7 +24,7 @@ export function useViewStateDebounced<T>(key: string): [T, (value: T) => void] {
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);
debouncedModelSaveChanges(model);
},
];
}
35 changes: 34 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/** Check for null and undefined */

import { _GlobeView as GlobeView } from "@deck.gl/core";
import {
_GlobeView as GlobeView,
GlobeViewState,
MapView,
MapViewState,
} from "@deck.gl/core";

import { MapRendererProps } from "./renderers";

Expand All @@ -17,3 +22,31 @@ export function isGlobeView(views: MapRendererProps["views"]) {
const firstView = Array.isArray(views) ? views[0] : views;
return firstView instanceof GlobeView;
}

export function isMapView(views: MapRendererProps["views"]) {
const firstView = Array.isArray(views) ? views[0] : views;
return firstView instanceof MapView;
}

export function sanitizeViewState(
views: MapRendererProps["views"],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
viewState: (MapViewState | GlobeViewState) & Record<string, any>,
): MapViewState | GlobeViewState {
const sanitized: MapViewState | GlobeViewState = {
longitude: Number.isFinite(viewState.longitude) ? viewState.longitude : 0,
latitude: Number.isFinite(viewState.latitude) ? viewState.latitude : 0,
zoom: Number.isFinite(viewState.zoom) ? viewState.zoom : 0,
...(Number.isFinite(viewState.minZoom)
? {
minZoom: viewState.minZoom,
}
: 0),
...(Number.isFinite(viewState.maxZoom)
? {
maxZoom: viewState.maxZoom,
}
: 0),
};
return sanitized;
}