Linked maps
Linked Maps¶
This notebook demonstrates how you can link two different Lonboard maps using the ipywidgets.observe
method, so panning/zooming one map will automatically pan/zoom the other map.
Linked maps can be useful in a variety of situations:
- Before/After maps, where one map shows data before something happened and the other after the event
- To showcase results of different processing methodologies
- To simply present multiple maps with different data that doesn't easily fit on one map
from functools import partial
from typing import List
import ipywidgets as widgets
import traitlets
import lonboard
from lonboard import Map
from lonboard.basemap import CartoBasemap
from lonboard.models import ViewState
Create the maps¶
Because layers don't matter for this example, we are going to create two maps without any layers, one map using the Positron basemap, and another using the Dark Matter basemap.
To start, the view state on the Positron map to be focused on the Gateway Arch in St. Louis Missouri, and the Dark Matter map will be centered on the Statue of Liberty in New York City, New York.
We'll present the two maps side by side in an ipywidgets HBox to keep them tidy. Setting the layout of the maps to "flex='1'" will allow the maps to display inside the HBox.
## Create postitron map focused on the arch
positron_map = Map(
layers=[],
basemap_style=CartoBasemap.Positron,
view_state={
"longitude": -90.1849,
"latitude": 38.6245,
"zoom": 16,
"pitch": 0,
"bearing": 0,
},
layout=widgets.Layout(flex="1"),
)
## Create postitron map focused on the lady liberty
darkmatter_map = Map(
layers=[],
basemap_style=CartoBasemap.DarkMatter,
view_state={
"longitude": -74.04454,
"latitude": 40.6892,
"zoom": 16,
"pitch": 0,
"bearing": 0,
},
layout=widgets.Layout(flex="1"),
)
maps_box = widgets.HBox([positron_map, darkmatter_map])
maps_box
Linking the Maps (the easy way to understand)¶
If you haven't yet run the cells below, you'll see that you can pan/zoom the two maps independent of one another. Panning/zooming one map will not affect the other map. After we run the code below though, the two maps will synchronize with each other, when we pan/zoom one map, the other map will automatically match the map that was modified.
To achieve the view state synchronization, we'll write two simple callback function for each of the maps. The functions will receive events from the interaction with the maps, and if the interaction with the map changed the view_state, we'll set the view_state on the other map to match the view_state of the the map that we interacted with.
def sync_positron_to_darkmatter(event: traitlets.utils.bunch.Bunch) -> None:
if isinstance(event.get("new"), ViewState):
darkmatter_map.view_state = positron_map.view_state
positron_map.observe(sync_positron_to_darkmatter)
def sync_darkmatter_to_positron(event: traitlets.utils.bunch.Bunch) -> None:
if isinstance(event.get("new"), ViewState):
positron_map.view_state = darkmatter_map.view_state
darkmatter_map.observe(sync_darkmatter_to_positron)
Linking the Maps (the more elegant/robust way)¶
In the block above we are typing a lot of code, and the two functions are basically the same, just with hard coded maps to target in the functions, and we're explicitly calling the originating map's view_state
even though the event["new"]
actually is the view state. Additionally if we had a lot of maps to sync, this would get out of hand quickly. None of that is idea, but it makes the concept easy to understand. Below is a better way to sync the maps, albeit a bit more abstract.
Luckily functools.partial
can help us out. Instead of writing a function per map, we can write one function that take the same events from the widget, but also another parameter which is a list of Lonboard maps. Then when we register the callback function with the map's observe()
method, we pass partial as the function and tell partial to use the link_maps
function and provide the list of the other maps to sync with this map. This way we have one function that we wrote which can be used to sync any map with any number of other maps.
def link_maps(event: traitlets.utils.bunch.Bunch, other_maps: List[Map] = []):
if isinstance(event.get("new"), ViewState):
for lonboard_map in other_maps:
lonboard_map.view_state = event["new"]
positron_map.observe(partial(link_maps, other_maps=[darkmatter_map]))
darkmatter_map.observe(partial(link_maps, other_maps=[positron_map]))