def bankfull_nhd_area(bankfull_path, nhd_path, clip_path, espg, output_path, out_name): clip_geom = collect_feature_class(clip_path) with TempGeopackage('sanitize_temp') as tempgpkg, \ GeopackageLayer(output_path, out_name, write=True) as lyr_output: merged_path = os.path.join(tempgpkg.filepath, f"bankfull_nhd_merge_{str(uuid4())}") with GeopackageLayer(merged_path, write=True, delete_dataset=True) as tmp_lyr: tmp_lyr.create_layer(ogr.wkbPolygon, espg) # Get merged and unioned Geom merge_feature_classes([nhd_path, bankfull_path], clip_geom, merged_path) out_geom = get_geometry_unary_union(merged_path) # Write Output lyr_output.create_layer(ogr.wkbPolygon, espg) feat = ogr.Feature(lyr_output.ogr_layer_def) feat.SetGeometry(out_geom) lyr_output.create_feature(out_geom)
def vbet_network(flow_lines_path: str, flow_areas_path: str, out_path: str, epsg: int = None, fcodes: List[str] = None): log = Logger('VBET Network') log.info('Generating perennial network') fcodes = ["46006"] if fcodes is None else fcodes with get_shp_or_gpkg(out_path, write=True) as vbet_net, \ get_shp_or_gpkg(flow_lines_path) as flow_lines_lyr: # Add input Layer Fields to the output Layer if it is the one we want vbet_net.create_layer_from_ref(flow_lines_lyr, epsg=epsg) # Perennial features log.info('Incorporating perennial features') fcode_filter = "FCode = " + " or FCode = ".join([ f"'{fcode}'" for fcode in fcodes ]) if len( fcodes) > 0 else "" # e.g. "FCode = '46006' or FCode = '55800'" fids = include_features(flow_lines_lyr, vbet_net, fcode_filter) # Flow area features polygon = get_geometry_unary_union(flow_areas_path, epsg=epsg) if polygon is not None: log.info('Incorporating flow areas.') include_features(flow_lines_lyr, vbet_net, "FCode <> '46006'", polygon, excluded_fids=fids) fcount = flow_lines_lyr.ogr_layer.GetFeatureCount() log.info('VBET network generated with {} features'.format(fcount))
def confinement(huc: int, flowlines_orig: Path, confining_polygon_orig: Path, output_folder: Path, buffer_field: str, confinement_type: str, reach_codes: List[str], min_buffer: float = 0.0, bankfull_expansion_factor: float = 1.0, debug: bool = False, meta=None): """Generate confinement attribute for a stream network Args: huc (integer): Huc identifier flowlines (path): input flowlines layer confining_polygon (path): valley bottom or other boundary defining confining margins output_folder (path): location to store confinement project and output geopackage buffer_field (string): name of float field with buffer values in meters (i.e. 'BFWidth') confinement_type (string): name of type of confinement generated reach_codes (List[int]): NHD reach codes for features to include in outputs min_buffer (float): minimum bankfull value to use in buffers e.g. raster cell resolution bankfull_expansion_factor (float): factor to expand bankfull on each side of bank debug (bool): run tool in debug mode (save intermediate outputs). Default = False meta (Dict[str,str]): dictionary of riverscapes metadata key: value pairs """ log = Logger("Confinement") log.info(f'Confinement v.{cfg.version}') # .format(cfg.version)) try: int(huc) except ValueError: raise Exception( 'Invalid HUC identifier "{}". Must be an integer'.format(huc)) if not (len(huc) == 4 or len(huc) == 8): raise Exception('Invalid HUC identifier. Must be four digit integer') # Make the projectXML project, _realization, proj_nodes, report_path = create_project( huc, output_folder, {'ConfinementType': confinement_type}) # Incorporate project metadata to the riverscapes project if meta is not None: project.add_metadata(meta) # Copy input shapes to a geopackage flowlines_path = os.path.join( output_folder, LayerTypes['INPUTS'].rel_path, LayerTypes['INPUTS'].sub_layers['FLOWLINES'].rel_path) confining_path = os.path.join( output_folder, LayerTypes['INPUTS'].rel_path, LayerTypes['INPUTS'].sub_layers['CONFINING_POLYGON'].rel_path) copy_feature_class(flowlines_orig, flowlines_path, epsg=cfg.OUTPUT_EPSG) copy_feature_class(confining_polygon_orig, confining_path, epsg=cfg.OUTPUT_EPSG) _nd, _inputs_gpkg_path, inputs_gpkg_lyrs = project.add_project_geopackage( proj_nodes['Inputs'], LayerTypes['INPUTS']) output_gpkg = os.path.join(output_folder, LayerTypes['CONFINEMENT'].rel_path) intermediates_gpkg = os.path.join(output_folder, LayerTypes['INTERMEDIATES'].rel_path) # Creates an empty geopackage and replaces the old one GeopackageLayer(output_gpkg, delete_dataset=True) GeopackageLayer(intermediates_gpkg, delete_dataset=True) # Add the flowlines file with some metadata project.add_metadata({'BufferField': buffer_field}, inputs_gpkg_lyrs['FLOWLINES'][0]) # Add the confinement polygon project.add_project_geopackage(proj_nodes['Intermediates'], LayerTypes['INTERMEDIATES']) _nd, _inputs_gpkg_path, out_gpkg_lyrs = project.add_project_geopackage( proj_nodes['Outputs'], LayerTypes['CONFINEMENT']) # Additional Metadata project.add_metadata( { 'Min Buffer': str(min_buffer), "Expansion Factor": str(bankfull_expansion_factor) }, out_gpkg_lyrs['CONFINEMENT_BUFFERS'][0]) # Generate confining margins log.info(f"Preparing output geopackage: {output_gpkg}") log.info(f"Generating Confinement from buffer field: {buffer_field}") # Load input datasets and set the global srs and a meter conversion factor with GeopackageLayer(flowlines_path) as flw_lyr: srs = flw_lyr.spatial_ref meter_conversion = flw_lyr.rough_convert_metres_to_vector_units(1) geom_confining_polygon = get_geometry_unary_union(confining_path, cfg.OUTPUT_EPSG) # Calculate Spatial Constants # Get a very rough conversion factor for 1m to whatever units the shapefile uses offset = 0.1 * meter_conversion selection_buffer = 0.1 * meter_conversion # Standard Outputs field_lookup = { 'side': ogr.FieldDefn("Side", ogr.OFTString), 'flowlineID': ogr.FieldDefn( "NHDPlusID", ogr.OFTString ), # ArcGIS cannot read Int64 and will show up as 0, however data is stored correctly in GPKG 'confinement_type': ogr.FieldDefn("Confinement_Type", ogr.OFTString), 'confinement_ratio': ogr.FieldDefn("Confinement_Ratio", ogr.OFTReal), 'constriction_ratio': ogr.FieldDefn("Constriction_Ratio", ogr.OFTReal), 'length': ogr.FieldDefn("ApproxLeng", ogr.OFTReal), 'confined_length': ogr.FieldDefn("ConfinLeng", ogr.OFTReal), 'constricted_length': ogr.FieldDefn("ConstrLeng", ogr.OFTReal), 'bankfull_width': ogr.FieldDefn("Bankfull_Width", ogr.OFTReal), 'buffer_width': ogr.FieldDefn("Buffer_Width", ogr.OFTReal), # Couple of Debug fields too 'process': ogr.FieldDefn("ErrorProcess", ogr.OFTString), 'message': ogr.FieldDefn("ErrorMessage", ogr.OFTString) } field_lookup['side'].SetWidth(5) field_lookup['confinement_type'].SetWidth(5) # Here we open all the necessary output layers and write the fields to them. There's no harm in quickly # Opening these layers to instantiate them # Standard Outputs with GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT']. sub_layers["CONFINEMENT_MARGINS"].rel_path, write=True) as margins_lyr: margins_lyr.create(ogr.wkbLineString, spatial_ref=srs) margins_lyr.ogr_layer.CreateField(field_lookup['side']) margins_lyr.ogr_layer.CreateField(field_lookup['flowlineID']) margins_lyr.ogr_layer.CreateField(field_lookup['length']) with GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT']. sub_layers["CONFINEMENT_RAW"].rel_path, write=True) as raw_lyr: raw_lyr.create(ogr.wkbLineString, spatial_ref=srs) raw_lyr.ogr_layer.CreateField(field_lookup['flowlineID']) raw_lyr.ogr_layer.CreateField(field_lookup['confinement_type']) raw_lyr.ogr_layer.CreateField(field_lookup['length']) with GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT']. sub_layers["CONFINEMENT_RATIO"].rel_path, write=True) as ratio_lyr: ratio_lyr.create(ogr.wkbLineString, spatial_ref=srs) ratio_lyr.ogr_layer.CreateField(field_lookup['flowlineID']) ratio_lyr.ogr_layer.CreateField(field_lookup['confinement_ratio']) ratio_lyr.ogr_layer.CreateField(field_lookup['constriction_ratio']) ratio_lyr.ogr_layer.CreateField(field_lookup['length']) ratio_lyr.ogr_layer.CreateField(field_lookup['confined_length']) ratio_lyr.ogr_layer.CreateField(field_lookup['constricted_length']) with GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES']. sub_layers["CONFINEMENT_BUFFER_SPLIT"].rel_path, write=True) as lyr: lyr.create(ogr.wkbPolygon, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['side']) lyr.ogr_layer.CreateField(field_lookup['flowlineID']) lyr.ogr_layer.CreateField(field_lookup['bankfull_width']) lyr.ogr_layer.CreateField(field_lookup['buffer_width']) with GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT']. sub_layers["CONFINEMENT_BUFFERS"].rel_path, write=True) as lyr: lyr.create(ogr.wkbPolygon, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['flowlineID']) lyr.ogr_layer.CreateField(field_lookup['bankfull_width']) lyr.ogr_layer.CreateField(field_lookup['buffer_width']) with GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES']. sub_layers["SPLIT_POINTS"].rel_path, write=True) as lyr: lyr.create(ogr.wkbPoint, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['side']) lyr.ogr_layer.CreateField(field_lookup['flowlineID']) with GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES']. sub_layers["FLOWLINE_SEGMENTS"].rel_path, write=True) as lyr: lyr.create(ogr.wkbLineString, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['side']) lyr.ogr_layer.CreateField(field_lookup['flowlineID']) with GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES']. sub_layers["ERROR_POLYLINES"].rel_path, write=True) as lyr: lyr.create(ogr.wkbLineString, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['process']) lyr.ogr_layer.CreateField(field_lookup['message']) with GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES']. sub_layers["ERROR_POLYGONS"].rel_path, write=True) as lyr: lyr.create(ogr.wkbPolygon, spatial_ref=srs) lyr.ogr_layer.CreateField(field_lookup['process']) lyr.ogr_layer.CreateField(field_lookup['message']) # Generate confinement per Flowline with GeopackageLayer(flowlines_path) as flw_lyr, \ GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT'].sub_layers["CONFINEMENT_MARGINS"].rel_path, write=True) as margins_lyr, \ GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT'].sub_layers["CONFINEMENT_RAW"].rel_path, write=True) as raw_lyr, \ GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT'].sub_layers["CONFINEMENT_RATIO"].rel_path, write=True) as ratio_lyr, \ GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES'].sub_layers["SPLIT_POINTS"].rel_path, write=True) as dbg_splitpts_lyr, \ GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES'].sub_layers["FLOWLINE_SEGMENTS"].rel_path, write=True) as dbg_flwseg_lyr, \ GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES'].sub_layers["CONFINEMENT_BUFFER_SPLIT"].rel_path, write=True) as conf_buff_split_lyr, \ GeopackageLayer(output_gpkg, layer_name=LayerTypes['CONFINEMENT'].sub_layers["CONFINEMENT_BUFFERS"].rel_path, write=True) as buff_lyr, \ GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES'].sub_layers["ERROR_POLYLINES"].rel_path, write=True) as dbg_err_lines_lyr, \ GeopackageLayer(intermediates_gpkg, layer_name=LayerTypes['INTERMEDIATES'].sub_layers["ERROR_POLYGONS"].rel_path, write=True) as dbg_err_polygons_lyr: err_count = 0 for flowline, _counter, progbar in flw_lyr.iterate_features( "Generating confinement for flowlines", attribute_filter="FCode IN ({0})".format(','.join( [key for key in reach_codes])), write_layers=[ margins_lyr, raw_lyr, ratio_lyr, dbg_splitpts_lyr, dbg_flwseg_lyr, buff_lyr, conf_buff_split_lyr, dbg_err_lines_lyr, dbg_err_polygons_lyr ]): # Load Flowline flowlineID = int(flowline.GetFieldAsInteger64("NHDPlusID")) bankfull_width = flowline.GetField(buffer_field) buffer_value = max(bankfull_width, min_buffer) geom_flowline = GeopackageLayer.ogr2shapely(flowline) if not geom_flowline.is_valid or geom_flowline.is_empty or geom_flowline.length == 0: progbar.erase() log.warning("Invalid flowline with id: {}".format(flowlineID)) continue # Generate buffer on each side of the flowline geom_buffer = geom_flowline.buffer( ((buffer_value * meter_conversion) / 2) * bankfull_expansion_factor, cap_style=2) # inital cleanup if geom is multipolygon if geom_buffer.geom_type == "MultiPolygon": log.warning(f"Cleaning multipolygon for id{flowlineID}") polys = [g for g in geom_buffer if g.intersects(geom_flowline)] if len(polys) == 1: geom_buffer = polys[0] if not geom_buffer.is_valid or geom_buffer.is_empty or geom_buffer.area == 0 or geom_buffer.geom_type not in [ "Polygon" ]: progbar.erase() log.warning("Invalid flowline (after buffering) id: {}".format( flowlineID)) dbg_err_lines_lyr.create_feature( geom_flowline, { "ErrorProcess": "Generate Buffer", "ErrorMessage": "Invalid Buffer" }) err_count += 1 continue buff_lyr.create_feature( geom_buffer, { "NHDPlusID": flowlineID, "Buffer_Width": buffer_value, "Bankfull_Width": bankfull_width }) # Split the Buffer by the flowline geom_buffer_splits = split( geom_buffer, geom_flowline ) # snap(geom, geom_buffer)) <--shapely does not snap vertex to edge. need to make new function for this to ensure more buffers have 2 split polygons # Process only if 2 buffers exist if len(geom_buffer_splits) != 2: # Lets try to split this again by slightly extending the line geom_newline = scale(geom_flowline, 1.1, 1.1, origin='center') geom_buffer_splits = split(geom_buffer, geom_newline) if len(geom_buffer_splits) != 2: # triage the polygon if still cannot split it error_message = f"WARNING: Flowline FID {flowline.GetFID()} | Incorrect number of split buffer polygons: {len(geom_buffer_splits)}" progbar.erase() log.warning(error_message) dbg_err_lines_lyr.create_feature( geom_flowline, { "ErrorProcess": "Buffer Split", "ErrorMessage": error_message }) err_count += 1 if len(geom_buffer_splits) > 1: for geom in geom_buffer_splits: dbg_err_polygons_lyr.create_feature( geom, { "ErrorProcess": "Buffer Split", "ErrorMessage": error_message }) else: dbg_err_polygons_lyr.create_feature( geom_buffer_splits, { "ErrorProcess": "Buffer Split", "ErrorMessage": error_message }) continue # Generate point to test side of flowline geom_offset = geom_flowline.parallel_offset(offset, "left") if not geom_offset.is_valid or geom_offset.is_empty or geom_offset.length == 0: progbar.erase() log.warning("Invalid flowline (after offset) id: {}".format( flowlineID)) err_count += 1 dbg_err_lines_lyr.create_feature( geom_flowline, { "ErrorProcess": "Offset Error", "ErrorMessage": "Invalid flowline (after offset) id: {}".format( flowlineID) }) continue geom_side_point = geom_offset.interpolate(0.5, True) # Store output segements lgeoms_right_confined_flowline_segments = [] lgeoms_left_confined_flowline_segments = [] for geom_side in geom_buffer_splits: # Identify side of flowline side = "LEFT" if geom_side.contains( geom_side_point) else "RIGHT" # Save the polygon conf_buff_split_lyr.create_feature( geom_side, { "Side": side, "NHDPlusID": flowlineID, "Buffer_Width": buffer_value, "Bankfull_Width": bankfull_width }) # Generate Confining margins geom_confined_margins = geom_confining_polygon.boundary.intersection( geom_side) # make sure intersection splits lines if geom_confined_margins.is_empty: continue # Multilinestring to individual linestrings lines = [ line for line in geom_confined_margins ] if geom_confined_margins.geom_type == 'MultiLineString' else [ geom_confined_margins ] for line in lines: margins_lyr.create_feature( line, { "Side": side, "NHDPlusID": flowlineID, "ApproxLeng": line.length / meter_conversion }) # Split flowline by Near Geometry pt_start = nearest_points(Point(line.coords[0]), geom_flowline)[1] pt_end = nearest_points(Point(line.coords[-1]), geom_flowline)[1] for point in [pt_start, pt_end]: dbg_splitpts_lyr.create_feature( point, { "Side": side, "NHDPlusID": flowlineID }) distance_sorted = sorted([ geom_flowline.project(pt_start), geom_flowline.project(pt_end) ]) segment = substring(geom_flowline, distance_sorted[0], distance_sorted[1]) # Store the segment by flowline side if segment.is_valid and segment.geom_type in [ "LineString", "MultiLineString" ]: if side == "LEFT": lgeoms_left_confined_flowline_segments.append( segment) else: lgeoms_right_confined_flowline_segments.append( segment) dbg_flwseg_lyr.create_feature(segment, { "Side": side, "NHDPlusID": flowlineID }) # Raw Confinement Output # Prepare flowline splits splitpoints = [ Point(x, y) for line in lgeoms_left_confined_flowline_segments + lgeoms_right_confined_flowline_segments for x, y in line.coords ] cut_distances = sorted( list( set([ geom_flowline.project(point) for point in splitpoints ]))) lgeoms_flowlines_split = [] current_line = geom_flowline cumulative_distance = 0.0 while len(cut_distances) > 0: distance = cut_distances.pop(0) - cumulative_distance if not distance == 0.0: outline = cut(current_line, distance) if len(outline) == 1: current_line = outline[0] else: current_line = outline[1] lgeoms_flowlines_split.append(outline[0]) cumulative_distance = cumulative_distance + distance lgeoms_flowlines_split.append(current_line) # Confined Segments lgeoms_confined_left_split = select_geoms_by_intersection( lgeoms_flowlines_split, lgeoms_left_confined_flowline_segments, buffer=selection_buffer) lgeoms_confined_right_split = select_geoms_by_intersection( lgeoms_flowlines_split, lgeoms_right_confined_flowline_segments, buffer=selection_buffer) lgeoms_confined_left = select_geoms_by_intersection( lgeoms_confined_left_split, lgeoms_confined_right_split, buffer=selection_buffer, inverse=True) lgeoms_confined_right = select_geoms_by_intersection( lgeoms_confined_right_split, lgeoms_confined_left_split, buffer=selection_buffer, inverse=True) geom_confined = unary_union(lgeoms_confined_left_split + lgeoms_confined_right_split) # Constricted Segments lgeoms_constricted_l = select_geoms_by_intersection( lgeoms_confined_left_split, lgeoms_confined_right_split, buffer=selection_buffer) lgeoms_constrcited_r = select_geoms_by_intersection( lgeoms_confined_right_split, lgeoms_confined_left_split, buffer=selection_buffer) lgeoms_constricted = [] for geom in lgeoms_constricted_l + lgeoms_constrcited_r: if not any(g.equals(geom) for g in lgeoms_constricted): lgeoms_constricted.append(geom) geom_constricted = MultiLineString(lgeoms_constricted) # Unconfined Segments lgeoms_unconfined = select_geoms_by_intersection( lgeoms_flowlines_split, lgeoms_confined_left_split + lgeoms_confined_right_split, buffer=selection_buffer, inverse=True) # Save Raw Confinement for con_type, geoms in zip(["Left", "Right", "Both", "None"], [ lgeoms_confined_left, lgeoms_confined_right, lgeoms_constricted, lgeoms_unconfined ]): for g in geoms: if g.geom_type == "LineString": raw_lyr.create_feature( g, { "NHDPlusID": flowlineID, "Confinement_Type": con_type, "ApproxLeng": g.length / meter_conversion }) elif geoms.geom_type in ["Point", "MultiPoint"]: progbar.erase() log.warning( f"Flowline FID: {flowline.GetFID()} | Point geometry identified generating outputs for Raw Confinement." ) else: progbar.erase() log.warning( f"Flowline FID: {flowline.GetFID()} | Unknown geometry identified generating outputs for Raw Confinement." ) # Calculated Confinement per Flowline confinement_ratio = geom_confined.length / geom_flowline.length if geom_confined else 0.0 constricted_ratio = geom_constricted.length / geom_flowline.length if geom_constricted else 0.0 # Save Confinement Ratio attributes = { "NHDPlusID": flowlineID, "Confinement_Ratio": confinement_ratio, "Constriction_Ratio": constricted_ratio, "ApproxLeng": geom_flowline.length / meter_conversion, "ConfinLeng": geom_confined.length / meter_conversion if geom_confined else 0.0, "ConstrLeng": geom_constricted.length / meter_conversion if geom_constricted else 0.0 } ratio_lyr.create_feature(geom_flowline, attributes) # Write a report report = ConfinementReport(output_gpkg, report_path, project) report.write() progbar.finish() log.info(f"Count of Flowline segments with errors: {err_count}") log.info('Confinement Finished') return
def test_get_geometry_unary_union(self): in_path = os.path.join(datadir, 'WBDHU12.shp') # Use this for the clip shape clip_path = os.path.join(datadir, 'WBDHU10.shp') # This is the whole file unioned result_all = vector_ops.get_geometry_unary_union(in_path, 4326) # This is one huc12 result201 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040201'") result202 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040202'") result203 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040203'") result101 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040101'") result102 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040102'") result103 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 = '170603040103'") # This is every huc12 with the pattern 1706030402% result20 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 LIKE '1706030402%'") result10 = vector_ops.get_geometry_unary_union( in_path, 4326, attribute_filter="HUC12 LIKE '1706030401%'") self.assertAlmostEqual(result_all.area, 0.06580, 4) self.assertAlmostEqual( result_all.area, result201.area + result202.area + result203.area + result101.area + result102.area + result103.area, 4) self.assertAlmostEqual( result10.area, result101.area + result102.area + result103.area, 4) self.assertAlmostEqual( result20.area, result201.area + result202.area + result203.area, 4) # Now test with clip_shape enabled with ShapefileLayer(clip_path) as clip_lyr: # Build a library of shapes to clip clip_shapes = {} for clip_feat, _counter, _progbar in clip_lyr.iterate_features( "Gettingshapes"): huc10 = clip_feat.GetFieldAsString("HUC10") clip_shapes[huc10] = GeopackageLayer.ogr2shapely(clip_feat) for huc10, clip_shape in clip_shapes.items(): debug_path = os.path.join( datadir, 'test_get_geometry_unary_union_{}.gpkg'.format(huc10)) buffered_clip_shape = clip_shape.buffer(-0.004) # Write the clipping shape with GeopackageLayer(debug_path, 'CLIP_{}'.format(huc10), write=True) as deb_lyr: deb_lyr.create_layer_from_ref(clip_lyr) out_feature = ogr.Feature(deb_lyr.ogr_layer_def) out_feature.SetGeometry( GeopackageLayer.shapely2ogr(buffered_clip_shape)) deb_lyr.ogr_layer.CreateFeature(out_feature) # This is every huc12 within a single huc 10 unioned result_clipped = vector_ops.get_geometry_union( in_path, clip_shape=buffered_clip_shape) with ShapefileLayer(in_path) as in_lyr, GeopackageLayer( debug_path, 'result_{}'.format(huc10), write=True) as deb_lyr: deb_lyr.create(in_lyr.ogr_geom_type, spatial_ref=in_lyr.spatial_ref) out_feature = ogr.Feature(deb_lyr.ogr_layer_def) out_feature.SetGeometry( GeopackageLayer.shapely2ogr(result_clipped)) deb_lyr.ogr_layer.CreateFeature(out_feature) self.assertAlmostEqual(clip_shape.area, result_clipped.area, 4)
def build_network(flowlines_path: str, flowareas_path: str, out_path: str, epsg: int = None, reach_codes: List[str] = None, waterbodies_path: str = None, waterbody_max_size=None, create_layer: bool = True): """[summary] Args: flowlines_path (str): [description] flowareas_path (str): [description] out_path (str): [description] epsg (int, optional): [description]. Defaults to None. reach_codes (List[str], optional): [description]. Defaults to None. waterbodies_path (str, optional): [description]. Defaults to None. waterbody_max_size ([type], optional): [description]. Defaults to None. create_layer (bool, optional): [description]. Defaults to True. Returns: [type]: [description] """ log = Logger('Build Network') log.info("Building network from flow lines {0}".format(flowlines_path)) if reach_codes: for r in reach_codes: log.info('Retaining {} reaches with code {}'.format( FCodeValues[int(r)], r)) else: log.info('Retaining all reaches. No reach filtering.') # Get the transformation required to convert to the target spatial reference if epsg is not None: with get_shp_or_gpkg(flowareas_path) as flowareas_lyr: out_spatial_ref, transform = VectorBase.get_transform_from_epsg( flowareas_lyr.spatial_ref, epsg) # Process all perennial/intermittment/ephemeral reaches first attribute_filter = None if reach_codes and len(reach_codes) > 0: _result = [ log.info("{0} {1} network features (FCode {2})".format( 'Retaining', FCodeValues[int(key)], key)) for key in reach_codes ] attribute_filter = "FCode IN ({0})".format(','.join( [key for key in reach_codes])) if create_layer is True: with get_shp_or_gpkg(flowlines_path) as flowlines_lyr, get_shp_or_gpkg( out_path, write=True) as out_lyr: out_lyr.create_layer_from_ref(flowlines_lyr) log.info('Processing all reaches') process_reaches(flowlines_path, out_path, attribute_filter=attribute_filter) # Process artifical paths through small waterbodies if waterbodies_path is not None and waterbody_max_size is not None: small_waterbodies = get_geometry_unary_union( waterbodies_path, epsg, 'AreaSqKm <= ({0})'.format(waterbody_max_size)) log.info( 'Retaining artificial features within waterbody features smaller than {0}km2' .format(waterbody_max_size)) process_reaches( flowlines_path, out_path, transform=transform, attribute_filter='FCode = {0}'.format(ARTIFICIAL_REACHES), clip_shape=small_waterbodies) # Retain artifical paths through flow areas if flowareas_path: flow_polygons = get_geometry_unary_union(flowareas_path, epsg) if flow_polygons: log.info('Retaining artificial features within flow area features') process_reaches( flowlines_path, out_path, transform=transform, attribute_filter='FCode = {0}'.format(ARTIFICIAL_REACHES), clip_shape=flow_polygons) else: log.info('Zero artifical paths to be retained.') with get_shp_or_gpkg(out_path) as out_lyr: log.info(('{:,} features written to {:}'.format( out_lyr.ogr_layer.GetFeatureCount(), out_path))) log.info('Process completed successfully.') return out_spatial_ref
def rs_context(huc, existing_veg, historic_veg, ownership, fair_market, ecoregions, prism_folder, output_folder, download_folder, scratch_dir, parallel, force_download, meta: Dict[str, str]): """ Download riverscapes context layers for the specified HUC and organize them as a Riverscapes project :param huc: Eight, 10 or 12 digit HUC identification number :param existing_veg: Path to the existing vegetation conditions raster :param historic_veg: Path to the historical vegetation conditions raster :param ownership: Path to the national land ownership Shapefile :param output_folder: Output location for the riverscapes context project :param download_folder: Temporary folder where downloads are cached. This can be shared between rs_context processes :param force_download: If false then downloads can be skipped if the files already exist :param prism_folder: folder containing PRISM rasters in *.bil format :param meta (Dict[str,str]): dictionary of riverscapes metadata key: value pairs :return: """ log = Logger("RS Context") log.info('Starting RSContext v.{}'.format(cfg.version)) try: int(huc) except ValueError: raise Exception( 'Invalid HUC identifier "{}". Must be an integer'.format(huc)) if not (len(huc) in [4, 8, 10, 12]): raise Exception( 'Invalid HUC identifier. Must be 4, 8, 10 or 12 digit integer') safe_makedirs(output_folder) safe_makedirs(download_folder) # We need a temporary folder for slope rasters, Stitching inputs, intermeditary products, etc. scratch_dem_folder = os.path.join(scratch_dir, 'rs_context', huc) safe_makedirs(scratch_dem_folder) project, realization = create_project(huc, output_folder) hydrology_gpkg_path = os.path.join(output_folder, LayerTypes['HYDROLOGY'].rel_path) dem_node, dem_raster = project.add_project_raster(realization, LayerTypes['DEM']) _node, hill_raster = project.add_project_raster(realization, LayerTypes['HILLSHADE']) _node, flow_accum = project.add_project_raster(realization, LayerTypes['FA']) _node, drain_area = project.add_project_raster(realization, LayerTypes['DA']) hand_node, hand_raster = project.add_project_raster( realization, LayerTypes['HAND']) _node, slope_raster = project.add_project_raster(realization, LayerTypes['SLOPE']) _node, existing_clip = project.add_project_raster(realization, LayerTypes['EXVEG']) _node, historic_clip = project.add_project_raster(realization, LayerTypes['HISTVEG']) _node, fair_market_clip = project.add_project_raster( realization, LayerTypes['FAIR_MARKET']) # Download the four digit NHD archive containing the flow lines and watershed boundaries log.info('Processing NHD') # Incorporate project metadata to the riverscapes project if meta is not None: project.add_metadata(meta) nhd_download_folder = os.path.join(download_folder, 'nhd', huc[:4]) nhd_unzip_folder = os.path.join(scratch_dir, 'nhd', huc[:4]) nhd, db_path, huc_name, nhd_url = clean_nhd_data( huc, nhd_download_folder, nhd_unzip_folder, os.path.join(output_folder, 'hydrology'), cfg.OUTPUT_EPSG, False) # Clean up the unzipped files. We won't need them again if parallel: safe_remove_dir(nhd_unzip_folder) project.add_metadata({'Watershed': huc_name}) boundary = 'WBDHU{}'.format(len(huc)) # For coarser rasters than the DEM we need to buffer our clip polygon to include enough pixels # This shouldn't be too much more data because these are usually integer rasters that are much lower res. buffered_clip_path100 = os.path.join( hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['BUFFEREDCLIP100'].rel_path) copy_feature_class(nhd[boundary], buffered_clip_path100, epsg=cfg.OUTPUT_EPSG, buffer=100) buffered_clip_path500 = os.path.join( hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['BUFFEREDCLIP500'].rel_path) copy_feature_class(nhd[boundary], buffered_clip_path500, epsg=cfg.OUTPUT_EPSG, buffer=500) # PRISM climate rasters mean_annual_precip = None bil_files = glob.glob(os.path.join(prism_folder, '**', '*.bil')) if (len(bil_files) == 0): raise Exception('Could not find any .bil files in the prism folder') for ptype in PrismTypes: try: # Next should always be guarded source_raster_path = next( x for x in bil_files if ptype.lower() in os.path.basename(x).lower()) except StopIteration: raise Exception( 'Could not find .bil file corresponding to "{}"'.format(ptype)) _node, project_raster_path = project.add_project_raster( realization, LayerTypes[ptype]) raster_warp(source_raster_path, project_raster_path, cfg.OUTPUT_EPSG, buffered_clip_path500, {"cutlineBlend": 1}) # Use the mean annual precipitation to calculate bankfull width if ptype.lower() == 'ppt': polygon = get_geometry_unary_union(nhd[boundary], epsg=cfg.OUTPUT_EPSG) mean_annual_precip = raster_buffer_stats2( {1: polygon}, project_raster_path)[1]['Mean'] log.info('Mean annual precipitation for HUC {} is {} mm'.format( huc, mean_annual_precip)) project.add_metadata( {'mean_annual_precipitation_mm': str(mean_annual_precip)}) calculate_bankfull_width(nhd['NHDFlowline'], mean_annual_precip) # Add the DB record to the Project XML db_lyr = RSLayer('NHD Tables', 'NHDTABLES', 'SQLiteDB', os.path.relpath(db_path, output_folder)) sqlite_el = project.add_dataset(realization, db_path, db_lyr, 'SQLiteDB') project.add_metadata({'origin_url': nhd_url}, sqlite_el) # Add any results to project XML for name, file_path in nhd.items(): lyr_obj = RSLayer(name, name, 'Vector', os.path.relpath(file_path, output_folder)) vector_nod, _fpath = project.add_project_vector(realization, lyr_obj) project.add_metadata({'origin_url': nhd_url}, vector_nod) states = get_nhd_states(nhd[boundary]) # Download the NTD archive containing roads and rail log.info('Processing NTD') ntd_raw = {} ntd_unzip_folders = [] ntd_urls = get_ntd_urls(states) for state, ntd_url in ntd_urls.items(): ntd_download_folder = os.path.join(download_folder, 'ntd', state.lower()) ntd_unzip_folder = os.path.join( scratch_dir, 'ntd', state.lower(), 'unzipped' ) # a little awkward but I need a folder for this and this was the best name I could find ntd_raw[state] = download_shapefile_collection(ntd_url, ntd_download_folder, ntd_unzip_folder, force_download) ntd_unzip_folders.append(ntd_unzip_folder) ntd_clean = clean_ntd_data(ntd_raw, nhd['NHDFlowline'], nhd[boundary], os.path.join(output_folder, 'transportation'), cfg.OUTPUT_EPSG) # clean up the NTD Unzip folder. We won't need it again if parallel: for unzip_path in ntd_unzip_folders: safe_remove_dir(unzip_path) # Write transportation layers to project file log.info('Write transportation layers to project file') # Add any results to project XML for name, file_path in ntd_clean.items(): lyr_obj = RSLayer(name, name, 'Vector', os.path.relpath(file_path, output_folder)) ntd_node, _fpath = project.add_project_vector(realization, lyr_obj) project.add_metadata({**ntd_urls}, ntd_node) # Download the HAND raster huc6 = huc[0:6] hand_download_folder = os.path.join(download_folder, 'hand') _hpath, hand_url = download_hand(huc6, cfg.OUTPUT_EPSG, hand_download_folder, nhd[boundary], hand_raster, warp_options={"cutlineBlend": 1}) project.add_metadata({'origin_url': hand_url}, hand_node) # download contributing DEM rasters, mosaic and reproject into compressed GeoTIF ned_download_folder = os.path.join(download_folder, 'ned') ned_unzip_folder = os.path.join(scratch_dir, 'ned') dem_rasters, urls = download_dem(nhd[boundary], cfg.OUTPUT_EPSG, 0.01, ned_download_folder, ned_unzip_folder, force_download) need_dem_rebuild = force_download or not os.path.exists(dem_raster) if need_dem_rebuild: raster_vrt_stitch(dem_rasters, dem_raster, cfg.OUTPUT_EPSG, clip=nhd[boundary], warp_options={"cutlineBlend": 1}) verify_areas(dem_raster, nhd[boundary]) # Calculate slope rasters seperately and then stitch them slope_parts = [] hillshade_parts = [] need_slope_build = need_dem_rebuild or not os.path.isfile(slope_raster) need_hs_build = need_dem_rebuild or not os.path.isfile(hill_raster) project.add_metadata( { 'num_rasters': str(len(urls)), 'origin_urls': json.dumps(urls) }, dem_node) for dem_r in dem_rasters: slope_part_path = os.path.join( scratch_dem_folder, 'SLOPE__' + os.path.basename(dem_r).split('.')[0] + '.tif') hs_part_path = os.path.join( scratch_dem_folder, 'HS__' + os.path.basename(dem_r).split('.')[0] + '.tif') slope_parts.append(slope_part_path) hillshade_parts.append(hs_part_path) if force_download or need_dem_rebuild or not os.path.exists( slope_part_path): gdal_dem_geographic(dem_r, slope_part_path, 'slope') need_slope_build = True if force_download or need_dem_rebuild or not os.path.exists( hs_part_path): gdal_dem_geographic(dem_r, hs_part_path, 'hillshade') need_hs_build = True if need_slope_build: raster_vrt_stitch(slope_parts, slope_raster, cfg.OUTPUT_EPSG, clip=nhd[boundary], clean=parallel, warp_options={"cutlineBlend": 1}) verify_areas(slope_raster, nhd[boundary]) else: log.info('Skipping slope build because nothing has changed.') if need_hs_build: raster_vrt_stitch(hillshade_parts, hill_raster, cfg.OUTPUT_EPSG, clip=nhd[boundary], clean=parallel, warp_options={"cutlineBlend": 1}) verify_areas(hill_raster, nhd[boundary]) else: log.info('Skipping hillshade build because nothing has changed.') # Remove the unzipped rasters. We won't need them anymore if parallel: safe_remove_dir(ned_unzip_folder) # Calculate flow accumulation raster based on the DEM log.info('Running flow accumulation and converting to drainage area.') flow_accumulation(dem_raster, flow_accum, dinfinity=False, pitfill=True) flow_accum_to_drainage_area(flow_accum, drain_area) # Clip and re-project the existing and historic vegetation log.info('Processing existing and historic vegetation rasters.') clip_vegetation(buffered_clip_path100, existing_veg, existing_clip, historic_veg, historic_clip, cfg.OUTPUT_EPSG) log.info('Process the Fair Market Value Raster.') raster_warp(fair_market, fair_market_clip, cfg.OUTPUT_EPSG, clip=buffered_clip_path500, warp_options={"cutlineBlend": 1}) # Clip the landownership Shapefile to a 10km buffer around the watershed boundary own_path = os.path.join(output_folder, LayerTypes['OWNERSHIP'].rel_path) project.add_dataset(realization, own_path, LayerTypes['OWNERSHIP'], 'Vector') clip_ownership(nhd[boundary], ownership, own_path, cfg.OUTPUT_EPSG, 10000) ####################################################### # Segmentation ####################################################### # For now let's just make a copy of the NHD FLowlines tmr = Timer() rs_segmentation(nhd['NHDFlowline'], ntd_clean['Roads'], ntd_clean['Rail'], own_path, hydrology_gpkg_path, SEGMENTATION['Max'], SEGMENTATION['Min'], huc) log.debug('Segmentation done in {:.1f} seconds'.format(tmr.ellapsed())) project.add_project_geopackage(realization, LayerTypes['HYDROLOGY']) # Add Bankfull Buffer Polygons bankfull_path = os.path.join( hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['BANKFULL_CHANNEL'].rel_path) bankfull_buffer( os.path.join(hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['NETWORK'].rel_path), cfg.OUTPUT_EPSG, bankfull_path, ) # TODO Add nhd/bankfull union when merge feature classes in vector.ops works with Geopackage layers # bankfull_nhd_path = os.path.join(hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['COMPOSITE_CHANNEL_AREA'].rel_path) # clip_path = os.path.join(hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['BUFFEREDCLIP500'].rel_path) # bankfull_nhd_area(bankfull_path, nhd['NHDArea'], clip_path, cfg.OUTPUT_EPSG, hydrology_gpkg_path, LayerTypes['HYDROLOGY'].sub_layers['COMPOSITE_CHANNEL_AREA'].rel_path) # Filter the ecoregions Shapefile to only include attributes that intersect with our HUC eco_path = os.path.join(output_folder, 'ecoregions', 'ecoregions.shp') project.add_dataset(realization, eco_path, LayerTypes['ECOREGIONS'], 'Vector') filter_ecoregions(nhd[boundary], ecoregions, eco_path, cfg.OUTPUT_EPSG, 10000) report_path = os.path.join(project.project_dir, LayerTypes['REPORT'].rel_path) project.add_report(realization, LayerTypes['REPORT'], replace=True) report = RSContextReport(report_path, project, output_folder) report.write() log.info('Process completed successfully.') return { 'DEM': dem_raster, 'Slope': slope_raster, 'ExistingVeg': existing_veg, 'HistoricVeg': historic_veg, 'NHD': nhd }
def floodplain_connectivity(vbet_network: Path, vbet_polygon: Path, roads: Path, railroads: Path, output_dir: Path, debug_gpkg: Path = None): """[summary] Args: vbet_network (Path): Filtered Flowline network used to generate VBET. Final selection is based on this intersection. vbet_polygon (Path): Vbet polygons with clipped NHD Catchments roads (Path): Road network railroads (Path): railroad network out_polygon (Path): Output path and layer name for floodplain polygons debug_gpkg (Path, optional): geopackage for saving debug layers (may substantially increase processing time). Defaults to None. """ log = Logger('Floodplain Connectivity') log.info("Starting Floodplain Connectivity Script") out_polygon = os.path.join(output_dir, 'fconn.gpkg/outputs') # Prepare vbet and catchments geom_vbet = get_geometry_unary_union(vbet_polygon) geoms_raw_vbet = list(load_geometries(vbet_polygon, None).values()) listgeoms = [] for geom in geoms_raw_vbet: if geom.geom_type == "MultiPolygon": for g in geom: listgeoms.append(g) else: listgeoms.append(geom) geoms_vbet = MultiPolygon(listgeoms) # Clip Transportation Network by VBET log.info("Merging Transportation Networks") # merge_feature_classes([roads, railroads], geom_vbet, os.path.join(debug_gpkg, "Transportation")) TODO: error when calling this method geom_roads = get_geometry_unary_union(roads) geom_railroads = get_geometry_unary_union(railroads) geom_transportation = geom_roads.union( geom_railroads) if geom_railroads is not None else geom_roads log.info("Clipping Transportation Network by VBET") geom_transportation_clipped = geom_vbet.intersection(geom_transportation) if debug_gpkg: quicksave(debug_gpkg, "Clipped_Transportation", geom_transportation_clipped, ogr.wkbLineString) # Split Valley Edges at transportation intersections log.info("Splitting Valley Edges at transportation network intersections") geom_vbet_edges = MultiLineString( [geom.exterior for geom in geoms_vbet] + [g for geom in geoms_vbet for g in geom.interiors]) geom_vbet_interior_pts = MultiPoint([ Polygon(g).representative_point() for geom in geom_vbet for g in geom.interiors ]) if debug_gpkg: quicksave(debug_gpkg, "Valley_Edges_Raw", geom_vbet_edges, ogr.wkbLineString) vbet_splitpoints = [] vbet_splitlines = [] counter = 0 for geom_edge in geom_vbet_edges: counter += 1 log.info('Splitting edge features {}/{}'.format( counter, len(geom_vbet_edges))) if geom_edge.is_valid: if not geom_edge.intersects(geom_transportation): vbet_splitlines = vbet_splitlines + [geom_edge] continue pts = geom_transportation.intersection(geom_edge) if pts.is_empty: vbet_splitlines = vbet_splitlines + [geom_edge] continue if isinstance(pts, Point): pts = [pts] geom_boundaries = [geom_edge] progbar = ProgressBar(len(geom_boundaries), 50, "Processing") counter = 0 for pt in pts: # TODO: I tried to break this out but I'm not sure new_boundaries = [] for line in geom_boundaries: if line is not None: split_line = line_splitter(line, pt) progbar.total += len(split_line) for new_line in split_line: counter += 1 progbar.update(counter) if new_line is not None: new_boundaries.append(new_line) geom_boundaries = new_boundaries # TODO: Not sure this is having the intended effect # geom_boundaries = [new_line for line in geom_boundaries if line is not None for new_line in line_splitter(line, pt) if new_line is not None] progbar.finish() vbet_splitlines = vbet_splitlines + geom_boundaries vbet_splitpoints = vbet_splitpoints + [pt for pt in pts] if debug_gpkg: quicksave(debug_gpkg, "Split_Points", vbet_splitpoints, ogr.wkbPoint) quicksave(debug_gpkg, "Valley_Edges_Split", vbet_splitlines, ogr.wkbLineString) # Generate Polygons from lines log.info("Generating Floodplain Polygons") geom_lines = unary_union( vbet_splitlines + [geom_tc for geom_tc in geom_transportation_clipped]) geoms_areas = [ geom for geom in polygonize(geom_lines) if not any(geom.contains(pt) for pt in geom_vbet_interior_pts) ] if debug_gpkg: quicksave(debug_gpkg, "Split_Polygons", geoms_areas, ogr.wkbPolygon) # Select Polygons by flowline intersection log.info("Selecting connected floodplains") geom_vbet_network = get_geometry_unary_union(vbet_network) geoms_connected = [] geoms_disconnected = [] progbar = ProgressBar(len(geoms_areas), 50, f"Running polygon selection") counter = 0 for geom in geoms_areas: progbar.update(counter) counter += 1 if geom_vbet_network.intersects(geom): geoms_connected.append(geom) else: geoms_disconnected.append(geom) log.info("Union connected floodplains") geoms_connected_output = [ geom for geom in list(unary_union(geoms_connected)) ] geoms_disconnected_output = [ geom for geom in list(unary_union(geoms_disconnected)) ] # Save Outputs log.info("Save Floodplain Output") with GeopackageLayer(out_polygon, write=True) as out_lyr: out_lyr.create_layer(ogr.wkbPolygon, epsg=4326) out_lyr.create_field("Connected", ogr.OFTInteger) progbar = ProgressBar( len(geoms_connected_output) + len(geoms_disconnected_output), 50, f"saving {out_lyr.ogr_layer_name} features") counter = 0 for shape in geoms_connected_output: progbar.update(counter) counter += 1 out_lyr.create_feature(shape, attributes={"Connected": 1}) for shape in geoms_disconnected_output: progbar.update(counter) counter += 1 out_lyr.create_feature(shape, attributes={"Connected": 0})
def rvd(huc: int, flowlines_orig: Path, existing_veg_orig: Path, historic_veg_orig: Path, valley_bottom_orig: Path, output_folder: Path, reach_codes: List[str], flow_areas_orig: Path, waterbodies_orig: Path, meta=None): """[Generate segmented reaches on flowline network and calculate RVD from historic and existing vegetation rasters Args: huc (integer): Watershed ID flowlines_orig (Path): Segmented flowlines feature layer existing_veg_orig (Path): LANDFIRE version 2.00 evt raster, with adjacent xml metadata file historic_veg_orig (Path): LANDFIRE version 2.00 bps raster, with adjacent xml metadata file valley_bottom_orig (Path): Vbet polygon feature layer output_folder (Path): destination folder for project output reach_codes (List[int]): NHD reach codes for features to include in outputs flow_areas_orig (Path): NHD flow area polygon feature layer waterbodies (Path): NHD waterbodies polygon feature layer meta (Dict[str,str]): dictionary of riverscapes metadata key: value pairs """ log = Logger("RVD") log.info('RVD v.{}'.format(cfg.version)) try: int(huc) except ValueError: raise Exception('Invalid HUC identifier "{}". Must be an integer'.format(huc)) if not (len(huc) == 4 or len(huc) == 8): raise Exception('Invalid HUC identifier. Must be four digit integer') safe_makedirs(output_folder) project, _realization, proj_nodes = create_project(huc, output_folder) # Incorporate project metadata to the riverscapes project if meta is not None: project.add_metadata(meta) log.info('Adding inputs to project') _prj_existing_path_node, prj_existing_path = project.add_project_raster(proj_nodes['Inputs'], LayerTypes['EXVEG'], existing_veg_orig) _prj_historic_path_node, prj_historic_path = project.add_project_raster(proj_nodes['Inputs'], LayerTypes['HISTVEG'], historic_veg_orig) # TODO: Don't forget the att_filter # _prj_flowlines_node, prj_flowlines = project.add_project_geopackage(proj_nodes['Inputs'], LayerTypes['INPUTS'], flowlines, att_filter="\"ReachCode\" Like '{}%'".format(huc)) # Copy in the vectors we need inputs_gpkg_path = os.path.join(output_folder, LayerTypes['INPUTS'].rel_path) intermediates_gpkg_path = os.path.join(output_folder, LayerTypes['INTERMEDIATES'].rel_path) outputs_gpkg_path = os.path.join(output_folder, LayerTypes['OUTPUTS'].rel_path) # Make sure we're starting with empty/fresh geopackages GeopackageLayer.delete(inputs_gpkg_path) GeopackageLayer.delete(intermediates_gpkg_path) GeopackageLayer.delete(outputs_gpkg_path) # Copy our input layers and also find the difference in the geometry for the valley bottom flowlines_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['FLOWLINES'].rel_path) vbottom_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['VALLEY_BOTTOM'].rel_path) copy_feature_class(flowlines_orig, flowlines_path, epsg=cfg.OUTPUT_EPSG) copy_feature_class(valley_bottom_orig, vbottom_path, epsg=cfg.OUTPUT_EPSG) with GeopackageLayer(flowlines_path) as flow_lyr: # Set the output spatial ref as this for the whole project out_srs = flow_lyr.spatial_ref meter_conversion = flow_lyr.rough_convert_metres_to_vector_units(1) distance_buffer = flow_lyr.rough_convert_metres_to_vector_units(1) # Transform issues reading 102003 as espg id. Using sr wkt seems to work, however arcgis has problems loading feature classes with this method... raster_srs = ogr.osr.SpatialReference() ds = gdal.Open(prj_existing_path, 0) raster_srs.ImportFromWkt(ds.GetProjectionRef()) raster_srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) transform_shp_to_raster = VectorBase.get_transform(out_srs, raster_srs) gt = ds.GetGeoTransform() cell_area = ((gt[1] / meter_conversion) * (-gt[5] / meter_conversion)) # Create the output feature class fields with GeopackageLayer(outputs_gpkg_path, layer_name='ReachGeometry', delete_dataset=True) as out_lyr: out_lyr.create_layer(ogr.wkbMultiLineString, spatial_ref=out_srs, options=['FID=ReachID'], fields={ 'GNIS_NAME': ogr.OFTString, 'ReachCode': ogr.OFTString, 'TotDASqKm': ogr.OFTReal, 'NHDPlusID': ogr.OFTReal, 'WatershedID': ogr.OFTInteger }) metadata = { 'RVD_DateTime': datetime.datetime.now().isoformat(), 'Reach_Codes': reach_codes } # Execute the SQL to create the lookup tables in the RVD geopackage SQLite database watershed_name = create_database(huc, outputs_gpkg_path, metadata, cfg.OUTPUT_EPSG, os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'database', 'rvd_schema.sql')) project.add_metadata({'Watershed': watershed_name}) geom_vbottom = get_geometry_unary_union(vbottom_path, spatial_ref=raster_srs) flowareas_path = None if flow_areas_orig: flowareas_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['FLOW_AREA'].rel_path) copy_feature_class(flow_areas_orig, flowareas_path, epsg=cfg.OUTPUT_EPSG) geom_flow_areas = get_geometry_unary_union(flowareas_path) # Difference with existing vbottom geom_vbottom = geom_vbottom.difference(geom_flow_areas) else: del LayerTypes['INPUTS'].sub_layers['FLOW_AREA'] waterbodies_path = None if waterbodies_orig: waterbodies_path = os.path.join(inputs_gpkg_path, LayerTypes['INPUTS'].sub_layers['WATERBODIES'].rel_path) copy_feature_class(waterbodies_orig, waterbodies_path, epsg=cfg.OUTPUT_EPSG) geom_waterbodies = get_geometry_unary_union(waterbodies_path) # Difference with existing vbottom geom_vbottom = geom_vbottom.difference(geom_waterbodies) else: del LayerTypes['INPUTS'].sub_layers['WATERBODIES'] # Add the inputs to the XML _nd, _in_gpkg_path, _sublayers = project.add_project_geopackage(proj_nodes['Inputs'], LayerTypes['INPUTS']) # Filter the flow lines to just the required features and then segment to desired length # TODO: These are brat methods that need to be refactored to use VectorBase layers cleaned_path = os.path.join(outputs_gpkg_path, 'ReachGeometry') build_network(flowlines_path, flowareas_path, cleaned_path, waterbodies_path=waterbodies_path, epsg=cfg.OUTPUT_EPSG, reach_codes=reach_codes, create_layer=False) # Generate Voroni polygons log.info("Calculating Voronoi Polygons...") # Add all the points (including islands) to the list flowline_thiessen_points_groups = centerline_points(cleaned_path, distance_buffer, transform_shp_to_raster) flowline_thiessen_points = [pt for group in flowline_thiessen_points_groups.values() for pt in group] simple_save([pt.point for pt in flowline_thiessen_points], ogr.wkbPoint, raster_srs, "Thiessen_Points", intermediates_gpkg_path) # Exterior is the shell and there is only ever 1 myVorL = NARVoronoi(flowline_thiessen_points) # Generate Thiessen Polys myVorL.createshapes() # Dissolve by flowlines log.info("Dissolving Thiessen Polygons") dissolved_polys = myVorL.dissolve_by_property('fid') # Clip Thiessen Polys log.info("Clipping Thiessen Polygons to Valley Bottom") clipped_thiessen = clip_polygons(geom_vbottom, dissolved_polys) # Save Intermediates simple_save(clipped_thiessen.values(), ogr.wkbPolygon, raster_srs, "Thiessen", intermediates_gpkg_path) simple_save(dissolved_polys.values(), ogr.wkbPolygon, raster_srs, "ThiessenPolygonsDissolved", intermediates_gpkg_path) simple_save(myVorL.polys, ogr.wkbPolygon, raster_srs, "ThiessenPolygonsRaw", intermediates_gpkg_path) _nd, _inter_gpkg_path, _sublayers = project.add_project_geopackage(proj_nodes['Intermediates'], LayerTypes['INTERMEDIATES']) # OLD METHOD FOR AUDIT # dissolved_polys2 = dissolve_by_points(flowline_thiessen_points_groups, myVorL.polys) # simple_save(dissolved_polys2.values(), ogr.wkbPolygon, out_srs, "ThiessenPolygonsDissolved_OLD", intermediates_gpkg_path) # Load Vegetation Rasters log.info(f"Loading Existing and Historic Vegetation Rasters") vegetation = {} vegetation["EXISTING"] = load_vegetation_raster(prj_existing_path, outputs_gpkg_path, True, output_folder=os.path.join(output_folder, 'Intermediates')) vegetation["HISTORIC"] = load_vegetation_raster(prj_historic_path, outputs_gpkg_path, False, output_folder=os.path.join(output_folder, 'Intermediates')) for epoch in vegetation.keys(): for name in vegetation[epoch].keys(): if not f"{epoch}_{name}" == "HISTORIC_LUI": project.add_project_raster(proj_nodes['Intermediates'], LayerTypes[f"{epoch}_{name}"]) if vegetation["EXISTING"]["RAW"].shape != vegetation["HISTORIC"]["RAW"].shape: raise Exception('Vegetation raster shapes are not equal Existing={} Historic={}. Cannot continue'.format(vegetation["EXISTING"]["RAW"].shape, vegetation["HISTORIC"]["RAW"].shape)) # Vegetation zone calculations riparian_zone_arrays = {} riparian_zone_arrays["RIPARIAN_ZONES"] = ((vegetation["EXISTING"]["RIPARIAN"] + vegetation["HISTORIC"]["RIPARIAN"]) > 0) * 1 riparian_zone_arrays["NATIVE_RIPARIAN_ZONES"] = ((vegetation["EXISTING"]["NATIVE_RIPARIAN"] + vegetation["HISTORIC"]["NATIVE_RIPARIAN"]) > 0) * 1 riparian_zone_arrays["VEGETATION_ZONES"] = ((vegetation["EXISTING"]["VEGETATED"] + vegetation["HISTORIC"]["VEGETATED"]) > 0) * 1 # Save Intermediate Rasters for name, raster in riparian_zone_arrays.items(): save_intarr_to_geotiff(raster, os.path.join(output_folder, "Intermediates", f"{name}.tif"), prj_existing_path) project.add_project_raster(proj_nodes['Intermediates'], LayerTypes[name]) # Calculate Riparian Departure per Reach riparian_arrays = {f"{epoch.capitalize()}{(name.capitalize()).replace('Native_riparian', 'NativeRiparian')}Mean": array for epoch, arrays in vegetation.items() for name, array in arrays.items() if name in ["RIPARIAN", "NATIVE_RIPARIAN"]} # Vegetation Cell Counts raw_arrays = {f"{epoch}": array for epoch, arrays in vegetation.items() for name, array in arrays.items() if name == "RAW"} # Generate Vegetation Conversions vegetation_change = (vegetation["HISTORIC"]["CONVERSION"] - vegetation["EXISTING"]["CONVERSION"]) save_intarr_to_geotiff(vegetation_change, os.path.join(output_folder, "Intermediates", "Conversion_Raster.tif"), prj_existing_path) project.add_project_raster(proj_nodes['Intermediates'], LayerTypes['VEGETATION_CONVERSION']) # load conversion types dictionary from database conn = sqlite3.connect(outputs_gpkg_path) conn.row_factory = dict_factory curs = conn.cursor() curs.execute('SELECT * FROM ConversionTypes') conversion_classifications = curs.fetchall() curs.execute('SELECT * FROM vwConversions') conversion_ids = curs.fetchall() # Split vegetation change classes into binary arrays vegetation_change_arrays = { c['FieldName']: (vegetation_change == int(c["TypeValue"])) * 1 if int(c["TypeValue"]) in np.unique(vegetation_change) else None for c in conversion_classifications } # Calcuate average and unique cell counts per reach progbar = ProgressBar(len(clipped_thiessen.keys()), 50, "Extracting array values by reach...") counter = 0 discarded = 0 with rasterio.open(prj_existing_path) as dataset: unique_vegetation_counts = {} reach_average_riparian = {} reach_average_change = {} for reachid, poly in clipped_thiessen.items(): counter += 1 progbar.update(counter) # we can discount a lot of shapes here. if not poly.is_valid or poly.is_empty or poly.area == 0 or poly.geom_type not in ["Polygon", "MultiPolygon"]: discarded += 1 continue raw_values_unique = {} change_values_mean = {} riparian_values_mean = {} reach_raster = np.ma.masked_invalid( features.rasterize( [poly], out_shape=dataset.shape, transform=dataset.transform, all_touched=True, fill=np.nan)) for raster_name, raster in raw_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) raw_values_unique[raster_name] = np.unique(np.ma.filled(current_raster, fill_value=0), return_counts=True) else: raw_values_unique[raster_name] = [] for raster_name, raster in riparian_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) riparian_values_mean[raster_name] = np.ma.mean(current_raster) else: riparian_values_mean[raster_name] = 0.0 for raster_name, raster in vegetation_change_arrays.items(): if raster is not None: current_raster = np.ma.masked_array(raster, mask=reach_raster.mask) change_values_mean[raster_name] = np.ma.mean(current_raster) else: change_values_mean[raster_name] = 0.0 unique_vegetation_counts[reachid] = raw_values_unique reach_average_riparian[reachid] = riparian_values_mean reach_average_change[reachid] = change_values_mean progbar.finish() with SQLiteCon(outputs_gpkg_path) as gpkg: # Ensure all reaches are present in the ReachAttributes table before storing RVD output values gpkg.curs.execute('INSERT INTO ReachAttributes (ReachID) SELECT ReachID FROM ReachGeometry;') errs = 0 for reachid, epochs in unique_vegetation_counts.items(): for epoch in epochs.values(): insert_values = [[reachid, int(vegetationid), float(count * cell_area), int(count)] for vegetationid, count in zip(epoch[0], epoch[1]) if vegetationid != 0] try: gpkg.curs.executemany('''INSERT INTO ReachVegetation ( ReachID, VegetationID, Area, CellCount) VALUES (?,?,?,?)''', insert_values) # Sqlite can't report on SQL errors so we have to print good log messages to help intuit what the problem is except sqlite3.IntegrityError as err: # THis is likely a constraint error. errstr = "Integrity Error when inserting records: ReachID: {} VegetationIDs: {}".format(reachid, str(list(epoch[0]))) log.error(errstr) errs += 1 except sqlite3.Error as err: # This is any other kind of error errstr = "SQL Error when inserting records: ReachID: {} VegetationIDs: {} ERROR: {}".format(reachid, str(list(epoch[0])), str(err)) log.error(errstr) errs += 1 if errs > 0: raise Exception('Errors were found inserting records into the database. Cannot continue.') gpkg.conn.commit() # load RVD departure levels from DepartureLevels database table with SQLiteCon(outputs_gpkg_path) as gpkg: gpkg.curs.execute('SELECT LevelID, MaxRVD FROM DepartureLevels ORDER BY MaxRVD ASC') departure_levels = gpkg.curs.fetchall() # Calcuate Average Departure for Riparian and Native Riparian riparian_departure_values = riparian_departure(reach_average_riparian, departure_levels) write_db_attributes(outputs_gpkg_path, riparian_departure_values, departure_type_columns) # Add Conversion Code, Type to Vegetation Conversion with SQLiteCon(outputs_gpkg_path) as gpkg: gpkg.curs.execute('SELECT LevelID, MaxValue, NAME FROM ConversionLevels ORDER BY MaxValue ASC') conversion_levels = gpkg.curs.fetchall() reach_values_with_conversion_codes = classify_conversions(reach_average_change, conversion_ids, conversion_levels) write_db_attributes(outputs_gpkg_path, reach_values_with_conversion_codes, rvd_columns) # # Write Output to GPKG table # log.info('Insert values to GPKG tables') # # TODO move this to write_attirubtes method # with get_shp_or_gpkg(outputs_gpkg_path, layer_name='ReachAttributes', write=True, ) as in_layer: # # Create each field and store the name and index in a list of tuples # field_indices = [(field, in_layer.create_field(field, field_type)) for field, field_type in { # "FromConifer": ogr.OFTReal, # "FromDevegetated": ogr.OFTReal, # "FromGrassShrubland": ogr.OFTReal, # "FromDeciduous": ogr.OFTReal, # "NoChange": ogr.OFTReal, # "Deciduous": ogr.OFTReal, # "GrassShrubland": ogr.OFTReal, # "Devegetation": ogr.OFTReal, # "Conifer": ogr.OFTReal, # "Invasive": ogr.OFTReal, # "Development": ogr.OFTReal, # "Agriculture": ogr.OFTReal, # "ConversionCode": ogr.OFTInteger, # "ConversionType": ogr.OFTString}.items()] # for feature, _counter, _progbar in in_layer.iterate_features("Writing Attributes", write_layers=[in_layer]): # reach = feature.GetFID() # if reach not in reach_values_with_conversion_codes: # continue # # Set all the field values and then store the feature # for field, _idx in field_indices: # if field in reach_values_with_conversion_codes[reach]: # if not reach_values_with_conversion_codes[reach][field]: # feature.SetField(field, None) # else: # feature.SetField(field, reach_values_with_conversion_codes[reach][field]) # in_layer.ogr_layer.SetFeature(feature) # # Create each field and store the name and index in a list of tuples # field_indices = [(field, in_layer.create_field(field, field_type)) for field, field_type in { # "EXISTING_RIPARIAN_MEAN": ogr.OFTReal, # "HISTORIC_RIPARIAN_MEAN": ogr.OFTReal, # "RIPARIAN_DEPARTURE": ogr.OFTReal, # "EXISTING_NATIVE_RIPARIAN_MEAN": ogr.OFTReal, # "HISTORIC_NATIVE_RIPARIAN_MEAN": ogr.OFTReal, # "NATIVE_RIPARIAN_DEPARTURE": ogr.OFTReal, }.items()] # for feature, _counter, _progbar in in_layer.iterate_features("Writing Attributes", write_layers=[in_layer]): # reach = feature.GetFID() # if reach not in riparian_departure_values: # continue # # Set all the field values and then store the feature # for field, _idx in field_indices: # if field in riparian_departure_values[reach]: # if not riparian_departure_values[reach][field]: # feature.SetField(field, None) # else: # feature.SetField(field, riparian_departure_values[reach][field]) # in_layer.ogr_layer.SetFeature(feature) # with sqlite3.connect(outputs_gpkg_path) as conn: # cursor = conn.cursor() # errs = 0 # for reachid, epochs in unique_vegetation_counts.items(): # for epoch in epochs.values(): # insert_values = [[reachid, int(vegetationid), float(count * cell_area), int(count)] for vegetationid, count in zip(epoch[0], epoch[1]) if vegetationid != 0] # try: # cursor.executemany('''INSERT INTO ReachVegetation ( # ReachID, # VegetationID, # Area, # CellCount) # VALUES (?,?,?,?)''', insert_values) # # Sqlite can't report on SQL errors so we have to print good log messages to help intuit what the problem is # except sqlite3.IntegrityError as err: # # THis is likely a constraint error. # errstr = "Integrity Error when inserting records: ReachID: {} VegetationIDs: {}".format(reachid, str(list(epoch[0]))) # log.error(errstr) # errs += 1 # except sqlite3.Error as err: # # This is any other kind of error # errstr = "SQL Error when inserting records: ReachID: {} VegetationIDs: {} ERROR: {}".format(reachid, str(list(epoch[0])), str(err)) # log.error(errstr) # errs += 1 # if errs > 0: # raise Exception('Errors were found inserting records into the database. Cannot continue.') # conn.commit() # Add intermediates and the report to the XML # project.add_project_geopackage(proj_nodes['Intermediates'], LayerTypes['INTERMEDIATES']) already # added above project.add_project_geopackage(proj_nodes['Outputs'], LayerTypes['OUTPUTS']) # Add the report to the XML report_path = os.path.join(project.project_dir, LayerTypes['REPORT'].rel_path) project.add_report(proj_nodes['Outputs'], LayerTypes['REPORT'], replace=True) report = RVDReport(report_path, project) report.write() log.info('RVD complete')
def calc_conflict_attributes(flowlines_path, valley_bottom, roads, rail, canals, ownership, buffer_distance_metres, cell_size_meters, epsg, canal_codes, intermediates_gpkg_path): log = Logger('Conflict') log.info('Calculating conflict attributes') # Create union of all reaches and another of the reaches without any canals reach_union = get_geometry_unary_union(flowlines_path) if canal_codes is None: reach_union_no_canals = reach_union else: reach_union_no_canals = get_geometry_unary_union( flowlines_path, attribute_filter='FCode NOT IN ({})'.format(','.join(canal_codes))) crossin = intersect_geometry_to_layer(intermediates_gpkg_path, 'road_crossings', ogr.wkbMultiPoint, reach_union, roads, epsg) diverts = intersect_geometry_to_layer(intermediates_gpkg_path, 'diversions', ogr.wkbMultiPoint, reach_union_no_canals, canals, epsg) road_vb = intersect_to_layer(intermediates_gpkg_path, valley_bottom, roads, 'road_valleybottom', ogr.wkbMultiLineString, epsg) rail_vb = intersect_to_layer(intermediates_gpkg_path, valley_bottom, rail, 'rail_valleybottom', ogr.wkbMultiLineString, epsg) private = os.path.join(intermediates_gpkg_path, 'private_land') copy_feature_class(ownership, private, epsg, "ADMIN_AGEN = 'PVT' OR ADMIN_AGEN = 'UND'") # Buffer all reaches (being careful to use the units of the Shapefile) reaches = load_geometries(flowlines_path, epsg=epsg) with get_shp_or_gpkg(flowlines_path) as lyr: buffer_distance = lyr.rough_convert_metres_to_vector_units( buffer_distance_metres) cell_size = lyr.rough_convert_metres_to_vector_units(cell_size_meters) geopackage_path = lyr.filepath polygons = { reach_id: polyline.buffer(buffer_distance) for reach_id, polyline in reaches.items() } results = {} tmp_folder = os.path.join(os.path.dirname(intermediates_gpkg_path), 'tmp_conflict') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, road_vb, 'Mean', 'iPC_RoadVB') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, crossin, 'Mean', 'iPC_RoadX') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, diverts, 'Mean', 'iPC_DivPts') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, private, 'Mean', 'iPC_Privat') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, rail_vb, 'Mean', 'iPC_RailVB') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, canals, 'Mean', 'iPC_Canal') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, roads, 'Mean', 'iPC_Road') distance_from_features(polygons, tmp_folder, reach_union.bounds, cell_size_meters, cell_size, results, rail, 'Mean', 'iPC_Rail') # Calculate minimum distance to conflict min_keys = [ 'iPC_Road', 'iPC_RoadX', 'iPC_RoadVB', 'iPC_Rail', 'iPC_RailVB' ] for values in results.values(): values['oPC_Dist'] = min([values[x] for x in min_keys if x in values]) # Retrieve the agency responsible for administering the land at the midpoint of each reach admin_agency(geopackage_path, reaches, ownership, results) log.info('Conflict attribute calculation complete') # Cleanup temporary feature classes safe_remove_dir(tmp_folder) return results