Skip to content

Commit d870982

Browse files
authored
Merge pull request #348 from mapswipe/dev
Dev
2 parents 179892c + cd4075f commit d870982

File tree

9 files changed

+647
-390
lines changed

9 files changed

+647
-390
lines changed

mapswipe_workers/mapswipe_workers/base/base_project.py

Lines changed: 272 additions & 239 deletions
Large diffs are not rendered by default.

mapswipe_workers/mapswipe_workers/definitions.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import logging
21
import logging.config
32
import os
4-
53
import sentry_sdk
64
from xdg import XDG_DATA_HOME
7-
85
from mapswipe_workers.config import SENTRY_DSN
6+
from enum import Enum
97

108

119
class CustomError(Exception):
1210
pass
1311

1412

13+
class MessageType(Enum):
14+
SUCCESS = 1
15+
FAIL = 2
16+
NOTIFICATION_90 = 3
17+
NOTIFICATION_100 = 4
18+
19+
1520
DATA_PATH = os.path.join(XDG_DATA_HOME, "mapswipe_workers")
1621
if not os.path.exists(DATA_PATH):
1722
os.makedirs(DATA_PATH)

mapswipe_workers/mapswipe_workers/mapswipe_workers.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import click
1010
from mapswipe_workers import auth
11-
from mapswipe_workers.definitions import CustomError, logger, sentry
11+
from mapswipe_workers.definitions import CustomError, logger, sentry, MessageType
1212
from mapswipe_workers.firebase_to_postgres import (
1313
archive_project,
1414
delete_project,
@@ -27,7 +27,10 @@
2727
from mapswipe_workers.project_types.footprint.footprint_project import FootprintProject
2828
from mapswipe_workers.utils import user_management
2929
from mapswipe_workers.utils.create_directories import create_directories
30-
from mapswipe_workers.utils.slack_helper import send_slack_message
30+
from mapswipe_workers.utils.slack_helper import (
31+
send_slack_message,
32+
send_progress_notification,
33+
)
3134

3235

3336
class PythonLiteralOption(click.Option):
@@ -86,12 +89,12 @@ def run_create_projects():
8689
project.calc_required_results()
8790
# Save project and its groups and tasks to Firebase and Postgres.
8891
project.save_project()
89-
send_slack_message("success", project_name, project.projectId)
92+
send_slack_message(MessageType.SUCCESS, project_name, project.projectId)
9093
logger.info("Success: Project Creation ({0})".format(project_name))
9194
except CustomError:
9295
ref = fb_db.reference(f"v2/projectDrafts/{project_draft_id}")
9396
ref.set({})
94-
send_slack_message("fail", project_name, project.projectId)
97+
send_slack_message(MessageType.FAIL, project_name, project.projectId)
9598
logger.exception("Failed: Project Creation ({0}))".format(project_name))
9699
sentry.capture_exception()
97100
continue
@@ -103,6 +106,8 @@ def run_firebase_to_postgres() -> list:
103106
update_data.update_user_data()
104107
update_data.update_project_data()
105108
project_ids = transfer_results.transfer_results()
109+
for project_id in project_ids:
110+
send_progress_notification(project_id)
106111
return project_ids
107112

108113

mapswipe_workers/mapswipe_workers/utils/geojson_functions.py

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def csv_to_geojson(filename: str, geometry_field: str = "geom"):
2727
outfile,
2828
filename,
2929
"-sql",
30-
f'SELECT *, CAST({geometry_field} as geometry) FROM "{filename_without_path}"',
30+
f'SELECT *, CAST({geometry_field} as geometry) FROM "{filename_without_path}"', # noqa E501
3131
],
3232
check=True,
3333
)
@@ -101,28 +101,60 @@ def add_metadata_to_geojson(filename: str, geometry_field: str = "geom"):
101101
logger.info(f"added metadata to {filename}.")
102102

103103

