Skip to content

Commit f671a3c

Browse files
authored
Merge pull request #210 from OpenGeoscience/task-improvements
Task improvements
2 parents 41893a5 + 7c26432 commit f671a3c

20 files changed

+265
-208
lines changed

dev/Dockerfile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@ RUN apt-get update && \
88
ENV PYTHONDONTWRITEBYTECODE=1
99
ENV PYTHONUNBUFFERED=1
1010

11-
# Install tile2net (may take up to 30 mins)
12-
RUN git clone https://github.com/VIDA-NYU/tile2net.git
13-
RUN python -m pip install ./tile2net
11+
ARG TASKS
1412

1513
COPY ./setup.py /opt/geoinsight-server/setup.py
1614
COPY ./manage.py /opt/geoinsight-server/manage.py
1715
COPY ./geoinsight /opt/geoinsight-server/geoinsight
16+
1817
RUN pip install large-image[all] large-image-converter --find-links https://girder.github.io/large_image_wheels
19-
RUN pip install --editable /opt/geoinsight-server[dev,tasks]
18+
RUN pip install --editable /opt/geoinsight-server[dev]
19+
20+
RUN if [ "$TASKS" = "1" ]; \
21+
then pip install --editable /opt/geoinsight-server[tasks]; \
22+
# Install tile2net (may take up to 30 mins)
23+
git clone https://github.com/VIDA-NYU/tile2net.git; \
24+
python -m pip install ./tile2net; \
25+
fi
2026

2127
# Use a directory name which will never be an import name, as isort considers this as first-party.
2228
WORKDIR /opt/geoinsight-server

docker-compose.override.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ services:
2525
build:
2626
context: .
2727
dockerfile: ./dev/Dockerfile
28+
args:
29+
TASKS: 1
2830
command:
2931
[
3032
"celery",

geoinsight/core/management/commands/ingest.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.core.management.base import BaseCommand
1414
import pooch
1515

16-
from geoinsight.core.models import Chart, Dataset, DatasetTag, FileItem, Project
16+
from geoinsight.core.models import Chart, Dataset, FileItem, Project
1717

1818
DATA_FOLDER = Path(os.environ.get('INGEST_BIND_MOUNT_POINT', 'sample_data'))
1919
DOWNLOADS_FOLDER = Path(DATA_FOLDER, 'downloads')
@@ -401,10 +401,5 @@ def ingest_datasets(
401401
)
402402
)
403403

404-
self.ingest_dataset_tags(dataset_for_conversion, dataset.get('tags'))
404+
dataset_for_conversion.set_tags(dataset.get('tags'))
405405
dataset_for_conversion.set_owner(superuser)
406-
407-
def ingest_dataset_tags(self, dataset, tags):
408-
for tag in tags:
409-
DatasetTag.objects.get_or_create(tag=tag)
410-
dataset.tags.set(DatasetTag.objects.filter(tag__in=tags))

geoinsight/core/management/commands/load_roads.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

geoinsight/core/models/dataset.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def set_owner(self, user: User):
5858
).delete()
5959
assign_perm('owner', user, self)
6060

