Skip to content

Commit b8c9df6

Browse files
committed
feat(street): Tasks and groups for street project
- Add Tutorial setup for street project - Add typing for the tutorial and project objects
1 parent 6a60cb8 commit b8c9df6

File tree

10 files changed

+128
-67
lines changed

10 files changed

+128
-67
lines changed

apps/project/graphql/inputs/project_types/street.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,5 @@
33
from project_types.street import project as street_project
44

55

6-
@strawberry.experimental.pydantic.input(model=street_project.ValidateObjectSourceConfig, all_fields=True)
7-
class ValidateObjectSourceConfigInput: ...
8-
9-
10-
@strawberry.experimental.pydantic.input(model=street_project.ValidateProjectProperty, all_fields=True)
11-
class ValidateProjectPropertyInput: ...
6+
@strawberry.experimental.pydantic.input(model=street_project.StreetProjectProperty, all_fields=True)
7+
class StreetProjectPropertyInput: ...
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import strawberry
2+
3+
from project_types.street import project as street_project
4+
5+
6+
@strawberry.experimental.pydantic.type(model=street_project.StreetMappilaryImageFilters, all_fields=True)
7+
class StreetMappilaryImageFilters: ...
8+
9+
10+
@strawberry.experimental.pydantic.type(model=street_project.StreetProjectProperty, all_fields=True)
11+
class StreetProjectPropertyType: ...

apps/project/graphql/types/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from apps.project.models import Organization, Project, ProjectAsset, ProjectAssetInputTypeEnum
99
from apps.tutorial.graphql.types.types import TutorialType
1010
from main.graphql.context import Info
11+
from project_types.street import project as street_project
1112
from project_types.tile_map_service.compare import project as compare_project
1213
from project_types.tile_map_service.completeness import project as completeness_project
1314
from project_types.tile_map_service.find import project as find_project
@@ -23,6 +24,7 @@
2324
from .project_types.compare import CompareProjectPropertyType
2425
from .project_types.completeness import CompletenessProjectPropertyType
2526
from .project_types.find import FindProjectPropertyType
27+
from .project_types.street import StreetProjectPropertyType
2628
from .project_types.validate import ValidateProjectPropertyType
2729
from .project_types.validate_image import ValidateImageProjectPropertyType
2830

@@ -167,6 +169,7 @@ async def project_type_specifics(
167169
| ValidateProjectPropertyType
168170
| ValidateImageProjectPropertyType
169171
| CompletenessProjectPropertyType
172+
| StreetProjectPropertyType
170173
| None
171174
):
172175
data = project.project_type_specifics
@@ -188,4 +191,9 @@ async def project_type_specifics(
188191
"CompletenessProjectPropertyType",
189192
completeness_project.CompletenessProjectProperty.model_validate(data),
190193
)
194+
if project.project_type_enum == Project.Type.STREET:
195+
return typing.cast(
196+
"StreetProjectPropertyType",
197+
street_project.StreetProjectProperty.model_validate(data),
198+
)
191199
typing.assert_never(project.project_type_enum)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import strawberry
2+
3+
from project_types.street import tutorial as street_tutorial
4+
5+
6+
@strawberry.experimental.pydantic.type(model=street_tutorial.StreetTutorialTaskProperty, all_fields=True)
7+
class StreetTutorialTaskPropertyType: ...

apps/tutorial/graphql/types/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import apps.project.graphql.types.asset_types # noqa: F401 # isort: skip # type: ignore[reportUnusedImport]
1919

20+
from project_types.street import tutorial as street_tutorial
2021
from project_types.tile_map_service.compare import tutorial as compare_tutorial
2122
from project_types.tile_map_service.completeness import tutorial as completeness_tutorial
2223
from project_types.tile_map_service.find import tutorial as find_tutorial
@@ -26,6 +27,7 @@
2627
from .project_types.compare import CompareTutorialTaskPropertyType
2728
from .project_types.completeness import CompletenessTutorialTaskPropertyType
2829
from .project_types.find import FindTutorialTaskPropertyType
30+
from .project_types.street import StreetTutorialTaskPropertyType
2931
from .project_types.validate import ValidateTutorialTaskPropertyType
3032
from .project_types.validate_image import ValidateImageTutorialTaskPropertyType
3133

