diff --git a/IDEAS.md b/IDEAS.md new file mode 100644 index 0000000..36da5b1 --- /dev/null +++ b/IDEAS.md @@ -0,0 +1,84 @@ +## Alternative strategies + +### Select all sidewalks within X meters of the intersection + +1. Select all sidewalks within X meters of the intersection +2. + +### By studying 'eye test' method + +Humans can figure out where to draw crossings pretty easily (even unmarked +ones). How do they do it? Here's some guesses. + +1. Locate sidewalk corners. This might be just one sidewalk approaching the +street, it may be a legit corner with 2 sidewalks meeting. + +2. If you could reasonably cross from corner to corner, draw a crossing. + +3. The location where the crossing connects to the sidewalk is somewhat +subjective and up to the style of the mapper (for now). A natural place is +along the line of the incoming sidewalk, if there are 2. Having them meet at +just one location is *fine* for now. + +So, the real strategy in code: + +1. Go up the street from the intersection X meters / half way up (whichever +comes first). X could be ~10-15 meters. Return this point for each outgoing +road. + +2. + +### Original-ish + +1. Go up each street in small increments (~half meter?), generating long +orthogonal lines. + +2. Restrict to sidewalks within X meters of the street (something moderately +big like 30 meters) + +3. Discard lines that don't intersect sidewalks on both the left and right +sides. Can be done by taking the intersection and asking the left/right question + +4. Subset remainder to line segment that intersects the street (trim by the +sidewalks) + +5. Remove crossings that intersect any other streets (i.e. crossing multiple +streets). This should be done very carefully given that some streets are +divided and have multiple intersections. osmnx tries to group these intersections +but I don't know what happens to the lines. i.e. we don't want to discard all +boulevards. Actual question is more complex? FIXME: revisit this step + +6. The first crossing remaining is probably legit. + +FIXME: the problem with this approach is that it will miss the situation where +there's only one sidewalk at the corner (one street doesn't have sidewalks, +e.g.). + +### Original-ish 2 + +1. Identify sidewalks to the left of street, to the right of street +2. Find point on the street where an orthogonal line first intersects the +sidewalk on the right and does not intersect any other streets. +3. Repeat for the left side +4. Whichever is farther down is the primary candidate. +5. Attempt to extend this orthogonal line such that it spans from right sidewalk +to left sidewalk. +6. If this fails, find the closest point on the 'other' sidewalk to the point +on the street. Draw a line from sidewalk to sidewalk. + +### Original-ish 3: ding ding ding! + +1. Walk down the street in X increments. +2. Find a line between the street point and the nearest sidewalk to the right +that does not intersect another street. One method for doing this is to +split the sidewalks up by block beforehand. Alternatively, use the 'block graph' +method used in sidewalkify to group sidewalks. Cyclic subgraphs = block +polygons, acyclic sugraphs = tricky +3. Repeat for the left side. +4. Compare the lines: if they're roughly parallel, keep the crossing endpoints +and draw a line between them. This is the crossing +5. If the crossing is too long (40 meters?), delete it. + +Note: rather than incrementing by small amounts and then stopping, this +strategy could use a binary search such that an arbitrary parallel-ness could +be found. diff --git a/README.md b/README.md index d2efd02..8449438 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # Crossify -`crossifyify` is a Python library and command line application for drawing +`crossify` is a Python library and command line application for drawing street crossing lines from street centerline and sidewalk centerline data. +`crossify` has two usage modes: + +- It can automatically fetch sidewalk data from OpenStreetMap +- It can read sidewalk data from a GIS file (e.g. GeoJSON or shapefile) + +In both modes, it fetches street data from OpenStreetMap and uses the sidewalk +and street data to construct likely crossing locations. + ## Introduction Pedestrian transportation network data are often missing. `crossify` works in @@ -28,20 +36,33 @@ to Python 2. Once installed, `crossify` is available both as a command line application and a Python library. -#### CLI +### Sidewalks should be fetched from OpenStreetMap + +To fetch sidewalk data from OpenStreetMap, use the `from_bbox` command: + + crossify from_bbox -- + +The values of west, south, east, and north define a rectangular bounding box +for your query, and should be in terms of latitude (south, north) and longitude +(west, east). The use of a double dash is necessary for the use of negative +coordinates not getting parsed as command line options (see the example below). Example: - crossify + crossify from_bbox -- -122.31846 47.65458 -122.31004 47.65783 + test/output/crossings.geojson + +### A sidewalks file is provided -##### Arguments +If you want to provide your own sidewalks layer, use the `from_file` command: -The input file can be any file type readable by `geopandas.read_file`, which -should be anything readable by `fiona`, i.e. GDAL. + crossify from_file + +Example: -For example, you could also use a GeoJSON input file: + crossify from_file test/input/sidewalks_udistrict.geojson + test/output/crossings.geojson - crossify #### Python Library diff --git a/STRATEGY.md b/STRATEGY.md new file mode 100644 index 0000000..eb39c16 --- /dev/null +++ b/STRATEGY.md @@ -0,0 +1,69 @@ +# Strategy + +This document describes the overall strategy behind `crossify`'s algorithms. + +At a glance, `crossify` generates lines that connect sidewalks across street +intersections. Some of these crossings may be crosswalks, or entirely unmarked: +such information is treated as metadata to be added later as attributes of +the crossings generated by `crossify`. + +Given a streets dataset and sidewalks dataset, crossify attempts to draw one +crossing per street in an intersection, given that there are appropriate +sidewalks on either side. It accomplishes this in an N-step process: + +1. Streets are split when they share a node (point) and are at the same +z-level. Streets are also joined end-to-end at all other locations. +Note: this step is not actually implemented yet, so datasets are +assumed to have been appropriately split in advance. + +2. Intersections are identified as shared nodes with more than 2 streets from +step one. + +3. Intersections are grouped based on proximity, as some may be complex +(e.g. 6-way intersections, boulevards). The street segment(s) connecting each +intersection node are ignored. All further logic happens on a +per-intersection group basis. + +4. Create a buffer of 10 meters (by default) around the intersection's node(s) +and do an intersection operation on the streets/sidewalks: all streets and +sidewalks are clipped at 10 meters from an intersection node. In most cases, +this will mean that all streets and sidewalks are cut off by a 30-meter radius +circle. If necessary, streets are temporarily extended to be 30 meters long. +Idea: possibly use polygonized street data? +Idea: use voronoi polygons? + +5. Select all incoming streets and split the buffer by them. This aids the +algorithm in ensure a connection across the street, guaranteeing each end of a +crossing connects to left or right. + +6. Extend 'up' each street in 1-meter increments, finding the +closest sidewalk on each side. Only extend at most half-way down the street. + +7. If each sidewalk side differs too much in length, the entire crossing is +discarded. The problem this is attempting to solve is one where incoming +streets have a very acute angle, and we don't want to select the sidewalk on +the opposite side. + +9. Choose the first 1-meter increment node that's relatively straight. Can +abort generating any further 1-meter increment nodes for this street. + +9. Make that crossing truly straight - connect each side with a straight line. + +10. If the option is set, the first and last ~1 meter of the crossing is +converted to a short line of 'sidewalk'. The point where a 1-meter segment of +new sidewalk meets a crossing is where a curb interface should eventually be +annotated. + +## Future work + +There are many ways that these strategies could be improved. Here's a list of +ideas: + +- Generate crossings automatically for long sidewalk stretches so that the +pedestrian network remains well-connected even with missing data. + +- Generate half-crossings where sidewalks end, so they are more appropriately +connected to the street network. + +- Add option to generate crossing lines from street node metadata, e.g. +OpenStreetMap's highway=crossing data. diff --git a/cache/.gitignore b/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/crossify/__init__.py b/crossify/__init__.py index f385838..b794fd4 100644 --- a/crossify/__init__.py +++ b/crossify/__init__.py @@ -1,4 +1 @@ -from . import draw, graph - - __version__ = '0.1.0' diff --git a/crossify/__main__.py b/crossify/__main__.py index 53c7891..5eeca46 100644 --- a/crossify/__main__.py +++ b/crossify/__main__.py @@ -1,37 +1,112 @@ import click -import geopandas as gpd +from os import path +import osmnx as ox -from . import populate, constrain, schema +from . import crossings, intersections, io -@click.command() +# TODO: See if there's a more proper way to find the project root dir +# TODO: use a temporary dir? +ox.utils.config(cache_folder=path.join(path.dirname(__file__), '../cache'), + use_cache=True) + +# Groups: +# - Download all data from OSM bounding box, produce OSM file +# - Download all data from OSM bounding box, produce GeoJSON file +# - Provide own sidewalks data, produce OSM file +# - Provide own sidewalks data, produce GeoJSON file + +# So, the arguments are: +# - Where is the info coming from? A file or a bounding box in OSM? +# - What is the output format? + +# crossify from_bbox [bbox] output.extension +# crossify from_file sidewalks.geojson output.extension + + +@click.group() +def crossify(): + pass + + +@crossify.command() @click.argument('sidewalks_in') -@click.argument('streets_in') @click.argument('outfile') -def crossify(sidewalks_in, streets_in, outfile): - # FIXME: these should be turned into configuration options - intersections_only = True - osm_schema = True +def from_file(sidewalks_in, outfile): + # + # Read, fetch, and standardize data + # + + # Note: all are converted to WGS84 by default + sidewalks = io.read_sidewalks(sidewalks_in) + core(sidewalks, outfile) + + +@crossify.command() +@click.argument('west') +@click.argument('south') +@click.argument('east') +@click.argument('north') +@click.argument('outfile') +@click.option('--debug', is_flag=True) +def from_bbox(west, south, east, north, outfile, debug): + # + # Read, fetch, and standardize data + # + + # Note: all are converted to WGS84 by default + sidewalks = io.fetch_sidewalks(west, south, east, north) + core(sidewalks, outfile, debug) + + +def core(sidewalks, outfile, debug=False): + # + # Read, fetch, and standardize data + # + + # Note: all are converted to WGS84 by default + click.echo('Fetching street network from OpenStreetMap...') + G_streets = io.fetch_street_graph(sidewalks) + + # Work in UTM + click.echo('Generating street graph...') + G_streets_u = ox.projection.project_graph(G_streets) + sidewalks_u = ox.projection.project_gdf(sidewalks) - sidewalks = gpd.read_file(sidewalks_in) - streets = gpd.read_file(streets_in) + # Get the undirected street graph + G_undirected_u = ox.save_load.get_undirected(G_streets_u) - # Ensure we're working in the same CRS as the sidewalks dataset - crs = sidewalks.crs - streets = streets.to_crs(crs) + # Extract streets from streets graph + click.echo('Extracting geospatial data from street graph...') + streets = ox.save_load.graph_to_gdfs(G_undirected_u, nodes=False, + edges=True) + streets.crs = sidewalks_u.crs - # FIXME: this is where we'd create the crossings - dense_crossings = populate.populate(sidewalks, streets) + # + # Isolate intersections that need crossings (degree > 3), group with + # their streets (all pointing out from the intersection) + # + click.echo('Isolating street intersections...') + ixns = intersections.group_intersections(G_streets_u) - if intersections_only: - crossings = constrain.constrain(dense_crossings) - else: - crossings = dense_crossings + # + # Draw crossings using the intersection + street + sidewalk info + # + click.echo('Drawing crossings...') + st_crossings = crossings.make_crossings(ixns, sidewalks_u, debug=debug) + if debug: + st_crossings, street_segments = st_crossings - if osm_schema: - crossings = schema.apply_schema(crossings) + # + # Write to file + # + click.echo('Writing to file...') + io.write_crossings(st_crossings, outfile) + if debug: + base, ext = path.splitext(outfile) + debug_outfile = '{}_debug{}'.format(base, ext) + io.write_debug(street_segments, debug_outfile) - crossings.to_file(outfile) if __name__ == '__main__': crossify() diff --git a/crossify/constrain.py b/crossify/constrain.py deleted file mode 100644 index e69de29..0000000 diff --git a/crossify/crossings.py b/crossify/crossings.py new file mode 100644 index 0000000..81a4319 --- /dev/null +++ b/crossify/crossings.py @@ -0,0 +1,234 @@ +import geopandas as gpd +import numpy as np +from shapely.geometry import LineString, Point, Polygon + + +def make_crossings(intersections_dict, sidewalks, debug=False): + crs = sidewalks.crs + st_crossings = [] + street_segments = [] + ixn_dat = [] + for i, (ixn, data) in enumerate(intersections_dict.items()): + ixn_dat.append({ + 'geometry': data['geometry'], + 'ixn': i + }) + for street in data['streets']: + new_crossing = make_crossing(street, sidewalks, data['streets'], + debug) + if debug: + new_crossing, street_segment = new_crossing + street_segments.append(street_segment) + if new_crossing is not None: + st_crossings.append(new_crossing) + + st_crossings = gpd.GeoDataFrame(st_crossings) + st_crossings = gpd.GeoDataFrame(st_crossings[['geometry']]) + st_crossings = st_crossings[st_crossings.type == 'LineString'] + st_crossings = st_crossings[st_crossings.is_valid] + + # Remove duplicates + def comp(geom): + p1 = np.round(geom.coords[0], 2) + p2 = np.round(geom.coords[-1], 2) + return str([p1, p2]) + + comparison = st_crossings.geometry.apply(comp) + comparison.name = 'comp' + unique = st_crossings.groupby(comparison).first() + st_crossings = gpd.GeoDataFrame(unique.reset_index()) + st_crossings.crs = crs + + st_crossings = st_crossings.to_crs({'init': 'epsg:4326'}) + + if debug: + street_segments = gpd.GeoDataFrame(street_segments) + street_segments.crs = sidewalks.crs + street_segments = street_segments.to_crs({'init': 'epsg:4326'}) + return st_crossings, street_segments + else: + return st_crossings + + +def make_crossing(street, sidewalks, streets_list, debug=False): + '''Attempts to create a street crossing line given a street segment and + a GeoDataFrame sidewalks dataset. The street and sidewalks should have + these properties: + + (1) The street should start at the street intersection and extend away + from it. + (2) The sidewalks should all be LineString geometries. + + If a crossing cannot be created that meets certain internal parameters, + None is returned. + + :param street: The street geometry. + :type street: shapely.geometry.LineString + :param sidewalks: The sidewalks dataset. + :type sidewalks: geopandas.GeoDataFrame + :returns: If a crossing can be made, a shapely Linestring. Otherwise, None. + :rtype: shapely.geometry.LineString or None + + ''' + # 'Walk' along the street in 1-meter increments, finding the closest + # sidewalk + the distance along each end. Reject those with inappropriate + # angles and differences in length. + # TODO: this is a good place for optimizations, it's a search problem. + # Can probably do something like binary search. + + # Clip street in half: don't want to cross too far in. + # TODO: this should be done in a more sophisticated way. e.g. dead ends + # shouldn't require this and we should use a max distance value as well + # street = street.interpolate(0.5, normalized=True) + + # New idea: use street buffers of MAX_CROSSING_DIST + small delta, use + # this to limit the sidewalks to be considered at each point. Fewer + # distance and side-of-line queries! + + # FIXME: use 'z layer' data if available (e.g. OSM) + + START_DIST = 4 + INCREMENT = 2 + MAX_DIST_ALONG = 25 + MAX_CROSSING_DIST = 30 + OFFSET = MAX_CROSSING_DIST / 2 + + st_distance = min(street.length / 2, MAX_DIST_ALONG) + start_dist = min(START_DIST, st_distance / 2) + + # Create buffer for the street search area, one for each side, then find + # the sidewalks intersecting that buffer - use as candidates for + # right/left + street_cut = cut(street, st_distance)[0] + + if debug: + street_segment = {'geometry': street_cut, 'issue': 'None'} + + sidewalk_sides = {} + + for side in ('left', 'right'): + side_sidewalks = get_side_sidewalks(OFFSET, side, street_cut, + sidewalks) + if side_sidewalks.shape[0] < 1: + # One of the sides has no sidewalks to connect to! Abort! + if debug: + street_segment['issue'] = 'no {} sidewalk'.format(side) + return None, street_segment + else: + return None + sidewalk_sides[side] = side_sidewalks + + candidates = [] + for dist in np.arange(start_dist, st_distance, INCREMENT): + crossing = crossing_from_dist(street, dist, + sidewalk_sides['left'], + sidewalk_sides['right']) + + # We now have the lines on the left and right sides. Let's now filter + # and *not* append if either are invalid + + # if side.length > MAX_DIST or crosses_streets(side, streets): + other_streets = [st for st in streets_list if st != street] + crosses_self, crosses_others = valid_crossing(crossing, street_cut, + other_streets) + + # The sides have passed the filter! Add their data to the list + if crosses_self and not crosses_others: + candidates.append({'geometry': crossing, 'distance': dist}) + + if not candidates: + if debug: + street_segment['issue'] = 'no candidates' + return None, street_segment + else: + return None + + # Return the shortest crossing. + # TODO: Should also bias towards *earlier* appearances, i.e. towards + # corner. + # lengths = np.array([line['crossing'].length for line in lines]) + # # Inverse distance function (distance from intersection) + # distance_metric = 1 / np.array([line['distance'] for line in lines]) + + # lengths * distance_metric + def metric(candidate): + return candidate['geometry'].length + 1e-1 * candidate['distance'] + + best = sorted(candidates, key=metric)[0] + + if debug: + return best, street_segment + else: + return best + + +def get_side_sidewalks(offset, side, street, sidewalks): + offset = street.parallel_offset(offset, side, 0, 1, 1) + if offset.type == 'MultiLineString': + # Convert to LineString + coords = [] + for geom in offset.geoms: + coords += list(geom.coords) + offset = LineString(coords) + if side == 'left': + offset.coords = offset.coords[::-1] + st_buffer = Polygon(list(street.coords) + + list(offset.coords) + + [street.coords[0]]) + query = sidewalks.sindex.intersection(st_buffer.bounds, objects=True) + query_sidewalks = sidewalks.loc[[q.object for q in query]] + side_sidewalks = query_sidewalks[query_sidewalks.intersects(st_buffer)] + + return side_sidewalks + + +def crossing_from_dist(street, dist, sidewalks_left, sidewalks_right): + # Grab a point along the outgoing street + point = street.interpolate(dist) + + # Find the closest left and right points + def closest_line_to_point(point, lines): + sorted_side = lines.distance(point).sort_values() + closest = lines.loc[sorted_side.index[0], 'geometry'] + return closest.interpolate(closest.project(point)) + + left = closest_line_to_point(point, sidewalks_left) + right = closest_line_to_point(point, sidewalks_right) + + # We now have the lines on the left and right sides. Let's now filter + # and *not* append if either are invalid + # (1) They cannot cross any other street line + # (2) They cannot be too far away (MAX_DIST) + crossing = LineString([left, right]) + + return crossing + + +def valid_crossing(crossing, street, other_streets): + crosses_street = street.intersects(crossing) + crosses_others = [other.intersects(crossing) for other in other_streets] + + return crosses_street, any(crosses_others) + + if any(crosses_others): + return False + + return True + + +def cut(line, distance): + # Cuts a line in two at a distance from its starting point + if distance <= 0.0 or distance >= line.length: + return [LineString(line)] + coords = list(line.coords) + for i, p in enumerate(coords): + pd = line.project(Point(p)) + if pd == distance: + return [ + LineString(coords[:i+1]), + LineString(coords[i:])] + if pd > distance: + cp = line.interpolate(distance) + return [ + LineString(coords[:i] + [(cp.x, cp.y)]), + LineString([(cp.x, cp.y)] + coords[i:])] diff --git a/crossify/intersections.py b/crossify/intersections.py new file mode 100644 index 0000000..96eecad --- /dev/null +++ b/crossify/intersections.py @@ -0,0 +1,50 @@ +from shapely.geometry import LineString, Point + + +def group_intersections(G): + # FIXME: require undirected graph for degree calcs + + # Isolate intersections for which to generate crossings + intersections = [node for node, degree in G.degree if degree > 2] + + # Find all incoming and outgoing streets, associate them with the + # intersection, and make sure the geometries all extend out from the node + intersection_groups = {} + + for intersection_id in intersections: + data = G.node[intersection_id] + intersection = Point(data['x'], data['y']) + + # Two-way streets will produce two edges: one in, one out. We will keep + # only the outgoing one + incoming = set(G.predecessors(intersection_id)) + outgoing = set(G.successors(intersection_id)) + incoming = incoming.difference(outgoing) + + geometries = [] + for node in incoming: + geometries.append(get_edge_geometry(G, node, intersection_id)) + + for node in outgoing: + geometries.append(get_edge_geometry(G, intersection_id, node)) + + for i, geometry in enumerate(geometries): + if Point(*geometry.coords[-1]).distance(intersection) < 1e-1: + geometries[i] = LineString(geometry.coords[::-1]) + + intersection_groups[intersection_id] = { + 'geometry': intersection, + 'streets': geometries + } + + return intersection_groups + + +def get_edge_geometry(G, from_node, to_node): + edge = G[from_node][to_node][0] + if 'geometry' in edge: + return edge['geometry'] + else: + start = Point((G.nodes[from_node]['x'], G.nodes[from_node]['y'])) + end = Point((G.nodes[to_node]['x'], G.nodes[to_node]['y'])) + return LineString([start, end]) diff --git a/crossify/io.py b/crossify/io.py new file mode 100644 index 0000000..87dfd47 --- /dev/null +++ b/crossify/io.py @@ -0,0 +1,92 @@ +import os +import shutil +from tempfile import mkdtemp + +import geopandas as gpd +import osmnx as ox +import overpass +from shapely.geometry import shape + +from . import validators + + +def read_sidewalks(path): + sidewalks = gpd.read_file(path) + + # Validate/convert input geometries, e.g. all LineStrings. + sidewalks = validators.validate_sidewalks(sidewalks) + + # Use WGS84 to start + sidewalks_wgs84 = sidewalks.to_crs({'init': 'epsg:4326'}) + + return sidewalks_wgs84 + + +def fetch_sidewalks(west, south, east, north): + api = overpass.API() + footpaths_filter = '[highway=footway][footway=sidewalk]' + response = api.Get('way{}({},{},{},{})'.format(footpaths_filter, south, + west, north, east)) + # Response is a GeoJSON FeatureCollection: convert to GeoDataFrame + rows = [] + for feature in response['features']: + data = feature['properties'] + data['geometry'] = shape(feature['geometry']) + rows.append(data) + + gdf = gpd.GeoDataFrame(rows) + gdf.crs = {'init': 'epsg:4326'} + + return gdf + + +def fetch_street_graph(sidewalks): + # Just in case, attempt to reproject + sidewalks = sidewalks.to_crs({'init': 'epsg:4326'}) + west, south, east, north = sidewalks.total_bounds + G_streets = ox.graph_from_bbox(north, south, east, west, + network_type='drive') + + return G_streets + + +def write_crossings(crossings, path): + # Just in case, attempt to reproject + crossings = crossings.to_crs({'init': 'epsg:4326'}) + + # Create a temporary directory and attempt to write the file + tempdir = mkdtemp() + tempfile = os.path.join(tempdir, 'crossings.geojson') + + # TODO: Check if extension is .osm and if so, apply proper schema and + # osmify (user osmizer?) + + try: + crossings.to_file(tempfile, driver='GeoJSON') + except Exception as e: + shutil.rmtree(tempdir) + raise e + + # Writing was successful, so move the file to the correct path + shutil.move(tempfile, path) + + +def write_debug(debug, path): + # Just in case, attempt to reproject + debug = debug.to_crs({'init': 'epsg:4326'}) + + # Create a temporary directory and attempt to write the file + tempdir = mkdtemp() + tempfile = os.path.join(tempdir, 'debug.geojson') + + # TODO: Check if extension is .osm and if so, apply proper schema and + # osmify (user osmizer?) + + try: + debug.to_file(tempfile, driver='GeoJSON') + except Exception as e: + shutil.rmtree(tempdir) + raise e + + # Writing was successful, so move the file to the correct path + shutil.move(tempfile, path) diff --git a/crossify/populate.py b/crossify/populate.py deleted file mode 100644 index e69de29..0000000 diff --git a/crossify/schema.py b/crossify/schema.py deleted file mode 100644 index e69de29..0000000 diff --git a/crossify/validators.py b/crossify/validators.py new file mode 100644 index 0000000..6b4dc06 --- /dev/null +++ b/crossify/validators.py @@ -0,0 +1,27 @@ +'''Functions for validating and sprucing-up inputs.''' + + +def validate_sidewalks(sidewalks): + sidewalks_ls = sidewalks[sidewalks.type == 'LineString'] + n = sidewalks_ls.shape[0] + if n: + if n < sidewalks.shape[0]: + m = sidewalks.shape[0] - n + print('Warning: Removed {} non-LineString sidewalks'.format(m)) + return sidewalks_ls + else: + raise Exception('No LineStrings in sidewalks dataset: are they' + + ' MultiLineStrings?') + + +def validate_streets(streets): + streets_ls = streets[streets.type == 'LineString'] + n = streets_ls.shape[0] + if n: + if n < streets.shape[0]: + m = streets.shape[0] - n + print('Warning: Removed {} non-LineString streets'.format(m)) + return streets_ls + else: + raise Exception('No LineStrings in streets dataset: are they' + + ' MultiLineStrings?') diff --git a/requirements.txt b/requirements.txt index 3b61048..dd89501 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ click==6.7 geopandas==0.2.1 numpy==1.12.1 +osmnx==0.6 +overpass==0.5.6 Shapely==1.5.17.post1 diff --git a/setup.py b/setup.py index b239306..6e9e38d 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,8 @@ 'install_requires': ['click', 'geopandas', 'numpy', + 'osmnx', + 'overpass', 'Shapely'], 'packages': find_packages(), 'include_package_data': True, diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..cdc2d77 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!input +!output diff --git a/test/input/.gitignore b/test/input/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/test/input/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/output/.gitignore b/test/output/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/test/output/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore