Skip to content

Commit 43ddbb2

Browse files
authored
Merge pull request #2 from AccessMap/develop
Develop
2 parents a8262dc + 7c96b7d commit 43ddbb2

18 files changed

Lines changed: 696 additions & 33 deletions

IDEAS.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
## Alternative strategies
2+
3+
### Select all sidewalks within X meters of the intersection
4+
5+
1. Select all sidewalks within X meters of the intersection
6+
2.
7+
8+
### By studying 'eye test' method
9+
10+
Humans can figure out where to draw crossings pretty easily (even unmarked
11+
ones). How do they do it? Here's some guesses.
12+
13+
1. Locate sidewalk corners. This might be just one sidewalk approaching the
14+
street, it may be a legit corner with 2 sidewalks meeting.
15+
16+
2. If you could reasonably cross from corner to corner, draw a crossing.
17+
18+
3. The location where the crossing connects to the sidewalk is somewhat
19+
subjective and up to the style of the mapper (for now). A natural place is
20+
along the line of the incoming sidewalk, if there are 2. Having them meet at
21+
just one location is *fine* for now.
22+
23+
So, the real strategy in code:
24+
25+
1. Go up the street from the intersection X meters / half way up (whichever
26+
comes first). X could be ~10-15 meters. Return this point for each outgoing
27+
road.
28+
29+
2.
30+
31+
### Original-ish
32+
33+
1. Go up each street in small increments (~half meter?), generating long
34+
orthogonal lines.
35+
36+
2. Restrict to sidewalks within X meters of the street (something moderately
37+
big like 30 meters)
38+
39+
3. Discard lines that don't intersect sidewalks on both the left and right
40+
sides. Can be done by taking the intersection and asking the left/right question
41+
42+
4. Subset remainder to line segment that intersects the street (trim by the
43+
sidewalks)
44+
45+
5. Remove crossings that intersect any other streets (i.e. crossing multiple
46+
streets). This should be done very carefully given that some streets are
47+
divided and have multiple intersections. osmnx tries to group these intersections
48+
but I don't know what happens to the lines. i.e. we don't want to discard all
49+
boulevards. Actual question is more complex? FIXME: revisit this step
50+
51+
6. The first crossing remaining is probably legit.
52+
53+
FIXME: the problem with this approach is that it will miss the situation where
54+
there's only one sidewalk at the corner (one street doesn't have sidewalks,
55+
e.g.).
56+
57+
### Original-ish 2
58+
59+
1. Identify sidewalks to the left of street, to the right of street
60+
2. Find point on the street where an orthogonal line first intersects the
61+
sidewalk on the right and does not intersect any other streets.
62+
3. Repeat for the left side
63+
4. Whichever is farther down is the primary candidate.
64+
5. Attempt to extend this orthogonal line such that it spans from right sidewalk
65+
to left sidewalk.
66+
6. If this fails, find the closest point on the 'other' sidewalk to the point
67+
on the street. Draw a line from sidewalk to sidewalk.
68+
69+
### Original-ish 3: ding ding ding!
70+
71+
1. Walk down the street in X increments.
72+
2. Find a line between the street point and the nearest sidewalk to the right
73+
that does not intersect another street. One method for doing this is to
74+
split the sidewalks up by block beforehand. Alternatively, use the 'block graph'
75+
method used in sidewalkify to group sidewalks. Cyclic subgraphs = block
76+
polygons, acyclic sugraphs = tricky
77+
3. Repeat for the left side.
78+
4. Compare the lines: if they're roughly parallel, keep the crossing endpoints
79+
and draw a line between them. This is the crossing
80+
5. If the crossing is too long (40 meters?), delete it.
81+
82+
Note: rather than incrementing by small amounts and then stopping, this
83+
strategy could use a binary search such that an arbitrary parallel-ness could
84+
be found.

README.md

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
# Crossify
22

3-
`crossifyify` is a Python library and command line application for drawing
3+
`crossify` is a Python library and command line application for drawing
44
street crossing lines from street centerline and sidewalk centerline data.
55

6+
`crossify` has two usage modes:
7+
8+
- It can automatically fetch sidewalk data from OpenStreetMap
9+
- It can read sidewalk data from a GIS file (e.g. GeoJSON or shapefile)
10+
11+
In both modes, it fetches street data from OpenStreetMap and uses the sidewalk
12+
and street data to construct likely crossing locations.
13+
614
## Introduction
715

816
Pedestrian transportation network data are often missing. `crossify` works in
@@ -28,20 +36,33 @@ to Python 2.
2836
Once installed, `crossify` is available both as a command line application
2937
and a Python library.
3038

