streetform 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streetform-0.1.0/CHANGELOG.md +23 -0
- streetform-0.1.0/CITATION.cff +22 -0
- streetform-0.1.0/LICENSE +21 -0
- streetform-0.1.0/MANIFEST.in +9 -0
- streetform-0.1.0/PKG-INFO +247 -0
- streetform-0.1.0/README.md +212 -0
- streetform-0.1.0/examples/quickstart.py +74 -0
- streetform-0.1.0/examples/sample_streets.geojson +289 -0
- streetform-0.1.0/pyproject.toml +109 -0
- streetform-0.1.0/setup.cfg +4 -0
- streetform-0.1.0/src/streetform/__init__.py +77 -0
- streetform-0.1.0/src/streetform/__main__.py +7 -0
- streetform-0.1.0/src/streetform/_geom.py +224 -0
- streetform-0.1.0/src/streetform/_version.py +1 -0
- streetform-0.1.0/src/streetform/blocks.py +181 -0
- streetform-0.1.0/src/streetform/cli.py +146 -0
- streetform-0.1.0/src/streetform/graph.py +531 -0
- streetform-0.1.0/src/streetform/io.py +194 -0
- streetform-0.1.0/src/streetform/metrics.py +452 -0
- streetform-0.1.0/src/streetform/py.typed +0 -0
- streetform-0.1.0/src/streetform/zones.py +205 -0
- streetform-0.1.0/src/streetform.egg-info/PKG-INFO +247 -0
- streetform-0.1.0/src/streetform.egg-info/SOURCES.txt +32 -0
- streetform-0.1.0/src/streetform.egg-info/dependency_links.txt +1 -0
- streetform-0.1.0/src/streetform.egg-info/entry_points.txt +2 -0
- streetform-0.1.0/src/streetform.egg-info/requires.txt +6 -0
- streetform-0.1.0/src/streetform.egg-info/top_level.txt +1 -0
- streetform-0.1.0/tests/conftest.py +48 -0
- streetform-0.1.0/tests/test_blocks.py +79 -0
- streetform-0.1.0/tests/test_geom.py +61 -0
- streetform-0.1.0/tests/test_graph.py +157 -0
- streetform-0.1.0/tests/test_io_cli.py +202 -0
- streetform-0.1.0/tests/test_metrics.py +228 -0
- streetform-0.1.0/tests/test_zones.py +106 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- `build_graph`: street graph from GeoJSON LineStrings (projected metres or lon/lat with a
|
|
10
|
+
local equirectangular projection), node snapping within a tolerance, splitting at
|
|
11
|
+
crossings, T-junctions and undershoots (or only at shared vertices with
|
|
12
|
+
`split_crossings=False`), duplicate removal and degree-2 simplification.
|
|
13
|
+
- Connectivity metrics: node degree distribution, intersection, node and street density,
|
|
14
|
+
link-node ratio, connected node ratio, cul-de-sac share, mean degree.
|
|
15
|
+
- Kansky alpha, beta, gamma and cyclomatic number (planar and non-planar maxima).
|
|
16
|
+
- Street orientation histogram, entropy and orientation order phi (Boeing 2019), unweighted
|
|
17
|
+
or length-weighted, by link end points or by segments; `order_from_entropy`.
|
|
18
|
+
- Blocks as bounded faces of the planar graph (half-edge face tracing, island holes,
|
|
19
|
+
cul-de-sacs excluded from perimeter) with area, perimeter and compactness summaries.
|
|
20
|
+
- Circuity: average link circuity, sampled node-pair circuity and route directness with a
|
|
21
|
+
pure-Python Dijkstra (`heapq`), and route directness for explicit OD pairs.
|
|
22
|
+
- Zone summaries by polygon layer or square grid; GeoJSON and CSV writers.
|
|
23
|
+
- `streetform` CLI with `metrics`, `export` and `zones` commands.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "geospacephilo@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "streetform"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/streetform_sdk"
|
|
13
|
+
url: "https://pypi.org/project/streetform/"
|
|
14
|
+
abstract: "Street-network form and connectivity metrics from GeoJSON LineStrings: planar graph building with snapping and splitting at intersections, intersection density, node degree distribution, link-node and connected node ratios, cul-de-sac share, Kansky alpha/beta/gamma indices, street orientation entropy and order (Boeing 2019), block size from polygonized faces, circuity and route directness, summarised by zone polygons or a grid."
|
|
15
|
+
keywords:
|
|
16
|
+
- street network
|
|
17
|
+
- urban form
|
|
18
|
+
- connectivity
|
|
19
|
+
- orientation entropy
|
|
20
|
+
- circuity
|
|
21
|
+
- urban planning
|
|
22
|
+
- gis
|
streetform-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: streetform
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Street-network form and connectivity metrics from GeoJSON lines: planar graph building, intersection density, link-node and connected node ratios, Kansky alpha/beta/gamma, orientation entropy and order, block size, circuity and route directness, by zone or grid.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/streetform_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/streetform_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/streetform_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/streetform_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: street-network,urban-form,connectivity,intersection-density,link-node-ratio,kansky,orientation-entropy,circuity,route-directness,urban-blocks,walkability,urban-planning,geojson,gis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# streetform
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/streetform/)
|
|
39
|
+
[](https://pypi.org/project/streetform/)
|
|
40
|
+
[](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml)
|
|
41
|
+
[](https://github.com/YusufEminoglu/streetform_sdk/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
**Street-network form and connectivity metrics from GeoJSON lines.**
|
|
44
|
+
|
|
45
|
+
`streetform` turns a layer of street centrelines (GeoJSON LineStrings, in a
|
|
46
|
+
projected CRS in metres or in lon/lat) into a clean planar graph and reports
|
|
47
|
+
the indicators planners use to describe urban grain and connectivity:
|
|
48
|
+
|
|
49
|
+
- node **degree distribution**, **intersection density**, street density
|
|
50
|
+
- **link-node ratio**, **connected node ratio**, **cul-de-sac share**
|
|
51
|
+
- Kansky's **alpha, beta, gamma** indices and the cyclomatic number
|
|
52
|
+
- street **orientation entropy** and **orientation order phi** (Boeing 2019)
|
|
53
|
+
- **blocks** from the faces of the planar graph: mean/median area and perimeter
|
|
54
|
+
- **circuity** (link-level and between sampled node pairs) and the
|
|
55
|
+
**route directness index**
|
|
56
|
+
- all of the above **per zone polygon or per grid cell**, written to CSV or GeoJSON
|
|
57
|
+
|
|
58
|
+
It runs on the Python standard library alone (shortest paths are a
|
|
59
|
+
`heapq` Dijkstra), so it installs anywhere, including inside QGIS.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install streetform
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires Python 3.9 or newer.
|
|
66
|
+
|
|
67
|
+
> **Need more?** [OSMnx](https://github.com/gboeing/osmnx) (Boeing 2017) is the
|
|
68
|
+
> much fuller alternative: it downloads OpenStreetMap networks, keeps one-way
|
|
69
|
+
> and attribute information, and computes these and many more statistics on top
|
|
70
|
+
> of NetworkX. `streetform` is a small, dependency-free tool for *your own*
|
|
71
|
+
> line data (survey plans, municipal centrelines, design proposals) that only
|
|
72
|
+
> needs GeoJSON in and CSV/GeoJSON out.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from streetform import build_graph, read_lines, network_metrics, grid_zones, summarize_zones
|
|
80
|
+
|
|
81
|
+
lines = read_lines("streets.geojson") # LineString / MultiLineString
|
|
82
|
+
g = build_graph(lines, crs="auto", tolerance=0.5) # metres
|
|
83
|
+
print(g) # StreetGraph(nodes=..., edges=..., length=... m, planar=True)
|
|
84
|
+
|
|
85
|
+
m = network_metrics(g, n_pairs=500, seed=0)
|
|
86
|
+
print(m["intersection_density_per_km2"], m["connected_node_ratio"])
|
|
87
|
+
print(m["alpha"], m["gamma"], m["orientation_order"], m["block_area_mean_m2"])
|
|
88
|
+
|
|
89
|
+
rows = summarize_zones(g, grid_zones(g, cell_size=250)) # one dict per cell
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
A runnable example with a bundled sample network is in
|
|
93
|
+
[`examples/quickstart.py`](https://github.com/YusufEminoglu/streetform_sdk/blob/main/examples/quickstart.py).
|
|
94
|
+
|
|
95
|
+
### Command line
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# network-wide metrics as JSON (or a one-row CSV with -o metrics.csv)
|
|
99
|
+
streetform metrics streets.geojson --tolerance 0.5 --pairs 500
|
|
100
|
+
|
|
101
|
+
# nodes (with degree), links (length, bearing, circuity) and blocks as GeoJSON
|
|
102
|
+
streetform export streets.geojson --nodes nodes.geojson --edges links.geojson --blocks blocks.geojson
|
|
103
|
+
|
|
104
|
+
# per-zone summaries: zone polygons or a square grid (metres)
|
|
105
|
+
streetform zones streets.geojson --zones districts.geojson --id-field code -o districts.csv
|
|
106
|
+
streetform zones streets.geojson --grid 500 -o grid.geojson
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Common options: `--crs auto|projected|lonlat`, `--no-split` (do not create
|
|
110
|
+
nodes at crossings, e.g. to keep bridges unconnected), `--keep-degree2`
|
|
111
|
+
(skip simplification).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## API overview
|
|
116
|
+
|
|
117
|
+
| Function / class | Purpose |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `read_lines(path_or_dict, where=None)` | LineString/MultiLineString coordinates from GeoJSON, optional attribute filter |
|
|
120
|
+
| `build_graph(lines, crs, tolerance, split_crossings, simplify, drop_duplicates)` | build a `StreetGraph` |
|
|
121
|
+
| `StreetGraph` | `nodes`, `edges`, `degrees()`, `adjacency()`, `components()`, `simplified()`, `nearest_node()` |
|
|
122
|
+
| `connectivity_metrics(g, area_m2=None)` | counts, densities, link-node ratio, connected node ratio, cul-de-sac share |
|
|
123
|
+
| `degree_distribution(g)` | `{degree: count}` |
|
|
124
|
+
| `kansky_indices(g, planar=True)` | cyclomatic number, alpha, beta, gamma |
|
|
125
|
+
| `orientation_histogram / orientation_entropy / orientation_order(g, ...)` | Boeing (2019) orientation measures; `order_from_entropy(H)` |
|
|
126
|
+
| `polygonize(g)`, `block_metrics(g)` | blocks (bounded faces) and their size statistics |
|
|
127
|
+
| `edge_circuity(g)` | sum of link lengths / sum of straight end-to-end distances |
|
|
128
|
+
| `sampled_circuity(g, n_pairs, seed)` | network vs Euclidean distance over random node pairs |
|
|
129
|
+
| `route_directness(g, pairs)` | RDI and circuity for explicit origin-destination points |
|
|
130
|
+
| `shortest_path_lengths(g, source, targets)` | Dijkstra distances |
|
|
131
|
+
| `network_metrics(g)` | everything above in one JSON-serialisable dict |
|
|
132
|
+
| `read_zones`, `grid_zones`, `summarize_zones`, `zones_to_geojson` | zone/grid summaries |
|
|
133
|
+
| `nodes_to_geojson`, `edges_to_geojson`, `blocks_to_geojson`, `write_geojson`, `write_csv` | output |
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Method and sources
|
|
138
|
+
|
|
139
|
+
### Graph building
|
|
140
|
+
|
|
141
|
+
1. Coordinates are projected to a planar metric frame. Lon/lat input uses a
|
|
142
|
+
local equirectangular projection centred on the data extent
|
|
143
|
+
(`x = R·Δλ·cos φ0`, `y = R·Δφ`, `R` = 6 371 008.8 m); projected input is used
|
|
144
|
+
as is and assumed to be in metres.
|
|
145
|
+
2. Nodes are created at line ends, at shared vertices, where a line end
|
|
146
|
+
lies within `tolerance` of another line (T-junctions, undershoots) and,
|
|
147
|
+
with `split_crossings=True` (default), at every crossing. Candidate pairs
|
|
148
|
+
are found with a uniform grid index.
|
|
149
|
+
3. Node candidates closer than `tolerance` are merged (grid hashing +
|
|
150
|
+
union-find); lines are split at the merged nodes.
|
|
151
|
+
4. Exact duplicate links are dropped and, with `simplify=True`, chains of
|
|
152
|
+
degree-2 nodes are dissolved so that nodes are only intersections and dead
|
|
153
|
+
ends, as in the simplified graphs of OSMnx (Boeing 2017, 2019).
|
|
154
|
+
|
|
155
|
+
### Indicators
|
|
156
|
+
|
|
157
|
+
| Indicator | Definition | Source |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| Intersection density | nodes with degree ≥ 3 per km² | Dill (2004); Cervero & Kockelman (1997) |
|
|
160
|
+
| Street density | km of street per km² | Dill (2004) |
|
|
161
|
+
| Link-node ratio | links / nodes; Ewing (1996) suggests 1.4 as a target | Dill (2004) |
|
|
162
|
+
| Connected node ratio | intersections / (intersections + cul-de-sacs); INDEX model: avoid < 0.5, favour ≥ 0.7 | Dill (2004) |
|
|
163
|
+
| Cul-de-sac share | dead-end nodes / all nodes (Boeing's *P_de*) | Boeing (2019) |
|
|
164
|
+
| Cyclomatic number μ | e − v + p | Kansky (1963) |
|
|
165
|
+
| Alpha | μ / (2v − 5) (planar) | Kansky (1963); Dill (2004) |
|
|
166
|
+
| Beta | e / v | Kansky (1963) |
|
|
167
|
+
| Gamma | e / 3(v − 2) (planar) | Kansky (1963); Dill (2004) |
|
|
168
|
+
| Orientation entropy *H* | −Σ P(oᵢ) ln P(oᵢ) over 36 bins of 10°, shifted by −5°, every bearing plus its reciprocal | Boeing (2019), eq. 1–2 |
|
|
169
|
+
| Orientation order φ | 1 − ((H − H_g)/(H_max − H_g))², H_g = ln 4 ≈ 1.386, H_max = ln 36 ≈ 3.584 | Boeing (2019), eq. 3 |
|
|
170
|
+
| Average circuity | Σ link lengths / Σ straight distances between adjacent nodes | Boeing (2019), eq. 4 |
|
|
171
|
+
| Pair circuity | Σ network / Σ Euclidean distance over sampled node pairs | Giacomin & Levinson (2015) |
|
|
172
|
+
| Route directness | Euclidean / network distance (0–1); its reciprocal is Dill's "pedestrian route directness" (Portland Metro max 1.5; INDEX 1.2–1.5) | Dill (2004) |
|
|
173
|
+
| Block size | area and perimeter of bounded faces of the planar graph | Dill (2004), Table 1 (Hess et al. 1999; Song 2003) |
|
|
174
|
+
|
|
175
|
+
The orientation measures were checked against the published values: the
|
|
176
|
+
(*H_o*, φ) pairs in Table 1 of Boeing (2019) are reproduced by
|
|
177
|
+
`order_from_entropy` in the test suite.
|
|
178
|
+
|
|
179
|
+
### Blocks
|
|
180
|
+
|
|
181
|
+
Faces are traced on the half-edge structure of the planar embedding: at
|
|
182
|
+
each node the next half-edge is the one immediately clockwise of the
|
|
183
|
+
incoming one, so bounded faces are counter-clockwise rings with positive
|
|
184
|
+
area. A street component lying entirely inside a face becomes a hole of
|
|
185
|
+
that face. A cul-de-sac reaching into a block is part of its ring (as a
|
|
186
|
+
zero-width spike) but not of its perimeter.
|
|
187
|
+
|
|
188
|
+
### Zones
|
|
189
|
+
|
|
190
|
+
Nodes are assigned to zones by location, links by their mid-point (half
|
|
191
|
+
their length) and blocks by an interior point. Node degree is taken from
|
|
192
|
+
the whole network, so a street that leaves a zone does not create a false
|
|
193
|
+
dead end at the zone boundary. Densities use the zone area.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Limitations
|
|
198
|
+
|
|
199
|
+
- Coordinates in a projected CRS are assumed to be metres; there is no CRS
|
|
200
|
+
database. `crs="auto"` treats data as lon/lat when every coordinate lies in
|
|
201
|
+
±180/±90, so a small local dataset in metres may be misread; pass
|
|
202
|
+
`crs="projected"` explicitly.
|
|
203
|
+
- The lon/lat projection is equirectangular: scale error grows with the
|
|
204
|
+
north-south extent (about 0.15 % at ±0.1° around 41° N). Reproject large
|
|
205
|
+
study areas first.
|
|
206
|
+
- Undirected graph only: one-way streets, turn restrictions and travel
|
|
207
|
+
speeds are ignored; circuity and directness are geometric.
|
|
208
|
+
- With `split_crossings=True` grade-separated crossings (bridges, tunnels)
|
|
209
|
+
become intersections; use `split_crossings=False` when the data shares
|
|
210
|
+
vertices at real junctions (as OpenStreetMap does). Blocks need the planar
|
|
211
|
+
mode.
|
|
212
|
+
- Collinear overlapping lines are not merged unless they are exact
|
|
213
|
+
duplicates between the same nodes; snapping can chain nearby nodes when the
|
|
214
|
+
tolerance is large compared to the street spacing.
|
|
215
|
+
- Kansky's planar maxima (3v − 6 links, 2v − 5 circuits) assume one connected
|
|
216
|
+
simple graph; with several components or parallel links they are
|
|
217
|
+
approximations, and gamma/alpha can exceed 1 for multigraphs.
|
|
218
|
+
- Default study area for densities is the convex hull of the network; pass
|
|
219
|
+
`area_m2` (or use zones) for a meaningful denominator.
|
|
220
|
+
- Pure Python: networks of tens of thousands of links take seconds; for
|
|
221
|
+
metropolitan networks use OSMnx/NetworkX or a compiled library.
|
|
222
|
+
|
|
223
|
+
## References
|
|
224
|
+
|
|
225
|
+
- Boeing, G. (2017). OSMnx: New methods for acquiring, constructing, analyzing, and visualizing complex street networks. *Computers, Environment and Urban Systems* 65, 126–139.
|
|
226
|
+
- Boeing, G. (2019). Urban spatial order: street network orientation, configuration, and entropy. *Applied Network Science* 4, 67. https://doi.org/10.1007/s41109-019-0189-1
|
|
227
|
+
- Cervero, R. & Kockelman, K. (1997). Travel demand and the 3Ds: density, diversity, and design. *Transportation Research Part D* 2(3), 199–219.
|
|
228
|
+
- Dill, J. (2004). Measuring network connectivity for bicycling and walking. 83rd Annual Meeting of the Transportation Research Board, Washington, DC.
|
|
229
|
+
- Ewing, R. (1996). *Best Development Practices*. Chicago: APA Planners Press.
|
|
230
|
+
- Giacomin, D. J. & Levinson, D. M. (2015). Road network circuity in metropolitan areas. *Environment and Planning B* 42(6), 1040–1053.
|
|
231
|
+
- Kansky, K. J. (1963). *Structure of Transportation Networks*. University of Chicago, Department of Geography, Research Paper 84.
|
|
232
|
+
|
|
233
|
+
## Academic citation
|
|
234
|
+
|
|
235
|
+
```bibtex
|
|
236
|
+
@software{eminoglu_streetform,
|
|
237
|
+
author = {Eminoğlu, Yusuf},
|
|
238
|
+
title = {streetform},
|
|
239
|
+
year = {2026},
|
|
240
|
+
version = {0.1.0},
|
|
241
|
+
url = {https://github.com/YusufEminoglu/streetform_sdk}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT, see [LICENSE](LICENSE). Contact: geospacephilo@gmail.com
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# streetform
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/streetform/)
|
|
4
|
+
[](https://pypi.org/project/streetform/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/YusufEminoglu/streetform_sdk/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Street-network form and connectivity metrics from GeoJSON lines.**
|
|
9
|
+
|
|
10
|
+
`streetform` turns a layer of street centrelines (GeoJSON LineStrings, in a
|
|
11
|
+
projected CRS in metres or in lon/lat) into a clean planar graph and reports
|
|
12
|
+
the indicators planners use to describe urban grain and connectivity:
|
|
13
|
+
|
|
14
|
+
- node **degree distribution**, **intersection density**, street density
|
|
15
|
+
- **link-node ratio**, **connected node ratio**, **cul-de-sac share**
|
|
16
|
+
- Kansky's **alpha, beta, gamma** indices and the cyclomatic number
|
|
17
|
+
- street **orientation entropy** and **orientation order phi** (Boeing 2019)
|
|
18
|
+
- **blocks** from the faces of the planar graph: mean/median area and perimeter
|
|
19
|
+
- **circuity** (link-level and between sampled node pairs) and the
|
|
20
|
+
**route directness index**
|
|
21
|
+
- all of the above **per zone polygon or per grid cell**, written to CSV or GeoJSON
|
|
22
|
+
|
|
23
|
+
It runs on the Python standard library alone (shortest paths are a
|
|
24
|
+
`heapq` Dijkstra), so it installs anywhere, including inside QGIS.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install streetform
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python 3.9 or newer.
|
|
31
|
+
|
|
32
|
+
> **Need more?** [OSMnx](https://github.com/gboeing/osmnx) (Boeing 2017) is the
|
|
33
|
+
> much fuller alternative: it downloads OpenStreetMap networks, keeps one-way
|
|
34
|
+
> and attribute information, and computes these and many more statistics on top
|
|
35
|
+
> of NetworkX. `streetform` is a small, dependency-free tool for *your own*
|
|
36
|
+
> line data (survey plans, municipal centrelines, design proposals) that only
|
|
37
|
+
> needs GeoJSON in and CSV/GeoJSON out.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from streetform import build_graph, read_lines, network_metrics, grid_zones, summarize_zones
|
|
45
|
+
|
|
46
|
+
lines = read_lines("streets.geojson") # LineString / MultiLineString
|
|
47
|
+
g = build_graph(lines, crs="auto", tolerance=0.5) # metres
|
|
48
|
+
print(g) # StreetGraph(nodes=..., edges=..., length=... m, planar=True)
|
|
49
|
+
|
|
50
|
+
m = network_metrics(g, n_pairs=500, seed=0)
|
|
51
|
+
print(m["intersection_density_per_km2"], m["connected_node_ratio"])
|
|
52
|
+
print(m["alpha"], m["gamma"], m["orientation_order"], m["block_area_mean_m2"])
|
|
53
|
+
|
|
54
|
+
rows = summarize_zones(g, grid_zones(g, cell_size=250)) # one dict per cell
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A runnable example with a bundled sample network is in
|
|
58
|
+
[`examples/quickstart.py`](https://github.com/YusufEminoglu/streetform_sdk/blob/main/examples/quickstart.py).
|
|
59
|
+
|
|
60
|
+
### Command line
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# network-wide metrics as JSON (or a one-row CSV with -o metrics.csv)
|
|
64
|
+
streetform metrics streets.geojson --tolerance 0.5 --pairs 500
|
|
65
|
+
|
|
66
|
+
# nodes (with degree), links (length, bearing, circuity) and blocks as GeoJSON
|
|
67
|
+
streetform export streets.geojson --nodes nodes.geojson --edges links.geojson --blocks blocks.geojson
|
|
68
|
+
|
|
69
|
+
# per-zone summaries: zone polygons or a square grid (metres)
|
|
70
|
+
streetform zones streets.geojson --zones districts.geojson --id-field code -o districts.csv
|
|
71
|
+
streetform zones streets.geojson --grid 500 -o grid.geojson
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Common options: `--crs auto|projected|lonlat`, `--no-split` (do not create
|
|
75
|
+
nodes at crossings, e.g. to keep bridges unconnected), `--keep-degree2`
|
|
76
|
+
(skip simplification).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API overview
|
|
81
|
+
|
|
82
|
+
| Function / class | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `read_lines(path_or_dict, where=None)` | LineString/MultiLineString coordinates from GeoJSON, optional attribute filter |
|
|
85
|
+
| `build_graph(lines, crs, tolerance, split_crossings, simplify, drop_duplicates)` | build a `StreetGraph` |
|
|
86
|
+
| `StreetGraph` | `nodes`, `edges`, `degrees()`, `adjacency()`, `components()`, `simplified()`, `nearest_node()` |
|
|
87
|
+
| `connectivity_metrics(g, area_m2=None)` | counts, densities, link-node ratio, connected node ratio, cul-de-sac share |
|
|
88
|
+
| `degree_distribution(g)` | `{degree: count}` |
|
|
89
|
+
| `kansky_indices(g, planar=True)` | cyclomatic number, alpha, beta, gamma |
|
|
90
|
+
| `orientation_histogram / orientation_entropy / orientation_order(g, ...)` | Boeing (2019) orientation measures; `order_from_entropy(H)` |
|
|
91
|
+
| `polygonize(g)`, `block_metrics(g)` | blocks (bounded faces) and their size statistics |
|
|
92
|
+
| `edge_circuity(g)` | sum of link lengths / sum of straight end-to-end distances |
|
|
93
|
+
| `sampled_circuity(g, n_pairs, seed)` | network vs Euclidean distance over random node pairs |
|
|
94
|
+
| `route_directness(g, pairs)` | RDI and circuity for explicit origin-destination points |
|
|
95
|
+
| `shortest_path_lengths(g, source, targets)` | Dijkstra distances |
|
|
96
|
+
| `network_metrics(g)` | everything above in one JSON-serialisable dict |
|
|
97
|
+
| `read_zones`, `grid_zones`, `summarize_zones`, `zones_to_geojson` | zone/grid summaries |
|
|
98
|
+
| `nodes_to_geojson`, `edges_to_geojson`, `blocks_to_geojson`, `write_geojson`, `write_csv` | output |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Method and sources
|
|
103
|
+
|
|
104
|
+
### Graph building
|
|
105
|
+
|
|
106
|
+
1. Coordinates are projected to a planar metric frame. Lon/lat input uses a
|
|
107
|
+
local equirectangular projection centred on the data extent
|
|
108
|
+
(`x = R·Δλ·cos φ0`, `y = R·Δφ`, `R` = 6 371 008.8 m); projected input is used
|
|
109
|
+
as is and assumed to be in metres.
|
|
110
|
+
2. Nodes are created at line ends, at shared vertices, where a line end
|
|
111
|
+
lies within `tolerance` of another line (T-junctions, undershoots) and,
|
|
112
|
+
with `split_crossings=True` (default), at every crossing. Candidate pairs
|
|
113
|
+
are found with a uniform grid index.
|
|
114
|
+
3. Node candidates closer than `tolerance` are merged (grid hashing +
|
|
115
|
+
union-find); lines are split at the merged nodes.
|
|
116
|
+
4. Exact duplicate links are dropped and, with `simplify=True`, chains of
|
|
117
|
+
degree-2 nodes are dissolved so that nodes are only intersections and dead
|
|
118
|
+
ends, as in the simplified graphs of OSMnx (Boeing 2017, 2019).
|
|
119
|
+
|
|
120
|
+
### Indicators
|
|
121
|
+
|
|
122
|
+
| Indicator | Definition | Source |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| Intersection density | nodes with degree ≥ 3 per km² | Dill (2004); Cervero & Kockelman (1997) |
|
|
125
|
+
| Street density | km of street per km² | Dill (2004) |
|
|
126
|
+
| Link-node ratio | links / nodes; Ewing (1996) suggests 1.4 as a target | Dill (2004) |
|
|
127
|
+
| Connected node ratio | intersections / (intersections + cul-de-sacs); INDEX model: avoid < 0.5, favour ≥ 0.7 | Dill (2004) |
|
|
128
|
+
| Cul-de-sac share | dead-end nodes / all nodes (Boeing's *P_de*) | Boeing (2019) |
|
|
129
|
+
| Cyclomatic number μ | e − v + p | Kansky (1963) |
|
|
130
|
+
| Alpha | μ / (2v − 5) (planar) | Kansky (1963); Dill (2004) |
|
|
131
|
+
| Beta | e / v | Kansky (1963) |
|
|
132
|
+
| Gamma | e / 3(v − 2) (planar) | Kansky (1963); Dill (2004) |
|
|
133
|
+
| Orientation entropy *H* | −Σ P(oᵢ) ln P(oᵢ) over 36 bins of 10°, shifted by −5°, every bearing plus its reciprocal | Boeing (2019), eq. 1–2 |
|
|
134
|
+
| Orientation order φ | 1 − ((H − H_g)/(H_max − H_g))², H_g = ln 4 ≈ 1.386, H_max = ln 36 ≈ 3.584 | Boeing (2019), eq. 3 |
|
|
135
|
+
| Average circuity | Σ link lengths / Σ straight distances between adjacent nodes | Boeing (2019), eq. 4 |
|
|
136
|
+
| Pair circuity | Σ network / Σ Euclidean distance over sampled node pairs | Giacomin & Levinson (2015) |
|
|
137
|
+
| Route directness | Euclidean / network distance (0–1); its reciprocal is Dill's "pedestrian route directness" (Portland Metro max 1.5; INDEX 1.2–1.5) | Dill (2004) |
|
|
138
|
+
| Block size | area and perimeter of bounded faces of the planar graph | Dill (2004), Table 1 (Hess et al. 1999; Song 2003) |
|
|
139
|
+
|
|
140
|
+
The orientation measures were checked against the published values: the
|
|
141
|
+
(*H_o*, φ) pairs in Table 1 of Boeing (2019) are reproduced by
|
|
142
|
+
`order_from_entropy` in the test suite.
|
|
143
|
+
|
|
144
|
+
### Blocks
|
|
145
|
+
|
|
146
|
+
Faces are traced on the half-edge structure of the planar embedding: at
|
|
147
|
+
each node the next half-edge is the one immediately clockwise of the
|
|
148
|
+
incoming one, so bounded faces are counter-clockwise rings with positive
|
|
149
|
+
area. A street component lying entirely inside a face becomes a hole of
|
|
150
|
+
that face. A cul-de-sac reaching into a block is part of its ring (as a
|
|
151
|
+
zero-width spike) but not of its perimeter.
|
|
152
|
+
|
|
153
|
+
### Zones
|
|
154
|
+
|
|
155
|
+
Nodes are assigned to zones by location, links by their mid-point (half
|
|
156
|
+
their length) and blocks by an interior point. Node degree is taken from
|
|
157
|
+
the whole network, so a street that leaves a zone does not create a false
|
|
158
|
+
dead end at the zone boundary. Densities use the zone area.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Limitations
|
|
163
|
+
|
|
164
|
+
- Coordinates in a projected CRS are assumed to be metres; there is no CRS
|
|
165
|
+
database. `crs="auto"` treats data as lon/lat when every coordinate lies in
|
|
166
|
+
±180/±90, so a small local dataset in metres may be misread; pass
|
|
167
|
+
`crs="projected"` explicitly.
|
|
168
|
+
- The lon/lat projection is equirectangular: scale error grows with the
|
|
169
|
+
north-south extent (about 0.15 % at ±0.1° around 41° N). Reproject large
|
|
170
|
+
study areas first.
|
|
171
|
+
- Undirected graph only: one-way streets, turn restrictions and travel
|
|
172
|
+
speeds are ignored; circuity and directness are geometric.
|
|
173
|
+
- With `split_crossings=True` grade-separated crossings (bridges, tunnels)
|
|
174
|
+
become intersections; use `split_crossings=False` when the data shares
|
|
175
|
+
vertices at real junctions (as OpenStreetMap does). Blocks need the planar
|
|
176
|
+
mode.
|
|
177
|
+
- Collinear overlapping lines are not merged unless they are exact
|
|
178
|
+
duplicates between the same nodes; snapping can chain nearby nodes when the
|
|
179
|
+
tolerance is large compared to the street spacing.
|
|
180
|
+
- Kansky's planar maxima (3v − 6 links, 2v − 5 circuits) assume one connected
|
|
181
|
+
simple graph; with several components or parallel links they are
|
|
182
|
+
approximations, and gamma/alpha can exceed 1 for multigraphs.
|
|
183
|
+
- Default study area for densities is the convex hull of the network; pass
|
|
184
|
+
`area_m2` (or use zones) for a meaningful denominator.
|
|
185
|
+
- Pure Python: networks of tens of thousands of links take seconds; for
|
|
186
|
+
metropolitan networks use OSMnx/NetworkX or a compiled library.
|
|
187
|
+
|
|
188
|
+
## References
|
|
189
|
+
|
|
190
|
+
- Boeing, G. (2017). OSMnx: New methods for acquiring, constructing, analyzing, and visualizing complex street networks. *Computers, Environment and Urban Systems* 65, 126–139.
|
|
191
|
+
- Boeing, G. (2019). Urban spatial order: street network orientation, configuration, and entropy. *Applied Network Science* 4, 67. https://doi.org/10.1007/s41109-019-0189-1
|
|
192
|
+
- Cervero, R. & Kockelman, K. (1997). Travel demand and the 3Ds: density, diversity, and design. *Transportation Research Part D* 2(3), 199–219.
|
|
193
|
+
- Dill, J. (2004). Measuring network connectivity for bicycling and walking. 83rd Annual Meeting of the Transportation Research Board, Washington, DC.
|
|
194
|
+
- Ewing, R. (1996). *Best Development Practices*. Chicago: APA Planners Press.
|
|
195
|
+
- Giacomin, D. J. & Levinson, D. M. (2015). Road network circuity in metropolitan areas. *Environment and Planning B* 42(6), 1040–1053.
|
|
196
|
+
- Kansky, K. J. (1963). *Structure of Transportation Networks*. University of Chicago, Department of Geography, Research Paper 84.
|
|
197
|
+
|
|
198
|
+
## Academic citation
|
|
199
|
+
|
|
200
|
+
```bibtex
|
|
201
|
+
@software{eminoglu_streetform,
|
|
202
|
+
author = {Eminoğlu, Yusuf},
|
|
203
|
+
title = {streetform},
|
|
204
|
+
year = {2026},
|
|
205
|
+
version = {0.1.0},
|
|
206
|
+
url = {https://github.com/YusufEminoglu/streetform_sdk}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT, see [LICENSE](LICENSE). Contact: geospacephilo@gmail.com
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""streetform quick start.
|
|
2
|
+
|
|
3
|
+
Runs on the bundled sample (a 4 x 4 block grid cut by a diagonal boulevard,
|
|
4
|
+
with two cul-de-sacs; projected coordinates in metres):
|
|
5
|
+
|
|
6
|
+
python examples/quickstart.py
|
|
7
|
+
|
|
8
|
+
Writes nodes, links, blocks and a 200 m grid summary to ``examples/out/``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from streetform import (
|
|
17
|
+
blocks_to_geojson,
|
|
18
|
+
build_graph,
|
|
19
|
+
edges_to_geojson,
|
|
20
|
+
grid_zones,
|
|
21
|
+
network_metrics,
|
|
22
|
+
nodes_to_geojson,
|
|
23
|
+
read_lines,
|
|
24
|
+
route_directness,
|
|
25
|
+
summarize_zones,
|
|
26
|
+
write_csv,
|
|
27
|
+
write_geojson,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HERE = Path(__file__).resolve().parent
|
|
31
|
+
OUT = HERE / "out"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
lines = read_lines(HERE / "sample_streets.geojson")
|
|
36
|
+
graph = build_graph(lines, crs="projected", tolerance=0.5)
|
|
37
|
+
print(graph)
|
|
38
|
+
|
|
39
|
+
m = network_metrics(graph, n_pairs=300, seed=42)
|
|
40
|
+
keys = [
|
|
41
|
+
"intersections",
|
|
42
|
+
"dead_ends",
|
|
43
|
+
"intersection_density_per_km2",
|
|
44
|
+
"link_node_ratio",
|
|
45
|
+
"connected_node_ratio",
|
|
46
|
+
"cul_de_sac_share",
|
|
47
|
+
"alpha",
|
|
48
|
+
"beta",
|
|
49
|
+
"gamma",
|
|
50
|
+
"orientation_entropy",
|
|
51
|
+
"orientation_order",
|
|
52
|
+
"edge_circuity",
|
|
53
|
+
"network_circuity",
|
|
54
|
+
"route_directness_index",
|
|
55
|
+
"blocks",
|
|
56
|
+
"block_area_mean_m2",
|
|
57
|
+
]
|
|
58
|
+
print(json.dumps({k: m[k] for k in keys}, indent=2))
|
|
59
|
+
|
|
60
|
+
# corner to corner: along the boulevard vs. around the grid
|
|
61
|
+
od = [((500000.0, 4540000.0), (500400.0, 4540400.0))]
|
|
62
|
+
print("RDI corner to corner:", route_directness(graph, od)[0]["rdi"])
|
|
63
|
+
|
|
64
|
+
OUT.mkdir(exist_ok=True)
|
|
65
|
+
write_geojson(nodes_to_geojson(graph), OUT / "nodes.geojson")
|
|
66
|
+
write_geojson(edges_to_geojson(graph), OUT / "links.geojson")
|
|
67
|
+
write_geojson(blocks_to_geojson(graph), OUT / "blocks.geojson")
|
|
68
|
+
rows = summarize_zones(graph, grid_zones(graph, 200.0))
|
|
69
|
+
write_csv(rows, OUT / "grid_200m.csv")
|
|
70
|
+
print(f"wrote {len(rows)} grid cells and GeoJSON layers to {OUT}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|