@@ -62,6 +64,7 @@ async def project_type_specifics(
6264
| ValidateTutorialTaskPropertyType
6365
| ValidateImageTutorialTaskPropertyType
6466
| CompletenessTutorialTaskPropertyType
67+
| StreetTutorialTaskPropertyType
6568
| None
6669
):
6770
data = task.project_type_specifics
@@ -91,6 +94,11 @@ async def project_type_specifics(
9194
"CompletenessTutorialTaskPropertyType",
9295
completeness_tutorial.CompletenessTutorialTaskProperty.model_validate(data),
9396
)
97+
if project_type_enum == Project.Type.STREET:
98+
return typing.cast(
99+
"StreetTutorialTaskPropertyType",
100+
street_tutorial.StreetTutorialTaskProperty.model_validate(data),
101+
)
94102
typing.assert_never(project_type_enum)
95103

96104

project_types/store.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import typing
22

33
from apps.project.models import ProjectTypeEnum
4+
from project_types.street.project import StreetProject, StreetProjectProperty
5+
from project_types.street.tutorial import StreetTutorial, StreetTutorialTaskProperty
46

57
from .tile_map_service.compare.project import CompareProject, CompareProjectProperty
68
from .tile_map_service.compare.tutorial import CompareTutorial, CompareTutorialTaskProperty
@@ -28,6 +30,8 @@ def get_tutorial_task_property(project_type: ProjectTypeEnum | None):
2830
return ("validate_image", ValidateImageTutorialTaskProperty)
2931
if project_type == ProjectTypeEnum.COMPLETENESS:
3032
return ("completeness", CompletenessTutorialTaskProperty)
33+
if project_type == ProjectTypeEnum.STREET:
34+
return ("street", StreetTutorialTaskProperty)
3135
typing.assert_never(project_type)
3236

3337

@@ -45,10 +49,14 @@ def get_project_property(project_type: ProjectTypeEnum | None):
4549
return ("validate_image", ValidateImageProjectProperty)
4650
if project_type == ProjectTypeEnum.COMPLETENESS:
4751
return ("completeness", CompletenessProjectProperty)
52+
if project_type == ProjectTypeEnum.STREET:
53+
return ("street", StreetProjectProperty)
4854
typing.assert_never(project_type)
4955

5056

51-
type ProjectTypeHandlers = type[CompareProject | ValidateProject | ValidateImageProject | FindProject | CompletenessProject]
57+
type ProjectTypeHandlers = type[
58+
CompareProject | ValidateProject | ValidateImageProject | FindProject | CompletenessProject | StreetProject
59+
]
5260

5361

5462
@typing.overload
@@ -81,6 +89,12 @@ def get_project_type_handler(
8189
) -> type[CompletenessProject]: ...
8290

8391

92+
@typing.overload
93+
def get_project_type_handler(
94+
project_type: typing.Literal[ProjectTypeEnum.STREET],
95+
) -> type[StreetProject]: ...
96+
97+
8498
def get_project_type_handler(project_type: ProjectTypeEnum) -> ProjectTypeHandlers:
8599
match project_type:
86100
case ProjectTypeEnum.FIND:
@@ -93,10 +107,12 @@ def get_project_type_handler(project_type: ProjectTypeEnum) -> ProjectTypeHandle
93107
return CompletenessProject
94108
case ProjectTypeEnum.VALIDATE_IMAGE:
95109
return ValidateImageProject
110+
case ProjectTypeEnum.STREET:
111+
return StreetProject
96112

97113

98114
type TutorialTypeHandlers = type[
99-
CompareTutorial | ValidateTutorial | FindTutorial | CompletenessTutorial | ValidateImageTutorial
115+
CompareTutorial | ValidateTutorial | FindTutorial | CompletenessTutorial | ValidateImageTutorial | StreetTutorial
100116
]
101117

102118

@@ -130,6 +146,13 @@ def get_tutorial_type_handler(
130146
) -> type[ValidateImageTutorial]: ...
131147

132148

149+
# FIXME(susilnem): Handle street
150+
@typing.overload
151+
def get_tutorial_type_handler(
152+
tutorial_type: typing.Literal[ProjectTypeEnum.STREET],
153+
) -> type[typing.Any]: ...
154+
155+
133156
def get_tutorial_type_handler(tutorial_type: ProjectTypeEnum) -> TutorialTypeHandlers:
134157
match tutorial_type:
135158
case ProjectTypeEnum.FIND:
@@ -141,4 +164,6 @@ def get_tutorial_type_handler(tutorial_type: ProjectTypeEnum) -> TutorialTypeHan
141164
case ProjectTypeEnum.COMPLETENESS:
142165
return CompletenessTutorial
143166
case ProjectTypeEnum.VALIDATE_IMAGE:
144-
return ValidateImageTutorial
167+
raise Exception("Validate Image tutorial is not yet supported")
168+
case ProjectTypeEnum.STREET:
169+
raise Exception("Street tutorial is not yet supported")

