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.
Files changed (34) hide show
  1. streetform-0.1.0/CHANGELOG.md +23 -0
  2. streetform-0.1.0/CITATION.cff +22 -0
  3. streetform-0.1.0/LICENSE +21 -0
  4. streetform-0.1.0/MANIFEST.in +9 -0
  5. streetform-0.1.0/PKG-INFO +247 -0
  6. streetform-0.1.0/README.md +212 -0
  7. streetform-0.1.0/examples/quickstart.py +74 -0
  8. streetform-0.1.0/examples/sample_streets.geojson +289 -0
  9. streetform-0.1.0/pyproject.toml +109 -0
  10. streetform-0.1.0/setup.cfg +4 -0
  11. streetform-0.1.0/src/streetform/__init__.py +77 -0
  12. streetform-0.1.0/src/streetform/__main__.py +7 -0
  13. streetform-0.1.0/src/streetform/_geom.py +224 -0
  14. streetform-0.1.0/src/streetform/_version.py +1 -0
  15. streetform-0.1.0/src/streetform/blocks.py +181 -0
  16. streetform-0.1.0/src/streetform/cli.py +146 -0
  17. streetform-0.1.0/src/streetform/graph.py +531 -0
  18. streetform-0.1.0/src/streetform/io.py +194 -0
  19. streetform-0.1.0/src/streetform/metrics.py +452 -0
  20. streetform-0.1.0/src/streetform/py.typed +0 -0
  21. streetform-0.1.0/src/streetform/zones.py +205 -0
  22. streetform-0.1.0/src/streetform.egg-info/PKG-INFO +247 -0
  23. streetform-0.1.0/src/streetform.egg-info/SOURCES.txt +32 -0
  24. streetform-0.1.0/src/streetform.egg-info/dependency_links.txt +1 -0
  25. streetform-0.1.0/src/streetform.egg-info/entry_points.txt +2 -0
  26. streetform-0.1.0/src/streetform.egg-info/requires.txt +6 -0
  27. streetform-0.1.0/src/streetform.egg-info/top_level.txt +1 -0
  28. streetform-0.1.0/tests/conftest.py +48 -0
  29. streetform-0.1.0/tests/test_blocks.py +79 -0
  30. streetform-0.1.0/tests/test_geom.py +61 -0
  31. streetform-0.1.0/tests/test_graph.py +157 -0
  32. streetform-0.1.0/tests/test_io_cli.py +202 -0
  33. streetform-0.1.0/tests/test_metrics.py +228 -0
  34. 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
@@ -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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/streetform/py.typed
7
+ recursive-include src/streetform *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py *.geojson
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/streetform.svg)](https://pypi.org/project/streetform/)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/streetform.svg)](https://pypi.org/project/streetform/)
40
+ [![CI](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/streetform.svg)](https://pypi.org/project/streetform/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/streetform.svg)](https://pypi.org/project/streetform/)
5
+ [![CI](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/streetform_sdk/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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()