mapstyle 0.2.0__tar.gz → 0.3.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.
- {mapstyle-0.2.0/src/mapstyle.egg-info → mapstyle-0.3.0}/PKG-INFO +13 -8
- {mapstyle-0.2.0 → mapstyle-0.3.0}/README.md +8 -6
- {mapstyle-0.2.0 → mapstyle-0.3.0}/pyproject.toml +4 -3
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/__init__.py +1 -1
- mapstyle-0.3.0/src/mapstyle/dashboard.js +164 -0
- mapstyle-0.3.0/src/mapstyle/levels.py +183 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/map.py +85 -31
- mapstyle-0.3.0/src/mapstyle/node_levels.py +526 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/planner.html +83 -36
- {mapstyle-0.2.0 → mapstyle-0.3.0/src/mapstyle.egg-info}/PKG-INFO +13 -8
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle.egg-info/SOURCES.txt +2 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle.egg-info/requires.txt +5 -1
- {mapstyle-0.2.0 → mapstyle-0.3.0}/tests/test_map.py +165 -11
- mapstyle-0.2.0/src/mapstyle/dashboard.js +0 -93
- {mapstyle-0.2.0 → mapstyle-0.3.0}/LICENSE +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/setup.cfg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/bicycle.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/bus.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/crossing.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/parking_p.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/traffic_light.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/icons/train.svg +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/layers.js +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/patterns.py +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/style.py +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/layers.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/modes.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/osm_carto.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/paths.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/themes/google.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle/styles/themes/grey.yaml +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle.egg-info/dependency_links.txt +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle.egg-info/entry_points.txt +0 -0
- {mapstyle-0.2.0 → mapstyle-0.3.0}/src/mapstyle.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mapstyle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A whole OpenStreetMap map in one offline HTML page: roads for every travel mode over a full base map, from a duckOSM database
|
|
5
5
|
Author: Kaveh Khoshkhah
|
|
6
6
|
License-Expression: MIT
|
|
@@ -18,15 +18,18 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: roadstyle>=0.
|
|
21
|
+
Requires-Dist: roadstyle>=0.12.0
|
|
22
22
|
Requires-Dist: duckdb
|
|
23
23
|
Requires-Dist: geopandas
|
|
24
24
|
Requires-Dist: pyyaml
|
|
25
25
|
Requires-Dist: pillow
|
|
26
26
|
Provides-Extra: tiles
|
|
27
27
|
Requires-Dist: roadstyle[tiles]; extra == "tiles"
|
|
28
|
+
Provides-Extra: solver
|
|
29
|
+
Requires-Dist: ortools>=9.10; extra == "solver"
|
|
28
30
|
Provides-Extra: dev
|
|
29
31
|
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Requires-Dist: ortools>=9.10; extra == "dev"
|
|
30
33
|
Provides-Extra: docs
|
|
31
34
|
Requires-Dist: mkdocs<2,>=1.6; extra == "docs"
|
|
32
35
|
Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
|
|
@@ -83,7 +86,7 @@ import mapstyle as ms
|
|
|
83
86
|
ms.render_map("monaco.duckdb").save("monaco.html") # the whole map
|
|
84
87
|
ms.render_map("monaco.duckdb", mode="walking", paths="komoot") # paths in front
|
|
85
88
|
ms.render_map("monaco.duckdb", theme="grey") # a quiet map for data
|
|
86
|
-
ms.render_map("monaco.duckdb", planner=True).save("planner.html") #
|
|
89
|
+
ms.render_map("monaco.duckdb", planner=True).save("planner.html") # a route planner demo
|
|
87
90
|
ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashboard
|
|
88
91
|
```
|
|
89
92
|
|
|
@@ -94,10 +97,12 @@ ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashbo
|
|
|
94
97
|
for the whole map: `osm` (detailed), `google` (clean), `grey` (quiet, for your data on top).
|
|
95
98
|
- **A full base map from the same database**: the sea, land use with openstreetmap.org's textures,
|
|
96
99
|
water, buildings, railways, parking, transit, traffic lights and crossings. No tile server, no API key.
|
|
97
|
-
- **Roads drawn right at junctions**: two-way roads as two lanes, bridges over, tunnels
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
- **Roads drawn right at junctions**: two-way roads as two lanes, bridges and overpasses over, tunnels
|
|
101
|
+
under, crossings over their street. The drawing order of the roads is solved (an interval per road,
|
|
102
|
+
`render_map(order=True)`; `pip install mapstyle[solver]` for the exact solution), so a joint of
|
|
103
|
+
roads at different levels shows no ring.
|
|
104
|
+
- **A route planner demo**: drag a start and an end and see duckOSM's route on the map (routing
|
|
105
|
+
itself is duckOSM's job).
|
|
101
106
|
- **A dashboard**: filter by mode, road class and layer, colour by mode, inspect what you click.
|
|
102
107
|
- **A JavaScript API**: every page is a [roadstyle](https://github.com/Khoshkhah/roadstyle) page
|
|
103
108
|
(`rsQuery`, `rsFilter`, `rsColor`, …) plus `rsSetModes`, `rsSetKinds` and `rsSetInteraction`;
|
|
@@ -109,7 +114,7 @@ ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashbo
|
|
|
109
114
|
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/roads_walkway.jpg" alt="A raised walkway over a roundabout"><br><sub>A raised walkway over a roundabout</sub></td>
|
|
110
115
|
</tr>
|
|
111
116
|
<tr>
|
|
112
|
-
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/planner.jpg" alt="The route planner"><br><sub>The route planner</sub></td>
|
|
117
|
+
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/planner.jpg" alt="The route planner"><br><sub>The route planner demo</sub></td>
|
|
113
118
|
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/dashboard.jpg" alt="The dashboard"><br><sub>The dashboard</sub></td>
|
|
114
119
|
</tr>
|
|
115
120
|
</table>
|
|
@@ -48,7 +48,7 @@ import mapstyle as ms
|
|
|
48
48
|
ms.render_map("monaco.duckdb").save("monaco.html") # the whole map
|
|
49
49
|
ms.render_map("monaco.duckdb", mode="walking", paths="komoot") # paths in front
|
|
50
50
|
ms.render_map("monaco.duckdb", theme="grey") # a quiet map for data
|
|
51
|
-
ms.render_map("monaco.duckdb", planner=True).save("planner.html") #
|
|
51
|
+
ms.render_map("monaco.duckdb", planner=True).save("planner.html") # a route planner demo
|
|
52
52
|
ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashboard
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -59,10 +59,12 @@ ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashbo
|
|
|
59
59
|
for the whole map: `osm` (detailed), `google` (clean), `grey` (quiet, for your data on top).
|
|
60
60
|
- **A full base map from the same database**: the sea, land use with openstreetmap.org's textures,
|
|
61
61
|
water, buildings, railways, parking, transit, traffic lights and crossings. No tile server, no API key.
|
|
62
|
-
- **Roads drawn right at junctions**: two-way roads as two lanes, bridges over, tunnels
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
- **Roads drawn right at junctions**: two-way roads as two lanes, bridges and overpasses over, tunnels
|
|
63
|
+
under, crossings over their street. The drawing order of the roads is solved (an interval per road,
|
|
64
|
+
`render_map(order=True)`; `pip install mapstyle[solver]` for the exact solution), so a joint of
|
|
65
|
+
roads at different levels shows no ring.
|
|
66
|
+
- **A route planner demo**: drag a start and an end and see duckOSM's route on the map (routing
|
|
67
|
+
itself is duckOSM's job).
|
|
66
68
|
- **A dashboard**: filter by mode, road class and layer, colour by mode, inspect what you click.
|
|
67
69
|
- **A JavaScript API**: every page is a [roadstyle](https://github.com/Khoshkhah/roadstyle) page
|
|
68
70
|
(`rsQuery`, `rsFilter`, `rsColor`, …) plus `rsSetModes`, `rsSetKinds` and `rsSetInteraction`;
|
|
@@ -74,7 +76,7 @@ ms.render_map("monaco.duckdb", dashboard=True).save("dashboard.html") # a dashbo
|
|
|
74
76
|
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/roads_walkway.jpg" alt="A raised walkway over a roundabout"><br><sub>A raised walkway over a roundabout</sub></td>
|
|
75
77
|
</tr>
|
|
76
78
|
<tr>
|
|
77
|
-
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/planner.jpg" alt="The route planner"><br><sub>The route planner</sub></td>
|
|
79
|
+
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/planner.jpg" alt="The route planner"><br><sub>The route planner demo</sub></td>
|
|
78
80
|
<td><img src="https://raw.githubusercontent.com/Khoshkhah/mapstyle/main/docs/img/dashboard.jpg" alt="The dashboard"><br><sub>The dashboard</sub></td>
|
|
79
81
|
</tr>
|
|
80
82
|
</table>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mapstyle"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "A whole OpenStreetMap map in one offline HTML page: roads for every travel mode over a full base map, from a duckOSM database"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
dependencies = [
|
|
25
|
-
"roadstyle>=0.
|
|
25
|
+
"roadstyle>=0.12.0", # the page (casing_level_col / fill_level_col, band_col, directed_col, cap_col)
|
|
26
26
|
"duckdb", # reads the duckOSM db
|
|
27
27
|
"geopandas",
|
|
28
28
|
"pyyaml", # styles/*.yaml
|
|
@@ -31,7 +31,8 @@ dependencies = [
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
tiles = ["roadstyle[tiles]"] # --tiles / render_map(tiles=True)
|
|
34
|
-
|
|
34
|
+
solver = ["ortools>=9.10"] # the drawing order of the roads (order=True) is solved exactly; without it a heuristic is used
|
|
35
|
+
dev = ["pytest", "ortools>=9.10"]
|
|
35
36
|
docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10", "mkdocs-llmstxt>=0.5,<0.6"]
|
|
36
37
|
|
|
37
38
|
[project.urls]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// mapstyle's dashboard (docs/design/dashboard.md) on roadstyle's report panel: a Modes box, and
|
|
2
|
+
// under each feature layer's box its clickable / tooltip / popup switches and its kinds. UI only,
|
|
3
|
+
// over rsSetModes, rsSetInteraction and rsSetKinds (layers.js). Without the report's panel
|
|
4
|
+
// (#rp-ovs) it falls back to a box in the page's corner.
|
|
5
|
+
(function wait(){
|
|
6
|
+
if (!(window.rsSetKinds && window.RS_KINDS && rsQuery(() => true).length)) return setTimeout(wait, 200);
|
|
7
|
+
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({"&": "&", "<": "<", ">": ">", '"': """}[c]));
|
|
8
|
+
const fmt = (n) => n.toLocaleString();
|
|
9
|
+
const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html) e.innerHTML = html; return e; };
|
|
10
|
+
const check = (text, on, change, count) => {
|
|
11
|
+
const lab = el("label", "rp-chk"), cb = el("input");
|
|
12
|
+
cb.type = "checkbox"; cb.checked = on; cb.onchange = () => change(cb.checked);
|
|
13
|
+
lab.appendChild(cb); lab.appendChild(document.createTextNode(" " + text));
|
|
14
|
+
if (count != null) lab.appendChild(el("span", "rp-ct", fmt(count)));
|
|
15
|
+
return lab;
|
|
16
|
+
};
|
|
17
|
+
document.head.appendChild(el("style", null,
|
|
18
|
+
".ms-grp{margin:0 0 10px;border:1px solid var(--line,#e5e7eb);border-radius:10px;padding:2px 10px 6px}" +
|
|
19
|
+
".ms-grp>summary{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:11px;font-weight:600;" +
|
|
20
|
+
"letter-spacing:.05em;text-transform:uppercase;color:var(--mut,#6b7280);padding:6px 0}" +
|
|
21
|
+
".ms-grp>summary .ms-n{font-weight:400;opacity:.7}.ms-grp>summary .ms-all{margin-left:auto;text-transform:none;letter-spacing:0;font-weight:400}" +
|
|
22
|
+
".ms-row{display:flex;flex-wrap:wrap;align-items:center}.ms-row>label{flex:1}.ms-set{flex-basis:100%;margin:0 0 4px 22px;font-size:12px}" +
|
|
23
|
+
".ms-gear{border:0;background:none;cursor:pointer;color:var(--mut,#9ca3af);font-size:13px;padding:0 4px;opacity:.7}.ms-gear:hover,.ms-gear.on{opacity:1}" +
|
|
24
|
+
"#rp-kpis{display:grid!important;grid-template-columns:repeat(3,1fr);gap:6px}#rp-kpis .rp-kpi{padding:6px 8px;min-width:0}#rp-kpis .rp-kv{font-size:17px}" +
|
|
25
|
+
".ms-modes{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 8px}.ms-modes .rp-grp{flex-basis:100%;margin:2px 0 0}" +
|
|
26
|
+
".ms-modes .rp-chk{display:inline-flex;align-items:center;gap:4px;margin:0;padding:2px 9px;border:1px solid var(--line,#e5e7eb);border-radius:14px}" +
|
|
27
|
+
".ms-modes .rp-ct{margin-left:4px}" +
|
|
28
|
+
"#rp-cls{max-height:260px;overflow-y:auto}#rp-cls .rp-grp{position:sticky;top:0;background:var(--bg,#fff);z-index:1;padding:2px 0}" +
|
|
29
|
+
".ms-sw{display:flex;gap:10px;margin:3px 0}.ms-sw label{cursor:pointer}" +
|
|
30
|
+
".ms-kinds{max-height:160px;overflow-y:auto}.ms-kinds .rp-chk{font-size:12px}" +
|
|
31
|
+
".ms-all{font-size:11px;margin:2px 0}.ms-all a{margin-right:8px;cursor:pointer;color:#555}" +
|
|
32
|
+
".ms-float{position:fixed;left:10px;bottom:30px;z-index:5;max-height:60vh;overflow-y:auto;" +
|
|
33
|
+
"background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px system-ui}"));
|
|
34
|
+
// ---- a road cut into pieces is one edge: the report counts rows, so count the first piece only ---
|
|
35
|
+
// (docs/design/levels_plan.md; the extra pieces carry _piece)
|
|
36
|
+
(function recount(n) {
|
|
37
|
+
const kv = [...document.querySelectorAll(".rp-kpi")].find((k) => /edges/.test(k.textContent));
|
|
38
|
+
const wrap = document.getElementById("rp-cls");
|
|
39
|
+
if (!kv || !wrap || !wrap.querySelector(".rp-ct")) return n > 0 && setTimeout(() => recount(n - 1), 300);
|
|
40
|
+
const real = rsQuery((p) => !p._piece), props = rsGetProps(real), col = window.RS_CLASS_COL, cnt = {};
|
|
41
|
+
props.forEach((p) => { const c = p[col]; if (c != null && c !== "") cnt[c] = (cnt[c] || 0) + 1; });
|
|
42
|
+
kv.querySelector(".rp-kv").textContent = fmt(real.length);
|
|
43
|
+
wrap.querySelectorAll("label.rp-chk").forEach((lab) => {
|
|
44
|
+
const ct = lab.querySelector(".rp-ct"); if (!ct) return;
|
|
45
|
+
ct.textContent = fmt(cnt[lab.textContent.replace(ct.textContent, "").trim()] || 0);
|
|
46
|
+
});
|
|
47
|
+
})(20);
|
|
48
|
+
const ovs = document.getElementById("rp-ovs");
|
|
49
|
+
const host = ovs ? ovs.parentNode : document.body.appendChild(el("div", "ms-float"));
|
|
50
|
+
|
|
51
|
+
// ---- Modes: the roads any ticked mode can use ------------------------------------------------
|
|
52
|
+
const modes = ["driving", "walking", "cycling"]
|
|
53
|
+
.map((m) => [m, rsQuery((p) => !p._piece && (p[m] === true || p[m] === "true")).length]).filter(([, n]) => n);
|
|
54
|
+
const on = new Set(modes.map(([m]) => m)), box = el("div", "ms-modes", '<div class="rp-grp">Modes</div>');
|
|
55
|
+
for (const [m, n] of modes) box.appendChild(check(m, true, (c) => {
|
|
56
|
+
c ? on.add(m) : on.delete(m);
|
|
57
|
+
rsSetModes(on.size === modes.length ? null : [...on]);
|
|
58
|
+
}, n));
|
|
59
|
+
// the roads you may not use (private, bus): shown / hidden like a mode (layers.js rsSetAccess)
|
|
60
|
+
for (const [k, label] of [["private", "private roads"], ["bus", "bus lanes"]])
|
|
61
|
+
if ((window.RS_ACCESS || {})[k]) box.appendChild(check(label, true, (c) => rsSetAccess(k, c), RS_ACCESS[k]));
|
|
62
|
+
host.insertBefore(box, ovs);
|
|
63
|
+
|
|
64
|
+
// ---- popup off also keeps a layer out of the panel's read-out --------------------------------
|
|
65
|
+
// the report fills #rp-detail from every rs:select, a road click listing the clickable layers
|
|
66
|
+
// under it; this listener runs after the report's and redraws it (the report's own markup)
|
|
67
|
+
// without the layers whose popup is off
|
|
68
|
+
const detail = document.getElementById("rp-detail"), idle = detail && detail.innerHTML;
|
|
69
|
+
const rowsOf = (p, only) => (only && only.length ? only : Object.keys(p))
|
|
70
|
+
.filter((k) => k[0] !== "_" && k !== "lvl" && p[k] != null && p[k] !== "")
|
|
71
|
+
.map((k) => "<b>" + esc(k) + "</b>: " + esc(p[k])).join("<br>");
|
|
72
|
+
const popupOn = (label) => { const s = rsGetInteraction(label); return !s || s.popup; };
|
|
73
|
+
if (detail) document.addEventListener("rs:select", (e) => {
|
|
74
|
+
const d = e.detail;
|
|
75
|
+
if (d.overlay) { if (!popupOn(d.overlay)) detail.innerHTML = idle; return; }
|
|
76
|
+
const ovs = (d.overlays || []).filter((o) => popupOn(o.label));
|
|
77
|
+
if (ovs.length === (d.overlays || []).length) return;
|
|
78
|
+
detail.innerHTML = rowsOf(d.properties, d.fields) + ovs.map((o) =>
|
|
79
|
+
'<hr style="border:none;border-top:1px solid var(--line);margin:6px 0"><span style="color:var(--mut)">'
|
|
80
|
+
+ esc(o.label) + "</span><br>" + rowsOf(o.properties, o.fields)).join("");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---- per layer: a row with a gear (switches and kinds), the layers grouped by meaning ---------
|
|
84
|
+
// (docs/design/dashboard_panel.md)
|
|
85
|
+
const GROUPS = [["Nature and water", ["ocean", "landcover", "institutional", "water", "waterways"]],
|
|
86
|
+
["Buildings and places", ["buildings", "parking", "parking_p", "platform", "bus_station"]],
|
|
87
|
+
["Transport", ["railways", "crossings", "traffic_signals", "bus_stations", "train_stations", "bicycle"]]];
|
|
88
|
+
const grpOf = (label) => (GROUPS.find(([, l]) => l.includes(label)) || [])[0] || "Other";
|
|
89
|
+
const rows = ovs ? [...ovs.querySelectorAll("label.rp-chk")] : [];
|
|
90
|
+
const sets = {}; // layer -> its switches / kinds box
|
|
91
|
+
for (const ov of window.RS_OVERLAYS || []) {
|
|
92
|
+
const st = rsGetInteraction(ov.label); if (!st) continue;
|
|
93
|
+
const d = el("div", "ms-set"), sw = el("div", "ms-sw"), cb = {};
|
|
94
|
+
// a layer that isn't clickable opens no popup: its popup switch waits, greyed
|
|
95
|
+
const grey = () => { const off = !cb.clickable.checked; cb.popup.disabled = off;
|
|
96
|
+
cb.popup.parentNode.style.opacity = off ? 0.45 : 1; cb.popup.parentNode.title = off ? "needs clickable" : ""; };
|
|
97
|
+
for (const k of ["clickable", "tooltip", "popup"]) {
|
|
98
|
+
const lab = check(k, st[k], (c) => { rsSetInteraction(ov.label, {[k]: c}); grey(); });
|
|
99
|
+
cb[k] = lab.querySelector("input"); sw.appendChild(lab);
|
|
100
|
+
}
|
|
101
|
+
grey();
|
|
102
|
+
d.appendChild(sw);
|
|
103
|
+
const counts = window.RS_KINDS[ov.label] || {}, names = Object.keys(counts);
|
|
104
|
+
if (names.length > 1) {
|
|
105
|
+
const shown = new Set(names), list = el("div", "ms-kinds"), cbs = {};
|
|
106
|
+
const apply = () => rsSetKinds(ov.label, shown.size === names.length ? null : [...shown]);
|
|
107
|
+
const all = el("div", "ms-all", "<a>all</a><a>none</a>");
|
|
108
|
+
const setAll = (v) => { names.forEach((n) => { cbs[n].querySelector("input").checked = v; v ? shown.add(n) : shown.delete(n); }); apply(); };
|
|
109
|
+
all.children[0].onclick = () => setAll(true);
|
|
110
|
+
all.children[1].onclick = () => setAll(false);
|
|
111
|
+
d.appendChild(all);
|
|
112
|
+
for (const n of names) {
|
|
113
|
+
cbs[n] = check(n, true, (c) => { c ? shown.add(n) : shown.delete(n); apply(); }, counts[n]);
|
|
114
|
+
list.appendChild(cbs[n]);
|
|
115
|
+
}
|
|
116
|
+
d.appendChild(list);
|
|
117
|
+
}
|
|
118
|
+
sets[ov.label] = d;
|
|
119
|
+
}
|
|
120
|
+
const rowOf = (label) => rows.find((r) => r.textContent.trim() === label);
|
|
121
|
+
const groups = new Map();
|
|
122
|
+
for (const r of rows) { // the report's own rows, in its order, into their group
|
|
123
|
+
const label = r.textContent.trim(), g = grpOf(label);
|
|
124
|
+
if (!groups.has(g)) {
|
|
125
|
+
const det = el("details", "ms-grp"), sum = el("summary", null, "<span>" + esc(g) + '</span><b class="ms-n"></b>');
|
|
126
|
+
const all = el("span", "ms-all", "<a>all</a><a>none</a>");
|
|
127
|
+
det.open = g !== "Other"; det.appendChild(sum); sum.appendChild(all);
|
|
128
|
+
const setAll = (v) => (e) => { e.preventDefault(); for (const q of det.querySelectorAll(".ms-row > label input")) if (q.checked !== v) q.click(); };
|
|
129
|
+
all.children[0].onclick = setAll(true); all.children[1].onclick = setAll(false);
|
|
130
|
+
groups.set(g, det);
|
|
131
|
+
}
|
|
132
|
+
const det = groups.get(g), wrap = el("div", "ms-row"), set = sets[label];
|
|
133
|
+
wrap.appendChild(r);
|
|
134
|
+
if (set) {
|
|
135
|
+
const gear = el("button", "ms-gear", "⚙"); gear.type = "button"; gear.title = "clicks, kinds";
|
|
136
|
+
gear.onclick = () => { set.hidden = !set.hidden; gear.classList.toggle("on", !set.hidden); };
|
|
137
|
+
set.hidden = true; wrap.appendChild(gear); wrap.appendChild(set);
|
|
138
|
+
}
|
|
139
|
+
det.appendChild(wrap);
|
|
140
|
+
}
|
|
141
|
+
const count = () => groups.forEach((det) => {
|
|
142
|
+
const q = [...det.querySelectorAll(".ms-row > label input")];
|
|
143
|
+
det.querySelector(".ms-n").textContent = q.filter((c) => c.checked).length + "/" + q.length;
|
|
144
|
+
});
|
|
145
|
+
if (ovs) {
|
|
146
|
+
const head = ovs.querySelector(".rp-grp"); if (head) head.remove(); // "Layers": the groups say it
|
|
147
|
+
// Roads first (modes, road types), then the layer groups
|
|
148
|
+
const roads = el("details", "ms-grp"), cls = document.getElementById("rp-cls");
|
|
149
|
+
roads.open = true; roads.appendChild(el("summary", null, "<span>Roads</span>"));
|
|
150
|
+
roads.appendChild(box);
|
|
151
|
+
if (cls) roads.appendChild(cls);
|
|
152
|
+
ovs.before(roads);
|
|
153
|
+
groups.forEach((det) => ovs.appendChild(det));
|
|
154
|
+
ovs.addEventListener("change", count); count();
|
|
155
|
+
} else for (const [, det] of groups) host.appendChild(det);
|
|
156
|
+
|
|
157
|
+
// ---- the legend repeats Road type when roads are coloured by class: shown for the other colourings ---
|
|
158
|
+
const co = document.getElementById("rp-co"), lg = document.getElementById("rp-legend");
|
|
159
|
+
if (co && lg) {
|
|
160
|
+
const legend = lg.closest("details"), roadType = () => (co.options[co.selectedIndex].text || "") === "Road class";
|
|
161
|
+
const sync = () => { legend.hidden = roadType(); };
|
|
162
|
+
co.addEventListener("change", sync); sync();
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""The level of a road, drawn only where it is needed (docs/design/levels_plan.md, rule R4).
|
|
2
|
+
|
|
3
|
+
A road with a ``layer`` tag (and no bridge tag) or a tunnel is drawn at ground level, except for the
|
|
4
|
+
stretch where it really passes over or under another road: lines that cross with no node in common,
|
|
5
|
+
at another level. That stretch, plus a clearance, is a **piece** of the edge drawn in its own band
|
|
6
|
+
(over / under the ground roads) with square ends; the rest of the edge is ground, so every joint is at
|
|
7
|
+
ground level and its casings merge. The database and the ``edge_id`` do not change: only the lines
|
|
8
|
+
sent to the map are cut. Bridges keep roadstyle's own rule.
|
|
9
|
+
"""
|
|
10
|
+
import math
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
# metres from a crossing road's edge to the cut, and the half width of a crossing road by class
|
|
15
|
+
CLEARANCE_M = 4.0
|
|
16
|
+
HALF_WIDTH_M = {"motorway": 6, "trunk": 5.5, "primary": 4.5, "secondary": 4, "tertiary": 3.5, "residential": 3,
|
|
17
|
+
"unclassified": 3, "living_street": 3, "service": 2, "pedestrian": 2.5, "footway": 1,
|
|
18
|
+
"path": 1, "cycleway": 1, "steps": 1, "corridor": 1}
|
|
19
|
+
MIN_PIECE_M = 2.0 # a ground piece shorter than this joins the stretch beside it
|
|
20
|
+
END_M = 4.0 # a joint end keeps a ground piece at least this long (the round end of the road meeting it needs it)
|
|
21
|
+
OVERLAP_M = 0.3 # neighbouring pieces overlap by this much each: two square ends that only touch leave a hairline
|
|
22
|
+
|
|
23
|
+
def _truthy(col):
|
|
24
|
+
return f"({col} IS NOT NULL AND lower(CAST({col} AS VARCHAR)) NOT IN ('', 'no', 'false', '0'))"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def crossing_pairs(con, union):
|
|
28
|
+
"""``(a, b, highway of b)`` edge-id pairs: ``a`` is a road to cut (a plain ``layer`` road or a tunnel) and ``b`` a
|
|
29
|
+
road at a level that makes ``a`` go over or under it, with no node in common and within about 20 m (``_pieces_of``
|
|
30
|
+
keeps those whose drawn roads cross or overlap). Also the roads to cut, with their level."""
|
|
31
|
+
con.execute(f"""
|
|
32
|
+
CREATE TEMP TABLE e0 AS
|
|
33
|
+
SELECT edge_id AS eid, any_value(source) AS s, any_value(target) AS t, any_value(layer) AS layer,
|
|
34
|
+
any_value(bridge) AS bridge, any_value(tunnel) AS tunnel, any_value(highway) AS hw,
|
|
35
|
+
any_value(geometry) AS g
|
|
36
|
+
FROM ({union}) GROUP BY edge_id""")
|
|
37
|
+
con.execute(f"""
|
|
38
|
+
CREATE TEMP TABLE lv AS
|
|
39
|
+
SELECT eid, s, t, hw, g, {_truthy('tunnel')} AS is_tunnel, {_truthy('bridge')} AS is_bridge,
|
|
40
|
+
COALESCE(NULLIF(TRY_CAST(layer AS INTEGER), 0),
|
|
41
|
+
CASE WHEN {_truthy('bridge')} THEN 1 WHEN {_truthy('tunnel')} THEN -1 ELSE 0 END) AS l
|
|
42
|
+
FROM e0""")
|
|
43
|
+
pairs = con.execute("""
|
|
44
|
+
SELECT CAST(a.eid AS VARCHAR) AS a, CAST(o.eid AS VARCHAR) AS b, o.hw AS hw
|
|
45
|
+
FROM lv a JOIN lv o ON o.eid <> a.eid AND ST_DWithin(a.g, o.g, 0.0002)
|
|
46
|
+
AND a.s NOT IN (o.s, o.t) AND a.t NOT IN (o.s, o.t)
|
|
47
|
+
WHERE NOT a.is_bridge AND (a.is_tunnel OR a.l <> 0)
|
|
48
|
+
AND CASE WHEN a.l > 0 THEN o.l >= 0 AND o.l < a.l ELSE o.l >= 0 END""").df()
|
|
49
|
+
cut = con.execute("SELECT CAST(eid AS VARCHAR) AS eid, l, is_tunnel, s, t FROM lv "
|
|
50
|
+
"WHERE NOT is_bridge AND (is_tunnel OR l <> 0)").df()
|
|
51
|
+
return pairs, cut
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _scales(geom):
|
|
55
|
+
lat = math.radians(geom.centroid.y)
|
|
56
|
+
return 111320.0 * math.cos(lat), 110574.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _metric(geom, sx, sy):
|
|
60
|
+
from shapely import affinity
|
|
61
|
+
return affinity.scale(geom, xfact=sx, yfact=sy, origin=(0, 0))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _pieces_of(ga, others, band, highway=None, clearance=CLEARANCE_M):
|
|
65
|
+
"""``[(m0, m1, band)]`` in metres along ``ga`` (a lon/lat line of class ``highway``): ground except the stretches
|
|
66
|
+
where its drawn road crosses or overlaps the drawn road of one of ``others`` (lon/lat lines with their class: no
|
|
67
|
+
node in common, so they are not connected, only drawn over each other), plus the clearance."""
|
|
68
|
+
sx, sy = _scales(ga)
|
|
69
|
+
gm = _metric(ga, sx, sy)
|
|
70
|
+
total = gm.length
|
|
71
|
+
spans = []
|
|
72
|
+
for gb, hw in others:
|
|
73
|
+
near = gm.intersection(_metric(gb, sx, sy).buffer(HALF_WIDTH_M.get(hw, 3) + HALF_WIDTH_M.get(highway, 1)))
|
|
74
|
+
if near.is_empty:
|
|
75
|
+
continue
|
|
76
|
+
pts = [p for g in getattr(near, "geoms", [near]) for p in (g.boundary.geoms if g.length else [g]) if not p.is_empty]
|
|
77
|
+
ts = [gm.project(p) for p in pts if p.geom_type == "Point"]
|
|
78
|
+
if not ts:
|
|
79
|
+
continue
|
|
80
|
+
spans.append((min(ts) - clearance, max(ts) + clearance, min(ts), max(ts)))
|
|
81
|
+
spans.sort()
|
|
82
|
+
merged = []
|
|
83
|
+
for s0, s1, r0, r1 in spans:
|
|
84
|
+
s0, s1 = max(0.0, s0), min(total, s1)
|
|
85
|
+
if merged and s0 <= merged[-1][1] + MIN_PIECE_M:
|
|
86
|
+
merged[-1][1] = max(merged[-1][1], s1)
|
|
87
|
+
merged[-1][3] = max(merged[-1][3], r1)
|
|
88
|
+
else:
|
|
89
|
+
merged.append([s0, s1, r0, r1])
|
|
90
|
+
if merged: # a road meeting the end keeps a ground piece there, unless the crossing is right at it
|
|
91
|
+
first, last = merged[0], merged[-1]
|
|
92
|
+
if first[0] < END_M:
|
|
93
|
+
first[0] = max(first[0], min(END_M, first[2] - 0.5))
|
|
94
|
+
if total - last[1] < END_M:
|
|
95
|
+
last[1] = min(last[1], max(total - END_M, last[3] + 0.5))
|
|
96
|
+
merged = [[a, b] for a, b, *_ in merged]
|
|
97
|
+
out, at = [], 0.0
|
|
98
|
+
for s0, s1 in merged:
|
|
99
|
+
if s0 - at >= MIN_PIECE_M:
|
|
100
|
+
out.append((at, s0, 0))
|
|
101
|
+
else:
|
|
102
|
+
s0 = at
|
|
103
|
+
out.append((s0, s1, band))
|
|
104
|
+
at = s1
|
|
105
|
+
if total - at >= MIN_PIECE_M:
|
|
106
|
+
out.append((at, total, 0))
|
|
107
|
+
elif out:
|
|
108
|
+
out[-1] = (out[-1][0], total, out[-1][2])
|
|
109
|
+
return out or [(0.0, total, 0)]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def level_pieces(con, union, geoms, highways):
|
|
113
|
+
"""``({edge_id: [(m0, m1, band), ...]}, tunnel edge ids, edge ids to end square)`` for every road to cut, in order
|
|
114
|
+
along the edge: ground pieces and the stretches that go over (band 1) or under (band -1) a road. ``geoms`` / ``highways``: ``{edge_id: ...}`` of the
|
|
115
|
+
db's roads (lon/lat lines, classes). A road that crosses nothing is one ground piece."""
|
|
116
|
+
pairs, cut = crossing_pairs(con, union)
|
|
117
|
+
by_a = {a: [] for a in cut["eid"]}
|
|
118
|
+
for a, b, hw in pairs.itertuples(index=False):
|
|
119
|
+
if b in geoms:
|
|
120
|
+
by_a[a].append((geoms[b], hw))
|
|
121
|
+
out = {}
|
|
122
|
+
for eid, lvl in zip(cut["eid"], cut["l"]):
|
|
123
|
+
if eid not in geoms:
|
|
124
|
+
continue
|
|
125
|
+
out[eid] = _pieces_of(geoms[eid], by_a.get(eid, []), 1 if lvl > 0 else -1, highways.get(eid))
|
|
126
|
+
# a stretch that reaches the node (a crossing right at the mouth) leaves no ground piece there: the roads that
|
|
127
|
+
# meet it there end square, or their round end would show as a ring over the lower stretch
|
|
128
|
+
nodes = set()
|
|
129
|
+
for eid, sn, tn in zip(cut["eid"], cut["s"], cut["t"]):
|
|
130
|
+
parts = out.get(eid)
|
|
131
|
+
if parts and len(parts) and parts[0][2] != 0:
|
|
132
|
+
nodes.add(int(sn))
|
|
133
|
+
if parts and parts[-1][2] != 0:
|
|
134
|
+
nodes.add(int(tn))
|
|
135
|
+
square = set()
|
|
136
|
+
if nodes:
|
|
137
|
+
ids = ", ".join(str(n) for n in nodes)
|
|
138
|
+
square = {str(e) for (e,) in con.execute(f"SELECT eid FROM lv WHERE s IN ({ids}) OR t IN ({ids})").fetchall()}
|
|
139
|
+
return out, {eid for eid, t in zip(cut["eid"], cut["is_tunnel"]) if t}, square - set(out)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def with_pieces(roads, pieces, tunnels, square=()):
|
|
143
|
+
"""The roads as drawn: each edge in ``pieces`` becomes its first piece (same row, so the feature index the
|
|
144
|
+
planner's graphs point to is unchanged) and its other pieces are appended after the last edge, with
|
|
145
|
+
``_piece`` set. Columns ``_band`` (the piece's band, else the edge's own) and ``_cap`` (square ends: a stretch
|
|
146
|
+
that goes over or under, and every piece of a tunnel). ``tunnels``: the set of tunnel edge ids."""
|
|
147
|
+
from shapely import affinity
|
|
148
|
+
from shapely.ops import substring
|
|
149
|
+
|
|
150
|
+
roads = roads.copy()
|
|
151
|
+
roads["_piece"] = False
|
|
152
|
+
roads["_cap"] = roads["edge_id"].isin(square) # roads meeting a stretch at a node end square
|
|
153
|
+
extra = []
|
|
154
|
+
for i, eid in zip(roads.index, roads["edge_id"]):
|
|
155
|
+
parts = pieces.get(eid)
|
|
156
|
+
if not parts:
|
|
157
|
+
continue
|
|
158
|
+
ga = roads.at[i, "geometry"]
|
|
159
|
+
sx, sy = _scales(ga)
|
|
160
|
+
gm = _metric(ga, sx, sy)
|
|
161
|
+
rows = []
|
|
162
|
+
last = len(parts) - 1
|
|
163
|
+
for n, (m0, m1, band) in enumerate(parts):
|
|
164
|
+
a, b = max(0.0, m0 - OVERLAP_M) if n else m0, min(gm.length, m1 + OVERLAP_M) if n < last else m1
|
|
165
|
+
line = affinity.scale(substring(gm, a, b), xfact=1 / sx, yfact=1 / sy, origin=(0, 0))
|
|
166
|
+
rows.append((line, band))
|
|
167
|
+
own = roads.at[i, "_band"]
|
|
168
|
+
for n, (line, band) in enumerate(rows):
|
|
169
|
+
cap = band != 0 or eid in tunnels
|
|
170
|
+
b = band if pd.isna(own) else own
|
|
171
|
+
if n == 0:
|
|
172
|
+
roads.at[i, "geometry"], roads.at[i, "_band"], roads.at[i, "_cap"] = line, b, cap
|
|
173
|
+
else:
|
|
174
|
+
r = roads.loc[i].copy()
|
|
175
|
+
r["geometry"], r["_band"], r["_cap"], r["_piece"] = line, b, cap, True
|
|
176
|
+
if "k" in r.index:
|
|
177
|
+
r["k"] = float("nan") # the planner's graphs point at the edge's first piece only
|
|
178
|
+
extra.append(r)
|
|
179
|
+
if extra:
|
|
180
|
+
import geopandas as gpd
|
|
181
|
+
roads = gpd.GeoDataFrame(pd.concat([roads, pd.DataFrame(extra)], ignore_index=True), geometry="geometry",
|
|
182
|
+
crs=roads.crs)
|
|
183
|
+
return roads
|