project_types/street/project.py

Lines changed: 36 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
import math
34
import typing
45
from typing import Any, TypedDict
56

@@ -27,45 +28,11 @@
2728
class ValidFeature(Feature[Polygon | MultiPolygon, dict[str, Any]]): ...
2829

2930

30-
class ValidateRawGroupItem(TypedDict):
31+
class StreetRawGroupItem(TypedDict):
3132
feature_ids: list[int]
3233
features: list[ValidFeature]
3334

3435

35-
def group_input_geometries(features: list[ValidFeature], group_size: int, tutorial: bool = False):
36-
groups: dict[str, ValidateRawGroupItem] = {}
37-
38-
# we will simply start with min group id = 100
39-
group_id = 100
40-
group_id_string = f"g{group_id}"
41-
for feature_count, feature in enumerate(features):
42-
feature_id = feature_count + 1
43-
if feature_id % (group_size + 1) == 0:
44-
group_id += 1
45-
group_id_string = f"g{group_id}"
46-
47-
try:
48-
groups[group_id_string]
49-
except KeyError:
50-
new_feature_group: ValidateRawGroupItem = {"feature_ids": [], "features": []}
51-
groups[group_id_string] = new_feature_group
52-
53-
# we use a new id here based on the count
54-
# since we are not sure that GetFID returns unique values
55-
if not tutorial:
56-
groups[group_id_string]["feature_ids"].append(feature_id)
57-
# In the tutorial the feature id is defined by the "screen" attribute.
58-
# We do this so that we can sort by the feature id later and
59-
# get the screens displayed in the right order on the app.
60-
elif feature.properties is not None:
61-
groups[group_id_string]["feature_ids"].append(
62-
feature.properties["screen"],
63-
)
64-
groups[group_id_string]["features"].append(feature)
65-
66-
return groups
67-
68-
6936
class StreetMappilaryImageFilters(BaseModel):
7037
is_pano: bool | None = Field(
7138
default=None,
@@ -97,7 +64,7 @@ class StreetMappilaryImageFilters(BaseModel):
9764
)
9865

9966

100-
class StreetProperty(base_project.BaseProjectProperty):
67+
class StreetProjectProperty(base_project.BaseProjectProperty):
10168
aoi_geometry: custom_fields.PydanticId
10269
custom_options: list[CustomOption] | None = None
10370
mappilary_image_filters: StreetMappilaryImageFilters
@@ -114,14 +81,14 @@ class StreetTaskProperty(base_project.BaseProjectTaskProperty):
11481

11582
class StreetProject(
11683
base_project.BaseProject[
117-
StreetProperty,
84+
StreetProjectProperty,
11885
StreetTaskGroupProperty,
11986
StreetTaskProperty,
120-
list[ValidFeature],
121-
ValidateRawGroupItem,
87+
StreetRawGroupItem,
88+
list[tuple[int, ValidFeature]],
12289
],
12390
):
124-
project_property_class = StreetProperty
91+
project_property_class = StreetProjectProperty
12592
project_task_group_property_class = StreetTaskGroupProperty
12693
project_task_property_class = StreetTaskProperty
12794

@@ -131,7 +98,7 @@ def __init__(self, project: Project):
13198
assert project.project_type == ProjectTypeEnum.STREET, f"{type(self)} is defined for STREET"
13299

133100
@typing.override
134-
def validate(self) -> list[ValidFeature]:
101+
def validate(self) -> StreetRawGroupItem:
135102
"""Validate project before creating groups"""
136103
self.project.update_processing_status(Project.ProcessingStatus.VALIDATING_GEOMETRY, True)
137104

@@ -147,7 +114,7 @@ def validate(self) -> list[ValidFeature]:
147114

148115
mappilary_image_filters = self.project_type_specifics.mappilary_image_filters
149116

