Skip to content

Commit 8708f18

Browse files
committed
refac: split and renamed the validate_geometry function for tile based projects
1 parent 933663d commit 8708f18

File tree

3 files changed

+156
-101
lines changed

3 files changed

+156
-101
lines changed

mapswipe_workers/mapswipe_workers/project_types/tile_map_service/project.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mapswipe_workers.utils import tile_functions, tile_grouping_functions
1717
from mapswipe_workers.utils.validate_input import (
1818
save_geojson_to_file,
19-
validate_geometries,
19+
validate_and_collect_geometries_to_multipolyon, multipolygon_to_wkt,
2020
)
2121

2222

@@ -51,9 +51,10 @@ def __init__(self, project_draft: dict):
5151
def validate_geometries(self):
5252
# TODO rename attribute validInputGeometries, it is a path to a geojson.
5353
self.validInputGeometries = save_geojson_to_file(self.projectId, self.geometry)
54-
wkt_geometry = validate_geometries(
54+
multi_polygon = validate_and_collect_geometries_to_multipolyon(
5555
self.projectId, self.zoomLevel, self.validInputGeometries
5656
)
57+
wkt_geometry = multipolygon_to_wkt(multi_polygon)
5758
return wkt_geometry
5859

5960
def save_project_to_firebase(self, project):

mapswipe_workers/mapswipe_workers/utils/validate_input.py

Lines changed: 142 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,7 @@ def save_geojson_to_file(project_id, geometry):
2525
return output_file_path
2626

2727

28-
def validate_geometries(projectId, zoomLevel, input_file_path):
29-
driver = ogr.GetDriverByName("GeoJSON")
30-
datasource = driver.Open(input_file_path, 0)
31-
32-
try:
33-
layer = datasource.GetLayer()
34-
except AttributeError:
35-
logger.warning(
36-
f"{projectId}"
37-
f" - validate geometry - "
38-
f"Could not get layer for datasource"
39-
)
40-
raise CustomError(
41-
"Could not get layer for datasource."
42-
"Your geojson file is not correctly defined."
43-
"Check if you can open the file e.g. in QGIS. "
44-
)
45-
46-
# check if layer is empty
28+
def check_if_layer_is_empty(projectId, layer):
4729
if layer.GetFeatureCount() < 1:
4830
logger.warning(
4931
f"{projectId}"
@@ -53,91 +35,64 @@ def validate_geometries(projectId, zoomLevel, input_file_path):
5335
)
5436
raise CustomError("Empty file. ")
5537

56-
# check if more than 1 geometry is provided
57-
elif layer.GetFeatureCount() > MAX_INPUT_GEOMETRIES:
38+
39+
def check_if_layer_has_too_many_geometries(projectId, multi_polygon: ogr.Geometry):
40+
if multi_polygon.GetGeometryCount() > MAX_INPUT_GEOMETRIES:
5841
logger.warning(
5942
f"{projectId}"
6043
f" - validate geometry - "
61-
f"Input file contains more than {MAX_INPUT_GEOMETRIES} geometries. "
62-
f"Make sure to provide less than {MAX_INPUT_GEOMETRIES} geometries."
44+
f"Input file contains more than {MAX_INPUT_GEOMETRIES} individuals polygons."
45+
f"Make sure to provide less than {MAX_INPUT_GEOMETRIES} polygons."
6346
)
6447
raise CustomError(
6548
f"Input file contains more than {MAX_INPUT_GEOMETRIES} geometries. "
6649
"You can split up your project into two or more projects. "
6750
"This can reduce the number of input geometries. "
6851
)
6952

70-
project_area = 0
71-
geometry_collection = ogr.Geometry(ogr.wkbMultiPolygon)
72-
# check if the input geometry is a valid polygon
73-
for feature in layer:
7453

75-
try:
76-
feat_geom = feature.GetGeometryRef()
77-
geom_name = feat_geom.GetGeometryName()
78-
except AttributeError:
79-
logger.warning(
80-
f"{projectId}"
81-
f" - validate geometry - "
82-
f"feature geometry is not defined. "
83-
)
84-
raise CustomError(
85-
"At least one feature geometry is not defined."
86-
"Check in your input file if all geometries are defined "
87-
"and no NULL geometries exist. "
88-
)
89-
# add geometry to geometry collection
90-
if geom_name == "MULTIPOLYGON":
91-
for singlepart_polygon in feat_geom:
92-
geometry_collection.AddGeometry(singlepart_polygon)
93-
if geom_name == "POLYGON":
94-
geometry_collection.AddGeometry(feat_geom)
95-
if not feat_geom.IsValid():
96-
logger.warning(
97-
f"{projectId}"
98-
f" - validate geometry - "
99-
f"Geometry is not valid: {geom_name}. "
100-
f"Tested with IsValid() ogr method. "
101-
f"Probably self-intersections."
102-
)
103-
raise CustomError(f"Geometry is not valid: {geom_name}. ")
104-
105-
# we accept only POLYGON or MULTIPOLYGON geometries
106-
if geom_name != "POLYGON" and geom_name != "MULTIPOLYGON":
107-
logger.warning(
108-
f"{projectId}"
109-
f" - validate geometry - "
110-
f"Invalid geometry type: {geom_name}. "
111-
f'Please provide "POLYGON" or "MULTIPOLYGON"'
112-
)
113-
raise CustomError(
114-
f"Invalid geometry type: {geom_name}. "
115-
"Make sure that all features in your dataset"
116-
"are of type POLYGON or MULTIPOLYGON. "
117-
)
118-
119-
# check size of project make sure its smaller than 5,000 sqkm
120-
# for doing this we transform the geometry
121-
# into Mollweide projection (EPSG Code 54009)
122-
source = feat_geom.GetSpatialReference()
123-
target = osr.SpatialReference()
124-
target.ImportFromProj4(
125-
"+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
54+
def check_if_zoom_level_is_too_high(zoomLevel):
55+
if zoomLevel > 22:
56+
raise CustomError(f"zoom level is too large (max: 22): {zoomLevel}.")
57+
58+
59+
def check_if_geom_type_is_valid(projectId, geom_name):
60+
# we accept only POLYGON or MULTIPOLYGON geometries
61+
if geom_name != "POLYGON" and geom_name != "MULTIPOLYGON":
62+
logger.warning(
63+
f"{projectId}"
64+
f" - validate geometry - "
65+
f"Invalid geometry type: {geom_name}. "
66+
f'Please provide "POLYGON" or "MULTIPOLYGON"'
67+
)
68+
raise CustomError(
69+
f"Invalid geometry type: {geom_name}. "
70+
"Make sure that all features in your dataset"
71+
"are of type POLYGON or MULTIPOLYGON. "
12672
)
12773

128-
transform = osr.CoordinateTransformation(source, target)
129-
feat_geom.Transform(transform)
130-
project_area += feat_geom.GetArea() / 1000000
13174

132-
# max zoom level is 22
133-
if zoomLevel > 22:
134-
raise CustomError(f"zoom level is too large (max: 22): {zoomLevel}.")
75+
def check_if_geom_is_valid(projectId, feat_geom):
76+
if not feat_geom.IsValid():
77+
logger.warning(
78+
f"{projectId}"
79+
f" - validate geometry - "
80+
f"Geometry is not valid:"
81+
f"Tested with IsValid() ogr method. "
82+
f"Probably self-intersections."
83+
)
84+
85+
raise CustomError(f"Geometry is not valid. ")
86+
87+
88+
def check_if_project_area_is_too_big(projectId, project_area, zoomLevel):
89+
"""We calculate the max area based on zoom level.
90+
This is an approximation to restrict the project size
91+
in respect to the number of tasks.
92+
At zoom level 22 the max area is set to 20 square kilometers.
93+
For zoom level 18 this will result in a max area of 5,120 square kilometers.
94+
"""
13595

136-
# We calculate the max area based on zoom level.
137-
# This is an approximation to restrict the project size
138-
# in respect to the number of tasks.
139-
# At zoom level 22 the max area is set to 20 square kilometers.
140-
# For zoom level 18 this will result in an max area of 5,120 square kilometers.
14196
max_area = 5 * 4 ** (23 - zoomLevel)
14297

14398
if project_area > max_area:
@@ -153,12 +108,107 @@ def validate_geometries(projectId, zoomLevel, input_file_path):
153108
"You can split your project into smaller projects and resubmit."
154109
)
155110

111+
112+
def load_geojson_to_ogr(projectId, input_file_path):
113+
driver = ogr.GetDriverByName("GeoJSON")
114+
datasource = driver.Open(input_file_path, 0)
115+
116+
try:
117+
return datasource.GetLayer(), datasource
118+
except AttributeError:
119+
logger.warning(
120+
f"{projectId}"
121+
f" - validate geometry - "
122+
f"Could not get layer for datasource"
123+
)
124+
raise CustomError(
125+
"Could not get layer for datasource."
126+
"Your geojson file is not correctly defined."
127+
"Check if you can open the file e.g. in QGIS. "
128+
)
129+
130+
131+
def get_feature_geometry(projectId, feature):
132+
try:
133+
feat_geom = feature.GetGeometryRef()
134+
geom_name = feat_geom.GetGeometryName()
135+
except AttributeError:
136+
logger.warning(
137+
f"{projectId}"
138+
f" - validate geometry - "
139+
f"feature geometry is not defined. "
140+
)
141+
raise CustomError(
142+
"At least one feature geometry is not defined."
143+
"Check in your input file if all geometries are defined "
144+
"and no NULL geometries exist. "
145+
)
146+
return feat_geom, geom_name
147+
148+
149+
def add_geom_to_multipolygon(multi_polygon, feat_geom, geom_name):
150+
if geom_name == "MULTIPOLYGON":
151+
for singlepart_polygon in feat_geom:
152+
multi_polygon.AddGeometry(singlepart_polygon)
153+
if geom_name == "POLYGON":
154+
multi_polygon.AddGeometry(feat_geom)
155+
return multi_polygon
156+
157+
158+
def calculate_polygon_area_in_km(geometry):
159+
"""Calculate the area of a polygon in Mollweide projection (EPSG Code: 54009)."""
160+
source = geometry.GetSpatialReference()
161+
target = osr.SpatialReference()
162+
target.ImportFromProj4(
163+
"+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs"
164+
)
165+
166+
transform = osr.CoordinateTransformation(source, target)
167+
geometry.Transform(transform)
168+
return geometry.GetArea() / 1000000
169+
170+
171+
def build_multipolygon_from_layer_geometries(projectId, layer):
172+
"""
173+
Collect all geometries from input collection into one Multipolygon,
174+
additionally get the total area covered by all polygons.
175+
"""
176+
project_area = 0
177+
multi_polygon = ogr.Geometry(ogr.wkbMultiPolygon)
178+
179+
# check if the input geometry is a valid polygon
180+
for feature in layer:
181+
feat_geom, geom_name = get_feature_geometry(projectId, feature)
182+
183+
check_if_geom_type_is_valid(projectId, geom_name)
184+
check_if_geom_is_valid(projectId, feat_geom)
185+
186+
multi_polygon = add_geom_to_multipolygon(multi_polygon, feat_geom, geom_name)
187+
project_area += calculate_polygon_area_in_km(feat_geom)
188+
189+
return multi_polygon, project_area
190+
191+
192+
def validate_and_collect_geometries_to_multipolyon(projectId, zoomLevel, input_file_path):
193+
"""Validate all geometries contained in input file and collect them to a single multi polygon."""
194+
layer, datasource = load_geojson_to_ogr(projectId, input_file_path)
195+
196+
# check if inputs fit constraints
197+
check_if_layer_is_empty(projectId, layer)
198+
check_if_zoom_level_is_too_high(zoomLevel)
199+
200+
multi_polygon, project_area = build_multipolygon_from_layer_geometries(projectId, layer)
201+
202+
check_if_layer_has_too_many_geometries(projectId, multi_polygon)
203+
check_if_project_area_is_too_big(projectId, project_area, zoomLevel)
204+
156205
del datasource
157206
del layer
158207

159208
logger.info(f"{projectId}" f" - validate geometry - " f"input geometry is correct.")
209+
return multi_polygon
160210

161-
dissolved_geometry = geometry_collection.UnionCascaded()
162-
wkt_geometry_collection = dissolved_geometry.ExportToWkt()
163211

164-
return wkt_geometry_collection
212+
def multipolygon_to_wkt(multi_polygon):
213+
dissolved_geometry = multi_polygon.UnionCascaded()
214+
return dissolved_geometry.ExportToWkt()

mapswipe_workers/tests/unittests/test_utils_validate_input.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mapswipe_workers.definitions import CustomError
88
from mapswipe_workers.utils.validate_input import (
99
save_geojson_to_file,
10-
validate_geometries,
10+
validate_and_collect_geometries_to_multipolyon, multipolygon_to_wkt,
1111
)
1212

1313
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -31,7 +31,7 @@ def test_area_is_too_large(self):
3131
"fixtures/tile_map_service_grid/projects/projectDraft_area_too_large.json"
3232
)
3333
project_draft, path_to_geometries = get_project_draft(path)
34-
self.assertRaises(CustomError, validate_geometries, 1, 18, path_to_geometries)
34+
self.assertRaises(CustomError, validate_and_collect_geometries_to_multipolyon, 1, 18, path_to_geometries)
3535

