def test_return_endsubstring(self): self.assertEqual( substring(self.line1, 0.6, 500).wkt, LineString(([0.6, 0], [2, 0])).wkt) self.assertEqual( substring(self.line1, 0.6, 1.1, True).wkt, LineString(([1.2, 0], [2, 0])).wkt)
def test_save(self): self.link.save() extension = random() name = 'just a non-important value' geo = substring(self.link.geometry, 0, extension, normalized=True) self.link.name = name self.link.geometry = geo self.link.save() self.links.refresh() link2 = self.links.get(self.lid) self.assertEqual(link2.name, name, 'Failed to save the link name') self.assertAlmostEqual(link2.geometry, geo, 3, 'Failed to save the link geometry') tot_prev = self.network.count_links() lnk = self.links.new() lnk.geometry = substring(self.link.geometry, 0, 0.88, normalized=True) lnk.modes = 'c' lnk.save() self.assertEqual(tot_prev + 1, self.network.count_links(), 'Failed to save new link')
def test_return_startpoint(self): self.assertTrue(substring(self.line1, -500, -600).equals(Point(0, 0))) self.assertTrue(substring(self.line1, -500, -500).equals(Point(0, 0))) self.assertTrue( substring(self.line1, -1, -1.1, True).equals(Point(0, 0))) self.assertTrue( substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))
def test_return_endsubstring(self): self.assertTrue( substring(self.line1, 0.6, 500).equals(LineString(([0.6, 0], [2, 0])))) self.assertTrue( substring(self.line1, 0.6, 1.1, True).equals(LineString(([1.2, 0], [2, 0]))))
def test_return_endpoint(self): self.assertTrue(substring(self.line1, 500, 600).equals(Point(2, 0))) self.assertTrue(substring(self.line1, 500, 500).equals(Point(2, 0))) self.assertTrue( substring(self.line1, 1, 1.1, True).equals(Point(2, 0))) self.assertTrue( substring(self.line1, 1.1, 1.1, True).equals(Point(2, 0)))
def test_return_startsubstring(self): self.assertTrue( substring(self.line1, -500, 0.6).equals(LineString(([0, 0], [0.6, 0])))) self.assertTrue( substring(self.line1, -1.1, 0.6, True).equals(LineString(([0, 0], [1.2, 0]))))
def test_return_startsubstring(self): self.assertEqual( substring(self.line1, -500, 0.6).wkt, LineString(([0, 0], [0.6, 0])).wkt) self.assertEqual( substring(self.line1, -1.1, 0.6, True).wkt, LineString(([0, 0], [1.2, 0])).wkt)
def test_return_midpoint(self): self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0))) self.assertTrue( substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0))) self.assertTrue( substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0))) self.assertTrue( substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))
def lineSplit(lineInfo, cpSegment, meters): if LineString(cpSegment).length == 0: return remainLength = LineString(cpSegment).length startLen = 0 while remainLength >= meters: segment = substring(LineString(cpSegment), startLen, startLen + meters) lineInfo.addSegment(segment) remainLength = remainLength - meters startLen = startLen + meters segment = substring(LineString(cpSegment), startLen, startLen + meters) lineInfo.addSegment(segment)
def get_half_geoms(g, a_node, b_node): ''' For splitting and orienting half geoms ''' # get edge data edge_data = g[a_node][b_node] # test for x coordinates if 'x' not in g.nodes[a_node] or 'y' not in g.nodes[a_node]: raise KeyError( f'Encountered node missing "x" or "y" coordinate attributes at node {a_node}.' ) # test for y coordinates if 'x' not in g.nodes[b_node] or 'y' not in g.nodes[b_node]: raise KeyError( f'Encountered node missing "x" or "y" coordinate attributes at node {b_node}.' ) a_x = g.nodes[a_node]['x'] a_y = g.nodes[a_node]['y'] b_x = g.nodes[b_node]['x'] b_y = g.nodes[b_node]['y'] # test for geom if 'geom' not in edge_data: raise KeyError( f'No edge geom found for edge {a_node}-{b_node}: ' f'Please add an edge "geom" attribute consisting of a shapely LineString.' ) # get edge geometry line_geom = edge_data['geom'] if line_geom.type != 'LineString': raise TypeError( f'Expecting LineString geometry but found {line_geom.type} geometry for edge {a_node}-{b_node}.' ) # check geom coordinates directionality - flip if facing backwards direction - beware 3d coords if not np.allclose( (a_x, a_y), line_geom.coords[0][:2], atol=0.001, rtol=0): line_geom = geometry.LineString(line_geom.coords[::-1]) # double check that coordinates now face the forwards direction if not np.allclose((a_x, a_y), line_geom.coords[0][:2], atol=0.001, rtol=0) or \ not np.allclose((b_x, b_y), line_geom.coords[-1][:2], atol=0.001, rtol=0): raise ValueError( f'Edge geometry endpoint coordinate mismatch for edge {a_node}-{b_node}' ) # generate the two half geoms a_half_geom = ops.substring(line_geom, 0, line_geom.length / 2) b_half_geom = ops.substring(line_geom, line_geom.length / 2, line_geom.length) assert np.allclose(a_half_geom.coords[-1][:2], b_half_geom.coords[0][:2], atol=0.001, rtol=0) return a_half_geom, b_half_geom
def test_return_midsubstring(self): self.assertEqual( substring(self.line1, 0.5, 0.6).wkt, LineString(([0.5, 0], [0.6, 0])).wkt) self.assertEqual( substring(self.line1, -0.6, -0.5).wkt, LineString(([1.4, 0], [1.5, 0])).wkt) self.assertEqual( substring(self.line1, 0.5, 0.6, True).wkt, LineString(([1, 0], [1.2, 0])).wkt) self.assertEqual( substring(self.line1, -0.6, -0.5, True).wkt, LineString(([0.8, 0], [1, 0])).wkt)
def test_return_substring_with_vertices(self): self.assertTrue( substring(self.line2, 1, 7).equals(LineString(([3, 1], [3, 6], [4, 6])))) self.assertTrue( substring(self.line2, 0.2, 0.9, True).equals(LineString(([3, 1.5], [3, 6], [3.75, 6])))) self.assertTrue( substring(self.line2, 0, 0.9, True).equals(LineString(([3, 0], [3, 6], [3.75, 6])))) self.assertTrue( substring(self.line2, 0.2, 1, True).equals(LineString(([3, 1.5], [3, 6], [4.5, 6]))))
def test_return_midsubstring(self): self.assertTrue( substring(self.line1, 0.5, 0.6).equals(LineString(([0.5, 0], [0.6, 0])))) self.assertTrue( substring(self.line1, 0.6, 0.5).equals(LineString(([0.6, 0], [0.5, 0])))) self.assertTrue( substring(self.line1, -0.5, -0.6).equals(LineString(([1.5, 0], [1.4, 0])))) self.assertTrue( substring(self.line1, -0.6, -0.5).equals(LineString(([1.4, 0], [1.5, 0])))) self.assertTrue( substring(self.line1, 0.5, 0.6, True).equals(LineString(([1, 0], [1.2, 0])))) self.assertTrue( substring(self.line1, 0.6, 0.5, True).equals(LineString(([1.2, 0], [1, 0])))) self.assertTrue( substring(self.line1, -0.5, -0.6, True).equals(LineString(([1, 0], [0.8, 0])))) self.assertTrue( substring(self.line1, -0.6, -0.5, True).equals(LineString(([0.8, 0], [1, 0]))))
def test_return_midsubstring(self): self.assertTrue(substring(self.line1, 0.5, 0.6).equals(LineString(([0.5, 0], [0.6, 0])))) self.assertTrue(substring(self.line1, 0.6, 0.5).equals(LineString(([0.6, 0], [0.5, 0])))) self.assertTrue(substring(self.line1, -0.5, -0.6).equals(LineString(([1.5, 0], [1.4, 0])))) self.assertTrue(substring(self.line1, -0.6, -0.5).equals(LineString(([1.4, 0], [1.5, 0])))) self.assertTrue(substring(self.line1, 0.5, 0.6, True).equals(LineString(([1, 0], [1.2, 0])))) self.assertTrue(substring(self.line1, 0.6, 0.5, True).equals(LineString(([1.2, 0], [1, 0])))) self.assertTrue(substring(self.line1, -0.5, -0.6, True).equals(LineString(([1, 0], [0.8, 0])))) self.assertTrue(substring(self.line1, -0.6, -0.5, True).equals(LineString(([0.8, 0], [1, 0]))))
def split_linestring( geometry: Union[LineString, MultiLineString, Iterable], max_length: float, pool: Pool = None) -> Union[List[LineString], List[List[LineString]]]: """ Splits a LineString into smaller parts with equal length not exceeding max_length. NOTE: the length must be provided in an appropriate unit as of the one used in LineString coordinate system. If the input geometry is a MultiLineString each LineString geometry is divided. If the input geometry is neither a LineString nor a MultiLineString the same geometry is returned within the list. :param geometry: The LineString object needs splitting :param max_length: The maximum length that each split will have. NOTE: The actual length would not exceed this value. :param pool: a process pool to be used for parallelism. The pool is only used if the geometry is of type iterable. :return: a list of LineString Object if the input is LineString or MultiLineString; or a list of list of LineString, if the input geometry is an iterable of LineString and MultiString. """ output = None if isinstance(geometry, LineString): length = geometry.length n_parts = int(np.ceil(length / max_length)) fractions = np.linspace(0, 1, n_parts + 1) output = [ ops.substring(geometry, start_fraction, end_fraction, normalized=True) for start_fraction, end_fraction in zip(fractions[:-1], fractions[1:]) ] elif isinstance(geometry, MultiLineString): output = [] for linestring in geometry.geoms: output.extend(split_linestring(linestring, max_length)) elif isinstance( geometry, Iterable ): # make sure this is listed after LineString & MultiLineString if pool is not None: # PARALLEL # print('PARALLEL') chunked_geometry = to_chunk(geometry, pool=pool) pool_output = [ pool.apply_async(split_linestring, (chunk, max_length)) for chunk in chunked_geometry ] output = [] for e in pool_output: output.extend(e.get()) else: # SERIAL # print('SERIAL') output = [split_linestring(e, max_length) for e in geometry] else: output = [geometry] return output
def getTerminusWidth(self): """Compute box width at points where terminus intersects box, and return average width in km.""" if self.referencebox.geom_type == 'MultiLineString': box = ops.linemerge(ops.MultiLineString(self.referencebox)) else: box = ops.LineString(self.referencebox) # Split box into two halves half1 = ops.substring(box, 0, 0.5, normalized=True) half2 = ops.substring(box, 0.5, 1, normalized=True) # Get terminus intersections with box halves tx1 = self.terminus.intersection(half1) tx2 = self.terminus.intersection(half2) # Get distance from intersection point to other half tx1_dist = tx1.distance(half2) tx2_dist = tx2.distance(half1) # Get average distance across box, i.e. average terminus width average_width = (tx1_dist + tx2_dist) / 2 / 10**3 return average_width
def test_return_midpoint(self): self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0))) self.assertTrue(substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0))) self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0))) self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0))) # Coming from opposite ends self.assertTrue(substring(self.line1, 1.5, -0.5).equals(Point(1.5, 0))) self.assertTrue(substring(self.line1, -0.5, 1.5).equals(Point(1.5, 0))) self.assertTrue(substring(self.line1, -0.7, 0.3, True).equals(Point(0.6, 0))) self.assertTrue(substring(self.line1, 0.3, -0.7, True).equals(Point(0.6, 0)))
def calculate_center_lane(self, resolution: float): """ Calculate center lane of the road by applying lane offsets and store them in the respective center lanes. """ ref_line = self.plan_view.midline if not self.lanes.lane_offsets: center_lane = ref_line else: sample_distances = np.linspace( 0.0, ref_line.length, int(ref_line.length / resolution) + 1) offsets = [] for offset_idx, offset in enumerate(self.lanes.lane_offsets): if offset_idx == len(self.lanes.lane_offsets) - 1: section_distances = sample_distances[ offset.start_offset <= sample_distances] else: next_offset = self.lanes.lane_offsets[offset_idx + 1] indices = ((offset.start_offset <= sample_distances) & (sample_distances < next_offset.start_offset) ).nonzero()[0] section_distances = sample_distances[indices] coefficients = list(reversed(offset.polynomial_coefficients)) section_offset = np.polyval( coefficients, section_distances - offset.start_offset) offsets.append(section_offset) offsets = np.hstack(offsets) points = [] for i, d in enumerate(sample_distances): point = ref_line.interpolate(d) theta = normalise_angle(self.plan_view.calc(d)[1] + np.pi / 2) normal = np.array([np.cos(theta), np.sin(theta)]) points.append(tuple(point + offsets[i] * normal)) center_lane = LineString(ramer_douglas(points, dist=0.01)) if not center_lane.is_simple: coords_list = [] for non_intersecting_ls in unary_union(center_lane): if non_intersecting_ls.length > 0.5: coords_list.extend(non_intersecting_ls.coords) center_lane = LineString(coords_list) # Assign midlines for each center lane for every lane section for ls in self.lanes.lane_sections: lane = ls.center_lanes[0] lane._ref_line = substring(center_lane, ls.start_distance / ref_line.length, (ls.start_distance + ls.length) / ref_line.length, normalized=True)
def cut_polygon_to_resolution(self, poly, resolution): """cuts the straight lines into equally sized line segments no larger than the prescribed resolution""" segments = [] poly_coords = list(poly.exterior.coords) for i in range(len(poly_coords) - 1): line = LineString((poly_coords[i], poly_coords[i + 1])) # we here determine the actual resolution of the polygons divs = line.length / (np.ceil(line.length / resolution)) for j in np.arange(0, line.length, divs): segment = substring(line, j, j + divs) segments.extend(list(segment.coords)[1:2]) final_polygon = Polygon(segments[:]) return final_polygon
def cut_vector(vector_path, cut_distances): """Cut vector at defined distances.""" sub_vectors = [] # Holds sub-vectors # Read the full vector vector = read_vector(vector_path) # Cut vector at every distance point in cut_distances for i, dist in enumerate(cut_distances): # First segment if i == 0: sub_v = op.substring(vector, 0, dist, normalized=True) # Later segments else: sub_v = op.substring(vector, cut_distances[i - 1], dist, normalized=True) # Add cut segment to list sub_vectors.append(sub_v) return sub_vectors
def test_return_endsubstring_reversed(self): # not normalized self.assertEqual(substring(self.line1, 500, -1).wkt, LineString(([2, 0], [1, 0])).wkt) self.assertEqual(substring(self.line3, 4, 2.5).wkt, LineString(([0, 4], [0, 3], [0, 2.5])).wkt) self.assertEqual(substring(self.line3, 500, -1.5).wkt, LineString(([0, 4], [0, 3], [0, 2.5])).wkt) # normalized self.assertEqual(substring(self.line1, 1.1, -0.5, True).wkt, LineString(([2, 0], [1.0, 0])).wkt) self.assertEqual(substring(self.line3, 1, 0.5, True).wkt, LineString(([0, 4], [0, 3], [0, 2.0])).wkt) self.assertEqual(substring(self.line3, 1.1, -0.5, True).wkt, LineString(([0, 4], [0, 3], [0, 2.0])).wkt)
def test_return_startsubstring_reversed(self): # not normalized self.assertEqual(substring(self.line1, -1, -500).wkt, LineString(([1, 0], [0, 0])).wkt) self.assertEqual(substring(self.line3, 3.5, 0).wkt, LineString(([0, 3.5], [0, 3], [0, 2], [0, 1], [0, 0])).wkt) self.assertEqual(substring(self.line3, -1.5, -500).wkt, LineString(([0, 2.5], [0, 2], [0, 1], [0, 0])).wkt) # normalized self.assertEqual(substring(self.line1, -0.5, -1.1, True).wkt, LineString(([1.0, 0], [0, 0])).wkt) self.assertEqual(substring(self.line3, 0.5, 0, True).wkt, LineString(([0, 2.0], [0, 1], [0, 0])).wkt) self.assertEqual(substring(self.line3, -0.5, -1.1, True).wkt, LineString(([0, 2.0], [0, 1], [0, 0])).wkt)
def make_messy_graph(G): # test that redundant (sraight) intersections are removed G_messy = G.copy(G) # complexify the graph - write changes to new graph to avoid in-place iteration errors for i, (s, e, k, d) in enumerate(G.edges(data=True, keys=True)): # flip each third geom if i % 3 == 0: flipped_coords = np.fliplr(d['geom'].coords.xy) G_messy[s][e][k]['geom'] = geometry.LineString( [[x, y] for x, y in zip(flipped_coords[0], flipped_coords[1])]) # split each second geom if i % 2 == 0: line_geom = G[s][e][k]['geom'] # check geom coordinates directionality - flip if facing backwards direction if not (G.nodes[s]['x'], G.nodes[s]['y']) == line_geom.coords[0][:2]: flipped_coords = np.fliplr(line_geom.coords.xy) line_geom = geometry.LineString([[x, y] for x, y in zip(flipped_coords[0], flipped_coords[1])]) # remove old edge G_messy.remove_edge(s, e) # new midpoint 'x' and 'y' coordinates s_geom = ops.substring(line_geom, 0, 0.5, normalized=True) e_geom = ops.substring(line_geom, 0.5, 1, normalized=True) # looking for the non-matching coordinates mid_x, mid_y = s_geom.coords[-1][:2] # add new edges G_messy.add_edge(s, f'{s}-{e}', geom=s_geom) G_messy.add_edge(e, f'{s}-{e}', geom=e_geom) G_messy.nodes[f'{s}-{e}']['x'] = mid_x G_messy.nodes[f'{s}-{e}']['y'] = mid_y # test recursive weld by manually adding a chained series of orphan nodes geom = G[10][43][0]['geom'] geom_a = ops.substring(geom, 0, 0.25, normalized=True) G_messy.add_edge(10, 't_1', geom=geom_a) a_x, a_y = geom_a.coords[-1][:2] G_messy.nodes['t_1']['x'] = a_x G_messy.nodes['t_1']['y'] = a_y geom_b = ops.substring(geom, 0.25, 0.5, normalized=True) G_messy.add_edge('t_1', 't_2', geom=geom_b) b_x, b_y = geom_b.coords[-1][:2] G_messy.nodes['t_2']['x'] = b_x G_messy.nodes['t_2']['y'] = b_y geom_c = ops.substring(geom, 0.5, 0.75, normalized=True) G_messy.add_edge('t_2', 't_3', geom=geom_c) c_x, c_y = geom_c.coords[-1][:2] G_messy.nodes['t_3']['x'] = c_x G_messy.nodes['t_3']['y'] = c_y geom_d = ops.substring(geom, 0.75, 1.0, normalized=True) G_messy.add_edge('t_3', 43, geom=geom_d) # remove original geom G_messy.remove_edge(10, 43) return G_messy
def get_as_shape(self, cap_style=CAP_STYLE.flat): """ Get marking as a shapely Polygon or MultiPolygon :param cap_style: cap style to use when performing buffer :return: shapely Polygon or MultiPolygon """ if self.dashes[1] == 0: # if solid line buffer = self.alignment.buffer(self.linewidth / 2, cap_style=cap_style) else: # if dashed line buffer = MultiPolygon() dash_length, gap = self.dashes for s in np.arange(0, self.alignment.length, dash_length + gap): assert hasattr(ops, "substring"), "Shapely>=1.7.0 is required for OBJ export of dashed lines." dash_segment = ops.substring(self.alignment, s, min(s + dash_length, self.alignment.length)) buffer = buffer.union(dash_segment.buffer(self.linewidth / 2, cap_style=cap_style)) return buffer
def shape(self) -> LineString: "Returns the shape of the route. The route is has to be continuous." if self.start.line.line_id == self.end.line.line_id: return substring(self.start.line.geometry, self.start.relative_offset, self.end.relative_offset, normalized=True) result = [] first = self.start.split()[1] last = self.end.split()[0] if first is not None: result.append(first) result += [line.geometry for line in self.path_inbetween] if last is not None: result.append(last) return join_lines(result)
def project(line: Line, coord: Coordinates) -> PointOnLine: """Computes the nearest point to `coord` on the line Returns: The point on `line` where this nearest point resides""" fraction = line.geometry.project(Point(coord.lon, coord.lat), normalized=True) to_projection_point = substring(line.geometry, 0.0, fraction, normalized=True) meters_to_projection_point = line_string_length(to_projection_point) geometry_length = line_string_length(line.geometry) length_fraction = meters_to_projection_point / geometry_length return PointOnLine(line, length_fraction)
def test_return_startpoint(self): self.assertTrue(substring(self.line1, -500, -600).equals(Point(0, 0))) self.assertTrue(substring(self.line1, -500, -500).equals(Point(0, 0))) self.assertTrue(substring(self.line1, -1, -1.1, True).equals(Point(0, 0))) self.assertTrue(substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))
def test_return_endpoint(self): self.assertTrue(substring(self.line1, 500, 600).equals(Point(2, 0))) self.assertTrue(substring(self.line1, 500, 500).equals(Point(2, 0))) self.assertTrue(substring(self.line1, 1, 1.1, True).equals(Point(2, 0))) self.assertTrue(substring(self.line1, 1.1, 1.1, True).equals(Point(2, 0)))
def test_return_endsubstring(self): self.assertTrue(substring(self.line1, 0.6, 500).equals(LineString(([0.6, 0], [2, 0])))) self.assertTrue(substring(self.line1, 0.6, 1.1, True).equals(LineString(([1.2, 0], [2, 0]))))
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_return_startsubstring(self): self.assertTrue(substring(self.line1, -500, 0.6).equals(LineString(([0, 0], [0.6, 0])))) self.assertTrue(substring(self.line1, -1.1, 0.6, True).equals(LineString(([0, 0], [1.2, 0]))))
def to_tpar_boundary(self, dest_path, boundary, interval, x_var='lon', y_var='lat', hs_var='sig_wav_ht', per_var='pk_wav_per', dir_var='pk_wav_dir', dir_spread=20.): ''' This function writes parametric boundary forcing to a set of TPAR files at a given distance based on gridded wave output. It returns the string to be included in the Swan INPUT file. At present simple nearest neighbour point lookup is used. Args: TBD ''' from shapely.ops import substring bound_string = "BOUNDSPEC SEGM XY " point_string = "&\n {xp:0.8f} {yp:0.8f} " file_string = "&\n {len:0.8f} '{fname}' 1 " for xp, yp in boundary.exterior.coords: bound_string += point_string.format(xp=xp, yp=yp) bound_string += "&\n VAR FILE " n_pts = int((boundary.length) / interval) splits = np.linspace(0, 1., n_pts) boundary_points = [] j = 0 for i in range(len(splits) - 1): segment = substring(boundary.exterior, splits[i], splits[i + 1], normalized=True) xp = segment.coords[1][0] yp = segment.coords[1][1] logger.debug(f'Extracting point: {xp},{yp}') ds_point = self._obj.sel(indexers={ x_var: xp, y_var: yp }, method='nearest', tolerance=interval) if len(ds_point.time) == len(self._obj.time): if not np.any(np.isnan(ds_point[hs_var])): with open(f'{dest_path}/{j}.TPAR', 'wt') as f: f.write('TPAR\n') for t in range(len(ds_point.time)): ds_row = ds_point.isel(time=t) lf = '{tt} {hs:0.2f} {per:0.2f} {dirn:0.1f} {spr:0.2f}\n' f.write( lf.format(tt=str(ds_row['time'].dt.strftime( '%Y%m%d.%H%M%S').values), hs=float(ds_row[hs_var]), per=float(ds_row[per_var]), dirn=float(ds_row[dir_var]), spr=dir_spread)) bound_string += file_string.format(len=splits[i + 1] * boundary.length, fname=f'{j}.TPAR') j += 1 return bound_string
def test_return_substring_issue682(self): assert list(substring(self.line2, 0.1, 0).coords) == [(3.0, 0.1), (3.0, 0.0)]
for i in range(len(boundaries)): if (boundaries[i] != []): neighbors = [] precinct_polygon = boundaries[i][0] precinct = LineString(list(precinct_polygon.exterior.coords)) size_precinct = precinct_polygon.boundary.length for j in possibleNeighbors[i]: other_polygon = boundaries[j][0] other = LineString(list(other_polygon.exterior.coords)) smaller = precinct if ( size_precinct < other_polygon.boundary.length) else other bigger = precinct if ( size_precinct >= other_polygon.boundary.length) else other distance = 0 while (distance < smaller.boundary.length): sub_boarder = substring(smaller, distance, distance + coincident) maximum_dist = max([ Point(point).distance(bigger) for point in sub_boarder.coords ]) if (maximum_dist <= coincident): neighbors.append(utah_precincts[j]['properties']['cname']) break else: distance += (coincident / 4) utah_precincts[i]['properties']['neighbors'] = neighbors with open("C:/Users/Denis/Software Engineering/Data/Utah/Precincts/Utah.json", "w") as utah: json.dump(utah_data, utah, indent=2)
def test_return_midpoint(self): self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0))) self.assertTrue(substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0))) self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0))) self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))
def test_return_substring_issue848(self): line = shape(json.loads(data_issue_848)) cut_line = substring(line, 0.7, 0.8, normalized=True) assert len(cut_line.coords) == 53
def test_return_substring_with_vertices(self): self.assertTrue(substring(self.line2, 1, 7).equals(LineString(([3, 1], [3, 6], [4, 6])))) self.assertTrue(substring(self.line2, 0.2, 0.9, True).equals(LineString(([3, 1.5], [3, 6], [3.75, 6]))))