31-
#### CLI
39+
### Sidewalks should be fetched from OpenStreetMap
40+
41+
To fetch sidewalk data from OpenStreetMap, use the `from_bbox` command:
42+
43+
crossify from_bbox -- <west> <south> <east> <north> <output file>
44+
45+
The values of west, south, east, and north define a rectangular bounding box
46+
for your query, and should be in terms of latitude (south, north) and longitude
47+
(west, east). The use of a double dash is necessary for the use of negative
48+
coordinates not getting parsed as command line options (see the example below).
3249

3350
Example:
3451

35-
crossify <streets.shp> <sidewalks.shp> <output.shp>
52+
crossify from_bbox -- -122.31846 47.65458 -122.31004 47.65783
53+
test/output/crossings.geojson
54+
55+
### A sidewalks file is provided
3656

37-
##### Arguments
57+
If you want to provide your own sidewalks layer, use the `from_file` command:
3858

39-
The input file can be any file type readable by `geopandas.read_file`, which
40-
should be anything readable by `fiona`, i.e. GDAL.
59+
crossify from_file <sidewalks file> <output file>
60+
61+
Example:
4162

42-
For example, you could also use a GeoJSON input file:
63+
crossify from_file test/input/sidewalks_udistrict.geojson
64+
test/output/crossings.geojson
4365

44-
crossify <streets.geojson> <sidewalks.geojson> <output.geojson>
4566

4667
#### Python Library
4768

STRATEGY.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Strategy
2+
3+
This document describes the overall strategy behind `crossify`'s algorithms.
4+
5+
At a glance, `crossify` generates lines that connect sidewalks across street
6+
intersections. Some of these crossings may be crosswalks, or entirely unmarked:
7+
such information is treated as metadata to be added later as attributes of
8+
the crossings generated by `crossify`.
9+
10+
Given a streets dataset and sidewalks dataset, crossify attempts to draw one
11+
crossing per street in an intersection, given that there are appropriate
12+
sidewalks on either side. It accomplishes this in an N-step process:
13+
14+
1. Streets are split when they share a node (point) and are at the same
15+
z-level. Streets are also joined end-to-end at all other locations.
16+
Note: this step is not actually implemented yet, so datasets are
17+
assumed to have been appropriately split in advance.
18+
19+
2. Intersections are identified as shared nodes with more than 2 streets from
20+
step one.
21+
22+
3. Intersections are grouped based on proximity, as some may be complex
23+
(e.g. 6-way intersections, boulevards). The street segment(s) connecting each
24+
intersection node are ignored. All further logic happens on a
25+
per-intersection group basis.
26+
27+
4. Create a buffer of 10 meters (by default) around the intersection's node(s)
28+
and do an intersection operation on the streets/sidewalks: all streets and
29+
sidewalks are clipped at 10 meters from an intersection node. In most cases,
30+
this will mean that all streets and sidewalks are cut off by a 30-meter radius
31+
circle. If necessary, streets are temporarily extended to be 30 meters long.
32+
Idea: possibly use polygonized street data?
33+
Idea: use voronoi polygons?
34+
35+
5. Select all incoming streets and split the buffer by them. This aids the
36+
algorithm in ensure a connection across the street, guaranteeing each end of a
37+
crossing connects to left or right.
38+
39+
6. Extend 'up' each street in 1-meter increments, finding the
40+
closest sidewalk on each side. Only extend at most half-way down the street.
41+
42+
7. If each sidewalk side differs too much in length, the entire crossing is
43+
discarded. The problem this is attempting to solve is one where incoming
44+
streets have a very acute angle, and we don't want to select the sidewalk on
45+
the opposite side.
46+
47+
9. Choose the first 1-meter increment node that's relatively straight. Can
48+
abort generating any further 1-meter increment nodes for this street.
49+
50+
9. Make that crossing truly straight - connect each side with a straight line.
51+
52+
10. If the option is set, the first and last ~1 meter of the crossing is
53+
converted to a short line of 'sidewalk'. The point where a 1-meter segment of
54+
new sidewalk meets a crossing is where a curb interface should eventually be
55+
annotated.
56+
57+
## Future work
58+
59+
There are many ways that these strategies could be improved. Here's a list of
60+
ideas:
61+
62+
- Generate crossings automatically for long sidewalk stretches so that the
63+
pedestrian network remains well-connected even with missing data.
64+
65+
- Generate half-crossings where sidewalks end, so they are more appropriately
66+
connected to the street network.
67+
68+
- Add option to generate crossing lines from street node metadata, e.g.
69+
OpenStreetMap's highway=crossing data.

cache/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

crossify/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
from . import draw, graph
2-
3-
41
__version__ = '0.1.0'

crossify/__main__.py

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,112 @@
11
import click
2-
import geopandas as gpd
2+
from os import path
3+
import osmnx as ox
34

4-
from . import populate, constrain, schema
5+
from . import crossings, intersections, io
56

67

7-
@click.command()
8+
# TODO: See if there's a more proper way to find the project root dir
9+
# TODO: use a temporary dir?
10+
ox.utils.config(cache_folder=path.join(path.dirname(__file__), '../cache'),
11+
use_cache=True)
12+
13+
# Groups:
14+
# - Download all data from OSM bounding box, produce OSM file
15+
# - Download all data from OSM bounding box, produce GeoJSON file
16+
# - Provide own sidewalks data, produce OSM file
17+
# - Provide own sidewalks data, produce GeoJSON file
18+
19+
# So, the arguments are:
20+
# - Where is the info coming from? A file or a bounding box in OSM?
21+
# - What is the output format?
22+
23+
# crossify from_bbox [bbox] output.extension
24+
# crossify from_file sidewalks.geojson output.extension
25+
26+
27+
@click.group()
28+
def crossify():
29+
pass
30+
31+
32+
@crossify.command()
833
@click.argument('sidewalks_in')
9-
@click.argument('streets_in')
1034
@click.argument('outfile')
11-
def crossify(sidewalks_in, streets_in, outfile):
12-
# FIXME: these should be turned into configuration options
13-
intersections_only = True
14-
osm_schema = True
35+
def from_file(sidewalks_in, outfile):
36+
#
37+
# Read, fetch, and standardize data
38+
#
39+
40+
# Note: all are converted to WGS84 by default
41+
sidewalks = io.read_sidewalks(sidewalks_in)
42+
core(sidewalks, outfile)
43+
44+
45+
@crossify.command()
46+
@click.argument('west')
47+
@click.argument('south')
48+
@click.argument('east')
49+
@click.argument('north')
50+
@click.argument('outfile')
51+
@click.option('--debug', is_flag=True)
52+
def from_bbox(west, south, east, north, outfile, debug):
53+
#
54+
# Read, fetch, and standardize data
55+
#
56+
57+
# Note: all are converted to WGS84 by default
58+
sidewalks = io.fetch_sidewalks(west, south, east, north)
59+
core(sidewalks, outfile, debug)
60+
61+
62+
def core(sidewalks, outfile, debug=False):
63+
#
64+
# Read, fetch, and standardize data
65+
#
66+
67+
# Note: all are converted to WGS84 by default
68+
click.echo('Fetching street network from OpenStreetMap...')
69+
G_streets = io.fetch_street_graph(sidewalks)
70+
71+
# Work in UTM
72+
click.echo('Generating street graph...')
73+
G_streets_u = ox.projection.project_graph(G_streets)
74+
sidewalks_u = ox.projection.project_gdf(sidewalks)
1575

16-
sidewalks = gpd.read_file(sidewalks_in)
17-
streets = gpd.read_file(streets_in)
76+
# Get the undirected street graph
77+
G_undirected_u = ox.save_load.get_undirected(G_streets_u)
1878

19-
# Ensure we're working in the same CRS as the sidewalks dataset
20-
crs = sidewalks.crs
21-
streets = streets.to_crs(crs)
79+
# Extract streets from streets graph
80+
click.echo('Extracting geospatial data from street graph...')
81+
streets = ox.save_load.graph_to_gdfs(G_undirected_u, nodes=False,
82+
edges=True)
83+
streets.crs = sidewalks_u.crs
2284

23-
# FIXME: this is where we'd create the crossings
24-
dense_crossings = populate.populate(sidewalks, streets)
85+
#
86+
# Isolate intersections that need crossings (degree > 3), group with
87+
# their streets (all pointing out from the intersection)
88+
#
89+
click.echo('Isolating street intersections...')
90+
ixns = intersections.group_intersections(G_streets_u)
2591

26-
if intersections_only:
27-
crossings = constrain.constrain(dense_crossings)
28-
else:
29-
crossings = dense_crossings
92+
#
93+
# Draw crossings using the intersection + street + sidewalk info
94+
#
95+
click.echo('Drawing crossings...')
96+
st_crossings = crossings.make_crossings(ixns, sidewalks_u, debug=debug)
97+
if debug:
98+
st_crossings, street_segments = st_crossings
3099

31-
if osm_schema:
32-
crossings = schema.apply_schema(crossings)
100+
#
101+
# Write to file
102+
#
103+
click.echo('Writing to file...')
104+
io.write_crossings(st_crossings, outfile)
105+
if debug:
106+
base, ext = path.splitext(outfile)
107+
debug_outfile = '{}_debug{}'.format(base, ext)
108+
io.write_debug(street_segments, debug_outfile)
33109

34-
crossings.to_file(outfile)
35110

36111
if __name__ == '__main__':
37112
crossify()

crossify/constrain.py

Whitespace-only changes.

0 commit comments

Comments
 (0)