Skip to content
Open
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
84 changes: 52 additions & 32 deletions folium/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
get_obj_in_upper_tree,
image_to_url,
javascript_identifier_path_to_array_notation,
none_max,
none_min,
remove_empty,
validate_locations,
)
Expand Down Expand Up @@ -1086,39 +1084,61 @@ def render(self, **kwargs):
super().render(**kwargs)

def get_bounds(self) -> TypeBoundsReturn:
"""
Computes the bounds of the object itself (not including it's children)
in the form [[lat_min, lon_min], [lat_max, lon_max]]

"""
"""Compute bounds of the selected TopoJSON object in latitude/longitude order."""
if not self.embed:
raise ValueError("Cannot compute bounds of non-embedded TopoJSON.")

xmin, xmax, ymin, ymax = None, None, None, None

for arc in self.data["arcs"]:
x, y = 0, 0
for dx, dy in arc:
x += dx
y += dy
xmin = none_min(x, xmin)
xmax = none_max(x, xmax)
ymin = none_min(y, ymin)
ymax = none_max(y, ymax)
return [
[
self.data["transform"]["translate"][1]
+ self.data["transform"]["scale"][1] * ymin, # noqa
self.data["transform"]["translate"][0]
+ self.data["transform"]["scale"][0] * xmin, # noqa
],
[
self.data["transform"]["translate"][1]
+ self.data["transform"]["scale"][1] * ymax, # noqa
self.data["transform"]["translate"][0]
+ self.data["transform"]["scale"][0] * xmax, # noqa
],
]
transform = self.data.get("transform")
scale = transform["scale"] if transform else (1, 1)
translate = transform["translate"] if transform else (0, 0)
xmin = xmax = ymin = ymax = None

def include_point(x, y):
nonlocal xmin, xmax, ymin, ymax
x = translate[0] + scale[0] * x
y = translate[1] + scale[1] * y
xmin = x if xmin is None else min(x, xmin)
xmax = x if xmax is None else max(x, xmax)
ymin = y if ymin is None else min(y, ymin)
ymax = y if ymax is None else max(y, ymax)

def iter_arc_indices(value):
if isinstance(value, int):
yield value if value >= 0 else ~value
else:
for item in value:
yield from iter_arc_indices(item)

def visit_coordinates(coordinates):
if not coordinates:
return
if isinstance(coordinates[0], (int, float)):
include_point(*coordinates[:2])
else:
for item in coordinates:
visit_coordinates(item)

def visit_geometry(geometry):
for child in geometry.get("geometries", []):
visit_geometry(child)
for index in iter_arc_indices(geometry.get("arcs", [])):
x = y = 0
for dx, dy in self.data["arcs"][index]:
if transform:
x += dx
y += dy
else:
x, y = dx, dy
include_point(x, y)
if "coordinates" in geometry:
visit_coordinates(geometry["coordinates"])

geometry = self.data
for key in self.object_path.split("."):
geometry = geometry[key]
visit_geometry(geometry)

return [[ymin, xmin], [ymax, xmax]]


class GeoJsonDetail(MacroElement):
Expand Down
61 changes: 61 additions & 0 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,67 @@ def test_topojson_non_collection_geometry_detail(detail_type):
assert "name" in map_.get_root().render()


@pytest.fixture
def topojson_bounds_data():
return {
"type": "Topology",
"objects": {
"near": {"type": "Polygon", "arcs": [[0]]},
"far": {"type": "Polygon", "arcs": [[1]]},
},
"arcs": [
[[0, 0], [2, 0], [0, 1], [-2, -1]],
[[100, 60], [2, 0], [0, 1], [-2, -1]],
],
"transform": {"scale": [1, 1], "translate": [0, 0]},
}


def test_topojson_bounds_use_selected_arcs(topojson_bounds_data):
near = folium.TopoJson(topojson_bounds_data, "objects.near")
far = folium.TopoJson(topojson_bounds_data, "objects.far")
assert near.get_bounds() == [[0, 0], [1, 2]]
assert far.get_bounds() == [[60, 100], [61, 102]]

map_ = folium.Map()
near.add_to(map_)
assert map_.get_bounds() == [[0, 0], [1, 2]]


def test_topojson_bounds_reversed_arc(topojson_bounds_data):
topojson_bounds_data["objects"]["near"]["arcs"] = [[-2]]
layer = folium.TopoJson(topojson_bounds_data, "objects.near")
assert layer.get_bounds() == [[60, 100], [61, 102]]


def test_topojson_bounds_mixed_geometry_collection(topojson_bounds_data):
topojson_bounds_data["objects"]["near"] = {
"type": "GeometryCollection",
"geometries": [
{"type": "Point", "coordinates": [4, 5]},
{"type": "Polygon", "arcs": [[0]]},
],
}
layer = folium.TopoJson(topojson_bounds_data, "objects.near")
assert layer.get_bounds() == [[0, 0], [5, 4]]


def test_topojson_bounds_negative_scale(topojson_bounds_data):
topojson_bounds_data["transform"] = {"scale": [-2, -3], "translate": [100, 200]}
layer = folium.TopoJson(topojson_bounds_data, "objects.near")
assert layer.get_bounds() == [[197, 96], [200, 100]]


def test_topojson_bounds_absolute_arcs_without_transform():
data = {
"type": "Topology",
"objects": {"near": {"type": "LineString", "arcs": [0]}},
"arcs": [[[12.0, 7.0], [14.0, 9.0], [10.0, 8.0]]],
}
layer = folium.TopoJson(data, "objects.near")
assert layer.get_bounds() == [[7, 10], [9, 14]]


@pytest.fixture
def tmpl():
yield ("""
Expand Down