3636
def test_broken_geojson_string(self):
3737
"""Test if validate_geometries throws an error
@@ -42,7 +42,7 @@ def test_broken_geojson_string(self):
4242
"fixtures/tile_map_service_grid/projects/projectDraft_broken_geojson.json"
4343
)
4444
project_draft, path_to_geometries = get_project_draft(path)
45-
self.assertRaises(CustomError, validate_geometries, 1, 18, path_to_geometries)
45+
self.assertRaises(CustomError, validate_and_collect_geometries_to_multipolyon, 1, 18, path_to_geometries)
4646

4747
def test_feature_is_none(self):
4848
"""Test if validate_geometries throws an error
@@ -52,15 +52,15 @@ def test_feature_is_none(self):
5252
"fixtures/tile_map_service_grid/projects/projectDraft_feature_is_none.json"
5353
)
5454
project_draft, path_to_geometries = get_project_draft(path)
55-
self.assertRaises(CustomError, validate_geometries, 1, 18, path_to_geometries)
55+
self.assertRaises(CustomError, validate_and_collect_geometries_to_multipolyon, 1, 18, path_to_geometries)
5656

5757
def test_no_features(self):
5858
"""Test if validate_geometries throws an error
5959
if the provided geojson contains no features."""
6060

6161
path = "fixtures/tile_map_service_grid/projects/projectDraft_no_features.json"
6262
project_draft, path_to_geometries = get_project_draft(path)
63-
self.assertRaises(CustomError, validate_geometries, 1, 18, path_to_geometries)
63+
self.assertRaises(CustomError, validate_and_collect_geometries_to_multipolyon, 1, 18, path_to_geometries)
6464

6565
def test_single_geom_validation(self):
6666
path = "fixtures/completeness/projectDraft_single.json"
@@ -87,7 +87,9 @@ def test_single_geom_validation(self):
8787
wkt_geometry_collection = dissolved_geometry.ExportToWkt()
8888

8989
# results coming from the validate_geometries function
90-
wkt = validate_geometries(1, 18, path_to_geometries)
90+
wkt = multipolygon_to_wkt(
91+
validate_and_collect_geometries_to_multipolyon(1, 18, path_to_geometries)
92+
)
9193
# Test that sequence first contains the same elements as second
9294
self.assertCountEqual(wkt, wkt_geometry_collection)
9395

@@ -121,7 +123,9 @@ def test_multiple_geom_validation(self):
121123
wkt_geometry_collection = geometry_collection.ExportToWkt()
122124

123125
# results coming from the validate_geometries function
124-
wkt = validate_geometries(1, 18, path_to_geometries)
126+
wkt = multipolygon_to_wkt(
127+
validate_and_collect_geometries_to_multipolyon(1, 18, path_to_geometries)
128+
)
125129
# Test that sequence first contains the same elements as second
126130
self.assertEqual(wkt, wkt_geometry_collection)
127131

0 commit comments

Comments
 (0)