61+
@transaction.atomic()
62+
def set_tags(self, tags: [str]):
63+
for tag in tags:
64+
DatasetTag.objects.get_or_create(tag=tag)
65+
self.tags.set(DatasetTag.objects.filter(tag__in=tags))
66+
6167
def spawn_conversion_task(
6268
self,
6369
layer_options=None,

geoinsight/core/rest/analytics.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class AnalyticsViewSet(ReadOnlyModelViewSet):
2828
def list_types(self, request, project_id: int, **kwargs):
2929
serialized = []
3030
for analysis_type in analysis_types:
31+
if not analysis_type.is_enabled():
32+
continue
3133
instance = analysis_type()
3234
filtered_input_options = {}
3335
for k, v in instance.get_input_options().items():
@@ -79,7 +81,7 @@ def run(self, request, project_id: int, task_type: str, **kwargs):
7981
analysis_type_class = next(
8082
iter(at for at in analysis_types if at().db_value == task_type), None
8183
)
82-
if analysis_type_class is None:
84+
if analysis_type_class is None or not analysis_type_class.is_enabled():
8385
return Response(f'Analysis type "{task_type}" not found', status=404)
8486
result = analysis_type_class().run_task(project, **request.data)
8587
return Response(

geoinsight/core/rest/project.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Any
33

44
from django.contrib.auth.models import User
5-
from django.http import HttpResponse
65
from drf_yasg.utils import swagger_auto_schema
76
from rest_framework.decorators import action
87
from rest_framework.request import Request
@@ -12,7 +11,6 @@
1211
from geoinsight.core.models import Project
1312
from geoinsight.core.rest.access_control import GuardianFilter, GuardianPermission
1413
from geoinsight.core.rest.serializers import ProjectPermissionsSerializer, ProjectSerializer
15-
from geoinsight.core.tasks.osmnx import load_roads
1614

1715

1816
class ProjectViewSet(ModelViewSet):
@@ -51,14 +49,3 @@ def permissions(self, request: Request, *args: Any, **kwargs: Any):
5149
)
5250

5351
return Response(ProjectSerializer(project).data, status=200)
54-
55-
# TODO: This should be a POST
56-
@action(
57-
detail=True,
58-
methods=['get'],
59-
url_path=r'load_roads/(?P<location>.+)',
60-
)
61-
def load_roads(self, request, location, **kwargs):
62-
project = self.get_object()
63-
load_roads.delay(project.id, location)
64-
return HttpResponse('Task spawned successfully.', status=200)
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
from .create_road_network import CreateRoadNetwork
12
from .flood_network_failure import FloodNetworkFailure
23
from .flood_simulation import FloodSimulation
34
from .geoai_segmentation import GeoAISegmentation
45
from .network_recovery import NetworkRecovery
5-
from .segment_curbs import SegmentCurbs
6+
from .tile2net_segmentation import Tile2NetSegmentation
67

7-
__all__ = [FloodSimulation, FloodNetworkFailure, NetworkRecovery, SegmentCurbs, GeoAISegmentation]
8+
__all__ = [
9+
FloodSimulation,
10+
FloodNetworkFailure,
11+
NetworkRecovery,
12+
Tile2NetSegmentation,
13+
GeoAISegmentation,
14+
CreateRoadNetwork,
15+
]

geoinsight/core/tasks/analytics/analysis_type.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def __init__(self, *args):
1010
self.output_types = {}
1111
self.attribution = 'Kitware, Inc.'
1212

13+
@abstractmethod
14+
def is_enabled(self):
15+
raise NotImplementedError
16+
1317
@abstractmethod
1418
def get_input_options(self):
1519
raise NotImplementedError
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import datetime
2+
3+
from celery import shared_task
4+
from django.conf import settings
5+
from django.contrib.gis.geos import LineString, Point
6+
7+
from geoinsight.core.models import (
8+
Dataset,
9+
Layer,
10+
LayerFrame,
11+
Network,
12+
NetworkEdge,
13+
NetworkNode,
14+
TaskResult,
15+
VectorData,
16+
)
17+
from geoinsight.core.tasks.data import create_vector_features
18+
from geoinsight.core.tasks.networks import geojson_from_network
19+
20+
from .analysis_type import AnalysisType
21+
22+
23+
class CreateRoadNetwork(AnalysisType):
24+
def __init__(self):
25+
super().__init__(self)
26+
self.name = 'Create Road Network'
27+
self.description = 'Leverage OSMnx to create a road network for a target location'
28+
self.db_value = 'create_road_network'
29+
self.input_types = {'location': 'string'}
30+
self.output_types = {'roads': 'Dataset'}
31+
self.attribution = 'https://osmnx.readthedocs.io/en/stable/'
32+
33+
@classmethod
34+
def is_enabled(cls):
35+
return settings.ENABLE_TASK_CREATE_ROAD_NETWORK
36+
37+
def get_input_options(self):
38+
return {'location': []}
39+
40+
def run_task(self, project, **inputs):
41+
location = inputs.get('location')
42+
result = TaskResult.objects.create(
43+
name=f'Create Road Network for {location}',
44+
task_type=self.db_value,
45+
inputs=inputs,
46+
project=project,
47+
status='Initializing task...',
48+
)
49+
create_road_network.delay(result.id)
50+
return result
51+
52+
53+
def metadata_for_row(row):
54+
return {
55+
k: v
56+
for k, v in row.to_dict().items()
57+
if k not in ['osmid', 'geometry', 'ref'] and str(v) != 'nan'
58+
}
59+
60+
61+
@shared_task
62+
def create_road_network(result_id):
63+
import osmnx
64+
65+
result = TaskResult.objects.get(id=result_id)
66+
try:
67+
location = result.inputs.get('location')
68+
if location is None:
69+
raise ValueError('location not provided')
70+
71+
result.write_status('Fetching road data via OSMnx...')
72+
roads = osmnx.graph_from_place(location, network_type='drive')
73+
road_nodes, road_edges = osmnx.graph_to_gdfs(roads)
74+
75+
result.write_status('Saving results to database...')
76+
name = f'Road Network for {location}'
77+
existing_count = Dataset.objects.filter(name__icontains=name).count()
78+
if existing_count:
79+
name += f' ({existing_count + 1})'
80+
dataset = Dataset.objects.create(
81+
name=name,
82+
description='Network generated by OSMnx from OpenStreetMap data',
83+
category='transportation',
84+
metadata={'creation_time': datetime.datetime.now(datetime.UTC).isoformat()},
85+
)
86+
dataset.set_tags(['analytics', 'osmnx', 'network'])
87+
vector_data = VectorData.objects.create(
88+
name=f'{location} Roads Vector Data', dataset=dataset
89+
)
90+
layer = Layer.objects.create(name=f'{location} Roads', dataset=dataset)
91+
LayerFrame.objects.create(name='Roads', layer=layer, vector=vector_data)
92+
network = Network.objects.create(
93+
name=f'{location} Road Network',
94+
category='roads',
95+
vector_data=vector_data,
96+
metadata={'source': 'Created with OSMnx'},
97+
)
98+
99+
for _, edge_data in road_edges.iterrows():
100+
edge_geom = edge_data['geometry'].coords
101+
start = edge_geom[0]
102+
end = edge_geom[-1]
103+
edge_name = edge_data['name']
104+
if str(edge_name) == 'nan' or len(str(edge_name)) < 2:
105+
# If name is invalid, write new name string
106+
edge_name = 'Unnamed Road at {:0.4f}/{:0.4f}'.format(
107+
*edge_geom[int(len(edge_geom) / 2)]
108+
)
109+
110+
start_node_data = road_nodes.loc[
111+
(road_nodes['x'] == start[0]) & (road_nodes['y'] == start[1])
112+
].iloc[0]
113+
end_node_data = road_nodes.loc[
114+
(road_nodes['x'] == end[0]) & (road_nodes['y'] == end[1])
115+
].iloc[0]
116+
117+
start_node, created = NetworkNode.objects.get_or_create(
118+
network=network,
119+
name='{:0.5f}/{:0.5f}'.format(*start),
120+
location=Point(*start),
121+
)
122+
start_node.metadata = metadata_for_row(start_node_data)
123+
start_node.save()
124+
end_node, created = NetworkNode.objects.get_or_create(
125+
network=network,
126+
name='{:0.5f}/{:0.5f}'.format(*end),
127+
location=Point(*end),
128+
)
129+
end_node.metadata = metadata_for_row(end_node_data)
130+
end_node.save()
131+
edge, created = NetworkEdge.objects.get_or_create(
132+
network=network,
133+
name=edge_name,
134+
directed=edge_data['oneway'],
135+
from_node=start_node,
136+
to_node=end_node,
137+
line_geometry=LineString(*[Point(*p) for p in edge_geom]),
138+
)
139+
edge.metadata = metadata_for_row(edge_data)
140+
edge.save()
141+
142+
vector_data.write_geojson_data(geojson_from_network(dataset))
143+
create_vector_features(vector_data)
144+
vector_data.get_summary()
145+
146+
result.outputs = dict(roads=dataset.id)
147+
except Exception as e:
148+
result.error = str(e)
149+
result.complete()

0 commit comments

Comments
 (0)