104-
def create_group_geom(group_data):
104+
def create_group_geom(group_data, shape="bounding_box"):
105+
"""Create bounding box or convex hull of input task geometries."""
106+
105107
result_geom_collection = ogr.Geometry(ogr.wkbMultiPolygon)
106108
for result, data in group_data.items():
107109
result_geom = ogr.CreateGeometryFromWkt(data["wkt"])
108110
result_geom_collection.AddGeometry(result_geom)
109111

110-
group_geom = result_geom_collection.ConvexHull()
112+
if shape == "convex_hull":
113+
group_geom = result_geom_collection.ConvexHull()
114+
elif shape == "bounding_box":
115+
# Get Envelope
116+
lon_left, lon_right, lat_top, lat_bottom = result_geom_collection.GetEnvelope()
117+
118+
# Create Geometry
119+
ring = ogr.Geometry(ogr.wkbLinearRing)
120+
ring.AddPoint(lon_left, lat_top)
121+
ring.AddPoint(lon_right, lat_top)
122+
ring.AddPoint(lon_right, lat_bottom)
123+
ring.AddPoint(lon_left, lat_bottom)
124+
ring.AddPoint(lon_left, lat_top)
125+
# TODO: Make sure to return 2D geom, currently 3D with z = 0.0
126+
group_geom = ogr.Geometry(ogr.wkbPolygon)
127+
group_geom.AddGeometry(ring)
128+
111129
return group_geom
112130

113131

114132
def create_geojson_file_from_dict(final_groups_dict, outfile):
115-
# TODO: adapt input name
133+
"""Take output from generate stats and create TM geometries.
134+
135+
In order to create a GeoJSON file with a coordinate precision of 7
136+
we take a small detour.
137+
First, we create a GeoJSONSeq file.
138+
This contains only the features.
139+
Then we add these features to the final GeoJSON file.
140+
The current shape of the output geometries is set to 'bounding_box'.
141+
"""
116142

117-
driver = ogr.GetDriverByName("GeoJSON")
143+
driver = ogr.GetDriverByName("GeoJSONSeq")
118144
# define spatial Reference
119145
srs = osr.SpatialReference()
120146
srs.ImportFromEPSG(4326)
147+
outfile_temp = outfile.replace(".geojson", "_temp.geojson")
148+
149+
if os.path.exists(outfile_temp):
150+
driver.DeleteDataSource(outfile_temp)
151+
121152
if os.path.exists(outfile):
122153
driver.DeleteDataSource(outfile)
123-
dataSource = driver.CreateDataSource(outfile)
154+
155+
dataSource = driver.CreateDataSource(outfile_temp)
124156
# create layer
125-
layer = dataSource.CreateLayer(outfile, srs, geom_type=ogr.wkbPolygon)
157+
layer = dataSource.CreateLayer(outfile_temp, srs, geom_type=ogr.wkbPolygon,)
126158

127159
# create fields
128160
field_id = ogr.FieldDefn("group_id", ogr.OFTInteger)
@@ -133,7 +165,8 @@ def create_geojson_file_from_dict(final_groups_dict, outfile):
133165
else:
134166
for group_id in final_groups_dict.keys():
135167
group_data = final_groups_dict[group_id]
136-
group_geom = create_group_geom(group_data)
168+
# create the final group geometry
169+
group_geom = create_group_geom(group_data, "bounding_box")
137170
final_groups_dict[group_id]["group_geom"] = group_geom
138171
# init feature
139172

@@ -163,21 +196,54 @@ def create_geojson_file_from_dict(final_groups_dict, outfile):
163196
print(group_geom)
164197
continue
165198

199+
# make sure to close layer and data source
166200
layer = None
167-
logger.info("created outfile: %s." % outfile)
201+
dataSource = None
202+
203+
# load the features from temp file
204+
feature_collection = []
205+
with open(outfile_temp, "r") as f:
206+
for cnt, line in enumerate(f):
207+
feature_collection.append(json.loads(line))
208+
209+
# create final geojson structure
210+
geojson_structure = {
211+
"type": "FeatureCollection",
212+
"name": outfile,
213+
"crs": {
214+
"type": "name",
215+
"properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"},
216+
},
217+
"features": feature_collection,
218+
}
219+
220+
# save to geojson
221+
with open(outfile, "w") as json_file:
222+
json.dump(geojson_structure, json_file)
223+
logger.info("created outfile: %s." % outfile)
224+
225+
# remove temp file
226+
if os.path.exists(outfile_temp):
227+
driver.DeleteDataSource(outfile_temp)
168228