150-
image_ids, geometries = get_image_metadata(
117+
image_metadata_data = get_image_metadata(
151118
aoi_geojson=aoi_geojson,
152119
is_pano=mappilary_image_filters.is_pano,
153120
creator_id=mappilary_image_filters.creator_id,
@@ -157,19 +124,24 @@ def validate(self) -> list[ValidFeature]:
157124
randomize_order=mappilary_image_filters.randomize_order,
158125
sampling_threshold=mappilary_image_filters.sampling_threshold,
159126
)
127+
return StreetRawGroupItem(
128+
feature_ids=image_metadata_data["ids"],
129+
features=image_metadata_data["geometries"],
130+
)
160131

161132
@typing.override
162-
def create_tasks(self, group: ProjectTaskGroup, raw_group: ValidateRawGroupItem) -> int:
133+
def create_tasks(
134+
self,
135+
/,
136+
group: ProjectTaskGroup,
137+
raw_group: list[tuple[int, ValidFeature]],
138+
previous_tasks_count: int,
139+
) -> int:
163140
"""Create tasks for a group."""
164141
bulk_mgr = BulkCreateManager(chunk_size=1000)
142+
tasks_count = previous_tasks_count
165143

166-
tasks_count = 0
167-
features = raw_group["features"]
168-
f_ids = raw_group["feature_ids"]
169-
170-
for i, f_id in enumerate(f_ids):
171-
feature = features[i]
172-
144+
for f_id, feature in raw_group:
173145
if feature.geometry is not None:
174146
geom = ogr.CreateGeometryFromJson(
175147
feature.geometry.model_dump_json(),
@@ -198,13 +170,21 @@ def create_tasks(self, group: ProjectTaskGroup, raw_group: ValidateRawGroupItem)
198170
return tasks_count
199171

200172
@typing.override
201-
def create_groups(self, resp: list[ValidFeature]):
173+
def create_groups(self, resp: StreetRawGroupItem):
202174
self.project.update_processing_status(Project.ProcessingStatus.GENERATING_GROUPS_AND_TASKS, True)
203-
raw_groups = group_input_geometries(resp, self.project.group_size)
175+
number_of_groups = math.ceil(len(resp["feature_ids"]) / self.project.group_size)
176+
177+
total_tasks_accumulated: int = 0
178+
179+
for group_id in range(number_of_groups):
180+
start_index = group_id * self.project.group_size
181+
end_index = start_index + self.project.group_size
204182

205-
for group_key, raw_group in raw_groups.items():
183+
group_features = resp["features"][start_index:end_index]
184+
group_feature_ids = resp["feature_ids"][start_index:end_index]
185+
raw_group = list(zip(group_feature_ids, group_features, strict=True))
206186
new_group = ProjectTaskGroup.objects.create(
207-
firebase_id=group_key,
187+
firebase_id=f"g{group_id}",
208188
project_id=self.project.pk,
209189
number_of_tasks=0,
210190
progress=0,
@@ -214,8 +194,9 @@ def create_groups(self, resp: list[ValidFeature]):
214194
)
215195

216196
# Create new tasks for this group
217-
total_tasks = self.create_tasks(new_group, raw_group)
197+
total_tasks = self.create_tasks(new_group, raw_group, total_tasks_accumulated)
218198
logger.info("Created %s tasks for group: %s", total_tasks, new_group.pk)
199+
total_tasks_accumulated += total_tasks
219200

220201
@typing.override
221202
def post_create_groups(self):

project_types/street/tutorial.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from apps.tutorial.models import Tutorial
2+
from project_types.base import tutorial as base_tutorial
3+
from project_types.street.project import StreetProjectProperty
4+
5+
6+
class StreetTutorialTaskProperty(base_tutorial.BaseTutorialTaskProperty):
7+
identifier: int
8+
object_geometry: str
9+
10+
11+
class StreetTutorial(
12+
base_tutorial.BaseTutorial[
13+
StreetProjectProperty,
14+
StreetTutorialTaskProperty,
15+
],
16+
):
17+
project_property_class = StreetProjectProperty
18+
tutorial_task_property_class = StreetTutorialTaskProperty
19+
20+
def __init__(self, tutorial: Tutorial):
21+
super().__init__(tutorial)

project_types/tile_map_service/base/project.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,16 @@ def get_feature(task: ProjectTask):
151151
# TODO(thenav56): Calculate: total_area, time_spent_max_allowed
152152

153153
@typing.override
154-
def create_tasks(self, group: ProjectTaskGroup, raw_group: tile_grouping.RawGroup) -> int:
154+
def create_tasks(
155+
self,
156+
group: ProjectTaskGroup,
157+
raw_group: tile_grouping.RawGroup,
158+
previous_tasks_count: int = 0,
159+
) -> int:
155160
"""Create tasks for a group."""
156161
bulk_mgr = BulkCreateManager(chunk_size=1000)
157162

158-
tasks_count = 0
163+
tasks_count = previous_tasks_count
159164
for tile_x in range(raw_group["xMin"], raw_group["xMax"] + 1):
160165
for tile_y in range(raw_group["yMin"], raw_group["yMax"] + 1):
161166
geometry = tile_functions.geometry_from_tile_coords(

0 commit comments

Comments
 (0)