|
| 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