169229

170230
def create_geojson_file(geometries, outfile):
171231

172-
driver = ogr.GetDriverByName("GeoJSON")
232+
driver = ogr.GetDriverByName("GeoJSONSeq")
173233
# define spatial Reference
174234
srs = osr.SpatialReference()
175235
srs.ImportFromEPSG(4326)
236+
outfile_temp = outfile.replace(".geojson", "_temp.geojson")
237+
238+
if os.path.exists(outfile_temp):
239+
driver.DeleteDataSource(outfile_temp)
240+
176241
if os.path.exists(outfile):
177242
driver.DeleteDataSource(outfile)
178-
dataSource = driver.CreateDataSource(outfile)
243+
244+
dataSource = driver.CreateDataSource(outfile_temp)
179245
# create layer
180-
layer = dataSource.CreateLayer(outfile, srs, geom_type=ogr.wkbPolygon)
246+
layer = dataSource.CreateLayer(outfile_temp, srs, geom_type=ogr.wkbPolygon,)
181247

182248
# create fields
183249
field_id = ogr.FieldDefn("id", ogr.OFTInteger)
@@ -198,5 +264,33 @@ def create_geojson_file(geometries, outfile):
198264
# add feature to layer
199265
layer.CreateFeature(feature)
200266

267+
# make sure to close layer and data source
201268
layer = None
269+
dataSource = None
270+
271+
# load the features from temp file
272+
feature_collection = []
273+
with open(outfile_temp, "r") as f:
274+
for cnt, line in enumerate(f):
275+
feature_collection.append(json.loads(line))
276+
277+
# create final geojson structure
278+
geojson_structure = {
279+
"type": "FeatureCollection",
280+
"name": outfile,
281+
"crs": {
282+
"type": "name",
283+
"properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"},
284+
},
285+
"features": feature_collection,
286+
}
287+
# save to geojson
288+
with open(outfile, "w") as json_file:
289+
json.dump(geojson_structure, json_file)
290+
logger.info("created outfile: %s." % outfile)
291+
292+
# remove temp file
293+
if os.path.exists(outfile_temp):
294+
driver.DeleteDataSource(outfile_temp)
295+
202296
logger.info("created outfile: %s." % outfile)
Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
"""Initialize slack client with values provided by the config file"""
2-
3-
import json
4-
51
import slack
6-
72
from mapswipe_workers.definitions import logger
3+
from mapswipe_workers import auth
84
from mapswipe_workers.config import SLACK_CHANNEL, SLACK_TOKEN
5+
from mapswipe_workers.definitions import MessageType
96

107

