# lanestyle > Lane-level road maps for Python on roadstyle - every road as wide as its lanes, with bus and bike lanes, lane lines, turn arrows, zebra crossings and sidewalks, from duckOSM's GMNS lanes lanestyle draws lane-level road maps as one offline HTML page on roadstyle: every road as wide as its lanes, with bus and bike lanes, lane lines, turn arrows, zebra crossings and sidewalks, from duckOSM's GMNS lanes and level area. An agent skill with the essentials: https://github.com/Khoshkhah/lanestyle/blob/master/skills/lanestyle/SKILL.md # Get started # lanestyle Lane-level road maps for Python: every road as wide as its lanes, with its bus and bike lanes, lane lines, turn arrows, zebra crossings and sidewalks. One offline HTML page. ``` pip install lanestyle "duckosm[levels]" ``` ``` import lanestyle as ls ls.lane_page("monaco.levels", "monaco_gmns.duckdb", "monaco.duckdb").save("lanes.html") ``` lanestyle draws nothing of its own. [roadstyle](https://khoshkhah.github.io/roadstyle/) draws every road: one line as wide as its lanes, with its outline, bridges over and tunnels under, street names, hover and click, exactly as roadstyle's level editor draws it. lanestyle reads the lanes that [duckOSM](https://github.com/Khoshkhah/duckOSM) exports (GMNS, with the turns from SUMO) and puts them on those roads, so a road you fix in the editor is fixed on the page too. ## What you can do - **Map an area** ______________________________________________________________________ Three duckOSM commands and one call, from an OpenStreetMap extract to the page. [Get started](https://khoshkhah.github.io/lanestyle/get-started/index.md) - **Lanes and markings** ______________________________________________________________________ Bus and bike lanes, dashed and solid lane lines, turn arrows, BUS and bike marks, in metres. [Lanes and markings](https://khoshkhah.github.io/lanestyle/guides/lanes-and-markings/index.md) - **Junctions, zebras, sidewalks** ______________________________________________________________________ Lanes stop where a junction begins; painted crossings and tagged sidewalks, nothing guessed. [Junctions, zebras and sidewalks](https://khoshkhah.github.io/lanestyle/guides/junctions-zebras-sidewalks/index.md) - **Fix the drawing** ______________________________________________________________________ Which road is over which, and how its ends look: roadstyle's level editor, with your lanes on it. [The editor](https://khoshkhah.github.io/lanestyle/guides/editor/index.md) - **Settings** ______________________________________________________________________ Colours, widths, dashes, marks: one `lanestyle.json`. [Settings](https://khoshkhah.github.io/lanestyle/guides/settings/index.md) - **See it live** ______________________________________________________________________ Monaco, lane by lane, full screen in your browser. [Live map](https://khoshkhah.github.io/lanestyle/maps/monaco.html) ## The family | | draws | from | | --------------------------------------------------- | ------------------------------------ | ------------------------------------------------- | | [roadstyle](https://khoshkhah.github.io/roadstyle/) | a road network, one line per edge | any edges GeoDataFrame | | [mapstyle](https://khoshkhah.github.io/mapstyle/) | a whole base map with the roads | a duckOSM database | | **lanestyle** | the lanes and markings on every road | a duckOSM database, its GMNS lanes and level area | All three save one HTML page that works offline and speaks the same `rs*` JavaScript API. # Get started From an OpenStreetMap extract to a lane-level page: three duckOSM commands and one call. ## Install Python 3.10 or newer. lanestyle brings roadstyle (0.21 or newer), geopandas, shapely 2, duckdb and pandas; duckOSM makes the data (it needs [SUMO](https://eclipse.dev/sumo/)'s `netconvert` for the lanes). ``` pip install lanestyle "duckosm[levels]" ``` ## Make the data ``` duckosm build --pbf monaco-latest.osm.pbf -o monaco.duckdb -m driving -m walking -m cycling duckosm gmns monaco.duckdb -o monaco_gmns.duckdb duckosm levels monaco.duckdb ``` | step | makes | what is in it | | -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `build` | `monaco.duckdb` | the roads of every mode, routable, with the OSM tags | | `gmns` | `monaco_gmns.duckdb` | the lanes (count, width, use: car, bus, bike), the turns between them and where each junction begins (from SUMO), the crossings | | `levels` | `monaco.levels/` | the drawing order of the roads: which is over which, their ends. Your fixes go here too ([the editor](https://khoshkhah.github.io/lanestyle/guides/editor/index.md)) | ## Draw it ``` import lanestyle as ls m = ls.lane_page("monaco.levels", "monaco_gmns.duckdb", "monaco.duckdb") m.save("monaco.html") ``` or from the repo: `python lane_page.py monaco.levels monaco_gmns.duckdb monaco.duckdb monaco.html`. The page is one HTML file that opens offline. Zoom in: lanes and their colours show at every zoom, lane lines, zebras and sidewalks from zoom 17, arrows and BUS / bike marks from 18. Click a lane to see it; click a road for its name, class and modes. The page is blank, just the lanes: no base map, no Roads box, no Tunnels slider. Extra keywords go to roadstyle's [`render_edges`](https://khoshkhah.github.io/roadstyle/reference/parameters/): `basemap=` (a base map under the lanes), `filter_control=True`, `tunnel_control=True`, `name=` and the rest. ## Next - [Lanes and markings](https://khoshkhah.github.io/lanestyle/guides/lanes-and-markings/index.md): what is drawn, and why that way. - [Junctions, zebras and sidewalks](https://khoshkhah.github.io/lanestyle/guides/junctions-zebras-sidewalks/index.md). - [Fix the drawing in the editor](https://khoshkhah.github.io/lanestyle/guides/editor/index.md): a road under the wrong one, an end that looks wrong. - [Settings](https://khoshkhah.github.io/lanestyle/guides/settings/index.md): colours, widths, dashes. # Guides # Lanes and markings Each road is one line as wide as its lanes; the lanes and their paint lie on it, in metres. ## The road roadstyle draws the road: one line whose width is its lanes together, plus the centre line between two directions (`centre_line_m`) and a thin outline (`casing_m`). Its outline, ends, bridges and tunnels, its place under or over other roads and its street name are roadstyle's, from the level area: the [editor](https://khoshkhah.github.io/lanestyle/guides/editor/index.md) changes them. ## The lanes | lane | drawn | width | | ------------ | ------------------------------------------------------------- | ------------------------------------------------------ | | car | invisible, but it highlights and opens its popup when clicked | its OSM `width:lanes`, else `default_width_m` (3.25 m) | | bus | in `colors.bus` | as a car lane | | bike | in `colors.bike` | `width_m_by_use.bike` (1.5 m) unless tagged | | bus and bike | in `colors.bus`, with both marks | as a car lane | A road cars do not use (a footway, a cycleway, a pedestrian street) is drawn in the colour of who uses it (`colors.groups`: walking, cycling); pushing a bike counts as walking. ## The paint - **Lane lines**: dashed between two car lanes (`lines.dash_m` 3 m, `lines.gap_m` 9 m), solid next to a bus or bike lane, a solid centre line between two directions; `lines.width_m` wide, from zoom 17. - **Arrows**: one per lane `arrows.end_m` before it reaches a junction, the moves the lane allows (from SUMO's turns: straight, left, right, U-turn, and the forks the movement codes say), from zoom 18. - **BUS and bike**: painted on bus and bike lanes from 15 m, drawn with strokes, from zoom 18. All of it is roadstyle items (`render_edges(items=)`), so a road over another covers its paint, and a tunnel's paint takes the tunnel's colour. # Junctions, zebras and sidewalks Lanes stop where a junction begins; painted crossings and tagged sidewalks are drawn, nothing is guessed. ## Junctions A junction is plain, as on a real road: the lanes, their lines and arrows stop where it begins, and the road surface goes on through it. Where it begins is SUMO's (duckOSM's GMNS export cuts each lane there); the lane stays on duckOSM's own line, so it never jumps sideways. Where a lane goes on straight into the lane of the next road (a road that splits into two, two that merge), the two lane lines meet: duckOSM bends the narrower road's lane to its place in the wider one. The connectors (SUMO's paths from lane to lane through a junction) are not drawn: `lane_page(..., connectors=True)` adds them unseen, for a page that highlights routes. ## Zebras A crossing duckOSM calls painted (OSM `crossing=marked` / `zebra`, `crossing:markings`) is white stripes across the road it crosses: `zebra.stripe_m` (0.5 m) wide, `zebra.gap_m` apart, as long as the crossing is wide, from zoom 17. The stripes are the street's own items: a bridge over the street covers them. An unmarked crossing shows only its footway. ## Sidewalks A sidewalk mapped as its own way (`footway=sidewalk`) is drawn by roadstyle like any footway. A sidewalk only in a street's tags (`sidewalk=left`, `right` or `both`) is a strip in the walk colour just outside the street, on that side of the OSM way, as wide as `sidewalk:width` (else `sidewalk.width_m`, 2 m), from zoom 17; it stops where the street's lanes stop. `sidewalk=separate`, `no`, a bare `yes` and no tag add nothing. # Fix the drawing in the editor The page draws your level area; roadstyle's level editor changes it, with your lanes on the roads. Which road is drawn over which, and how each road ends (its heads, round, square or flat caps), comes from the level area (`duckosm levels` makes `monaco.levels/`). Fix a place in the editor, and the page you build next draws it the same way, because both use the same drawing: ``` LANESTYLE_GMNS=monaco_gmns.duckdb LANESTYLE_SOURCE_DB=monaco.duckdb \ roadstyle-levels edit monaco.levels --items lanestyle.editor:lane_items ``` Open `http://localhost:8780/?at=7.4218,43.7350,18` to start at a place (longitude, latitude, zoom). - Click two roads to see the rules between them and add one: a road over another (an order), one of its ends over it (a stack), or two roads that meet. - A rule you add that the solver cannot keep says why: the rules it makes a loop with. Switch one of them off. - Apply redraws the changed roads and their lanes in place. Your edits are kept in `monaco.levels/edits.csv`, `heads.csv` and `caps.csv`; `duckosm levels` keeps them when it makes the area again. # Settings One file: lanestyle's `data/lanestyle.json`, with a `lanestyle.json` in the current folder on top. Your `lanestyle.json` states only what it changes, as roadstyle's settings do: ``` {"lanes": {"colors": {"bus": "#e8590c"}, "lines": {"dash_m": 2.0}}} ``` | key | default | what | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `colors.auto` / `bus` / `bike` / `walk` | `#a3a3a3` / `#d6336c` / `#1c7ed6` / `#f0cb8c` | a lane by its use (car lanes are drawn invisible; the road is in `auto`) | | `colors.groups` | walking `#f0cb8c`, cycling and walking+cycling `#1c7ed6` | a road cars do not use, by who uses it | | `default_width_m` | `3.25` | a lane with no width tag | | `width_m_by_use` | walk `2.0`, bike `1.5` | an untagged footpath, an on-road bike lane | | `path_width_m` | footway `2.0`, steps `2.0`, path `1.5`, pedestrian `5.0`, service `3.0`, residential `3.0`, track `2.5`, default `2.0` | a road without lanes (footway, steps, path, a walking-only service road ...) with no OSM `width` tag, by its class | | `casing_m` / `casing_min_px` | `0.14` / `1` | the road's outline, metres, and at least this many pixels | | `centre_line_m` | `0.15` | the gap between two directions, where the centre line lies | | `lines` | dashes 3 m / gaps 9 m, `0.15` m wide, `#f2f2f2`, from zoom 17 | lane lines (`divider`, `centre` switch them) | | `junction_trim_m` | `1` | a lane line stops this far before its lane's end | | `arrows` | `end_m` 10, `repeat_m` 0, `at_junctions` true, from zoom 18 | turn arrows: before the lane's end, again every `repeat_m` (0: once) | | `zebra` | `stripe_m` 0.5, `gap_m` 0.5, from zoom 17 | painted crossings | | `sidewalk` | `width_m` 2.0, from zoom 17 | sidewalks a street's tags name | | `tunnel_body` | `#f6f4ee` | the ground colour a tunnel's paint is faded toward | The road's own look (tunnels, bridges, names) is roadstyle's: a `roadstyle.json` in the current folder ([roadstyle's settings](https://khoshkhah.github.io/roadstyle/reference/parameters/)). # AI agents Ask your coding agent for a lane map ("Monaco, lane by lane, bus lanes in orange") and it can do it right the first time: lanestyle ships what an agent needs in one page. ## The agent skill The [skill](https://github.com/Khoshkhah/lanestyle/blob/master/skills/lanestyle/SKILL.md) is one page for agents that write code (Claude Code, Codex, Cursor, …): the install, where lanes come from, the lane table, the one call and its settings, the JavaScript API, and the traps (64-bit ids, feature ids versus lane ids, lane 1 on the left, what needs the turns table). **Claude Code**, as a plugin: ``` /plugin marketplace add Khoshkhah/lanestyle /plugin install lanestyle@lanestyle ``` or as a plain skill: ``` mkdir -p ~/.claude/skills/lanestyle && curl -fsSL -o ~/.claude/skills/lanestyle/SKILL.md \ https://raw.githubusercontent.com/Khoshkhah/lanestyle/master/skills/lanestyle/SKILL.md ``` Other agents: point them at that file, or paste it into the project's agent instructions. ## The docs as text The whole site is also published for language models, following the [llms.txt](https://llmstxt.org) convention: - [`llms.txt`](https://khoshkhah.github.io/lanestyle/llms.txt): a short index of the pages; - [`llms-full.txt`](https://khoshkhah.github.io/lanestyle/llms-full.txt): every page as Markdown, in one file. ## Working on lanestyle Coding agents that change lanestyle itself read [`AGENTS.md`](https://github.com/Khoshkhah/lanestyle/blob/master/AGENTS.md): the layout, the commands, and the project's rules (Claude Code reads it through `CLAUDE.md`). ## The rest of the stack [roadstyle](https://khoshkhah.github.io/roadstyle/guides/agents/) has its own skill and an MCP server for drawing road maps without code; [mapstyle](https://khoshkhah.github.io/mapstyle/guides/agents/) and [duckOSM](https://github.com/Khoshkhah/duckOSM) have skills too. A lanestyle page is a roadstyle page, so roadstyle's JavaScript API and host-page advice apply as they are. # Reference # Python API One call makes the page; the rest is what it is built from. ## `lane_page(area, gmns, source_db, connectors=False, **kw)` The lanes of `gmns` on the roads of the level `area`, as roadstyle's level editor draws them: a roadstyle `WebMap` (`.save(path)`, `.html`). | argument | what | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `area` | the level area folder (`duckosm levels DB` makes `DB.levels`) | | `gmns` | duckOSM's GMNS file (`duckosm gmns DB -o GMNS`) | | `source_db` | the duckOSM database both were made from (the OSM tags of the sidewalks) | | `connectors` | also the lane connectors through junctions, unseen (route highlights) | | `**kw` | to roadstyle's `render_edges`. By default the page is blank (`basemap="blank"`, no Roads box): `basemap=`, `filter_control=True`, `tunnel_control=True`, `name=` … | ## `lanestyle.editor.lane_items(roads, gmns=None, source_db=None, connectors=None)` The editor's hook (`roadstyle-levels edit AREA --items lanestyle.editor:lane_items`; the files from `LANESTYLE_GMNS` / `LANESTYLE_SOURCE_DB` there): it sets each road's width and line from its lanes and returns `(overlays, render_edges keywords)` with the items. `lane_page` calls it. ## Reading GMNS - `from_gmns(gmns_db, mode="driving", source_db=None, modes=None)` → `(lanes, turns)`: one row per lane (`lane_id`, `link_id`, `lane_num`, `use`, `width_m`, `turn`, the link's tags, `geometry` to the nodes, `cut_geometry` where SUMO's junction begins) and one per turn (`from_lane`, `to_lane`, `type`). `modes=("driving", "walking", "cycling")` reads several; cycling needs `source_db` (where bikes are pushed). - `read_crossings(gmns_db)`: the crossings and the lanes they cross. `read_boundary(db)`, `boundary_from_geojson(path)`: an area's outline. - `lane_settings()`: the settings in effect ([Settings](https://khoshkhah.github.io/lanestyle/guides/settings/index.md)). Ids: `link_id` is duckOSM's BIGINT edge id; keep it `Int64`. On the page a lane's ids are text. # Command line One script in the repo, `lane_page.py`: the level area, the GMNS file and the duckOSM database in, a page out. ``` python lane_page.py AREA GMNS_DB SOURCE_DB OUT.html [--connectors] ``` | argument | what | | -------------- | -------------------------------------------------------------------- | | `AREA` | the level area (`duckosm levels SOURCE_DB` makes `SOURCE_DB.levels`) | | `GMNS_DB` | `duckosm gmns SOURCE_DB -o GMNS_DB` | | `SOURCE_DB` | the duckOSM database | | `OUT.html` | the page | | `--connectors` | also the lane connectors, unseen | The page is blank, just the lanes. The editor with the same drawing: `roadstyle-levels edit AREA --items lanestyle.editor:lane_items` ([the editor](https://khoshkhah.github.io/lanestyle/guides/editor/index.md)).