Skip to content
Draft
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
9 changes: 9 additions & 0 deletions project_types/tile_map_service/base/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ class TileMapServiceBaseProject[
],
ABC,
):
# Each group's tile width/height is forced to a multiple of these values.
# Defaults match the historical 3-tile-tall x even-width "6 tasks per screen"
# layout used by find/completeness. Override on subclasses (e.g. compare)
# that want one tile per task.
min_tile_x_multiplier: int = 2
min_tile_y_multiplier: int = 3
Comment on lines +76 to +77

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frozenhelium if you have these numbers here, we can also re-use this number to calculate the no. of swipes.

Currently, find and completeness should have one swipe per 6 results.


def __init__(self, project: Project):
super().__init__(project)

Expand Down Expand Up @@ -177,6 +184,8 @@ def create_groups(self, resp: tile_grouping.AoiGeometry):
resp,
self.project_type_specifics.zoom_level,
self.project.group_size,
min_tile_x_multiplier=self.min_tile_x_multiplier,
min_tile_y_multiplier=self.min_tile_y_multiplier,
)

for group_key, raw_group in raw_groups.items():
Expand Down
4 changes: 4 additions & 0 deletions project_types/tile_map_service/compare/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class CompareProject(
project_task_group_property_class = CompareProjectTaskGroupProperty
project_task_property_class = CompareProjectTaskProperty

# Compare uses one tile per task, so groups are 1x1 in tile units.
min_tile_x_multiplier = 1
min_tile_y_multiplier = 1

def __init__(self, project: Project):
super().__init__(project)
if typing.TYPE_CHECKING:
Expand Down
32 changes: 32 additions & 0 deletions utils/geo/tests/tile_grouping_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_project_geometries_intersection(self):
assert len(groups_with_overlaps) == 92

def test_group_size(self):
"""Default 2x3 layout used by find/completeness projects."""
zoom = 18
project_extent_file = Path(Config.BASE_DIR, "assets/fixtures/aoi.geojson")

Expand All @@ -42,3 +43,34 @@ def test_group_size(self):
# check if group x size is of factor 2
x_group_size = int(group["xMax"]) - int(group["xMin"]) + 1
assert x_group_size % 2 == 0

def test_group_size_1x1(self):
"""1x1 layout used by compare projects (one tile per task).

group_size = 100 ≈ 100 tasks per group. Group widths may exceed 100
when the overlap-merge step joins adjacent groups; height stays 1.
"""
zoom = 18
project_extent_file = Path(Config.BASE_DIR, "assets/fixtures/aoi.geojson")

project_extent_file_json = get_geometry_from_file(str(project_extent_file))

groups_dict = extent_to_groups(
project_extent_file_json,
zoom,
group_size=100,
min_tile_x_multiplier=1,
min_tile_y_multiplier=1,
)

assert len(groups_dict) == 2130

total_tiles = sum(
(int(g["yMax"]) - int(g["yMin"]) + 1) * (int(g["xMax"]) - int(g["xMin"]) + 1) for g in groups_dict.values()
)
assert total_tiles == 167372

for _, group in groups_dict.items():
# 1-tile-tall stripes
y_group_size = int(group["yMax"]) - int(group["yMin"]) + 1
assert y_group_size == 1
88 changes: 63 additions & 25 deletions utils/geo/tile_grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ def _get_horizontal_slice(
extent: _GeoExtent,
polygons: list[Polygon],
zoom: int,
min_tile_y_multiplier: int,
) -> _HorizontalSliceInfo:
"""The function slices all input geometries vertically
using a height of max 3 tiles per geometry.
using a height of `min_tile_y_multiplier` tiles per geometry.
The function iterates over all input geometries.
For each geometry tile coordinates are calculated.
Then this geometry is split into several geometries using the min
Expand Down Expand Up @@ -98,7 +99,7 @@ def _get_horizontal_slice(
TileY = TileY_top

# get rows
rows = math.ceil(TileHeight / 3)
rows = math.ceil(TileHeight / min_tile_y_multiplier)

############################################################

Expand All @@ -109,7 +110,7 @@ def _get_horizontal_slice(
lon_left, lat_top = tile_functions.pixel_coords_zoom_to_lat_lon(PixelX, PixelY, zoom)

PixelX = TileX_right * 256
PixelY = (TileY + 3) * 256
PixelY = (TileY + min_tile_y_multiplier) * 256
lon_right, lat_bottom = tile_functions.pixel_coords_zoom_to_lat_lon(PixelX, PixelY, zoom)

# Create Geometry
Expand All @@ -128,15 +129,15 @@ def _get_horizontal_slice(
if sliced_poly:
if sliced_poly.geom_type.upper() == "POLYGON":
tile_y_top.append(TileY)
tile_y_bottom.append(TileY + 3)
tile_y_bottom.append(TileY + min_tile_y_multiplier)
slice_collection.append(sliced_poly)
elif sliced_poly.geom_type.upper() == "MULTIPOLYGON":
for geom_part in sliced_poly:
tile_y_top.append(TileY)
tile_y_bottom.append(TileY + 3)
tile_y_bottom.append(TileY + min_tile_y_multiplier)
slice_collection.append(geom_part)

TileY = TileY + 3
TileY = TileY + min_tile_y_multiplier

return _HorizontalSliceInfo(
tile_y_top=tile_y_top,
Expand All @@ -149,11 +150,12 @@ def _get_vertical_slice( # noqa: D417
slice_infos: _HorizontalSliceInfo,
zoom: int,
width_threshold: int = 40,
min_tile_x_multiplier: int = 2,
) -> dict[str, RawGroup]:
"""Slices the horizontal stripes vertically.
Each input stripe has a height of three tiles
and will be split into vertical parts.
The width of each part is defined by the width threshold set below.
Each input stripe will be split into vertical parts.
The width of each part is defined by the width threshold set below
and rounded up to a multiple of `min_tile_x_multiplier`.

Parameters
----------
Expand All @@ -163,6 +165,10 @@ def _get_vertical_slice( # noqa: D417
width_threshold: int
the number of vertical tiles for a group,
this defines how "long" groups are.
min_tile_x_multiplier: int
each group's tile width is forced to a multiple of this value
(default 2 keeps the historical "always 6 tasks per screen" layout
for find/completeness; set to 1 for one-tile-wide groups).

Returns
-------
Expand Down Expand Up @@ -224,17 +230,19 @@ def _get_vertical_slice( # noqa: D417
# and do equally for all slices
step_size = math.ceil(TileWidth / cols)

# the step_size should be always and even number
# this will make sure that there will be always 6 tasks per screen
if step_size % 2 == 1:
step_size += 1
# the step_size should always be a multiple of min_tile_x_multiplier
# (default 2 keeps the historical "always 6 tasks per screen" layout)
remainder = step_size % min_tile_x_multiplier
if remainder != 0:
step_size += min_tile_x_multiplier - remainder

for i in range(cols):
# we need to make sure that geometries are not clipped at the edge
if i == (cols - 1):
step_size = TileX_right - TileX + 1
if step_size % 2 == 1:
step_size += 1
remainder = step_size % min_tile_x_multiplier
if remainder != 0:
step_size += min_tile_x_multiplier - remainder

# Calculate lat, lon of upper left corner of tile
PixelX = TileX * 256
Expand Down Expand Up @@ -292,7 +300,12 @@ def _groups_intersect(group_a: RawGroup, group_b: RawGroup) -> bool:
return (x_min <= x_maxB) and (x_minB <= x_max) and (y_min <= y_maxB) and (y_minB <= y_max)


def _merge_groups(group_a: RawGroup, group_b: RawGroup, zoom: int) -> RawGroup:
def _merge_groups(
group_a: RawGroup,
group_b: RawGroup,
zoom: int,
min_tile_x_multiplier: int,
) -> RawGroup:
"""Merge two overlapping groups into a single group.

This can result in groups that are "longer" than
Expand All @@ -312,9 +325,10 @@ def _merge_groups(group_a: RawGroup, group_b: RawGroup, zoom: int) -> RawGroup:
new_x_min = min([x_min, x_minB])
new_x_max = max([x_max, x_maxB])

# check if group_x_size is even and adjust new_x_max
if (new_x_max - new_x_min + 1) % 2 == 1:
new_x_max += 1
# ensure resulting tile width is a multiple of min_tile_x_multiplier
width_remainder = (new_x_max - new_x_min + 1) % min_tile_x_multiplier
if width_remainder != 0:
new_x_max += min_tile_x_multiplier - width_remainder

# Calculate lat, lon of upper left corner of tile
PixelX = int(new_x_min) * 256
Expand Down Expand Up @@ -352,6 +366,7 @@ def _merge_groups(group_a: RawGroup, group_b: RawGroup, zoom: int) -> RawGroup:
def _adjust_overlapping_groups(
groups: dict[str, RawGroup],
zoom: int,
min_tile_x_multiplier: int,
) -> tuple[dict[str, RawGroup], int]:
"""Loop through groups dict and merge overlapping groups."""
groups_without_overlap: dict[str, RawGroup] = {}
Expand All @@ -370,7 +385,12 @@ def _adjust_overlapping_groups(

if _groups_intersect(groups[group_id], groups[group_id_b]):
overlap_count += 1
new_group = _merge_groups(groups[group_id], groups[group_id_b], zoom)
new_group = _merge_groups(
groups[group_id],
groups[group_id_b],
zoom,
min_tile_x_multiplier,
)
del groups[group_id_b]
groups_without_overlap[group_id] = new_group

Expand All @@ -384,7 +404,14 @@ def _adjust_overlapping_groups(
return groups_without_overlap, overlaps_total


def extent_to_groups(aoi_geometry: AoiGeometry, zoom: int, group_size: int) -> dict[str, RawGroup]: # noqa: D417
def extent_to_groups( # noqa: D417
aoi_geometry: AoiGeometry,
zoom: int,
group_size: int,
*,
min_tile_x_multiplier: int = 2,
min_tile_y_multiplier: int = 3,
Comment on lines +412 to +413

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not add defaults here. We already have default value in in the project classes

) -> dict[str, RawGroup]:
"""The function to polygon geometries of a given input file
into horizontal slices and then vertical slices.

Expand All @@ -395,6 +422,13 @@ def extent_to_groups(aoi_geometry: AoiGeometry, zoom: int, group_size: int) -> d
or .geojson file containing the input geometries
zoom : int
the tile map service zoom level
min_tile_x_multiplier : int
each group's tile width is forced to a multiple of this value.
Defaults to 2 (the historical "always 6 tasks per screen" layout
for find/completeness); set to 1 for one-tile-wide groups (compare).
min_tile_y_multiplier : int
each group's tile height (the horizontal stripe height).
Defaults to 3 (find/completeness); set to 1 for one-tile-tall groups.

Returns
-------
Expand All @@ -408,13 +442,13 @@ def extent_to_groups(aoi_geometry: AoiGeometry, zoom: int, group_size: int) -> d
polygons = aoi_geometry["polygons"]

# get horizontal slices --> rows
horizontal_slice_infos = _get_horizontal_slice(extent, polygons, zoom)
horizontal_slice_infos = _get_horizontal_slice(extent, polygons, zoom, min_tile_y_multiplier)

# then get vertical slices --> columns
raw_groups_dict = _get_vertical_slice(horizontal_slice_infos, zoom, group_size)
raw_groups_dict = _get_vertical_slice(horizontal_slice_infos, zoom, group_size, min_tile_x_multiplier)

# finally remove overlapping groups
groups_dict, overlaps_total = _adjust_overlapping_groups(raw_groups_dict, zoom)
groups_dict, overlaps_total = _adjust_overlapping_groups(raw_groups_dict, zoom, min_tile_x_multiplier)

# check if there are still overlaps
c = 0
Expand All @@ -424,6 +458,10 @@ def extent_to_groups(aoi_geometry: AoiGeometry, zoom: int, group_size: int) -> d
if c == 5:
break

groups_dict, overlaps_total = _adjust_overlapping_groups(groups_dict.copy(), zoom)
groups_dict, overlaps_total = _adjust_overlapping_groups(
groups_dict.copy(),
zoom,
min_tile_x_multiplier,
)

return groups_dict
Loading