11-
def send_slack_message(message_type: str, project_name: str, project_id: str = None):
12-
8+
def send_slack_message(
9+
message_type: MessageType, project_name: str, project_id: str = None
10+
):
11+
"""Initialize slack client with values provided in environment."""
1312
if SLACK_TOKEN is None or SLACK_CHANNEL is None:
1413
logger.info(
1514
"No configuration for Slack was found. "
@@ -19,22 +18,68 @@ def send_slack_message(message_type: str, project_name: str, project_id: str = N
1918

2019
slack_client = slack.WebClient(token=SLACK_TOKEN)
2120

22-
if message_type == "success":
21+
if message_type == MessageType.SUCCESS:
2322
message = (
2423
"### PROJECT CREATION SUCCESSFUL ###\n"
25-
+ "Project Name: {0}\n".format(project_name)
26-
+ "Project Id: {0}\n\n".format(project_id)
24+
+ f"Project Name: {project_name}\n"
25+
+ f"Project Id: {project_id}\n\n"
2726
+ "Make sure to activate the project using the manager dashboard.\n"
2827
+ "Happy Swiping. :)"
2928
)
30-
elif message_type == "fail":
29+
slack_client.chat_postMessage(channel=SLACK_CHANNEL, text=message)
30+
elif message_type == MessageType.FAIL:
3131
message = (
3232
"### PROJECT CREATION FAILED ###\n"
33-
+ "Project Name: {0}\n".format(project_name)
33+
+ f"Project Name: {project_name}\n"
3434
+ "Project draft is deleted."
3535
)
36+
slack_client.chat_postMessage(channel=SLACK_CHANNEL, text=message)
37+
elif message_type == MessageType.NOTIFICATION_90:
38+
message = (
39+
"### ALMOST THERE! PROJECT REACHED 90% ###\n"
40+
+ f"Project Name: {project_name}\n"
41+
+ f"Project Id: {project_id}\n\n"
42+
+ "Get your next projects ready."
43+
)
44+
slack_client.chat_postMessage(channel="mapswipe_managers", text=message)
45+
elif message_type == MessageType.NOTIFICATION_100:
46+
message = (
47+
"### GREAT! PROJECT REACHED 100% ###\n"
48+
+ f"Project Name: {project_name}\n"
49+
+ f"Project Id: {project_id}\n\n"
50+
+ "You can set this project to 'finished' "
51+
+ "and activate another one."
52+
)
53+
slack_client.chat_postMessage(channel="mapswipe_managers", text=message)
3654
else:
3755
# TODO: Raise an Exception
3856
pass
3957

40-
slack_client.chat_postMessage(channel=SLACK_CHANNEL, text=message)
58+
59+
def send_progress_notification(project_id: int):
60+
"""Send progress notification to project managers in Slack."""
61+
fb_db = auth.firebaseDB()
62+
progress = fb_db.reference(f"v2/projects/{project_id}/progress").get()
63+
64+
if progress >= 90:
65+
project_name = fb_db.reference(f"v2/projects/{project_id}/name").get()
66+
notification_90_sent = fb_db.reference(
67+
f"v2/projects/{project_id}/notification_90_sent"
68+
).get()
69+
notification_100_sent = fb_db.reference(
70+
f"v2/projects/{project_id}/notification_100_sent"
71+
).get()
72+
logger.info(
73+
f"{project_id} - progress: {progress},"
74+
f"notifications: {notification_90_sent} {notification_100_sent}"
75+
)
76+
77+
if progress >= 90 and not notification_90_sent:
78+
# send notification and set value in firebase
79+
send_slack_message(MessageType.NOTIFICATION_90, project_name, project_id)
80+
fb_db.reference(f"v2/projects/{project_id}/notification_90_sent").set(True)
81+
82+
if progress >= 100 and not notification_100_sent:
83+
# send notification and set value in firebase
84+
send_slack_message(MessageType.NOTIFICATION_100, project_name, project_id)
85+
fb_db.reference(f"v2/projects/{project_id}/notification_100_sent").set(True)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from mapswipe_workers.definitions import DATA_PATH
2+
from mapswipe_workers import auth
3+
from mapswipe_workers.utils import geojson_functions
4+
import ogr
5+
6+
7+
def add_project_geometries_to_api():
8+
"""Load project geometries from postgres and save as geojson."""
9+
10+
# load from postgres
11+
pg_db = auth.postgresDB()
12+
sql_query = """
13+
SELECT
14+
project_id
15+
,ST_AsText(geom) as geom
16+
FROM projects
17+
"""
18+
data = pg_db.retr_query(sql_query)
19+
print(len(data))
20+
21+
# save as geojson one by one
22+
for project in data:
23+
project_id = project[0]
24+
wkt_geom = project[1]
25+
26+
outfile = (
27+
f"{DATA_PATH}/api/project_geometries/project_geom_{project_id}.geojson"
28+
)
29+
try:
30+
geometries = [ogr.CreateGeometryFromWkt(wkt_geom)]
31+
geojson_functions.create_geojson_file(geometries, outfile)
32+
except Exception:
33+
print(f"got an error for {project_id}")
34+
# just ignore if this fails
35+
pass
36+
37+
38+
add_project_geometries_to_api()

0 commit comments

Comments
 (0)