Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TripsLayer for animating moving objects and connect to MovingPandas #292

Merged
merged 50 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c582438
bump deck version
kylebarron Dec 5, 2023
1bd1ad3
Add trips layer
kylebarron Dec 5, 2023
8075fbb
Add movingpandas import
kylebarron Dec 5, 2023
8e5a479
Merge branch 'main' into kyle/trips-layer
kylebarron Mar 25, 2024
96ac30b
update lockfile
kylebarron Mar 25, 2024
acaaddb
Restore TimestampAccessor trait
kylebarron Mar 25, 2024
daae1b4
fmt
kylebarron Mar 25, 2024
702a853
Merge branch 'main' into kyle/trips-layer
kylebarron Sep 19, 2024
159ff0b
Use isDefined on the model
kylebarron Sep 19, 2024
d3154b6
Add dev dep
kylebarron Sep 19, 2024
0fb89c4
Update trips layer
kylebarron Sep 19, 2024
72e0bdb
Unpack typing extensions
kylebarron Sep 19, 2024
c707aad
Update timestamp accessor trait
kylebarron Sep 19, 2024
7247452
fix default parameters
kylebarron Sep 19, 2024
fa65b87
fix test
kylebarron Sep 19, 2024
1ced621
Implement generic MovingPandas to GeoArrow
kylebarron Sep 22, 2024
fff32f6
Update lonboard/_geoarrow/movingpandas_interop.py
kylebarron Sep 23, 2024
0dcf3fe
lint
kylebarron Sep 23, 2024
a4bc5b9
fix timestamp type
kylebarron Sep 23, 2024
dd56ef3
Handle timezone in timestamp dtype
kylebarron Sep 23, 2024
9944c66
Merge branch 'main' into kyle/trips-layer
kylebarron Sep 25, 2024
b32939d
trait updates for timestamp accessor
kylebarron Sep 25, 2024
ca9dcd1
Manage precision reduction
kylebarron Sep 27, 2024
37a64c6
Validate list offsets
kylebarron Sep 27, 2024
6addb2e
Store min timestamp on the layer trait
kylebarron Sep 27, 2024
b346810
Add custom timestamp accessor serialization
kylebarron Sep 27, 2024
b08965a
Add trips layer to docs
kylebarron Sep 30, 2024
d2a5202
Update layer docs
kylebarron Sep 30, 2024
150c46b
Update arro3
kylebarron Sep 30, 2024
b94194a
Add ship-data example
kylebarron Sep 30, 2024
2bbcd3e
Improved docs
kylebarron Oct 1, 2024
ddb64f8
Update fiona, arro3
kylebarron Oct 1, 2024
9391062
Mapping back to real time
kylebarron Oct 1, 2024
3297403
wip
kylebarron Oct 2, 2024
6cca53a
Merge branch 'main' into kyle/trips-layer
kylebarron Oct 3, 2024
28938c8
update lockfile
kylebarron Oct 3, 2024
837bb1d
fix import
kylebarron Oct 3, 2024
49b20fc
Fix test
kylebarron Oct 3, 2024
7d262bf
Merge branch 'main' into kyle/trips-layer
kylebarron Oct 4, 2024
4b0ee7b
WIP: air traffic control example
kylebarron Oct 4, 2024
61c5923
Add from_geopandas and from_duckdb methods to TripsLayer
kylebarron Oct 7, 2024
e859ce1
Bump arro3 to 0.4.1
kylebarron Oct 7, 2024
5a026eb
Change interval to fps
kylebarron Oct 7, 2024
3e75895
Change top-level description
kylebarron Oct 7, 2024
79cc520
Update air traffic control notebook
kylebarron Oct 7, 2024
186c419
Update animate
kylebarron Oct 7, 2024
c790753
print tz info
kylebarron Oct 7, 2024
b64b330
Updated ship data example
kylebarron Oct 7, 2024
7f59af9
Add gif to atc notebook
kylebarron Oct 7, 2024
5247af0
Add examples to docs
kylebarron Oct 7, 2024
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
2 changes: 1 addition & 1 deletion lonboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Python library for fast, interactive geospatial vector data visualization in Jupyter.
"""

from . import colormap, controls, layer_extension, traits
from . import colormap, controls, experimental, layer_extension, traits
from ._layer import (
BaseArrowLayer,
BaseLayer,
Expand Down
2 changes: 1 addition & 1 deletion lonboard/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
unexpected behavior when using them.
"""

