lanestyle 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.
- lanestyle-0.1.0/LICENSE +21 -0
- lanestyle-0.1.0/PKG-INFO +196 -0
- lanestyle-0.1.0/README.md +163 -0
- lanestyle-0.1.0/pyproject.toml +42 -0
- lanestyle-0.1.0/setup.cfg +4 -0
- lanestyle-0.1.0/src/lanestyle/__init__.py +7 -0
- lanestyle-0.1.0/src/lanestyle/data/lanestyle.json +23 -0
- lanestyle-0.1.0/src/lanestyle/gmns.py +132 -0
- lanestyle-0.1.0/src/lanestyle/junctions.py +50 -0
- lanestyle-0.1.0/src/lanestyle/lines.py +268 -0
- lanestyle-0.1.0/src/lanestyle/render.py +403 -0
- lanestyle-0.1.0/src/lanestyle.egg-info/PKG-INFO +196 -0
- lanestyle-0.1.0/src/lanestyle.egg-info/SOURCES.txt +15 -0
- lanestyle-0.1.0/src/lanestyle.egg-info/dependency_links.txt +1 -0
- lanestyle-0.1.0/src/lanestyle.egg-info/requires.txt +8 -0
- lanestyle-0.1.0/src/lanestyle.egg-info/top_level.txt +1 -0
- lanestyle-0.1.0/tests/test_lanestyle.py +254 -0
lanestyle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaveh
|
|
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.
|
lanestyle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lanestyle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lane-level road maps on roadstyle: every lane drawn at its real width, with lane lines, turns and a click-to-see-connectivity view, from a lane table or a duckOSM GMNS database
|
|
5
|
+
Author: Kaveh
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Khoshkhah/lanestyle
|
|
8
|
+
Project-URL: Repository, https://github.com/Khoshkhah/lanestyle
|
|
9
|
+
Project-URL: Issues, https://github.com/Khoshkhah/lanestyle/issues
|
|
10
|
+
Project-URL: roadstyle, https://github.com/Khoshkhah/roadstyle
|
|
11
|
+
Project-URL: duckOSM, https://github.com/Khoshkhah/duckOSM
|
|
12
|
+
Keywords: lanes,lane-level map,GMNS,OpenStreetMap,roadstyle,duckOSM,MapLibre,traffic
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: roadstyle>=0.11
|
|
26
|
+
Requires-Dist: geopandas
|
|
27
|
+
Requires-Dist: shapely>=2
|
|
28
|
+
Requires-Dist: duckdb
|
|
29
|
+
Requires-Dist: pandas
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/logo.svg" alt="lanestyle logo" width="96">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<h1 align="center">lanestyle</h1>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<b>Lane-level road maps from Python.</b><br>
|
|
42
|
+
Every lane drawn at its real width, with painted lane lines, turns, and a click that shows where a lane leads. One offline HTML file.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<a href="https://pypi.org/project/lanestyle/"><img src="https://img.shields.io/pypi/v/lanestyle.svg" alt="PyPI"></a>
|
|
47
|
+
<a href="https://github.com/Khoshkhah/lanestyle/actions/workflows/test.yml"><img src="https://github.com/Khoshkhah/lanestyle/actions/workflows/test.yml/badge.svg?branch=master" alt="Tests"></a>
|
|
48
|
+
<a href="https://khoshkhah.github.io/lanestyle/"><img src="https://img.shields.io/badge/docs-khoshkhah.github.io%2Flanestyle-3f51b5.svg" alt="Docs"></a>
|
|
49
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-3776AB.svg" alt="Python 3.10+">
|
|
50
|
+
<a href="https://github.com/Khoshkhah/lanestyle/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="#install">Install</a> ·
|
|
55
|
+
<a href="#quickstart">Quickstart</a> ·
|
|
56
|
+
<a href="#gallery">Gallery</a> ·
|
|
57
|
+
<a href="#what-goes-in">What goes in</a> ·
|
|
58
|
+
<a href="https://khoshkhah.github.io/lanestyle/">Documentation</a>
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
## Why lanestyle
|
|
64
|
+
|
|
65
|
+
- **Lanes, not roads.** Each lane is its own line, exactly its width in metres from zoom 16 on
|
|
66
|
+
(3.25 m where OSM says nothing), so neighbours sit side by side with no gap and no overlap.
|
|
67
|
+
- **Painted lane lines.** Dashed dividers between lanes of one direction, a solid centre line
|
|
68
|
+
between the two directions, grey edges: drawn to scale, cut at junctions, styled per type.
|
|
69
|
+
- **Turns you can see.** Click a lane: it turns red, the lanes it leads into green, U-turns purple.
|
|
70
|
+
Every lane carries its turn label (`left + thru`, `right`, `fork`, `merge`, `end`).
|
|
71
|
+
- **Built on [roadstyle](https://github.com/Khoshkhah/roadstyle).** Bridges over, tunnels under
|
|
72
|
+
(from the OSM `layer` tag), base maps, the filter box, popups, arrows and the `rs*` JavaScript API
|
|
73
|
+
come with it. lanestyle is to lanes what [mapstyle](https://github.com/Khoshkhah/mapstyle) is to the
|
|
74
|
+
full base map.
|
|
75
|
+
- **From OpenStreetMap.** [duckOSM](https://github.com/Khoshkhah/duckOSM) places the lanes and works
|
|
76
|
+
out the lane-to-lane turns as a GMNS database; `from_gmns` reads it. Any other lane table draws too.
|
|
77
|
+
- **One offline file.** Map, data and styling in a single HTML page: open it, serve it, embed it.
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install lanestyle
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Python ≥ 3.10. Brings roadstyle ≥ 0.11 (line widths in metres), geopandas, shapely 2, duckdb and pandas.
|
|
86
|
+
|
|
87
|
+
## Quickstart
|
|
88
|
+
|
|
89
|
+
**No data needed.** Monaco's lanes ship with the repo as GeoParquet
|
|
90
|
+
([`docs/data/`](docs/data), 1 MB):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import geopandas as gpd, pandas as pd
|
|
94
|
+
import lanestyle as ls
|
|
95
|
+
|
|
96
|
+
lanes = gpd.read_parquet("docs/data/monaco_lanes.parquet")
|
|
97
|
+
turns = pd.read_parquet("docs/data/monaco_turns.parquet")
|
|
98
|
+
ls.render_lanes(lanes, turns=turns).save("monaco.html") # open it: no server needed
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Your own area**, with duckOSM (`pip install "duckosm @ git+https://github.com/Khoshkhah/duckOSM"`):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
duckosm build --pbf monaco-latest.osm.pbf -o monaco.duckdb -m driving
|
|
105
|
+
duckosm gmns monaco.duckdb -m driving -o monaco_gmns.duckdb
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
lanes, turns = ls.from_gmns("monaco_gmns.duckdb", source_db="monaco.duckdb") # + levels, boundary
|
|
110
|
+
ls.render_lanes(lanes, turns=turns, boundary=ls.read_boundary("monaco.duckdb")).save("monaco.html")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or from the shell: `python render_lanes.py monaco_gmns.duckdb monaco.html --source-db monaco.duckdb`.
|
|
114
|
+
The whole chain from a raw `.osm.pbf`: [From OSM to lanes](https://khoshkhah.github.io/lanestyle/pipeline/).
|
|
115
|
+
|
|
116
|
+
**More looks:** every roadstyle keyword passes through.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
ls.render_lanes(lanes, turns=turns, basemap="dark_matter") # dark
|
|
120
|
+
ls.render_lanes(lanes, turns=turns, settings={"lanes": {"colors": {"bus": "#d35400"}}}) # your colours
|
|
121
|
+
ls.render_lanes(lanes, settings={"lanes": {"lines": False, "type_label_zoom": None}}) # plain lanes
|
|
122
|
+
ls.render_lanes(lanes, turns=turns, overlays=[rs.Overlay(route_gdf, label="route")]) # a lane route on top
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Gallery
|
|
126
|
+
|
|
127
|
+
<table>
|
|
128
|
+
<tr>
|
|
129
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/lane_lines.jpg" alt="Lane lines" width="100%"></a><br><b>Lane lines</b><br><sub>dividers, centre line, edges, to scale</sub></td>
|
|
130
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/roundabout.jpg" alt="A roundabout" width="100%"></a><br><b>A roundabout</b><br><sub>one ring, one road</sub></td>
|
|
131
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/tunnel.jpg" alt="A tunnel" width="100%"></a><br><b>A tunnel</b><br><sub>under the street, from the <code>layer</code> tag</sub></td>
|
|
132
|
+
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/bus_lane.jpg" alt="Bus lanes" width="100%"></a><br><b>Bus lanes</b><br><sub>painted over the palette, a row in the Roads box</sub></td>
|
|
135
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/click.jpg" alt="Click a lane" width="100%"></a><br><b>Click a lane</b><br><sub>red, into green, U-turns purple</sub></td>
|
|
136
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/overview.jpg" alt="Zoomed out" width="100%"></a><br><b>Zoomed out</b><br><sub>roadstyle's class widths below zoom 16</sub></td>
|
|
137
|
+
</tr>
|
|
138
|
+
</table>
|
|
139
|
+
|
|
140
|
+
Every look with its code: **[the gallery](https://khoshkhah.github.io/lanestyle/gallery/)**.
|
|
141
|
+
|
|
142
|
+
## What goes in
|
|
143
|
+
|
|
144
|
+
`lanes` is a GeoDataFrame, one row per lane, each line in the direction of travel. Three columns
|
|
145
|
+
are required; the others switch features on:
|
|
146
|
+
|
|
147
|
+
| Column | Powers |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `lane_id`, `geometry`, `highway` | the lane: unique id, centre line, road class (colour, filter box) |
|
|
150
|
+
| `width_m` | its width in metres (3.25 where null) |
|
|
151
|
+
| `use` | `auto`, `bus` or `bike`: bus and bike lanes are painted over the palette |
|
|
152
|
+
| `bridge` / `tunnel` / `layer` | grade separation, read as roadstyle reads them |
|
|
153
|
+
| `name` | the street label (set it on one lane per road) |
|
|
154
|
+
| `link_id`, `lane_num` (+ `reverse_link_id`, `from_node_id`, `to_node_id`) | the lane lines: which lanes share a road, which road is the other direction, where junctions are |
|
|
155
|
+
| `connector`, `from_lane`, `to_lane` | lane connectors: the curves through a junction, drawn like lanes |
|
|
156
|
+
| anything else | shown in the popup |
|
|
157
|
+
|
|
158
|
+
`turns` (optional) has `from_lane`, `to_lane` and a `type` (`thru`, `left`, `right`, `uturn`,
|
|
159
|
+
`diverge`, `merge`): the click colours, the lane type labels, and `turns_in` / `turns_out` in the
|
|
160
|
+
popup. Lane 1 is the leftmost lane (right-hand traffic). Details:
|
|
161
|
+
[The lane table](https://khoshkhah.github.io/lanestyle/guides/lane-table/).
|
|
162
|
+
|
|
163
|
+
## Settings
|
|
164
|
+
|
|
165
|
+
roadstyle's settings apply as they are; lanestyle adds a `lanes` key
|
|
166
|
+
([defaults](src/lanestyle/data/lanestyle.json)): the lane and click colours, the three lane line
|
|
167
|
+
types (`color`, `width_m`, `dash_m`), `junction_trim_m`, `type_label_zoom`, `default_width_m`,
|
|
168
|
+
`width_m_zoom`. Override them the roadstyle way, a `lanestyle.json` in the current folder or
|
|
169
|
+
`settings={"lanes": {...}}`, stating only what changes.
|
|
170
|
+
|
|
171
|
+
## Drive it from JavaScript
|
|
172
|
+
|
|
173
|
+
A lanestyle page is a roadstyle page, so the whole `window.rs*` API is there, with the lane table's
|
|
174
|
+
columns as properties:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
const bus = rsQuery(p => p.use === "bus"); rsColor(bus, "#ff8800"); rsFocus(bus);
|
|
178
|
+
rsSelect(rsQuery(p => p.lane_id === "8121729169906061189_2")[0]); // click a lane by id
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Fidelity
|
|
182
|
+
|
|
183
|
+
Lane geometry is duckOSM's drive-side offset centre line, one continuous curve along a road.
|
|
184
|
+
Widths are the 3.25 m default wherever OSM lacks `width:lanes`, which today is nearly everywhere.
|
|
185
|
+
Lane-to-lane turns follow `turn:lanes` where tagged and osm2gmns's defaults elsewhere. A lane-level
|
|
186
|
+
*map*, not a survey-grade HD map.
|
|
187
|
+
|
|
188
|
+
## Documentation
|
|
189
|
+
|
|
190
|
+
**[khoshkhah.github.io/lanestyle](https://khoshkhah.github.io/lanestyle/)**: get started, guides
|
|
191
|
+
(the lane table, lane lines, turns and clicks, the duckOSM pipeline, websites), the gallery, the
|
|
192
|
+
Python API, settings, the command line, and the design notes.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/logo.svg" alt="lanestyle logo" width="96">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">lanestyle</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>Lane-level road maps from Python.</b><br>
|
|
9
|
+
Every lane drawn at its real width, with painted lane lines, turns, and a click that shows where a lane leads. One offline HTML file.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://pypi.org/project/lanestyle/"><img src="https://img.shields.io/pypi/v/lanestyle.svg" alt="PyPI"></a>
|
|
14
|
+
<a href="https://github.com/Khoshkhah/lanestyle/actions/workflows/test.yml"><img src="https://github.com/Khoshkhah/lanestyle/actions/workflows/test.yml/badge.svg?branch=master" alt="Tests"></a>
|
|
15
|
+
<a href="https://khoshkhah.github.io/lanestyle/"><img src="https://img.shields.io/badge/docs-khoshkhah.github.io%2Flanestyle-3f51b5.svg" alt="Docs"></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-3776AB.svg" alt="Python 3.10+">
|
|
17
|
+
<a href="https://github.com/Khoshkhah/lanestyle/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="#install">Install</a> ·
|
|
22
|
+
<a href="#quickstart">Quickstart</a> ·
|
|
23
|
+
<a href="#gallery">Gallery</a> ·
|
|
24
|
+
<a href="#what-goes-in">What goes in</a> ·
|
|
25
|
+
<a href="https://khoshkhah.github.io/lanestyle/">Documentation</a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
## Why lanestyle
|
|
31
|
+
|
|
32
|
+
- **Lanes, not roads.** Each lane is its own line, exactly its width in metres from zoom 16 on
|
|
33
|
+
(3.25 m where OSM says nothing), so neighbours sit side by side with no gap and no overlap.
|
|
34
|
+
- **Painted lane lines.** Dashed dividers between lanes of one direction, a solid centre line
|
|
35
|
+
between the two directions, grey edges: drawn to scale, cut at junctions, styled per type.
|
|
36
|
+
- **Turns you can see.** Click a lane: it turns red, the lanes it leads into green, U-turns purple.
|
|
37
|
+
Every lane carries its turn label (`left + thru`, `right`, `fork`, `merge`, `end`).
|
|
38
|
+
- **Built on [roadstyle](https://github.com/Khoshkhah/roadstyle).** Bridges over, tunnels under
|
|
39
|
+
(from the OSM `layer` tag), base maps, the filter box, popups, arrows and the `rs*` JavaScript API
|
|
40
|
+
come with it. lanestyle is to lanes what [mapstyle](https://github.com/Khoshkhah/mapstyle) is to the
|
|
41
|
+
full base map.
|
|
42
|
+
- **From OpenStreetMap.** [duckOSM](https://github.com/Khoshkhah/duckOSM) places the lanes and works
|
|
43
|
+
out the lane-to-lane turns as a GMNS database; `from_gmns` reads it. Any other lane table draws too.
|
|
44
|
+
- **One offline file.** Map, data and styling in a single HTML page: open it, serve it, embed it.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install lanestyle
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python ≥ 3.10. Brings roadstyle ≥ 0.11 (line widths in metres), geopandas, shapely 2, duckdb and pandas.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
**No data needed.** Monaco's lanes ship with the repo as GeoParquet
|
|
57
|
+
([`docs/data/`](docs/data), 1 MB):
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import geopandas as gpd, pandas as pd
|
|
61
|
+
import lanestyle as ls
|
|
62
|
+
|
|
63
|
+
lanes = gpd.read_parquet("docs/data/monaco_lanes.parquet")
|
|
64
|
+
turns = pd.read_parquet("docs/data/monaco_turns.parquet")
|
|
65
|
+
ls.render_lanes(lanes, turns=turns).save("monaco.html") # open it: no server needed
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Your own area**, with duckOSM (`pip install "duckosm @ git+https://github.com/Khoshkhah/duckOSM"`):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
duckosm build --pbf monaco-latest.osm.pbf -o monaco.duckdb -m driving
|
|
72
|
+
duckosm gmns monaco.duckdb -m driving -o monaco_gmns.duckdb
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
lanes, turns = ls.from_gmns("monaco_gmns.duckdb", source_db="monaco.duckdb") # + levels, boundary
|
|
77
|
+
ls.render_lanes(lanes, turns=turns, boundary=ls.read_boundary("monaco.duckdb")).save("monaco.html")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or from the shell: `python render_lanes.py monaco_gmns.duckdb monaco.html --source-db monaco.duckdb`.
|
|
81
|
+
The whole chain from a raw `.osm.pbf`: [From OSM to lanes](https://khoshkhah.github.io/lanestyle/pipeline/).
|
|
82
|
+
|
|
83
|
+
**More looks:** every roadstyle keyword passes through.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
ls.render_lanes(lanes, turns=turns, basemap="dark_matter") # dark
|
|
87
|
+
ls.render_lanes(lanes, turns=turns, settings={"lanes": {"colors": {"bus": "#d35400"}}}) # your colours
|
|
88
|
+
ls.render_lanes(lanes, settings={"lanes": {"lines": False, "type_label_zoom": None}}) # plain lanes
|
|
89
|
+
ls.render_lanes(lanes, turns=turns, overlays=[rs.Overlay(route_gdf, label="route")]) # a lane route on top
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Gallery
|
|
93
|
+
|
|
94
|
+
<table>
|
|
95
|
+
<tr>
|
|
96
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/lane_lines.jpg" alt="Lane lines" width="100%"></a><br><b>Lane lines</b><br><sub>dividers, centre line, edges, to scale</sub></td>
|
|
97
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/roundabout.jpg" alt="A roundabout" width="100%"></a><br><b>A roundabout</b><br><sub>one ring, one road</sub></td>
|
|
98
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/tunnel.jpg" alt="A tunnel" width="100%"></a><br><b>A tunnel</b><br><sub>under the street, from the <code>layer</code> tag</sub></td>
|
|
99
|
+
</tr>
|
|
100
|
+
<tr>
|
|
101
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/bus_lane.jpg" alt="Bus lanes" width="100%"></a><br><b>Bus lanes</b><br><sub>painted over the palette, a row in the Roads box</sub></td>
|
|
102
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/click.jpg" alt="Click a lane" width="100%"></a><br><b>Click a lane</b><br><sub>red, into green, U-turns purple</sub></td>
|
|
103
|
+
<td width="33%" valign="top"><a href="https://khoshkhah.github.io/lanestyle/gallery/"><img src="https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/docs/img/gallery/overview.jpg" alt="Zoomed out" width="100%"></a><br><b>Zoomed out</b><br><sub>roadstyle's class widths below zoom 16</sub></td>
|
|
104
|
+
</tr>
|
|
105
|
+
</table>
|
|
106
|
+
|
|
107
|
+
Every look with its code: **[the gallery](https://khoshkhah.github.io/lanestyle/gallery/)**.
|
|
108
|
+
|
|
109
|
+
## What goes in
|
|
110
|
+
|
|
111
|
+
`lanes` is a GeoDataFrame, one row per lane, each line in the direction of travel. Three columns
|
|
112
|
+
are required; the others switch features on:
|
|
113
|
+
|
|
114
|
+
| Column | Powers |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `lane_id`, `geometry`, `highway` | the lane: unique id, centre line, road class (colour, filter box) |
|
|
117
|
+
| `width_m` | its width in metres (3.25 where null) |
|
|
118
|
+
| `use` | `auto`, `bus` or `bike`: bus and bike lanes are painted over the palette |
|
|
119
|
+
| `bridge` / `tunnel` / `layer` | grade separation, read as roadstyle reads them |
|
|
120
|
+
| `name` | the street label (set it on one lane per road) |
|
|
121
|
+
| `link_id`, `lane_num` (+ `reverse_link_id`, `from_node_id`, `to_node_id`) | the lane lines: which lanes share a road, which road is the other direction, where junctions are |
|
|
122
|
+
| `connector`, `from_lane`, `to_lane` | lane connectors: the curves through a junction, drawn like lanes |
|
|
123
|
+
| anything else | shown in the popup |
|
|
124
|
+
|
|
125
|
+
`turns` (optional) has `from_lane`, `to_lane` and a `type` (`thru`, `left`, `right`, `uturn`,
|
|
126
|
+
`diverge`, `merge`): the click colours, the lane type labels, and `turns_in` / `turns_out` in the
|
|
127
|
+
popup. Lane 1 is the leftmost lane (right-hand traffic). Details:
|
|
128
|
+
[The lane table](https://khoshkhah.github.io/lanestyle/guides/lane-table/).
|
|
129
|
+
|
|
130
|
+
## Settings
|
|
131
|
+
|
|
132
|
+
roadstyle's settings apply as they are; lanestyle adds a `lanes` key
|
|
133
|
+
([defaults](src/lanestyle/data/lanestyle.json)): the lane and click colours, the three lane line
|
|
134
|
+
types (`color`, `width_m`, `dash_m`), `junction_trim_m`, `type_label_zoom`, `default_width_m`,
|
|
135
|
+
`width_m_zoom`. Override them the roadstyle way, a `lanestyle.json` in the current folder or
|
|
136
|
+
`settings={"lanes": {...}}`, stating only what changes.
|
|
137
|
+
|
|
138
|
+
## Drive it from JavaScript
|
|
139
|
+
|
|
140
|
+
A lanestyle page is a roadstyle page, so the whole `window.rs*` API is there, with the lane table's
|
|
141
|
+
columns as properties:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const bus = rsQuery(p => p.use === "bus"); rsColor(bus, "#ff8800"); rsFocus(bus);
|
|
145
|
+
rsSelect(rsQuery(p => p.lane_id === "8121729169906061189_2")[0]); // click a lane by id
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Fidelity
|
|
149
|
+
|
|
150
|
+
Lane geometry is duckOSM's drive-side offset centre line, one continuous curve along a road.
|
|
151
|
+
Widths are the 3.25 m default wherever OSM lacks `width:lanes`, which today is nearly everywhere.
|
|
152
|
+
Lane-to-lane turns follow `turn:lanes` where tagged and osm2gmns's defaults elsewhere. A lane-level
|
|
153
|
+
*map*, not a survey-grade HD map.
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
**[khoshkhah.github.io/lanestyle](https://khoshkhah.github.io/lanestyle/)**: get started, guides
|
|
158
|
+
(the lane table, lane lines, turns and clicks, the duckOSM pipeline, websites), the gallery, the
|
|
159
|
+
Python API, settings, the command line, and the design notes.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lanestyle"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lane-level road maps on roadstyle: every lane drawn at its real width, with lane lines, turns and a click-to-see-connectivity view, from a lane table or a duckOSM GMNS database"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Kaveh" }]
|
|
14
|
+
keywords = ["lanes", "lane-level map", "GMNS", "OpenStreetMap", "roadstyle", "duckOSM", "MapLibre", "traffic"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["roadstyle>=0.11", "geopandas", "shapely>=2", "duckdb", "pandas"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["pytest"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Khoshkhah/lanestyle"
|
|
33
|
+
Repository = "https://github.com/Khoshkhah/lanestyle"
|
|
34
|
+
Issues = "https://github.com/Khoshkhah/lanestyle/issues"
|
|
35
|
+
roadstyle = "https://github.com/Khoshkhah/roadstyle"
|
|
36
|
+
duckOSM = "https://github.com/Khoshkhah/duckOSM"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
lanestyle = ["data/*.json"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""lanestyle — lane-level maps on roadstyle: a lane table (from_gmns reads one from a duckOSM GMNS db)
|
|
2
|
+
drawn one line per lane, at its width in metres."""
|
|
3
|
+
from lanestyle.gmns import boundary_from_geojson, from_gmns, read_boundary
|
|
4
|
+
from lanestyle.render import lane_settings, render_lanes, write_serve
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["from_gmns", "read_boundary", "boundary_from_geojson", "render_lanes", "lane_settings", "write_serve"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lanes": {
|
|
3
|
+
"colors": {
|
|
4
|
+
"bus": "#9db8d9",
|
|
5
|
+
"bike": "#3f8fc9",
|
|
6
|
+
"clicked": "#e0453a",
|
|
7
|
+
"turns_into": "#149a86",
|
|
8
|
+
"uturn": "#8e44ad"
|
|
9
|
+
},
|
|
10
|
+
"lines": {
|
|
11
|
+
"divider": {"color": "#e8e8e8", "width_m": 0.15, "dash_m": [3, 9]},
|
|
12
|
+
"centre": {"color": "#e8e8e8", "width_m": 0.15, "dash_m": null},
|
|
13
|
+
"edge": {"color": "#6b6b6b", "width_m": 0.10, "dash_m": null}
|
|
14
|
+
},
|
|
15
|
+
"junction_trim_m": 1,
|
|
16
|
+
"fillet_m": 0,
|
|
17
|
+
"line_min_device_px": 1.5,
|
|
18
|
+
"type_label_zoom": 18,
|
|
19
|
+
"default_width_m": 3.25,
|
|
20
|
+
"casing_m": 0,
|
|
21
|
+
"width_m_zoom": 16
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""The GMNS reader: a duckOSM GMNS database -> the lane table and turns render_lanes draws."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _cols(con, schema, table, db=None):
|
|
5
|
+
q = "SELECT column_name FROM duckdb_columns() WHERE schema_name = ? AND table_name = ?"
|
|
6
|
+
args = [schema, table]
|
|
7
|
+
if db:
|
|
8
|
+
q, args = q + " AND database_name = ?", args + [db]
|
|
9
|
+
return {c for (c,) in con.execute(q, args).fetchall()}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def from_gmns(gmns_db, mode="driving", source_db=None):
|
|
13
|
+
"""Read ``gmns_<mode>.lane`` / ``.link`` / ``.movement`` into ``(lanes, turns)``.
|
|
14
|
+
|
|
15
|
+
``lanes``: a GeoDataFrame, one row per lane (EPSG:4326, each line in the direction of travel)
|
|
16
|
+
with ``lane_id``, ``highway`` (the link's ``facility_type``), ``width_m`` (``lane.width``; null
|
|
17
|
+
where untagged, render_lanes fills the default), ``use``, ``name`` (lane 1 only: one label per
|
|
18
|
+
road), ``link_id``, ``lane_num``, ``turn``, ``from_node_id`` / ``to_node_id`` and
|
|
19
|
+
``reverse_link_id`` (the same road the other way: same nodes swapped and the same geometry, so
|
|
20
|
+
the two halves of a one-way loop are not a pair; null on a one-way road),
|
|
21
|
+
for the lane lines, ``lanes`` (the link's lane count) and, from ``source_db``, ``bridge`` /
|
|
22
|
+
``tunnel`` / ``layer`` and the OSM way ``osm_id``.
|
|
23
|
+
|
|
24
|
+
Levels come from the GMNS ``link`` if it has those columns, else from ``source_db`` (the
|
|
25
|
+
duckOSM database the GMNS file was made from: ``link_id`` = ``<mode>.edges.edge_id``), else
|
|
26
|
+
every lane is at ground level (no level columns).
|
|
27
|
+
|
|
28
|
+
With duckOSM's ``lane_connector`` table, each lane-to-lane connector is a row too
|
|
29
|
+
(``connector`` True, ``from_lane`` / ``to_lane``; its level and road class from the lane it leaves).
|
|
30
|
+
|
|
31
|
+
``turns``: a DataFrame ``from_lane``, ``to_lane``, ``type`` (the movement type; ``uturn`` is
|
|
32
|
+
drawn in its own colour), from ``movement``: each lane of the inbound link in
|
|
33
|
+
``start_ib_lane``..``end_ib_lane`` into the outbound lane at the same place in
|
|
34
|
+
``start_ob_lane``..``end_ob_lane`` (equal-length ranges paired in order, as osm2gmns and duckOSM
|
|
35
|
+
write them; NULL = every lane). Empty without a movement table."""
|
|
36
|
+
import uuid
|
|
37
|
+
|
|
38
|
+
import duckdb
|
|
39
|
+
import geopandas as gpd
|
|
40
|
+
import pandas as pd
|
|
41
|
+
|
|
42
|
+
g = f"gmns_{mode}"
|
|
43
|
+
con = duckdb.connect(str(gmns_db), read_only=True)
|
|
44
|
+
try:
|
|
45
|
+
con.execute("INSTALL spatial; LOAD spatial;")
|
|
46
|
+
if not _cols(con, g, "lane"):
|
|
47
|
+
raise ValueError(f"no '{g}.lane' in {gmns_db}: build a GMNS db first (duckOSM `duckosm gmns`)")
|
|
48
|
+
link_cols = _cols(con, g, "link")
|
|
49
|
+
same = " AND ST_Equals(r.geom, k.geom)" if "geom" in link_cols else ""
|
|
50
|
+
nlanes = "k.lanes, " if "lanes" in link_cols else ""
|
|
51
|
+
lvl, src = "", f"lanestyle_src_{uuid.uuid4().hex[:8]}" # one db instance per file per process:
|
|
52
|
+
# a caller's own connection to gmns_db may already hold an attachment, so never reuse a name
|
|
53
|
+
if {"bridge", "tunnel", "layer"} <= link_cols:
|
|
54
|
+
lvl = ", k.bridge, k.tunnel, k.layer"
|
|
55
|
+
elif source_db:
|
|
56
|
+
con.execute(f"ATTACH '{str(source_db).replace(chr(39), chr(39) * 2)}' AS {src} (READ_ONLY)")
|
|
57
|
+
src_cols = _cols(con, mode, "edges", db=src)
|
|
58
|
+
if not {"edge_id", "bridge", "tunnel", "layer"} <= src_cols:
|
|
59
|
+
raise ValueError(f"no {mode}.edges with bridge / tunnel / layer in {source_db}")
|
|
60
|
+
lvl = ", e.bridge, e.tunnel, e.layer" + (", e.osm_id" if "osm_id" in src_cols else "")
|
|
61
|
+
join = f"LEFT JOIN {src}.{mode}.edges e ON e.edge_id = l.link_id" if lvl.startswith(", e.") else ""
|
|
62
|
+
df = con.execute(
|
|
63
|
+
f"SELECT l.lane_id::VARCHAR AS lane_id, k.facility_type AS highway, l.width AS width_m, "
|
|
64
|
+
f" COALESCE(l.allowed_uses, 'auto') AS use, "
|
|
65
|
+
f" CASE WHEN l.lane_num = 1 THEN k.name END AS name, "
|
|
66
|
+
f" l.link_id, l.lane_num, {nlanes}l.turn, k.from_node_id, k.to_node_id, "
|
|
67
|
+
f" (SELECT min(r.link_id) FROM {g}.link r WHERE r.from_node_id = k.to_node_id "
|
|
68
|
+
f" AND r.to_node_id = k.from_node_id AND r.link_id <> k.link_id{same}) AS reverse_link_id"
|
|
69
|
+
f" {lvl}, ST_AsWKB(l.geom) AS geom "
|
|
70
|
+
f"FROM {g}.lane l JOIN {g}.link k ON k.link_id = l.link_id {join} "
|
|
71
|
+
f"WHERE l.geom IS NOT NULL ORDER BY l.link_id, l.lane_num").df()
|
|
72
|
+
conn = None # duckOSM's lane connectors (docs/design/gmns_lane_connectors.md)
|
|
73
|
+
if _cols(con, g, "lane_connector"):
|
|
74
|
+
conn = con.execute(f"SELECT connector_id::VARCHAR AS lane_id, from_lane_id::VARCHAR AS from_lane, "
|
|
75
|
+
f"to_lane_id::VARCHAR AS to_lane, width AS width_m, ST_AsWKB(geom) AS geom "
|
|
76
|
+
f"FROM {g}.lane_connector WHERE geom IS NOT NULL").df()
|
|
77
|
+
turns = pd.DataFrame({"from_lane": [], "to_lane": [], "type": []}, dtype=object)
|
|
78
|
+
if _cols(con, g, "movement"):
|
|
79
|
+
turns = con.execute(
|
|
80
|
+
f"SELECT DISTINCT il.lane_id::VARCHAR AS from_lane, ol.lane_id::VARCHAR AS to_lane, m.type "
|
|
81
|
+
f"FROM {g}.movement m "
|
|
82
|
+
f"JOIN {g}.lane il ON il.link_id = m.ib_link_id AND (m.start_ib_lane IS NULL OR "
|
|
83
|
+
f" il.lane_num BETWEEN m.start_ib_lane AND COALESCE(m.end_ib_lane, m.start_ib_lane)) "
|
|
84
|
+
f"JOIN {g}.lane ol ON ol.link_id = m.ob_link_id AND (m.start_ob_lane IS NULL OR "
|
|
85
|
+
f" m.start_ib_lane IS NULL OR ol.lane_num = m.start_ob_lane + il.lane_num - m.start_ib_lane) "
|
|
86
|
+
f"ORDER BY 1, 2").df()
|
|
87
|
+
finally:
|
|
88
|
+
if lvl.startswith(", e."):
|
|
89
|
+
con.execute(f"DETACH {src}")
|
|
90
|
+
con.close()
|
|
91
|
+
for c in [c for c in ("link_id", "reverse_link_id", "from_node_id", "to_node_id", "osm_id") if c in df]:
|
|
92
|
+
df[c] = df[c].astype("Int64") # BIGINT hash ids: never float64
|
|
93
|
+
df["connector"] = False
|
|
94
|
+
if conn is not None and len(conn): # a connector looks like the lane it leaves
|
|
95
|
+
lane_cols = [c for c in df.columns if c not in ("lane_id", "width_m", "name", "lane_num", "turn", "geom",
|
|
96
|
+
"connector")]
|
|
97
|
+
conn = conn.merge(df[["lane_id"] + lane_cols].rename(columns={"lane_id": "from_lane"}), on="from_lane", how="left")
|
|
98
|
+
conn["connector"] = True
|
|
99
|
+
df = pd.concat([df, conn], ignore_index=True)
|
|
100
|
+
for c in [c for c in ("link_id", "reverse_link_id", "from_node_id", "to_node_id", "osm_id") if c in df]:
|
|
101
|
+
df[c] = df[c].astype("Int64")
|
|
102
|
+
geom = gpd.GeoSeries.from_wkb(df.pop("geom").map(bytes), crs=4326)
|
|
103
|
+
return gpd.GeoDataFrame(df, geometry=geom, crs=4326), turns
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def read_boundary(db):
|
|
107
|
+
"""The area's boundary from a duckOSM database (``main.boundary``, written when the area was built
|
|
108
|
+
with one) as a shapely geometry, or None. Pass it on: ``render_lanes(..., boundary=geom)``."""
|
|
109
|
+
import duckdb
|
|
110
|
+
from shapely import wkb
|
|
111
|
+
|
|
112
|
+
con = duckdb.connect(str(db), read_only=True)
|
|
113
|
+
try:
|
|
114
|
+
con.execute("INSTALL spatial; LOAD spatial;")
|
|
115
|
+
if "geom" not in _cols(con, "main", "boundary"):
|
|
116
|
+
return None
|
|
117
|
+
row = con.execute("SELECT ST_AsWKB(ST_Union_Agg(geom)) FROM main.boundary").fetchone()
|
|
118
|
+
finally:
|
|
119
|
+
con.close()
|
|
120
|
+
return wkb.loads(bytes(row[0])) if row and row[0] is not None else None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def boundary_from_geojson(path):
|
|
124
|
+
"""A GeoJSON file's geometries as one shapely geometry (plain json + shapely, no GDAL)."""
|
|
125
|
+
import json
|
|
126
|
+
|
|
127
|
+
import shapely
|
|
128
|
+
from shapely.geometry import shape
|
|
129
|
+
|
|
130
|
+
gj = json.loads(open(path, encoding="utf-8").read())
|
|
131
|
+
feats = gj.get("features", [gj] if gj.get("type") == "Feature" else [{"geometry": gj}])
|
|
132
|
+
return shapely.union_all([shape(f["geometry"]) for f in feats if f.get("geometry")])
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Fillets: the paved gaps between lane surfaces. Where a lane joins a road, or two carriageways
|
|
2
|
+
start to diverge, a sliver between the surfaces shows the background. A real road paves it. The
|
|
3
|
+
whole lane surface is closed (grown by ``fillet_m``, shrunk back) and what the closing adds is drawn
|
|
4
|
+
under the lanes, in the colour of the nearest lane."""
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
from lanestyle.lines import _band
|
|
8
|
+
|
|
9
|
+
_CLASS_ORDER = ["motorway", "trunk", "primary", "secondary", "tertiary", "unclassified", "residential",
|
|
10
|
+
"living_street", "service"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _rank(cls):
|
|
14
|
+
c = str(cls or "").replace("_link", "")
|
|
15
|
+
return _CLASS_ORDER.index(c) if c in _CLASS_ORDER else len(_CLASS_ORDER)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def junction_fillets(lanes, s):
|
|
19
|
+
"""GeoJSON polygons (``cls``: the road class whose colour to use, ``b``: the band) of the gaps
|
|
20
|
+
narrower than ``2 × fillet_m`` anywhere between lane surfaces (lanes and connectors): the whole
|
|
21
|
+
surface is closed (grown by ``fillet_m``, shrunk back) and what the closing adds is drawn under
|
|
22
|
+
the lanes, in the colour and band of the nearest lane. Corners at junctions, the slivers between
|
|
23
|
+
two carriageways that start to diverge, the notches between adjacent lanes' round ends. Real
|
|
24
|
+
medians wider than ``2 × fillet_m`` stay open. None when off."""
|
|
25
|
+
import shapely
|
|
26
|
+
|
|
27
|
+
r_m = float(s.get("fillet_m") or 0)
|
|
28
|
+
if not r_m or not len(lanes):
|
|
29
|
+
return None
|
|
30
|
+
g = lanes.to_crs(lanes.estimate_utm_crs())
|
|
31
|
+
surf = g.geometry.buffer(g["width_m"] / 2, cap_style="round")
|
|
32
|
+
u = shapely.union_all(surf.to_numpy())
|
|
33
|
+
add = shapely.make_valid(u.buffer(r_m).buffer(-r_m).difference(u)).simplify(0.05)
|
|
34
|
+
parts = [q for p in getattr(add, "geoms", [add]) for q in getattr(shapely.make_valid(p), "geoms", [shapely.make_valid(p)])
|
|
35
|
+
if q.geom_type == "Polygon" and q.area > 0.3 and not q.buffer(-0.12).is_empty and q.is_valid]
|
|
36
|
+
if not parts:
|
|
37
|
+
return None
|
|
38
|
+
tree = shapely.STRtree(g.geometry.to_numpy())
|
|
39
|
+
cls = list(g["highway"]) if "highway" in g else [None] * len(g)
|
|
40
|
+
band = [_band(r) for r in g.itertuples(index=False)]
|
|
41
|
+
import geopandas as gpd
|
|
42
|
+
|
|
43
|
+
geo = gpd.GeoSeries(parts, crs=g.crs).to_crs(4326)
|
|
44
|
+
rnd = lambda ring: [[round(x, 7), round(y, 7)] for x, y in ring] # noqa: E731
|
|
45
|
+
feats = []
|
|
46
|
+
for part, gm in zip(parts, geo, strict=True):
|
|
47
|
+
k = int(tree.nearest(part.representative_point()))
|
|
48
|
+
feats.append({"type": "Feature", "properties": {"cls": cls[k], "b": band[k]},
|
|
49
|
+
"geometry": {"type": "Polygon", "coordinates": [rnd(gm.exterior.coords)] + [rnd(i.coords) for i in gm.interiors]}})
|
|
50
|
+
return {"type": "FeatureCollection", "features": feats}
|