Skip to content

Commit 6bc241a

Browse files
authored
Merge pull request #980 from mapswipe/feature/street
feat: add project for street level imagery
2 parents 2849130 + 4787dd4 commit 6bc241a

File tree

24 files changed

+1318
-3
lines changed

24 files changed

+1318
-3
lines changed

.github/workflows/actions.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ jobs:
8080
POSTGRES_DB: postgres
8181
OSMCHA_API_KEY: ${{ secrets.OSMCHA_API_KEY }}
8282
DJANGO_SECRET_KEY: test-django-secret-key
83+
MAPILLARY_API_KEY: ${{ secrets.MAPILLARY_API_KEY }}
8384
COMPOSE_FILE: ../docker-compose.yaml:../docker-compose-ci.yaml
8485
run: |
8586
docker compose run --rm mapswipe_workers_creation python -m unittest discover --verbose --start-directory tests/unittests/

django/apps/existing_database/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ class Type(models.IntegerChoices):
6666
FOOTPRINT = 2, "Validate"
6767
CHANGE_DETECTION = 3, "Compare"
6868
COMPLETENESS = 4, "Completeness"
69+
MEDIA = 5, "Media"
70+
DIGITIZATION = 6, "Digitization"
71+
STREET = 7, "Street"
6972

7073
project_id = models.CharField(primary_key=True, max_length=999)
7174
created = models.DateTimeField(blank=True, null=True)
@@ -127,7 +130,7 @@ class Task(Model):
127130
project = models.ForeignKey(Project, models.DO_NOTHING, related_name="+")
128131
group_id = models.CharField(max_length=999)
129132
task_id = models.CharField(max_length=999)
130-
geom = gis_models.MultiPolygonField(blank=True, null=True)
133+
geom = gis_models.GeometryField(blank=True, null=True)
131134
# Database uses JSON instead of JSONB (not supported by django)
132135
project_type_specifics = models.TextField(blank=True, null=True)
133136

django/schema.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ enum ProjectTypeEnum {
9797
FOOTPRINT
9898
CHANGE_DETECTION
9999
COMPLETENESS
100+
MEDIA
101+
DIGITIZATION
102+
STREET
100103
}
101104

102105
type ProjectTypeSwipeStatsType {

docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ x-mapswipe-workers: &base_mapswipe_workers
7676
SLACK_CHANNEL: '${SLACK_CHANNEL}'
7777
SENTRY_DSN: '${SENTRY_DSN}'
7878
OSMCHA_API_KEY: '${OSMCHA_API_KEY}'
79+
MAPILLARY_API_KEY: '${MAPILLARY_API_KEY}'
7980
depends_on:
8081
- postgres
8182
volumes:

example.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ COMMUNITY_DASHBOARD_GRAPHQL_ENDPOINT=https://api.example.com/graphql/
7575
COMMUNITY_DASHBOARD_SENTRY_DSN=
7676
COMMUNITY_DASHBOARD_SENTRY_TRACES_SAMPLE_RATE=
7777
COMMUNITY_DASHBOARD_MAPSWIPE_WEBSITE=https://mapswipe.org
78+
79+
# Mapillary
80+
MAPILLARY_API_KEY=

mapswipe_workers/mapswipe_workers/definitions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
OSM_API_LINK = "https://www.openstreetmap.org/api/0.6/"
1717
OSMCHA_API_LINK = "https://osmcha.org/api/v1/"
1818
OSMCHA_API_KEY = os.environ["OSMCHA_API_KEY"]
19+
MAPILLARY_API_LINK = "https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/"
20+
MAPILLARY_API_KEY = os.environ["MAPILLARY_API_KEY"]
1921

2022
# number of geometries for project geometries
2123
MAX_INPUT_GEOMETRIES = 10
@@ -134,6 +136,7 @@ class ProjectType(Enum):
134136
COMPLETENESS = 4
135137
MEDIA_CLASSIFICATION = 5
136138
DIGITIZATION = 6
139+
STREET = 7
137140

138141
@property
139142
def constructor(self):
@@ -145,6 +148,7 @@ def constructor(self):
145148
DigitizationProject,
146149
FootprintProject,
147150
MediaClassificationProject,
151+
StreetProject,
148152
)
149153

150154
project_type_classes = {
@@ -154,6 +158,7 @@ def constructor(self):
154158
4: CompletenessProject,
155159
5: MediaClassificationProject,
156160
6: DigitizationProject,
161+
7: StreetProject,
157162
}
158163
return project_type_classes[self.value]
159164

mapswipe_workers/mapswipe_workers/project_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .arbitrary_geometry.footprint.project import FootprintProject
33
from .arbitrary_geometry.footprint.tutorial import FootprintTutorial
44
from .media_classification.project import MediaClassificationProject
5+
from .street.project import StreetProject
56
from .tile_map_service.change_detection.project import ChangeDetectionProject
67
from .tile_map_service.change_detection.tutorial import ChangeDetectionTutorial
78
from .tile_map_service.classification.project import ClassificationProject
@@ -20,4 +21,5 @@
2021
"FootprintProject",
2122
"FootprintTutorial",
2223
"DigitizationProject",
24+
"StreetProject",
2325
]

mapswipe_workers/mapswipe_workers/project_types/street/__init__.py