from ._layer import ArcLayer, TextLayer
from ._layer import ArcLayer, TextLayer, TripsLayer
241 changes: 241 additions & 0 deletions lonboard/experimental/_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,45 @@

from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Optional

import numpy as np
import traitlets
from arro3.core import (
Array,
ChunkedArray,
Field,
Table,
fixed_size_list_array,
list_array,
)
from arro3.core.types import ArrowStreamExportable

from lonboard._constants import EXTENSION_NAME
from lonboard._layer import BaseArrowLayer
from lonboard.experimental.traits import TimestampAccessor
from lonboard.traits import (
ArrowTableTrait,
ColorAccessor,
FloatAccessor,
PointAccessor,
TextAccessor,
)
from lonboard.types.layer import TripsLayerKwargs

if TYPE_CHECKING:
from movingpandas import TrajectoryCollection

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self

if sys.version_info >= (3, 12):
from typing import Unpack
else:
from typing_extensions import Unpack


class ArcLayer(BaseArrowLayer):
Expand Down Expand Up @@ -329,3 +357,216 @@ class TextLayer(BaseArrowLayer):
default [0, 0]
"""
# ?: Accessor<DataT, [number, number]>;


class TripsLayer(BaseArrowLayer):
""" """

_layer_type = traitlets.Unicode("trip").tag(sync=True)

table = ArrowTableTrait(
allowed_geometry_types={
EXTENSION_NAME.LINESTRING,
}
)

width_units = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
"""
The units of the line width, one of `'meters'`, `'common'`, and `'pixels'`. See
[unit
system](https://deck.gl/docs/developer-guide/coordinate-systems#supported-units).

- Type: `str`, optional
- Default: `'meters'`
"""

width_scale = traitlets.Float(default_value=None, allow_none=True, min=0).tag(
sync=True
)
"""
The path width multiplier that multiplied to all paths.

- Type: `float`, optional
- Default: `1`
"""

width_min_pixels = traitlets.Float(default_value=None, allow_none=True, min=0).tag(
sync=True
)
"""
The minimum path width in pixels. This prop can be used to prevent the path from
getting too thin when zoomed out.

- Type: `float`, optional
- Default: `0`
"""

width_max_pixels = traitlets.Float(default_value=None, allow_none=True, min=0).tag(
sync=True
)
"""
The maximum path width in pixels. This prop can be used to prevent the path from
getting too thick when zoomed in.

- Type: `float`, optional
- Default: `None`
"""

joint_rounded = traitlets.Bool(default_value=None, allow_none=True).tag(sync=True)
"""
Type of joint. If `True`, draw round joints. Otherwise draw miter joints.

- Type: `bool`, optional
- Default: `False`
"""

cap_rounded = traitlets.Bool(default_value=None, allow_none=True).tag(sync=True)
"""
Type of caps. If `True`, draw round caps. Otherwise draw square caps.

- Type: `bool`, optional
- Default: `False`
"""

miter_limit = traitlets.Int(default_value=None, allow_none=True).tag(sync=True)
"""
The maximum extent of a joint in ratio to the stroke width.
Only works if `jointRounded` is `False`.

- Type: `float`, optional
- Default: `4`
"""

billboard = traitlets.Bool(default_value=None, allow_none=True).tag(sync=True)
"""
If `True`, extrude the path in screen space (width always faces the camera).
If `False`, the width always faces up.

- Type: `bool`, optional
- Default: `False`
"""

fade_trail = traitlets.Bool(default_value=None, allow_none=True).tag(sync=True)
"""Whether or not the path fades out.

- Type: `bool`, optional
- Default: `True`
"""

trail_length = traitlets.Float(default_value=None, allow_none=True).tag(sync=True)
"""Trail length.

- Type: `float`, optional
- Default: `120`
"""

current_time = traitlets.Float(0).tag(sync=True)
"""The current time of the frame.

- Type: `float`, optional
- Default: `0`
"""

get_color = ColorAccessor(None, allow_none=True)
"""
The color of each path in the format of `[r, g, b, [a]]`. Each channel is a number
between 0-255 and `a` is 255 if not supplied.

- Type: [ColorAccessor][lonboard.traits.ColorAccessor], optional
- If a single `list` or `tuple` is provided, it is used as the color for all
paths.
- If a numpy or pyarrow array is provided, each value in the array will be used
as the color for the path at the same row index.
- Default: `[0, 0, 0, 255]`.
"""

get_width = FloatAccessor(None, allow_none=True)
"""
The width of each path, in units specified by `width_units` (default `'meters'`).

- Type: [FloatAccessor][lonboard.traits.FloatAccessor], optional
- If a number is provided, it is used as the width for all paths.
- If an array is provided, each value in the array will be used as the width for
the path at the same row index.
- Default: `1`.
"""

get_timestamps = TimestampAccessor(None, allow_none=True)
"""
The timestamp of each coordinate.

- Type: [TimestampAccessor][lonboard.traits.TimestampAccessor]
"""

def __init__(
self,
*,
table: ArrowStreamExportable,
get_timestamps: ArrowStreamExportable,
_rows_per_chunk: Optional[int] = None,
**kwargs: Unpack[TripsLayerKwargs],
):
super().__init__(
table=table,
_rows_per_chunk=_rows_per_chunk,
get_timestamps=get_timestamps, # type: ignore
**kwargs,
)

@classmethod
def from_movingpandas(
cls,
traj_collection: TrajectoryCollection,
**kwargs: Unpack[TripsLayerKwargs],
) -> Self:
import shapely

num_coords = 0
num_rows = len(traj_collection)
offsets = np.zeros(num_rows + 1, dtype=np.int32)

for i, traj in enumerate(traj_collection.trajectories):
num_coords += traj.size()
offsets[i + 1] = num_coords

coords = np.zeros((num_coords, 2), dtype=np.float64)
timestamps = np.zeros(num_coords, dtype=np.int64)

for i, traj in enumerate(traj_collection.trajectories):
start_offset = offsets[i]
end_offset = offsets[i + 1]

# millisecond-based timestamps
int64_ms_timestamps = traj.df.index.to_series().astype(np.int64) // (
kylebarron marked this conversation as resolved.
Show resolved Hide resolved
1000**2
)
timestamps[start_offset:end_offset] = int64_ms_timestamps

coords[start_offset:end_offset, 0] = shapely.get_x(traj.df.geometry).values
coords[start_offset:end_offset, 1] = shapely.get_y(traj.df.geometry).values

# offset by earliest timestamp
timestamps -= timestamps.min()

# Cast to float32
timestamps = timestamps.astype(np.float32)

coords_arr = Array.from_numpy(coords.ravel("C"))
coords_fixed_size_list = fixed_size_list_array(coords_arr, 2)
linestrings_arr = list_array(Array.from_numpy(offsets), coords_fixed_size_list)
timestamp_arr = list_array(
Array.from_numpy(offsets), Array.from_numpy(timestamps)
)
timestamp_col = ChunkedArray([timestamp_arr])

linestrings_field = Field(
"geometry",
linestrings_arr.type,
nullable=True,
metadata={"ARROW:extension:name": "geoarrow.linestring"},
)

# TODO: don't add timestamps onto table
table = Table.from_pydict({"timestamps": timestamp_col})
table = table.append_column(linestrings_field, ChunkedArray([linestrings_arr]))
return cls(table=table, get_timestamps=timestamp_col, **kwargs)
48 changes: 48 additions & 0 deletions lonboard/experimental/traits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import Any, Union

from arro3.core import Array, ChunkedArray, DataType
from traitlets.traitlets import TraitType

from lonboard._serialization import ACCESSOR_SERIALIZATION
from lonboard.traits import FixedErrorTraitType


class TimestampAccessor(FixedErrorTraitType):
"""A representation of a deck.gl coordinate-timestamp accessor.

- A pyarrow [`ListArray`][pyarrow.ListArray] containing either a numeric array. Each
value in the array will be used as the value for the object at the same row
index.
- Any Arrow list array from a library that implements the [Arrow PyCapsule
Interface](https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html).
"""

default_value = None
info_text = "a Arrow ListArray representing a nested array of timestamps"

def __init__(
self: TraitType,
*args,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self.tag(sync=True, **ACCESSOR_SERIALIZATION)

def validate(self, obj, value) -> Union[Array, ChunkedArray]:
# Check for Arrow PyCapsule Interface
if hasattr(value, "__arrow_c_array__"):
value = Array.from_arrow(value)

elif hasattr(value, "__arrow_c_stream__"):
value = ChunkedArray.from_arrow(value)

if isinstance(value, (ChunkedArray, Array)):
if not DataType.is_list(value.type):
self.error(obj, value, info="timestamp array to be a list-type array")

return value

self.error(obj, value)
assert False
Loading