diff --git a/pyproject.toml b/pyproject.toml index 4c717ea..28e4dc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ pyarrow = "^14.0.1" celery = "^5.3.6" redis = "^5.0.1" tqdm = "^4.66.1" +sentry-sdk = {extras = ["celery", "fastapi"], version = "^2.14.0"} [tool.poetry.group.dev.dependencies] diff --git a/src/core/config.py b/src/core/config.py index f718af4..8dfa41b 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Optional +from uuid import UUID from pydantic import BaseSettings, PostgresDsn, validator @@ -13,7 +14,9 @@ class SyncPostgresDsn(PostgresDsn): class Settings(BaseSettings): - DEBUG_MODE: bool = True + # Monitoring + SENTRY_DSN: Optional[str] = None + ENVIRONMENT: Optional[str] = "dev" CUSTOMER_SCHEMA: str = "customer" USER_DATA_SCHEMA: str = "user_data" @@ -22,13 +25,15 @@ class Settings(BaseSettings): PROJECT_NAME: Optional[str] = "GOAT Routing API" CACHE_DIR: str = "/app/src/cache" - STREET_NETWORK_EDGE_DEFAULT_LAYER_PROJECT_ID = 36126 - STREET_NETWORK_NODE_DEFAULT_LAYER_PROJECT_ID = 37319 - NETWORK_REGION_TABLE = "basic.geofence_active_mobility" CATCHMENT_AREA_CAR_BUFFER_DEFAULT_SPEED = 80 # km/h - CATCHMENT_AREA_HOLE_THRESHOLD_SQM = 10000 # 100m x 100m + CATCHMENT_AREA_HOLE_THRESHOLD_SQM = 200000 # 20 hectares, ~450m x 450m + + BASE_STREET_NETWORK: Optional[UUID] = "903ecdca-b717-48db-bbce-0219e41439cf" + DEFAULT_STREET_NETWORK_NODE_LAYER_PROJECT_ID = ( + 37319 # Hardcoded until node layers are added to GOAT projects by default + ) DATA_INSERT_BATCH_SIZE = 800 diff --git a/src/core/street_network/street_network_cache.py b/src/core/street_network/street_network_cache.py index 7e245a4..7725bbb 100644 --- a/src/core/street_network/street_network_cache.py +++ b/src/core/street_network/street_network_cache.py @@ -1,4 +1,5 @@ import os +from uuid import UUID import polars as pl from polars import DataFrame @@ -15,56 +16,50 @@ def __init__(self): def _get_edge_cache_file_name( self, - edge_layer_project_id: int, + edge_layer_id: UUID, h3_short: str, ): """Get edge cache file path for the specified H3_3 cell.""" return os.path.join( settings.CACHE_DIR, - f"{edge_layer_project_id}_{h3_short}_edge.parquet", + f"{str(edge_layer_id)}_{h3_short}_edge.parquet", ) def _get_node_cache_file_name( self, - node_layer_project_id: int, + node_layer_id: UUID, h3_short: str, ): """Get node cache file path for the specified H3_3 cell.""" return os.path.join( settings.CACHE_DIR, - f"{node_layer_project_id}_{h3_short}_node.parquet", + f"{node_layer_id}_{h3_short}_node.parquet", ) - def edge_cache_exists(self, edge_layer_project_id: int, h3_short: str): + def edge_cache_exists(self, edge_layer_id: UUID, h3_short: str): """Check if edge data for the specified H3_3 cell is cached.""" - edge_cache_file = self._get_edge_cache_file_name( - edge_layer_project_id, h3_short - ) + edge_cache_file = self._get_edge_cache_file_name(edge_layer_id, h3_short) return os.path.exists(edge_cache_file) - def node_cache_exists(self, node_layer_project_id: int, h3_short: str): + def node_cache_exists(self, node_layer_id: UUID, h3_short: str): """Check if node data for the specified H3_3 cell is cached.""" - node_cache_file = self._get_node_cache_file_name( - node_layer_project_id, h3_short - ) + node_cache_file = self._get_node_cache_file_name(node_layer_id, h3_short) return os.path.exists(node_cache_file) def read_edge_cache( self, - edge_layer_project_id: int, + edge_layer_id: UUID, h3_short: str, ): """Read edge data for the specified H3_3 cell from cache.""" edge_df: DataFrame = None - edge_cache_file = self._get_edge_cache_file_name( - edge_layer_project_id, h3_short - ) + edge_cache_file = self._get_edge_cache_file_name(edge_layer_id, h3_short) try: with open(edge_cache_file, "rb") as file: @@ -78,16 +73,14 @@ def read_edge_cache( def read_node_cache( self, - node_layer_project_id: int, + node_layer_id: UUID, h3_short: str, ): """Read node data for the specified H3_3 cell from cache.""" node_df: DataFrame = None - node_cache_file = self._get_node_cache_file_name( - node_layer_project_id, h3_short - ) + node_cache_file = self._get_node_cache_file_name(node_layer_id, h3_short) try: with open(node_cache_file, "rb") as file: @@ -101,15 +94,13 @@ def read_node_cache( def write_edge_cache( self, - edge_layer_project_id: int, + edge_layer_id: UUID, h3_short: str, edge_df: DataFrame, ): """Write edge data for the specified H3_3 cell into cache.""" - edge_cache_file = self._get_edge_cache_file_name( - edge_layer_project_id, h3_short - ) + edge_cache_file = self._get_edge_cache_file_name(edge_layer_id, h3_short) try: with open(edge_cache_file, "wb") as file: @@ -124,15 +115,13 @@ def write_edge_cache( def write_node_cache( self, - node_layer_project_id: int, + node_layer_id: UUID, h3_short: str, node_df: DataFrame, ): """Write node data for the specified H3_3 cell into cache.""" - node_cache_file = self._get_node_cache_file_name( - node_layer_project_id, h3_short - ) + node_cache_file = self._get_node_cache_file_name(node_layer_id, h3_short) try: with open(node_cache_file, "wb") as file: diff --git a/src/core/street_network/street_network_util.py b/src/core/street_network/street_network_util.py index f95b10b..5a95fdc 100644 --- a/src/core/street_network/street_network_util.py +++ b/src/core/street_network/street_network_util.py @@ -14,23 +14,12 @@ class StreetNetworkUtil: def __init__(self, db_connection: AsyncSession): self.db_connection = db_connection - async def _get_layer_and_user_id(self, layer_project_id: int): - """Get the layer ID and user ID of the specified layer project ID.""" + async def _get_user_id(self, layer_id: UUID): + """Get the user ID of the specified layer ID.""" - layer_id: UUID = None user_id: UUID = None try: - # Get the associated layer ID - result = await self.db_connection.execute( - text( - f"""SELECT layer_id - FROM {settings.CUSTOMER_SCHEMA}.layer_project - WHERE id = {layer_project_id};""" - ) - ) - layer_id = UUID(str(result.fetchone()[0])) - # Get the user ID of the layer result = await self.db_connection.execute( text( @@ -41,55 +30,47 @@ async def _get_layer_and_user_id(self, layer_project_id: int): ) user_id = UUID(str(result.fetchone()[0])) except Exception: - raise ValueError( - f"Could not fetch layer and user ID for layer project ID {layer_project_id}." - ) + raise ValueError(f"Could not fetch user ID for layer ID {layer_id}.") - return layer_id, user_id + return user_id async def _get_street_network_tables( self, - street_network_edge_layer_project_id: int, - street_network_node_layer_project_id: int, + edge_layer_id: UUID, + node_layer_id: UUID, ): """Get table names and layer IDs of the edge and node tables.""" edge_table: str = None - edge_layer_id: UUID = None node_table: str = None - node_layer_id: UUID = None - # Get edge table name if a layer project ID is specified - if street_network_edge_layer_project_id: + # Get edge table name if a layer ID is specified + if edge_layer_id: try: # Get the edge layer ID and associated user ID - edge_layer_id, user_id = await self._get_layer_and_user_id( - street_network_edge_layer_project_id - ) + user_id = await self._get_user_id(edge_layer_id) # Produce the edge table name edge_table = f"{settings.USER_DATA_SCHEMA}.street_network_line_{str(user_id).replace('-', '')}" except Exception: raise ValueError( - f"Could not fetch edge table name for layer project ID {street_network_edge_layer_project_id}." + f"Could not fetch edge table name for layer ID {edge_layer_id}." ) - # Get node table name if a layer project ID is specified - if street_network_node_layer_project_id: + # Get node table name if a layer ID is specified + if node_layer_id: try: # Get the node layer ID and associated user ID - node_layer_id, user_id = await self._get_layer_and_user_id( - street_network_node_layer_project_id - ) + user_id = await self._get_user_id(node_layer_id) # Produce the node table name node_table = f"{settings.USER_DATA_SCHEMA}.street_network_point_{str(user_id).replace('-', '')}" except Exception: raise ValueError( - f"Could not fetch node table name for layer project ID {street_network_node_layer_project_id}." + f"Could not fetch node table name for layer ID {node_layer_id}." ) - return edge_table, edge_layer_id, node_table, node_layer_id + return edge_table, node_table async def _get_street_network_region_h3_3_cells(self, region_geofence_table: str): """Get list of H3_3 cells covering the street network region.""" @@ -118,8 +99,8 @@ async def _get_street_network_region_h3_3_cells(self, region_geofence_table: str async def fetch( self, - edge_layer_project_id: int, - node_layer_project_id: int, + edge_layer_id: UUID, + node_layer_id: UUID, region_geofence_table: str, ): """Fetch street network from specified layer and load into Polars dataframes.""" @@ -139,28 +120,22 @@ async def fetch( # Get table names and layer IDs of the edge and node tables ( street_network_edge_table, - street_network_edge_layer_id, street_network_node_table, - street_network_node_layer_id, - ) = await self._get_street_network_tables( - edge_layer_project_id, node_layer_project_id - ) + ) = await self._get_street_network_tables(edge_layer_id, node_layer_id) # Initialize cache street_network_cache = StreetNetworkCache() try: for h3_short in street_network_region_h3_3_cells: - if edge_layer_project_id is not None: - if street_network_cache.edge_cache_exists( - edge_layer_project_id, h3_short - ): + if edge_layer_id is not None: + if street_network_cache.edge_cache_exists(edge_layer_id, h3_short): # Read edge data from cache edge_df = street_network_cache.read_edge_cache( - edge_layer_project_id, h3_short + edge_layer_id, h3_short ) else: - if settings.DEBUG_MODE: + if settings.ENVIRONMENT == "dev": print( f"Fetching street network edge data for H3_3 cell {h3_short}" ) @@ -174,7 +149,7 @@ async def fetch( maxspeed_backward, source, target, h3_3, h3_6 FROM {street_network_edge_table} WHERE h3_3 = {h3_short} - AND layer_id = '{str(street_network_edge_layer_id)}' + AND layer_id = '{str(edge_layer_id)}' """, uri=settings.POSTGRES_DATABASE_URI, schema_overrides=SEGMENT_DATA_SCHEMA, @@ -185,22 +160,20 @@ async def fetch( # Write edge data into cache street_network_cache.write_edge_cache( - edge_layer_project_id, h3_short, edge_df + edge_layer_id, h3_short, edge_df ) # Update street network edge dictionary and memory usage street_network_edge[h3_short] = edge_df street_network_size += edge_df.estimated_size("gb") - if node_layer_project_id is not None: - if street_network_cache.node_cache_exists( - node_layer_project_id, h3_short - ): + if node_layer_id is not None: + if street_network_cache.node_cache_exists(node_layer_id, h3_short): # Read node data from cache node_df = street_network_cache.read_node_cache( - node_layer_project_id, h3_short + node_layer_id, h3_short ) else: - if settings.DEBUG_MODE: + if settings.ENVIRONMENT == "dev": print( f"Fetching street network node data for H3_3 cell {h3_short}" ) @@ -211,7 +184,7 @@ async def fetch( SELECT node_id AS id, h3_3, h3_6 FROM {street_network_node_table} WHERE h3_3 = {h3_short} - AND layer_id = '{str(street_network_node_layer_id)}' + AND layer_id = '{str(node_layer_id)}' """, uri=settings.POSTGRES_DATABASE_URI, schema_overrides=CONNECTOR_DATA_SCHEMA, @@ -219,7 +192,7 @@ async def fetch( # Write node data into cache street_network_cache.write_node_cache( - node_layer_project_id, h3_short, node_df + node_layer_id, h3_short, node_df ) # Update street network node dictionary and memory usage @@ -231,23 +204,20 @@ async def fetch( ) # Raise error if a edge layer project ID is specified but no edge data is fetched - if edge_layer_project_id is not None and len(street_network_edge) == 0: + if edge_layer_id is not None and len(street_network_edge) == 0: raise RuntimeError( - f"Failed to fetch street network edge data for layer project ID {edge_layer_project_id}." + f"Failed to fetch street network edge data for layer project ID {edge_layer_id}." ) # Raise error if a node layer project ID is specified but no node data is fetched - if node_layer_project_id is not None and len(street_network_node) == 0: + if node_layer_id is not None and len(street_network_node) == 0: raise RuntimeError( - f"Failed to fetch street network node data for layer project ID {node_layer_project_id}." + f"Failed to fetch street network node data for layer project ID {node_layer_id}." ) end_time = time.time() - if settings.DEBUG_MODE: - print( - f"Street network load time: {round((end_time - start_time) / 60, 1)} min" - ) - print(f"Street network in-memory size: {round(street_network_size, 1)} GB") + print(f"Street network load time: {round((end_time - start_time) / 60, 1)} min") + print(f"Street network in-memory size: {round(street_network_size, 1)} GB") return street_network_edge, street_network_node diff --git a/src/core/worker.py b/src/core/worker.py index 909d4fa..b5d7050 100644 --- a/src/core/worker.py +++ b/src/core/worker.py @@ -1,6 +1,7 @@ import asyncio -from celery import Celery +import sentry_sdk +from celery import Celery, signals from redis import Redis from src.core.config import settings @@ -16,6 +17,15 @@ crud_catchment_area = CRUDCatchmentArea(async_session(), redis) +@signals.celeryd_init.connect +def init_sentry(**_kwargs): + sentry_sdk.init( + dsn=settings.SENTRY_DSN, + environment=settings.ENVIRONMENT, + traces_sample_rate=1.0 if settings.ENVIRONMENT == "prod" else 0.1, + ) + + @celery_app.task def run_catchment_area(params): loop = asyncio.get_event_loop() diff --git a/src/crud/crud_catchment_area.py b/src/crud/crud_catchment_area.py index 7f2e7de..a619104 100644 --- a/src/crud/crud_catchment_area.py +++ b/src/crud/crud_catchment_area.py @@ -27,6 +27,7 @@ ) from src.schemas.error import BufferExceedsNetworkError, DisconnectedOriginError from src.schemas.status import ProcessingStatus +from src.utils import format_value_null_sql class CRUDCatchmentArea: @@ -116,71 +117,70 @@ async def read_network( sub_network = sub_df # Produce all network modifications required to apply the specified scenario - network_modifications_table = f"temporal.{str(uuid.uuid4()).replace('-', '_')}" - scenario_id = ( - f"'{obj_in.scenario_id}'" if obj_in.scenario_id is not None else "NULL" - ) - await self.db_connection.execute( - text( + network_modifications_table = None + if obj_in.scenario_id: + sql_produce_network_modifications = text( f""" - SELECT basic.produce_network_modifications( - {scenario_id}, - {settings.STREET_NETWORK_EDGE_DEFAULT_LAYER_PROJECT_ID}, - {settings.STREET_NETWORK_NODE_DEFAULT_LAYER_PROJECT_ID}, - '{network_modifications_table}' - ); - """ - ) - ) - - # Apply network modifications to the sub-network - segments_to_discard = [] - sql_get_network_modifications = text( - f""" - SELECT edit_type, id, class_, source, target, - length_m, length_3857, CAST(coordinates_3857 AS TEXT) AS coordinates_3857, - impedance_slope, impedance_slope_reverse, impedance_surface, maxspeed_forward, - maxspeed_backward, h3_6, h3_3 - FROM "{network_modifications_table}"; - """ - ) - result = ( - await self.db_connection.execute(sql_get_network_modifications) - ).fetchall() - - for modification in result: - if modification[0] == "d": - segments_to_discard.append(modification[1]) - continue - - new_segment = pl.DataFrame( - [ - { - "id": modification[1], - "length_m": modification[5], - "length_3857": modification[6], - "class_": modification[2], - "impedance_slope": modification[8], - "impedance_slope_reverse": modification[9], - "impedance_surface": modification[10], - "coordinates_3857": modification[7], - "maxspeed_forward": modification[11], - "maxspeed_backward": modification[12], - "source": modification[3], - "target": modification[4], - "h3_3": modification[13], - "h3_6": modification[14], - } - ], - schema_overrides=SEGMENT_DATA_SCHEMA, + SELECT basic.produce_network_modifications( + {format_value_null_sql(obj_in.scenario_id)}, + {obj_in.street_network.edge_layer_project_id}, + {obj_in.street_network.node_layer_project_id} + ); + """ ) - new_segment = new_segment.with_columns( - pl.col("coordinates_3857").str.json_extract() + network_modifications_table = ( + await self.db_connection.execute(sql_produce_network_modifications) + ).fetchone()[0] + + if network_modifications_table: + # Apply network modifications to the sub-network + segments_to_discard = [] + sql_get_network_modifications = text( + f""" + SELECT edit_type, id, class_, source, target, + length_m, length_3857, CAST(coordinates_3857 AS TEXT) AS coordinates_3857, + impedance_slope, impedance_slope_reverse, impedance_surface, maxspeed_forward, + maxspeed_backward, h3_6, h3_3 + FROM "{network_modifications_table}"; + """ ) - sub_network.extend(new_segment) + result = ( + await self.db_connection.execute(sql_get_network_modifications) + ).fetchall() + + for modification in result: + if modification[0] == "d": + segments_to_discard.append(modification[1]) + continue + + new_segment = pl.DataFrame( + [ + { + "id": modification[1], + "length_m": modification[5], + "length_3857": modification[6], + "class_": modification[2], + "impedance_slope": modification[8], + "impedance_slope_reverse": modification[9], + "impedance_surface": modification[10], + "coordinates_3857": modification[7], + "maxspeed_forward": modification[11], + "maxspeed_backward": modification[12], + "source": modification[3], + "target": modification[4], + "h3_3": modification[13], + "h3_6": modification[14], + } + ], + schema_overrides=SEGMENT_DATA_SCHEMA, + ) + new_segment = new_segment.with_columns( + pl.col("coordinates_3857").str.json_extract() + ) + sub_network.extend(new_segment) - # Remove segments which are deleted or modified due to the scenario - sub_network = sub_network.filter(~pl.col("id").is_in(segments_to_discard)) + # Remove segments which are deleted or modified due to the scenario + sub_network = sub_network.filter(~pl.col("id").is_in(segments_to_discard)) # Create necessary artifical segments and add them to our sub network origin_point_connectors = [] @@ -198,9 +198,9 @@ async def read_network( maxspeed_forward, maxspeed_backward, source, target, h3_3, h3_6, point_cell_index, point_h3_3 FROM basic.get_artificial_segments( - {settings.STREET_NETWORK_EDGE_DEFAULT_LAYER_PROJECT_ID}, - '{network_modifications_table}', - '{input_table}', + {format_value_null_sql(settings.BASE_STREET_NETWORK)}, + {format_value_null_sql(network_modifications_table)}, + {format_value_null_sql(input_table)}, {num_points}, '{",".join(valid_segment_classes)}', 10 @@ -341,9 +341,10 @@ async def drop_temp_tables( """Delete the temporary input and network modifications tables.""" await self.db_connection.execute(text(f'DROP TABLE "{input_table}";')) - await self.db_connection.execute( - text(f'DROP TABLE "{network_modifications_table}";') - ) + if network_modifications_table is not None: + await self.db_connection.execute( + text(f'DROP TABLE "{network_modifications_table}";') + ) await self.db_connection.commit() def compute_segment_cost(self, sub_network, mode, speed): @@ -569,34 +570,37 @@ async def save_result(self, obj_in, shapes, network, grid_index, grid): '{grid_index[i]}', ROUND({grid[i]}) ),""" - insert_string = text( - f""" - INSERT INTO {obj_in.result_table} (layer_id, geom, text_attr1, integer_attr1) - VALUES {insert_string.rstrip(",")}; - """ - ) - await self.db_connection.execute(insert_string) - await self.db_connection.commit() + + # Insert only if any grid data was added to the query in this batch + if insert_string: + insert_string = text( + f""" + INSERT INTO {obj_in.result_table} (layer_id, geom, text_attr1, integer_attr1) + VALUES {insert_string.rstrip(",")}; + """ + ) + await self.db_connection.execute(insert_string) + await self.db_connection.commit() async def run(self, obj_in: ICatchmentAreaActiveMobility | ICatchmentAreaCar): """Compute catchment areas for the given request parameters.""" + if obj_in["routing_type"] != CatchmentAreaRoutingTypeCar.car.value: + obj_in = ICatchmentAreaActiveMobility(**obj_in) + else: + obj_in = ICatchmentAreaCar(**obj_in) + # Fetch routing network (processed segments) and load into memory if self.routing_network is None: self.routing_network, _ = await StreetNetworkUtil(self.db_connection).fetch( - edge_layer_project_id=settings.STREET_NETWORK_EDGE_DEFAULT_LAYER_PROJECT_ID, - node_layer_project_id=None, + edge_layer_id=settings.BASE_STREET_NETWORK, + node_layer_id=None, region_geofence_table=settings.NETWORK_REGION_TABLE, ) routing_network = self.routing_network total_start = time.time() - if obj_in["routing_type"] != CatchmentAreaRoutingTypeCar.car.value: - obj_in = ICatchmentAreaActiveMobility(**obj_in) - else: - obj_in = ICatchmentAreaCar(**obj_in) - # Read & process routing network to extract relevant sub-network start_time = time.time() sub_routing_network = None diff --git a/src/crud/crud_catchment_area_sync.py b/src/crud/crud_catchment_area_sync.py index 11077b3..a34793c 100644 --- a/src/crud/crud_catchment_area_sync.py +++ b/src/crud/crud_catchment_area_sync.py @@ -22,6 +22,10 @@ from src.schemas.error import BufferExceedsNetworkError, DisconnectedOriginError from src.utils import make_dir +#################################################################################################### +# TODO: Refactor and fix +#################################################################################################### + class FetchRoutingNetwork: def __init__(self, db_cursor): @@ -196,7 +200,7 @@ def read_network( maxspeed_forward, maxspeed_backward, source, target, h3_3, h3_6, point_cell_index, point_h3_3 FROM basic.get_artificial_segments( - {settings.STREET_NETWORK_EDGE_DEFAULT_LAYER_PROJECT_ID}, + {obj_in.street_network_edge_layer_project_id}, '{input_table}', {num_points}, '{",".join(valid_segment_classes)}', diff --git a/src/db/functions/create_intersection_line.sql b/src/db/functions/create_intersection_line.sql deleted file mode 100644 index 507b3a6..0000000 --- a/src/db/functions/create_intersection_line.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE OR REPLACE FUNCTION basic.create_intersection_line(point_geom geometry, length_line double precision) - RETURNS SETOF geometry - LANGUAGE sql - IMMUTABLE -AS $function$ - SELECT ST_SETSRID(ST_MAKELINE( - ST_Translate(point_geom, length_line, length_line), - ST_Translate(point_geom, -length_line, -length_line) - ), 4326) -$function$; \ No newline at end of file diff --git a/src/db/functions/extend_line.sql b/src/db/functions/extend_line.sql deleted file mode 100644 index 067d234..0000000 --- a/src/db/functions/extend_line.sql +++ /dev/null @@ -1,55 +0,0 @@ -CREATE OR REPLACE FUNCTION basic.extend_line(geom geometry, extend_distance NUMERIC, point_to_extend text) -RETURNS geometry -LANGUAGE plpgsql -AS $function$ -DECLARE - start_geom geometry; - end_geom geometry; - azimuth_A float; - azimuth_B float; - length_A NUMERIC; - length_B NUMERIC; - newpoint_A geometry; - newpoint_B geometry; - new_line geometry; -BEGIN - - -- get the points A and B given a line L - start_geom = ST_STARTPOINT(geom); - end_geom = ST_ENDPOINT(geom); - - -- Start line section - azimuth_A = ST_AZIMUTH(ST_POINTN(geom,2),start_geom); - - -- End line section - azimuth_B = ST_AZIMUTH(ST_POINTN(geom,-2),end_geom); - - -- get the length of the line A --> B - length_A = ST_DISTANCE(ST_STARTPOINT(geom),ST_POINTN(geom,2)); - length_B = ST_DISTANCE(ST_ENDPOINT(geom),ST_POINTN(geom,-2)); - - newpoint_A = ST_TRANSLATE(start_geom, sin(azimuth_A) * extend_distance, cos(azimuth_A) * extend_distance); - newpoint_B = ST_TRANSLATE(end_geom, sin(azimuth_B) * extend_distance, cos(azimuth_B) * extend_distance); - - IF point_to_extend = 'start' THEN - new_line = st_addpoint(geom, newpoint_a, 0); - ELSEIF point_to_extend = 'end' THEN - new_line = st_addpoint(geom, newpoint_b, -1); - ELSEIF point_to_extend = 'both' THEN - new_line = st_addpoint(st_addpoint(geom,newpoint_B), newpoint_A, 0); - ELSE - RAISE EXCEPTION 'Please specify a valid point_to_extend type.'; - END IF; - - If new_line IS NULL THEN - RAISE NOTICE 'The new line is NULL. Please check the input parameters.'; - new_line = geom; - END IF; - - RETURN new_line; -END -$function$ -/*point_to_extend = 'start', 'end', 'both'*/ ---1 meter in Germany approx. 0.0000127048 ---SELECT basic.extend_line(geom, 0.0127048, 'both') ---FROM customer.way_modified WHERE id = 112; diff --git a/src/db/functions/fill_polygon_h3_10.sql b/src/db/functions/fill_polygon_h3_10.sql deleted file mode 100644 index 6fc7298..0000000 --- a/src/db/functions/fill_polygon_h3_10.sql +++ /dev/null @@ -1,31 +0,0 @@ -/*This function returns the h3 indexes that are intersecting the borderpoints of a specified geometry*/ -DROP FUNCTION IF EXISTS basic.fill_polygon_h3_10; -CREATE OR REPLACE FUNCTION basic.fill_polygon_h3_10(geom geometry) -RETURNS TABLE (h3_index h3index, h3_short bigint, h3_boundary geometry(linestring, 4326), h3_geom geometry(polygon, 4326)) -LANGUAGE plpgsql -AS $function$ -BEGIN - RETURN query - WITH border_points AS - ( - SELECT ((ST_DUMPPOINTS(geom)).geom)::point AS geom - ), - polygons AS - ( - SELECT ((ST_DUMP(geom)).geom)::polygon AS geom - ), - h3_ids AS - ( - SELECT h3_lat_lng_to_cell(b.geom, 10) h3_index - FROM border_points b - UNION ALL - SELECT h3_polygon_to_cells(p.geom, ARRAY[]::polygon[], 10) h3_index - FROM polygons p - ) - SELECT sub.h3_index, basic.to_short_h3_10(sub.h3_index::bigint) AS h3_short, - ST_ExteriorRing(ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326)) as h3_boundary, - ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326) as h3_geom - FROM h3_ids sub - GROUP BY sub.h3_index; -END; -$function$ diff --git a/src/db/functions/fill_polygon_h3_3.sql b/src/db/functions/fill_polygon_h3_3.sql deleted file mode 100644 index 3ac6305..0000000 --- a/src/db/functions/fill_polygon_h3_3.sql +++ /dev/null @@ -1,31 +0,0 @@ -/*This function returns the h3 indexes that are intersecting the borderpoints of a specified geometry*/ -DROP FUNCTION IF EXISTS basic.fill_polygon_h3_3; -CREATE OR REPLACE FUNCTION basic.fill_polygon_h3_3(geom geometry) -RETURNS TABLE (h3_index h3index, h3_short smallint, h3_boundary geometry(linestring, 4326), h3_geom geometry(polygon, 4326)) -LANGUAGE plpgsql -AS $function$ -BEGIN - RETURN query - WITH border_points AS - ( - SELECT ((ST_DUMPPOINTS(geom)).geom)::point AS geom - ), - polygons AS - ( - SELECT ((ST_DUMP(geom)).geom)::polygon AS geom - ), - h3_ids AS - ( - SELECT h3_lat_lng_to_cell(b.geom, 3) h3_index - FROM border_points b - UNION ALL - SELECT h3_polygon_to_cells(p.geom, ARRAY[]::polygon[], 3) h3_index - FROM polygons p - ) - SELECT sub.h3_index, basic.to_short_h3_3(sub.h3_index::bigint) AS h3_short, - ST_ExteriorRing(ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326)) as h3_boundary, - ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326) as h3_geom - FROM h3_ids sub - GROUP BY sub.h3_index; -END; -$function$ diff --git a/src/db/functions/fill_polygon_h3_6.sql b/src/db/functions/fill_polygon_h3_6.sql deleted file mode 100644 index 2ecab44..0000000 --- a/src/db/functions/fill_polygon_h3_6.sql +++ /dev/null @@ -1,31 +0,0 @@ -/*This function returns the h3 indexes that are intersecting the borderpoints of a specified geometry*/ -DROP FUNCTION IF EXISTS basic.fill_polygon_h3_6; -CREATE OR REPLACE FUNCTION basic.fill_polygon_h3_6(geom geometry) -RETURNS TABLE (h3_index h3index, h3_short int, h3_boundary geometry(linestring, 4326), h3_geom geometry(polygon, 4326)) -LANGUAGE plpgsql -AS $function$ -BEGIN - RETURN query - WITH border_points AS - ( - SELECT ((ST_DUMPPOINTS(geom)).geom)::point AS geom - ), - polygons AS - ( - SELECT ((ST_DUMP(geom)).geom)::polygon AS geom - ), - h3_ids AS - ( - SELECT h3_lat_lng_to_cell(b.geom, 6) h3_index - FROM border_points b - UNION ALL - SELECT h3_polygon_to_cells(p.geom, ARRAY[]::polygon[], 6) h3_index - FROM polygons p - ) - SELECT sub.h3_index, basic.to_short_h3_6(sub.h3_index::bigint) AS h3_short, - ST_ExteriorRing(ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326)) as h3_boundary, - ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326) as h3_geom - FROM h3_ids sub - GROUP BY sub.h3_index; -END; -$function$ diff --git a/src/db/functions/fill_polygon_h3_9.sql b/src/db/functions/fill_polygon_h3_9.sql deleted file mode 100644 index c88e7c7..0000000 --- a/src/db/functions/fill_polygon_h3_9.sql +++ /dev/null @@ -1,31 +0,0 @@ -/*This function returns the h3 indexes that are intersecting the borderpoints of a specified geometry*/ -DROP FUNCTION IF EXISTS basic.fill_polygon_h3_9; -CREATE OR REPLACE FUNCTION basic.fill_polygon_h3_9(geom geometry) -RETURNS TABLE (h3_index h3index, h3_short bigint, h3_boundary geometry(linestring, 4326), h3_geom geometry(polygon, 4326)) -LANGUAGE plpgsql -AS $function$ -BEGIN - RETURN query - WITH border_points AS - ( - SELECT ((ST_DUMPPOINTS(geom)).geom)::point AS geom - ), - polygons AS - ( - SELECT ((ST_DUMP(geom)).geom)::polygon AS geom - ), - h3_ids AS - ( - SELECT h3_lat_lng_to_cell(b.geom, 9) h3_index - FROM border_points b - UNION ALL - SELECT h3_polygon_to_cells(p.geom, ARRAY[]::polygon[], 9) h3_index - FROM polygons p - ) - SELECT sub.h3_index, basic.to_short_h3_9(sub.h3_index::bigint) AS h3_short, - ST_ExteriorRing(ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326)) as h3_boundary, - ST_SetSRID(geometry(h3_cell_to_boundary(sub.h3_index)), 4326) as h3_geom - FROM h3_ids sub - GROUP BY sub.h3_index; -END; -$function$ diff --git a/src/db/functions/fill_polygon_holes.sql b/src/db/functions/fill_polygon_holes.sql deleted file mode 100644 index e94f5dd..0000000 --- a/src/db/functions/fill_polygon_holes.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Function to remove small holes from a polygon --- threshold: minimum area of a hole to keep (in square meters) -DROP FUNCTION IF EXISTS basic.fill_polygon_holes; -CREATE OR REPLACE FUNCTION basic.fill_polygon_holes(geom geometry, threshold float8) -RETURNS geometry AS $$ -DECLARE - outer_ring geometry; - inner_ring geometry; - num_rings integer; - ring_index integer; - new_rings geometry[]; -BEGIN - -- Ensure geometry is a valid polygon - IF NOT ST_IsValid(geom) OR NOT ST_GeometryType(geom) = 'ST_Polygon' THEN - RETURN NULL; - END IF; - - -- Extract the outer ring - outer_ring := ST_ExteriorRing(geom); - - -- Get the number of interior rings - num_rings := ST_NumInteriorRings(geom); - - -- Initialize array to store remaining interior rings - new_rings := ARRAY[]::geometry[]; - - -- Loop through each interior ring - FOR ring_index IN 1..num_rings LOOP - -- Extract the current interior ring - inner_ring := ST_InteriorRingN(geom, ring_index); - - -- Check if the area of the interior ring is larger than the threshold - IF ST_Area(ST_MakePolygon(inner_ring)::geography) >= threshold THEN - -- Add the ring to the new_rings array - new_rings := array_append(new_rings, inner_ring); - END IF; - END LOOP; - - -- Construct a new polygon from the outer ring and the remaining interior rings - RETURN ST_MakePolygon(outer_ring, new_rings) AS geom; -END; -$$ LANGUAGE plpgsql; diff --git a/src/db/functions/get_artificial_segments.sql b/src/db/functions/get_artificial_segments.sql deleted file mode 100644 index 82f4a40..0000000 --- a/src/db/functions/get_artificial_segments.sql +++ /dev/null @@ -1,269 +0,0 @@ -DROP TYPE IF EXISTS basic.origin_segment; -CREATE TYPE basic.origin_segment AS ( - id INT, class_ TEXT, impedance_slope FLOAT8, impedance_slope_reverse FLOAT8, - impedance_surface FLOAT8, maxspeed_forward INT, maxspeed_backward INT, source INT, - target INT, geom GEOMETRY, h3_3 INT2, h3_6 INT, fraction FLOAT[], fraction_geom GEOMETRY[], - point_id INT2[], point_geom GEOMETRY[] -); - - -DROP TYPE IF EXISTS basic.artificial_segment CASCADE; -CREATE TYPE basic.artificial_segment AS ( - point_id INT2, point_geom GEOMETRY, point_cell_index H3INDEX, point_h3_3 INT, old_id INT, - id INT, length_m FLOAT, length_3857 FLOAT, class_ TEXT, impedance_slope FLOAT8, - impedance_slope_reverse FLOAT8, impedance_surface FLOAT8, coordinates_3857 JSONB, - maxspeed_forward INT, maxspeed_backward INT, source INT, target INT, geom GEOMETRY, - h3_3 INT2, h3_6 INT -); - - -DROP FUNCTION IF EXISTS basic.get_artificial_segments; -CREATE OR REPLACE FUNCTION basic.get_artificial_segments( - edge_layer_project_id INT, - network_modifications_table TEXT, - origin_points_table TEXT, - num_origin_points INT, - classes TEXT, - point_cell_resolution INT -) -RETURNS SETOF basic.artificial_segment -LANGUAGE plpgsql -AS $function$ -DECLARE - edge_layer_id UUID := (SELECT layer_id FROM customer.layer_project WHERE id = edge_layer_project_id); - edge_network_table TEXT := 'user_data.street_network_line_' || REPLACE(( - SELECT user_id FROM customer.layer WHERE id = edge_layer_id - )::TEXT, '-', ''); - - custom_cursor REFCURSOR; - origin_segment basic.origin_segment; - artificial_segment basic.artificial_segment; - - -- Increment everytime a new artificial segment is created - artificial_seg_index INT = 1000000000; -- Defaults to 1 billion - - -- Increment everytime a new artificial connector/node is created - artificial_con_index INT = 1000000000; -- Defaults to 1 billion - - -- Increment everytime a new artificial origin node is created (for isochrone starting points) - artifical_origin_index INT = 2000000000; -- Defaults to 2 billion - - fraction FLOAT; - new_geom GEOMETRY; -BEGIN - - OPEN custom_cursor FOR EXECUTE - FORMAT ( - 'WITH origin AS ( - SELECT - id, geom, - ST_SETSRID(ST_Buffer(geom::geography, 100)::GEOMETRY, 4326) AS buffer_geom, - basic.to_short_h3_3(h3_lat_lng_to_cell(ST_Centroid(geom)::point, 3)::bigint) AS h3_3 - FROM %I - LIMIT %s::int - ), - modified_network AS ( - SELECT original_features.* - FROM ( - SELECT s.edge_id AS id, s.class_, s.source, s.target, s.length_m, s.length_3857, - s.coordinates_3857, s.impedance_slope, s.impedance_slope_reverse, - s.impedance_surface, s.maxspeed_forward, s.maxspeed_backward, s.geom, - s.h3_6, s.h3_3 - FROM %s s, - origin o - WHERE s.class_ = ANY(string_to_array(''%s'', '','')) - AND ST_Intersects(s.geom, o.buffer_geom) - ) original_features - LEFT JOIN %I scenario_features ON original_features.id = scenario_features.id - WHERE scenario_features.id IS NULL - UNION ALL - SELECT id, class_, source, target, length_m, length_3857, - coordinates_3857::json, impedance_slope, impedance_slope_reverse, - impedance_surface, maxspeed_forward, maxspeed_backward, geom, - h3_6, h3_3 - FROM %I scenario_features - WHERE edit_type = ''n'' - AND class_ = ANY(string_to_array(''%s'', '','')) - ), - best_segment AS ( - SELECT DISTINCT ON (o.id) - o.id AS point_id, o.geom AS point_geom, o.buffer_geom AS point_buffer, - s.id, s.class_, s.impedance_slope, s.impedance_slope_reverse, - s.impedance_surface, s.maxspeed_forward, s.maxspeed_backward, - s."source", s.target, s.geom, s.h3_3, s.h3_6, - ST_LineLocatePoint(s.geom, o.geom) AS fraction, - ST_ClosestPoint(s.geom, o.geom) AS fraction_geom - FROM modified_network s, origin o - WHERE ST_Intersects(s.geom, o.buffer_geom) - ORDER BY o.id, ST_ClosestPoint(s.geom, o.geom) <-> o.geom - ) - SELECT - bs.id, bs.class_, bs.impedance_slope, - bs.impedance_slope_reverse, bs.impedance_surface, - bs.maxspeed_forward, bs.maxspeed_backward, bs."source", - bs.target, bs.geom, bs.h3_3, bs.h3_6, - ARRAY_AGG(bs.fraction) AS fraction, - ARRAY_AGG(bs.fraction_geom) AS fraction_geom, - ARRAY_AGG(bs.point_id) AS point_id, - ARRAY_AGG(bs.point_geom) AS point_geom - FROM (SELECT * FROM best_segment ORDER BY fraction) bs - GROUP BY - bs.id, bs.class_, bs.impedance_slope, bs.impedance_slope_reverse, - bs.impedance_surface, bs.maxspeed_forward, bs.maxspeed_backward, - bs."source", bs.target, bs.geom, bs.h3_3, bs.h3_6;' - , origin_points_table, num_origin_points, edge_network_table, classes, - network_modifications_table, network_modifications_table, classes); - - LOOP - FETCH custom_cursor INTO origin_segment; - EXIT WHEN NOT FOUND; - - -- Assign values carried over from origin segment - artificial_segment.old_id = origin_segment.id; - artificial_segment.class_ = origin_segment.class_; - artificial_segment.impedance_slope = origin_segment.impedance_slope; - artificial_segment.impedance_slope_reverse = origin_segment.impedance_slope_reverse; - artificial_segment.impedance_surface = origin_segment.impedance_surface; - artificial_segment.maxspeed_forward = origin_segment.maxspeed_forward; - artificial_segment.maxspeed_backward = origin_segment.maxspeed_backward; - artificial_segment.h3_3 = origin_segment.h3_3; - artificial_segment.h3_6 = origin_segment.h3_6; - - IF origin_segment.fraction[1] != 0 THEN - -- Generate the first artifical segment for this origin segment - artificial_segment.point_id = NULL; - artificial_segment.point_geom = NULL; - artificial_segment.point_cell_index = NULL; - artificial_segment.point_h3_3 = NULL; - artificial_segment.id = artificial_seg_index; - new_geom = ST_LineSubstring(origin_segment.geom, 0, origin_segment.fraction[1]); - artificial_segment.length_m = ST_Length(new_geom::geography); - artificial_segment.length_3857 = ST_Length(ST_Transform(new_geom, 3857)); - artificial_segment.coordinates_3857 = (ST_AsGeoJSON(ST_Transform(new_geom, 3857))::jsonb)->'coordinates'; - artificial_segment.source = origin_segment.source; - IF origin_segment.fraction[1] = 1 THEN - artificial_segment.target = origin_segment.target; - ELSE - artificial_segment.target = artificial_con_index; - artificial_con_index = artificial_con_index + 1; - END IF; - artificial_segment.geom = new_geom; - RETURN NEXT artificial_segment; - artificial_seg_index = artificial_seg_index + 1; - END IF; - - -- Iterate over fractions if the origin segment is the origin for multiple isochrone starting points - IF array_length(origin_segment.fraction, 1) > 1 THEN - FOR i IN 2..array_length(origin_segment.fraction, 1) LOOP - -- Generate an artificial segment connecting the origin point to the new artificial segment - artificial_segment.point_id = origin_segment.point_id[i - 1]; - artificial_segment.point_geom = origin_segment.point_geom[i - 1]; - artificial_segment.point_cell_index = h3_lat_lng_to_cell(artificial_segment.point_geom::point, point_cell_resolution); - artificial_segment.point_h3_3 = basic.to_short_h3_3(h3_lat_lng_to_cell(artificial_segment.point_geom::point, 3)::bigint); - artificial_segment.id = artificial_seg_index; - new_geom = ST_SetSRID(ST_MakeLine( - origin_segment.point_geom[i - 1], - origin_segment.fraction_geom[i - 1] - ), 4326); - artificial_segment.length_m = ST_Length(new_geom::geography); - artificial_segment.length_3857 = ST_Length(ST_Transform(new_geom, 3857)); - artificial_segment.coordinates_3857 = (ST_AsGeoJSON(ST_Transform(new_geom, 3857))::jsonb)->'coordinates'; - artificial_segment.maxspeed_forward = 30; - artificial_segment.maxspeed_backward = 30; - artificial_segment.source = artifical_origin_index; - artifical_origin_index = artifical_origin_index + 1; - IF origin_segment.fraction[i - 1] = 0 THEN - artificial_segment.target = origin_segment.source; - ELSIF origin_segment.fraction[i] = 1 THEN - artificial_segment.target = origin_segment.target; - ELSE - artificial_segment.target = artificial_con_index - 1; - END IF; - artificial_segment.geom = new_geom; - RETURN NEXT artificial_segment; - artificial_seg_index = artificial_seg_index + 1; - - IF origin_segment.fraction[i] != origin_segment.fraction[i - 1] THEN - artificial_segment.point_id = NULL; - artificial_segment.point_geom = NULL; - artificial_segment.point_cell_index = NULL; - artificial_segment.point_h3_3 = NULL; - artificial_segment.id = artificial_seg_index; - new_geom = ST_LineSubstring(origin_segment.geom, origin_segment.fraction[i - 1], origin_segment.fraction[i]); - artificial_segment.length_m = ST_Length(new_geom::geography); - artificial_segment.length_3857 = ST_Length(ST_Transform(new_geom, 3857)); - artificial_segment.coordinates_3857 = (ST_AsGeoJSON(ST_Transform(new_geom, 3857))::jsonb)->'coordinates'; - IF origin_segment.fraction[i - 1] = 0 THEN - artificial_segment.source = origin_segment.source; - ELSE - artificial_segment.source = artificial_con_index - 1; - END IF; - IF origin_segment.fraction[i] = 1 THEN - artificial_segment.target = origin_segment.target; - ELSE - artificial_segment.target = artificial_con_index; - artificial_con_index = artificial_con_index + 1; - END IF; - artificial_segment.geom = new_geom; - RETURN NEXT artificial_segment; - artificial_seg_index = artificial_seg_index + 1; - END IF; - END LOOP; - END IF; - - -- Generate an artificial segment connecting the origin point to the new artificial segment - artificial_segment.point_id = origin_segment.point_id[array_length(origin_segment.point_id, 1)]; - artificial_segment.point_geom = origin_segment.point_geom[array_length(origin_segment.point_geom, 1)]; - artificial_segment.point_cell_index = h3_lat_lng_to_cell(artificial_segment.point_geom::point, point_cell_resolution); - artificial_segment.point_h3_3 = basic.to_short_h3_3(h3_lat_lng_to_cell(artificial_segment.point_geom::point, 3)::bigint); - artificial_segment.id = artificial_seg_index; - new_geom = ST_SetSRID(ST_MakeLine( - origin_segment.point_geom[array_length(origin_segment.point_geom, 1)], - origin_segment.fraction_geom[array_length(origin_segment.fraction_geom, 1)] - ), 4326); - artificial_segment.length_m = ST_Length(new_geom::geography); - artificial_segment.length_3857 = ST_Length(ST_Transform(new_geom, 3857)); - artificial_segment.coordinates_3857 = (ST_AsGeoJSON(ST_Transform(new_geom, 3857))::jsonb)->'coordinates'; - artificial_segment.maxspeed_forward = 30; - artificial_segment.maxspeed_backward = 30; - artificial_segment.source = artifical_origin_index; - artifical_origin_index = artifical_origin_index + 1; - IF origin_segment.fraction[array_length(origin_segment.fraction, 1)] = 0 THEN - artificial_segment.target = origin_segment.source; - ELSIF origin_segment.fraction[array_length(origin_segment.fraction, 1)] = 1 THEN - artificial_segment.target = origin_segment.target; - ELSE - artificial_segment.target = artificial_con_index - 1; - END IF; - artificial_segment.geom = new_geom; - RETURN NEXT artificial_segment; - artificial_seg_index = artificial_seg_index + 1; - - IF origin_segment.fraction[array_length(origin_segment.fraction, 1)] != 1 THEN - -- Generate the last artificial segment for this origin segment - artificial_segment.point_id = NULL; - artificial_segment.point_geom = NULL; - artificial_segment.point_cell_index = NULL; - artificial_segment.point_h3_3 = NULL; - artificial_segment.id = artificial_seg_index; - new_geom = ST_LineSubstring(origin_segment.geom, origin_segment.fraction[array_length(origin_segment.fraction, 1)], 1); - artificial_segment.length_m = ST_Length(new_geom::geography); - artificial_segment.length_3857 = ST_Length(ST_Transform(new_geom, 3857)); - artificial_segment.coordinates_3857 = (ST_AsGeoJSON(ST_Transform(new_geom, 3857))::jsonb)->'coordinates'; - IF origin_segment.fraction[array_length(origin_segment.fraction, 1)] = 0 THEN - artificial_segment.source = origin_segment.source; - ELSE - artificial_segment.source = artificial_con_index - 1; - END IF; - artificial_segment.target = origin_segment.target; - artificial_segment.geom = new_geom; - RETURN NEXT artificial_segment; - artificial_seg_index = artificial_seg_index + 1; - END IF; - - END LOOP; - - CLOSE custom_cursor; - -END; -$function$ diff --git a/src/db/functions/get_network_modifications.sql b/src/db/functions/get_network_modifications.sql deleted file mode 100644 index 74edc06..0000000 --- a/src/db/functions/get_network_modifications.sql +++ /dev/null @@ -1,636 +0,0 @@ -DROP FUNCTION IF EXISTS basic.produce_network_modifications; -CREATE OR REPLACE FUNCTION basic.produce_network_modifications( - scenario_id_input UUID, - edge_layer_project_id INTEGER, - node_layer_project_id INTEGER, - network_modifications_table TEXT -) -RETURNS void -LANGUAGE plpgsql -AS $function$ -DECLARE - edge_layer_id UUID := (SELECT layer_id FROM customer.layer_project WHERE id = edge_layer_project_id); - edge_network_table TEXT := 'user_data.street_network_line_' || REPLACE(( - SELECT user_id FROM customer.layer WHERE id = edge_layer_id - )::TEXT, '-', ''); - node_layer_id UUID := (SELECT layer_id FROM customer.layer_project WHERE id = node_layer_project_id); - node_network_table TEXT := 'user_data.street_network_point_' || REPLACE(( - SELECT user_id FROM customer.layer WHERE id = node_layer_id - )::TEXT, '-', ''); - - cnt integer; - rec record; - - max_node_id integer; - max_node_id_increment integer := 1; - max_edge_id integer; - max_edge_id_increment integer := 1; -BEGIN - --------------------------------------------------------------------------------------------------------------------- - --Proceed only if the scenario contains features which apply to the specified edge layer - --------------------------------------------------------------------------------------------------------------------- - - -- Create network modifications table - EXECUTE FORMAT(' - DROP TABLE IF EXISTS %I; - CREATE TABLE %I ( - edit_type TEXT, id INTEGER, class_ TEXT, source INTEGER, - target INTEGER, length_m FLOAT8, length_3857 FLOAT8, - coordinates_3857 JSONB, impedance_slope FLOAT8, - impedance_slope_reverse FLOAT8, impedance_surface FLOAT8, - maxspeed_forward INTEGER, maxspeed_backward INTEGER, - geom GEOMETRY(LINESTRING, 4326), h3_6 INTEGER, h3_3 INTEGER - ); - ', network_modifications_table, network_modifications_table); - - IF NOT EXISTS ( - SELECT 1 - FROM customer.scenario_scenario_feature ssf - INNER JOIN customer.scenario_feature sf ON sf.id = ssf.scenario_feature_id - WHERE ssf.scenario_id = scenario_id_input - AND sf.layer_project_id = edge_layer_project_id - ) THEN - RETURN; - END IF; - - --------------------------------------------------------------------------------------------------------------------- - --Prepare Table - --------------------------------------------------------------------------------------------------------------------- - - EXECUTE FORMAT('SELECT max(node_id) FROM %s;', node_network_table) INTO max_node_id; - EXECUTE FORMAT('SELECT max(edge_id) FROM %s;', edge_network_table) INTO max_edge_id; - - DROP TABLE IF EXISTS modified_attributes_only; - EXECUTE FORMAT(' - CREATE TEMP TABLE modified_attributes_only AS - SELECT w.*, e.edge_id AS original_id - FROM %s e, ( - SELECT sf.* - FROM customer.scenario_scenario_feature ssf - INNER JOIN customer.scenario_feature sf ON sf.id = ssf.scenario_feature_id - WHERE ssf.scenario_id = %L - AND sf.layer_project_id = %s - ) w - WHERE e.h3_3 = w.h3_3 - AND e.id = w.feature_id - AND ST_ASTEXT(ST_ReducePrecision(w.geom,0.00001)) = ST_ASTEXT(ST_ReducePrecision(e.geom,0.00001)) - AND edit_type = ''m''; - ', edge_network_table, scenario_id_input, edge_layer_project_id); - CREATE INDEX ON modified_attributes_only USING GIST(geom); - - DROP TABLE IF EXISTS drawn_features; - EXECUTE FORMAT(' - CREATE TEMP TABLE drawn_features AS - WITH scenario_features AS ( - SELECT sf.*, ssf.scenario_id - FROM customer.scenario_scenario_feature ssf - INNER JOIN customer.scenario_feature sf ON sf.id = ssf.scenario_feature_id - WHERE ssf.scenario_id = %L - AND sf.layer_project_id = %s - AND sf.edit_type IN (''n'', ''m'') - ) - SELECT w.id, ST_RemoveRepeatedPoints(w.geom) AS geom, ''road'' AS way_type, - e.class_, e.impedance_surface, e.maxspeed_forward, e.maxspeed_backward, - w.scenario_id, e.edge_id AS original_id, e.h3_6, e.h3_3 - FROM %s e, - scenario_features w - WHERE w.feature_id IS NOT NULL - AND e.h3_3 = w.h3_3 - AND e.id = w.feature_id - UNION ALL - SELECT w.id, ST_RemoveRepeatedPoints(w.geom) AS geom, ''road'' AS way_type, - ''tertiary'' AS class_, NULL AS impedance_surface, 50 AS maxspeed_forward, - 50 AS maxspeed_backward, w.scenario_id, NULL AS original_id, NULL AS h3_6, - NULL AS h3_3 - FROM scenario_features w - WHERE w.feature_id IS NULL; - ', scenario_id_input, edge_layer_project_id, edge_network_table); - CREATE INDEX ON drawn_features USING GIST(geom); - - --------------------------------------------------------------------------------------------------------------------- - --Snap start and end points - --------------------------------------------------------------------------------------------------------------------- - /*Round start and end point for snapping*/ - DROP TABLE IF EXISTS snapped_drawn_features; - CREATE TEMP TABLE snapped_drawn_features AS - WITH start_end_point AS - ( - SELECT d.id AS did, st_startpoint(d.geom) geom, 's' AS point_type, FALSE AS snapped, NULL::integer AS node_id - FROM drawn_features d - UNION ALL - SELECT d.id AS did, st_endpoint(d.geom) geom, 'e' AS point_type, FALSE AS snapped, NULL AS node_id - FROM drawn_features d - ), - clusters AS - ( - SELECT did, geom, ST_ClusterDBSCAN(geom, eps := 0.00001, minpoints := 1) OVER() AS cid, point_type - FROM start_end_point - ), - grouped AS - ( - SELECT ARRAY_AGG(did) AS did, ST_CENTROID(ST_COLLECT(geom)) AS geom, ARRAY_AGG(point_type)::text[] AS point_types - FROM clusters - GROUP BY cid - ) - SELECT UNNEST(did) AS did, geom, UNNEST(point_types) AS point_type, FALSE AS snapped, - NULL::integer AS node_id, ARRAY_LENGTH(point_types, 1) AS group_size, - basic.to_short_h3_3(h3_lat_lng_to_cell(geom::point, 3)::bigint) AS h3_3 - FROM grouped; - - ALTER TABLE snapped_drawn_features ADD COLUMN id serial; - CREATE INDEX ON snapped_drawn_features USING GIST(geom); - CREATE INDEX ON snapped_drawn_features (id); - - /*Snapping to existing Nodes*/ - DROP TABLE IF EXISTS snapped_to_node; - EXECUTE FORMAT(' - CREATE TEMP TABLE snapped_to_node AS - SELECT r.id, r.did, r.point_type, r.geom original_geom, - s.geom node_geom, s.node_id, s.h3_3 - FROM snapped_drawn_features r - CROSS JOIN LATERAL - ( - SELECT n.node_id, n.geom, n.h3_3 - FROM %s n - WHERE ST_Intersects(ST_BUFFER(r.geom,0.00001), n.geom) - ORDER BY r.geom <-> n.geom - LIMIT 1 - ) s; - CREATE INDEX ON snapped_to_node USING GIST(node_geom); - - UPDATE snapped_drawn_features d - SET geom = node_geom, snapped = TRUE, node_id = s.node_id, h3_3 = s.h3_3 - FROM snapped_to_node s - WHERE s.did = d.did - AND d.point_type = s.point_type; - ', node_network_table); - - /*Snapping to existing edges*/ - DROP TABLE IF EXISTS snapped_to_edge; - EXECUTE FORMAT(' - CREATE TEMP TABLE snapped_to_edge AS - WITH closest_points AS ( - SELECT - r.id, r.did, r.point_type, r.geom AS original_geom, ST_CLOSESTPOINT(n.geom, r.geom) AS closest_point_edge, - basic.to_short_h3_3(h3_lat_lng_to_cell(ST_CLOSESTPOINT(n.geom, r.geom)::point, 3)::bigint) AS h3_3, - ROW_NUMBER() OVER (PARTITION BY r.geom ORDER BY r.geom <-> ST_CLOSESTPOINT(n.geom, r.geom)) AS rnk - FROM snapped_drawn_features r - JOIN %s n - ON n.h3_3 = r.h3_3 - WHERE ST_Intersects(ST_BUFFER(r.geom, 0.00001), n.geom) - AND r.snapped = False - ) - SELECT id, did, point_type, original_geom, closest_point_edge, h3_3 - FROM closest_points - WHERE rnk = 1; - ', edge_network_table); - - /*Update based on snapped to new*/ - UPDATE snapped_drawn_features d - SET geom = closest_point_edge, snapped = True, h3_3 = s.h3_3 - FROM snapped_to_edge s - WHERE s.did = d.did - AND d.point_type = s.point_type; - - /*Snapping to each other*/ - DROP TABLE IF EXISTS snapped_to_each_other; - CREATE TEMP TABLE snapped_to_each_other AS - SELECT r.id, r.did, r.point_type, r.geom original_geom, s.geom closest_point_edge - FROM snapped_drawn_features r - CROSS JOIN LATERAL - ( - SELECT n.id, ST_CLOSESTPOINT(n.geom, r.geom) AS geom - FROM drawn_features n - WHERE ST_Intersects(ST_BUFFER(r.geom,0.00001), n.geom) - AND r.did <> n.id - ORDER BY r.geom <-> ST_CLOSESTPOINT(n.geom, r.geom) - LIMIT 1 - ) s - WHERE r.snapped = FALSE - AND r.group_size = 1 - UNION ALL - SELECT s.id, s.did, s.point_type, s.geom, s.geom - FROM snapped_drawn_features s - WHERE s.group_size > 1; - - /*Update based on snapped to each other*/ - UPDATE snapped_drawn_features d - SET geom = closest_point_edge, snapped = True - FROM snapped_to_each_other s - WHERE s.did = d.did - AND d.point_type = s.point_type; - - /*Update drawn features*/ - UPDATE drawn_features d - SET geom = st_setpoint(d.geom, 0, s.geom) - FROM snapped_drawn_features s - WHERE d.id = s.did - AND s.snapped = TRUE - AND s.point_type = 's'; - - UPDATE drawn_features d - SET geom = st_setpoint(d.geom, -1, s.geom) - FROM snapped_drawn_features s - WHERE d.id = s.did - AND s.snapped = TRUE - AND s.point_type = 'e'; - - UPDATE drawn_features d - SET geom = st_setpoint(d.geom, 0, s.geom) - FROM snapped_drawn_features s - WHERE s.snapped = FALSE - AND s.point_type = 's' - AND d.id = s.did; - - UPDATE drawn_features d - SET geom = st_setpoint(d.geom, -1, s.geom) - FROM snapped_drawn_features s - WHERE s.snapped = FALSE - AND s.point_type = 'e' - AND d.id = s.did; - - --------------------------------------------------------------------------------------------------------------------- - --Cut network - --------------------------------------------------------------------------------------------------------------------- - - /*Extend lines to cut network*/ - DROP TABLE IF EXISTS extended_lines; - CREATE TEMP TABLE extended_lines AS - WITH agg_snapped_nodes AS - ( - SELECT d.id, ARRAY_AGG(point_type) AS point_type - FROM snapped_to_node s, drawn_features d - WHERE d.id = s.did - GROUP BY d.id - ) - SELECT CASE WHEN ARRAY['e', 's'] && point_type THEN d.geom - WHEN ARRAY['s'] = point_type THEN basic.extend_line(d.geom, 0.00001, 'end') - WHEN ARRAY['e'] = point_type THEN basic.extend_line(d.geom, 0.00001, 'start') - END AS geom, d.original_id - FROM agg_snapped_nodes a, drawn_features d - WHERE a.id = d.id - AND (d.way_type = 'road' OR d.way_type IS NULL) - UNION ALL - SELECT basic.extend_line(d.geom, 0.00001, 'both'), d.original_id - FROM drawn_features d - LEFT JOIN snapped_to_node s - ON d.id = s.did - WHERE s.id IS NULL - AND (d.way_type = 'road' OR d.way_type IS NULL); - - /*Intersects drawn bridges*/ - DROP TABLE IF EXISTS start_end_bridges; - CREATE TEMP TABLE start_end_bridges AS - SELECT st_startpoint(geom) AS geom, - basic.to_short_h3_3(h3_lat_lng_to_cell(st_startpoint(geom)::point, 3)::bigint) AS h3_3 - FROM drawn_features - WHERE way_type = 'bridge' - UNION - SELECT ST_endpoint(geom) AS geom, - basic.to_short_h3_3(h3_lat_lng_to_cell(st_endpoint(geom)::point, 3)::bigint) AS h3_3 - FROM drawn_features - WHERE way_type = 'bridge'; - CREATE INDEX ON start_end_bridges USING GIST(geom); - - /*Intersect drawn ways with existing ways*/ - DROP TABLE IF EXISTS intersection_existing_network; - EXECUTE FORMAT(' - CREATE TEMP TABLE intersection_existing_network AS - WITH intersection_result AS - ( - SELECT (ST_DUMP(ST_Intersection(d.geom, w.geom))).geom AS geom, w.edge_id AS id - FROM extended_lines d, %s w - WHERE ST_Intersects(ST_BUFFER(d.geom, 0.00001), w.geom) - ) - SELECT i.* - FROM intersection_result i - LEFT JOIN extended_lines e - ON i.id = e.original_id - WHERE e.original_id IS NULL - AND st_geometrytype(i.geom) = ''ST_Point''; - ', edge_network_table); - - EXECUTE FORMAT(' - INSERT INTO intersection_existing_network (geom) - WITH closest_points AS ( - SELECT - %L AS scenario_id, - ST_CLOSESTPOINT(w.geom, s.geom) AS closest_point, - ST_LineLocatePoint(w.geom, s.geom) AS fraction, - s.geom AS s_geom, - ROW_NUMBER() OVER (PARTITION BY s.geom ORDER BY ST_CLOSESTPOINT(w.geom, s.geom) <-> s.geom) AS rnk - FROM start_end_bridges s - JOIN %s w - ON w.h3_3 = s.h3_3 - WHERE ST_Intersects(ST_Buffer(s.geom, 0.00001), w.geom) - ) - SELECT closest_point - FROM closest_points - WHERE rnk = 1 - AND NOT EXISTS ( - SELECT 1 - FROM intersection_existing_network i - WHERE ST_Intersects(ST_Buffer(closest_points.closest_point, 0.00001), i.geom) - ); - ', scenario_id_input, edge_network_table); - - DROP TABLE IF EXISTS distinct_intersection_existing_network; - CREATE TABLE distinct_intersection_existing_network AS - SELECT DISTINCT geom - FROM intersection_existing_network i; - - CREATE INDEX ON distinct_intersection_existing_network USING GIST(geom); - ALTER TABLE distinct_intersection_existing_network ADD COLUMN id serial; - ALTER TABLE distinct_intersection_existing_network ADD PRIMARY key(id); - - /*Filter out snapped start or end point*/ - DELETE FROM intersection_existing_network h - USING - ( - SELECT h.geom - FROM snapped_to_node n, distinct_intersection_existing_network h - WHERE ST_Intersects(ST_BUFFER(n.node_geom,0.00001), h.geom) - ) d - WHERE h.geom = d.geom; - - DROP TABLE IF EXISTS split_drawn_features; - /*Split network with itself*/ - SELECT count(*) - INTO cnt - FROM drawn_features - WHERE (way_type IS NULL OR way_type <> 'bridge') - LIMIT 2; - - IF cnt <= 1 THEN - CREATE TEMP TABLE split_drawn_features as - SELECT id as did, geom, class_, impedance_surface, maxspeed_forward, - maxspeed_backward, way_type, scenario_id, original_id, h3_6, h3_3 - FROM drawn_features; - ELSE - CREATE TEMP TABLE split_drawn_features AS - SELECT id AS did, basic.split_by_drawn_lines(id, geom) AS geom, class_, - impedance_surface, maxspeed_forward, maxspeed_backward, way_type, - scenario_id, original_id, h3_6, h3_3 - FROM drawn_features x - WHERE (way_type IS NULL OR way_type <> 'bridge') - UNION ALL - SELECT id AS did, geom, class_, impedance_surface, maxspeed_forward, - maxspeed_backward, way_type, scenario_id, original_id, h3_6, h3_3 - FROM drawn_features - WHERE way_type = 'bridge'; - END IF; - CREATE INDEX ON split_drawn_features USING GIST(geom); - - /*Create perpendicular lines to split new network*/ - DROP TABLE IF EXISTS perpendicular_split_lines; - CREATE TEMP TABLE perpendicular_split_lines AS - SELECT basic.create_intersection_line(i.geom, 0.000001) AS geom - FROM intersection_existing_network i; - - DROP TABLE IF EXISTS union_perpendicular_split_lines; - CREATE TEMP TABLE union_perpendicular_split_lines AS - SELECT ST_Union(geom) AS geom - FROM perpendicular_split_lines p; - - /*Split new network with existing network*/ - DROP TABLE IF EXISTS new_network; - CREATE TEMP TABLE new_network AS - SELECT d.did, (dp.geom).geom, class_, impedance_surface, maxspeed_forward, - maxspeed_backward, way_type, scenario_id, original_id, 'geom' AS edit_type, - h3_6, h3_3 - FROM split_drawn_features d, union_perpendicular_split_lines w, - LATERAL (SELECT ST_DUMP(ST_CollectionExtract(ST_SPLIT(d.geom,w.geom),2)) AS geom) dp - WHERE (d.way_type IS NULL OR d.way_type <> 'bridge'); - CREATE INDEX ON new_network USING GIST(geom); - - /*Delete extended part*/ - DELETE FROM new_network - WHERE st_length(geom) < 0.0000011; - - /*Inject drawn bridges*/ - INSERT INTO new_network(did, geom, way_type, scenario_id, original_id, edit_type) - SELECT id, geom, way_type, scenario_id, original_id, 'geom' - FROM drawn_features - WHERE way_type = 'bridge'; - - ALTER TABLE new_network ADD COLUMN id serial; - ALTER TABLE new_network ADD COLUMN source integer; - ALTER TABLE new_network ADD COLUMN target integer; - - --------------------------------------------------------------------------------------------------------------------- - --Prepare source and target - --------------------------------------------------------------------------------------------------------------------- - /*Existing network is split using perpendicular lines*/ - DROP TABLE IF EXISTS existing_network; - EXECUTE FORMAT(' - CREATE TEMP TABLE existing_network as - SELECT w.edge_id AS original_id, (dp.geom).geom, w.source, w.target, - w.class_, w.impedance_slope, w.impedance_slope_reverse, w.impedance_surface, - w.maxspeed_forward, w.maxspeed_backward, w.h3_6, w.h3_3 - FROM %s w, union_perpendicular_split_lines p, - LATERAL (SELECT ST_DUMP(ST_CollectionExtract(ST_SPLIT(w.geom,p.geom),2)) AS geom) dp - WHERE ST_Intersects(w.geom, p.geom) - AND w.edge_id NOT IN (SELECT original_id FROM modified_attributes_only); - ALTER TABLE existing_network ADD COLUMN id serial; - ALTER TABLE existing_network ADD PRIMARY KEY(id); - CREATE INDEX ON existing_network USING GIST(geom); - ', edge_network_table); - - /*Assign vertices that where snapped to new features*/ - UPDATE new_network n - SET SOURCE = s.node_id - FROM snapped_drawn_features s - WHERE n.did = s.did - AND s.node_id IS NOT NULL - AND s.point_type = 's' - AND ST_ASTEXT(st_startpoint(n.geom)) = ST_ASTEXT(s.geom); - - UPDATE new_network n - SET target = s.node_id - FROM snapped_drawn_features s - WHERE n.did = s.did - AND s.node_id IS NOT NULL - AND ST_ASTEXT(st_endpoint(n.geom)) = ST_ASTEXT(s.geom); - - /*Create new vertices*/ - DROP TABLE IF EXISTS loop_vertices; - CREATE TEMP TABLE loop_vertices AS - WITH start_end_point AS - ( - SELECT e.id, st_startpoint(geom) geom, 's' AS point_type - FROM new_network e - WHERE SOURCE IS NULL - UNION ALL - SELECT e.id, st_endpoint(geom) geom, 'e' AS point_type - FROM new_network e - WHERE target IS NULL - ), - clusters AS - ( - SELECT s.id, s.geom, ST_ClusterDBSCAN(geom, eps := 0.000001, minpoints := 1) OVER() AS cid, point_type - FROM start_end_point s - ), - grouped AS - ( - SELECT ST_CENTROID(ST_COLLECT(geom)) AS geom, ARRAY_AGG(point_type)::text[] AS point_types, ARRAY_AGG(id)::integer[] new_network_ids - FROM clusters c - GROUP BY cid - ) - SELECT geom, point_types, new_network_ids - FROM grouped; - - DROP TABLE IF EXISTS new_vertices; - CREATE TEMP TABLE new_vertices - ( - node_id integer, - new_network_ids integer[], - point_types text[], - geom geometry - ); - /* - DO $$ - DECLARE - rec record; - max_id integer; - BEGIN*/ - FOR rec IN SELECT * FROM loop_vertices v - LOOP - INSERT INTO new_vertices(node_id, new_network_ids, point_types, geom) - SELECT max_node_id + max_node_id_increment AS id, rec.new_network_ids, - rec.point_types, rec.geom; - max_node_id_increment := max_node_id_increment + 1; - END LOOP; - /*END $$;*/ - CREATE INDEX ON new_vertices USING GIST(geom); - - WITH unnest_to_update AS - ( - SELECT v.node_id, UNNEST(v.new_network_ids) new_network_id, UNNEST(v.point_types) point_type - FROM new_vertices v - ) - UPDATE new_network n - SET SOURCE = u.node_id - FROM unnest_to_update u - WHERE n.id = u.new_network_id - AND point_type = 's'; - - WITH unnest_to_update AS - ( - SELECT v.node_id, UNNEST(v.new_network_ids) new_network_id, UNNEST(v.point_types) point_type - FROM new_vertices v - ) - UPDATE new_network n - SET target = u.node_id - FROM unnest_to_update u - WHERE n.id = u.new_network_id - AND point_type = 'e'; - - DROP TABLE IF EXISTS new_source_target_existing; - CREATE TEMP TABLE new_source_target_existing AS - WITH start_and_end AS - ( - SELECT e.id, st_startpoint(geom) geom, 's' AS point_type - FROM existing_network e - UNION ALL - SELECT e.id, st_endpoint(geom) geom, 'e' AS point_type - FROM existing_network e - ) - SELECT v.id, point_type, c.node_id, v.geom - FROM start_and_end v - CROSS JOIN LATERAL - ( - SELECT n.node_id - FROM new_vertices n - WHERE ST_Intersects(ST_BUFFER(v.geom, 0.00001), n.geom) - ORDER BY n.geom <-> v.geom - LIMIT 1 - ) c; - - UPDATE existing_network e - SET SOURCE = n.node_id - FROM new_source_target_existing n - WHERE e.id = n.id - AND n.point_type = 's'; - - UPDATE existing_network e - SET target = n.node_id - FROM new_source_target_existing n - WHERE e.id = n.id - AND n.point_type = 'e'; - - ---------------------------------------------------------------------------------------------------------------------- - --PRODUCE FINAL LIST OF NETWORK MODIFICATIONS (SIMPLIFIED INTO ADDITIONS AND DELETIONS) - ---------------------------------------------------------------------------------------------------------------------- - - -- Edges to be explicitly deleted according to the scenario - EXECUTE FORMAT(' - INSERT INTO %I (edit_type, id, h3_6, h3_3) - SELECT ''d'' AS edit_type, e.edge_id AS id, e.h3_6, e.h3_3 - FROM %s e, ( - SELECT sf.* - FROM customer.scenario_scenario_feature ssf - INNER JOIN customer.scenario_feature sf ON sf.id = ssf.scenario_feature_id - WHERE ssf.scenario_id = %L - AND sf.layer_project_id = %s - AND sf.edit_type = ''d'' - ) w - WHERE e.h3_3 = w.h3_3 - AND e.id = w.feature_id; - ', network_modifications_table, edge_network_table, scenario_id_input, edge_layer_project_id); - - -- Existing edges to be deleted due to a modified or new edge replacing it - EXECUTE FORMAT(' - INSERT INTO %I (edit_type, id, h3_6, h3_3) - SELECT ''d'' AS edit_type, original_id AS id, h3_6, h3_3 - FROM existing_network - UNION - SELECT ''d'' AS edit_type, original_id AS id, h3_6, h3_3 - FROM new_network - WHERE original_id IS NOT NULL; - ', network_modifications_table); - - -- Create temp table to store all new edges before copying them into the final network modifications table - DROP TABLE IF EXISTS new_edges; - EXECUTE FORMAT(' - CREATE TEMP TABLE new_edges AS - SELECT * FROM %I LIMIT 0; - ', network_modifications_table); - - -- Modified edges to be added to the network - FOR rec IN SELECT e.* - FROM existing_network e - LEFT JOIN new_network n ON e.original_id = n.original_id - WHERE n.original_id IS NULL - LOOP - INSERT INTO new_edges - SELECT 'n' AS edit_type, max_edge_id + max_edge_id_increment AS id, rec.class_, rec.source, rec.target, - ST_Length(rec.geom::geography) AS length_m, ST_Length(ST_Transform(rec.geom, 3857)) AS length_3857, - ST_AsGeoJSON(ST_Transform(rec.geom, 3857))::JSONB->'coordinates' AS coordinates_3857, - rec.impedance_slope, rec.impedance_slope_reverse, rec.impedance_surface, - rec.maxspeed_forward, rec.maxspeed_backward, rec.geom, rec.h3_6, rec.h3_3; - max_edge_id_increment := max_edge_id_increment + 1; - END LOOP; - - -- New edges to be added to the network - -- TODO: Compute slope impedances - FOR rec IN SELECT * FROM new_network - LOOP - INSERT INTO new_edges - SELECT 'n' as edit_type, max_edge_id + max_edge_id_increment AS id, rec.class_, rec.source, rec.target, - ST_Length(rec.geom::geography) AS length_m, ST_Length(ST_Transform(rec.geom, 3857)) AS length_3857, - ST_AsGeoJSON(ST_Transform(rec.geom, 3857))::JSONB->'coordinates' AS coordinates_3857, - NULL AS impedance_slope, NULL AS impedance_slope_reverse, rec.impedance_surface, - rec.maxspeed_forward, rec.maxspeed_backward, rec.geom, - basic.to_short_h3_6(h3_lat_lng_to_cell(ST_Centroid(rec.geom)::point, 6)::bigint) AS h3_6, - basic.to_short_h3_3(h3_lat_lng_to_cell(ST_Centroid(rec.geom)::point, 3)::bigint) AS h3_3; - max_edge_id_increment := max_edge_id_increment + 1; - END LOOP; - - -- Copy new edges into the final network modifications table - EXECUTE FORMAT(' - INSERT INTO %I - SELECT * FROM new_edges; - ', network_modifications_table); - -END -$function$; diff --git a/src/db/functions/split_by_drawn_lines.sql b/src/db/functions/split_by_drawn_lines.sql deleted file mode 100644 index fc45a2c..0000000 --- a/src/db/functions/split_by_drawn_lines.sql +++ /dev/null @@ -1,37 +0,0 @@ -CREATE OR REPLACE FUNCTION basic.split_by_drawn_lines(id_input UUID, input_geom geometry) - RETURNS SETOF geometry - LANGUAGE plpgsql - AS $function$ - DECLARE - union_geom geometry; - does_intersect boolean := FALSE; - BEGIN - - does_intersect = ( - SELECT TRUE - FROM drawn_features d - WHERE ST_Intersects(basic.extend_line(d.geom, 0.00001, 'both'), - (SELECT geom FROM drawn_features WHERE id = id_input)) - LIMIT 1 - ); - - IF does_intersect = TRUE THEN - union_geom = - ( - SELECT ST_UNION(basic.extend_line(geom, 0.00001, 'both')) AS geom - FROM drawn_features - WHERE id <> id_input - AND ST_Intersects(input_geom, basic.extend_line(geom, 0.00001, 'both')) - AND (way_type IS NULL OR way_type <> 'bridge') - ); - END IF; - - IF union_geom IS NOT NULL THEN - RETURN query - SELECT (dump).geom - FROM (SELECT ST_DUMP(ST_CollectionExtract(ST_SPLIT(input_geom, union_geom),2)) AS dump) d; - ELSE - RETURN query SELECT input_geom; - END IF; - END -$function$; diff --git a/src/main.py b/src/main.py index 2a5aaa7..4eae3c9 100644 --- a/src/main.py +++ b/src/main.py @@ -1,16 +1,22 @@ import logging -import os from contextlib import asynccontextmanager -from fastapi import FastAPI, Request, status +import sentry_sdk +from fastapi import FastAPI from fastapi.openapi.docs import get_swagger_ui_html -from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles from starlette.middleware.cors import CORSMiddleware from src.core.config import settings from src.endpoints.v2.api import router as api_router_v2 +if settings.SENTRY_DSN and settings.ENVIRONMENT: + sentry_sdk.init( + dsn=settings.SENTRY_DSN, + environment=settings.ENVIRONMENT, + traces_sample_rate=1.0 if settings.ENVIRONMENT == "prod" else 0.1, + ) + + @asynccontextmanager async def lifespan(app: FastAPI): print("Starting up...") @@ -29,6 +35,7 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) + @app.get("/api/docs", include_in_schema=False) async def swagger_ui_html(): return get_swagger_ui_html( @@ -38,6 +45,7 @@ async def swagger_ui_html(): swagger_ui_parameters={"persistAuthorization": True}, ) + app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -46,4 +54,4 @@ async def swagger_ui_html(): allow_headers=["*"], ) -app.include_router(api_router_v2, prefix=settings.API_V2_STR) \ No newline at end of file +app.include_router(api_router_v2, prefix=settings.API_V2_STR) diff --git a/src/schemas/catchment_area.py b/src/schemas/catchment_area.py index acf98e2..4c18140 100644 --- a/src/schemas/catchment_area.py +++ b/src/schemas/catchment_area.py @@ -1,10 +1,12 @@ from enum import Enum -from typing import List +from typing import List, Optional from uuid import UUID import polars as pl from pydantic import BaseModel, Field, validator +from src.core.config import settings + SEGMENT_DATA_SCHEMA = { "id": pl.Int64, "length_m": pl.Float64, @@ -231,6 +233,26 @@ def valid_num_steps(cls, v): return v +class CatchmentAreaStreetNetwork(BaseModel): + def __init__(self, **data): + super().__init__(**data) + if self.node_layer_project_id is None: + self.node_layer_project_id = ( + settings.DEFAULT_STREET_NETWORK_NODE_LAYER_PROJECT_ID + ) + + edge_layer_project_id: int = Field( + ..., + title="Edge Layer Project ID", + description="The layer project ID of the street network edge layer.", + ) + node_layer_project_id: Optional[int] = Field( + default=None, + title="Node Layer Project ID", + description="The layer project ID of the street network node layer.", + ) + + class ICatchmentAreaActiveMobility(BaseModel): """Model for the active mobility catchment area request.""" @@ -257,6 +279,11 @@ class ICatchmentAreaActiveMobility(BaseModel): title="Scenario ID", description="The ID of the scenario that is to be applied on the base network.", ) + street_network: Optional[CatchmentAreaStreetNetwork] = Field( + None, + title="Street Network Layer Config", + description="The configuration of the street network layers to use.", + ) catchment_area_type: CatchmentAreaType = Field( ..., title="Return Type", @@ -278,6 +305,15 @@ class ICatchmentAreaActiveMobility(BaseModel): description="The ID of the layer the results should be saved.", ) + # Ensure street network is specified if a scenario ID is provided + @validator("street_network", pre=True, always=True) + def check_street_network(cls, v, values): + if values["scenario_id"] is not None and v is None: + raise ValueError( + "The street network must be set if a scenario ID is provided." + ) + return v + # Check that polygon difference exists if catchment area type is polygon @validator("polygon_difference", pre=True, always=True) def check_polygon_difference(cls, v, values): @@ -329,6 +365,11 @@ class ICatchmentAreaCar(BaseModel): title="Scenario ID", description="The ID of the scenario that is used for the routing.", ) + street_network: Optional[CatchmentAreaStreetNetwork] = Field( + None, + title="Street Network Layer Config", + description="The configuration of the street network layers to use.", + ) catchment_area_type: CatchmentAreaType = Field( ..., title="Return Type", @@ -350,6 +391,15 @@ class ICatchmentAreaCar(BaseModel): description="The ID of the layer the results should be saved.", ) + # Ensure street network is specified if a scenario ID is provided + @validator("street_network", pre=True, always=True) + def check_street_network(cls, v, values): + if values["scenario_id"] is not None and v is None: + raise ValueError( + "The street network must be set if a scenario ID is provided." + ) + return v + # Check that polygon difference exists if catchment area type is polygon @validator("polygon_difference", pre=True, always=True) def check_polygon_difference(cls, v, values): diff --git a/src/utils.py b/src/utils.py index beba0e7..8787537 100644 --- a/src/utils.py +++ b/src/utils.py @@ -188,3 +188,10 @@ def make_dir(dir_path: str): """Creates a new directory if it doesn't already exist""" if not os.path.exists(dir_path): os.makedirs(dir_path) + + +def format_value_null_sql(value) -> str: + if value is None: + return "NULL" + else: + return f"'{value}'"