Whitespace-only changes.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import math
2+
from dataclasses import dataclass
3+
from typing import Dict, List
4+
5+
from mapswipe_workers.definitions import logger
6+
from mapswipe_workers.firebase.firebase import Firebase
7+
from mapswipe_workers.firebase_to_postgres.transfer_results import (
8+
results_to_file,
9+
save_results_to_postgres,
10+
truncate_temp_results,
11+
)
12+
from mapswipe_workers.generate_stats.project_stats import (
13+
get_statistics_for_integer_result_project,
14+
)
15+
from mapswipe_workers.project_types.project import BaseGroup, BaseProject, BaseTask
16+
from mapswipe_workers.utils.process_mapillary import get_image_metadata
17+
from mapswipe_workers.utils.validate_input import (
18+
build_multipolygon_from_layer_geometries,
19+
check_if_layer_has_too_many_geometries,
20+
check_if_layer_is_empty,
21+
load_geojson_to_ogr,
22+
multipolygon_to_wkt,
23+
save_geojson_to_file,
24+
)
25+
26+
27+
@dataclass
28+
class StreetGroup(BaseGroup):
29+
# todo: does client use this, or only for the implementation of project creation?
30+
pass
31+
32+
33+
@dataclass
34+
class StreetTask(BaseTask):
35+
geometry: str
36+
37+
38+
class StreetProject(BaseProject):
39+
def __init__(self, project_draft):
40+
super().__init__(project_draft)
41+
self.groups: Dict[str, StreetGroup] = {}
42+
self.tasks: Dict[str, List[StreetTask]] = {}
43+
44+
self.geometry = project_draft["geometry"]
45+
46+
# TODO: validate inputs
47+
ImageMetadata = get_image_metadata(
48+
self.geometry,
49+
is_pano=project_draft.get("isPano", None),
50+
start_time=project_draft.get("startTimestamp", None),
51+
end_time=project_draft.get("endTimestamp", None),
52+
organization_id=project_draft.get("organizationId", None),
53+
sampling_threshold=project_draft.get("samplingThreshold", None),
54+
)
55+
56+
self.imageIds = ImageMetadata["ids"]
57+
self.imageGeometries = ImageMetadata["geometries"]
58+
59+
def save_tasks_to_firebase(self, projectId: str, tasks: dict):
60+
firebase = Firebase()
61+
firebase.save_tasks_to_firebase(projectId, tasks, useCompression=False)
62+
63+
@staticmethod
64+
def results_to_postgres(results: dict, project_id: str, filter_mode: bool):
65+
"""How to move the result data from firebase to postgres."""
66+
results_file, user_group_results_file = results_to_file(results, project_id)
67+
truncate_temp_results()
68+
save_results_to_postgres(results_file, project_id, filter_mode)
69+
return user_group_results_file
70+
71+
@staticmethod
72+
def get_per_project_statistics(project_id, project_info):
73+
"""How to aggregate the project results."""
74+
return get_statistics_for_integer_result_project(
75+
project_id, project_info, generate_hot_tm_geometries=False
76+
)
77+
78+
def validate_geometries(self):
79+
self.inputGeometriesFileName = save_geojson_to_file(
80+
self.projectId, self.geometry
81+
)
82+
layer, datasource = load_geojson_to_ogr(
83+
self.projectId, self.inputGeometriesFileName
84+
)
85+
86+
# check if inputs fit constraints
87+
check_if_layer_is_empty(self.projectId, layer)
88+
89+
multi_polygon, project_area = build_multipolygon_from_layer_geometries(
90+
self.projectId, layer
91+
)
92+
93+
check_if_layer_has_too_many_geometries(self.projectId, multi_polygon)
94+
95+
del datasource
96+
del layer
97+
98+
logger.info(
99+
f"{self.projectId}" f" - validate geometry - " f"input geometry is correct."
100+
)
101+
wkt_geometry = multipolygon_to_wkt(multi_polygon)
102+
return wkt_geometry
103+
104+
def create_groups(self):
105+
self.numberOfGroups = math.ceil(len(self.imageIds) / self.groupSize)
106+
for group_id in range(self.numberOfGroups):
107+
self.groups[f"g{group_id}"] = StreetGroup(
108+
projectId=self.projectId,
109+
groupId=f"g{group_id}",
110+
progress=0,
111+
finishedCount=0,
112+
requiredCount=0,
113+
numberOfTasks=self.groupSize,
114+
)
115+
116+
def create_tasks(self):
117+
if len(self.groups) == 0:
118+
raise ValueError("Groups needs to be created before tasks can be created.")
119+
for group_id, group in self.groups.items():
120+
self.tasks[group_id] = []
121+
for i in range(self.groupSize):
122+
task = StreetTask(
123+
projectId=self.projectId,
124+
groupId=group_id,
125+
geometry=self.imageGeometries.pop(),
126+
taskId=self.imageIds.pop(),
127+
)
128+
self.tasks[group_id].append(task)
129+
130+
# list now empty? if usual group size is not reached
131+
# the actual number of tasks for the group is updated
132+
if not self.imageIds:
133+
group.numberOfTasks = i + 1
134+
break
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from mapswipe_workers.project_types.tutorial import BaseTutorial
2+
3+
4+
class StreetTutorial(BaseTutorial):
5+
"""The subclass for an TMS Grid based Tutorial."""
6+
7+
def save_tutorial(self):
8+
raise NotImplementedError("Currently Street has no Tutorial")
9+
10+
def create_tutorial_groups(self):
11+
raise NotImplementedError("Currently Street has no Tutorial")
12+
13+
def create_tutorial_tasks(self):
14+
raise NotImplementedError("Currently Street has no Tutorial")

0 commit comments

Comments
 (0)