zero2geospatial3d-sdk 0.1.0__py3-none-any.whl

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 (89) hide show
  1. zero2geospatial3d/__init__.py +105 -0
  2. zero2geospatial3d/acoustics.py +284 -0
  3. zero2geospatial3d/classify.py +379 -0
  4. zero2geospatial3d/cli.py +367 -0
  5. zero2geospatial3d/comfort.py +687 -0
  6. zero2geospatial3d/data/epw_stations.json.gz +0 -0
  7. zero2geospatial3d/daylight.py +337 -0
  8. zero2geospatial3d/dispersion.py +249 -0
  9. zero2geospatial3d/epw.py +162 -0
  10. zero2geospatial3d/geo.py +138 -0
  11. zero2geospatial3d/grid.py +119 -0
  12. zero2geospatial3d/microclimate.py +388 -0
  13. zero2geospatial3d/morphology.py +283 -0
  14. zero2geospatial3d/nsga2.py +315 -0
  15. zero2geospatial3d/osm.py +450 -0
  16. zero2geospatial3d/osm_tags.py +180 -0
  17. zero2geospatial3d/py.typed +0 -0
  18. zero2geospatial3d/results.py +231 -0
  19. zero2geospatial3d/road_sources.py +141 -0
  20. zero2geospatial3d/scene.py +555 -0
  21. zero2geospatial3d/server.py +332 -0
  22. zero2geospatial3d/shading.py +443 -0
  23. zero2geospatial3d/solar.py +207 -0
  24. zero2geospatial3d/terrain.py +610 -0
  25. zero2geospatial3d/urban_form.py +326 -0
  26. zero2geospatial3d/walkability.py +380 -0
  27. zero2geospatial3d/weather.py +373 -0
  28. zero2geospatial3d/web/index.html +50 -0
  29. zero2geospatial3d/web/src/analysis/climate.js +278 -0
  30. zero2geospatial3d/web/src/analysis/compliance.js +345 -0
  31. zero2geospatial3d/web/src/analysis/gpu.js +431 -0
  32. zero2geospatial3d/web/src/analysis/optimize.js +208 -0
  33. zero2geospatial3d/web/src/analysis/runners.js +1644 -0
  34. zero2geospatial3d/web/src/analysis/terrain_results.js +145 -0
  35. zero2geospatial3d/web/src/app.js +630 -0
  36. zero2geospatial3d/web/src/core/acoustics.js +341 -0
  37. zero2geospatial3d/web/src/core/classify.js +158 -0
  38. zero2geospatial3d/web/src/core/comfort.js +686 -0
  39. zero2geospatial3d/web/src/core/daylight_rule.js +199 -0
  40. zero2geospatial3d/web/src/core/dispersion.js +264 -0
  41. zero2geospatial3d/web/src/core/microclimate.js +372 -0
  42. zero2geospatial3d/web/src/core/morphology.js +153 -0
  43. zero2geospatial3d/web/src/core/nsga2.js +181 -0
  44. zero2geospatial3d/web/src/core/party_walls.js +60 -0
  45. zero2geospatial3d/web/src/core/road_sources.js +127 -0
  46. zero2geospatial3d/web/src/core/solar.js +112 -0
  47. zero2geospatial3d/web/src/core/terrain_analysis.js +267 -0
  48. zero2geospatial3d/web/src/core/terrain_math.js +279 -0
  49. zero2geospatial3d/web/src/core/urban_form.js +405 -0
  50. zero2geospatial3d/web/src/core/walkability.js +316 -0
  51. zero2geospatial3d/web/src/core/weather.js +225 -0
  52. zero2geospatial3d/web/src/core/wind.js +131 -0
  53. zero2geospatial3d/web/src/export/check_pdf.js +74 -0
  54. zero2geospatial3d/web/src/export/exporters.js +556 -0
  55. zero2geospatial3d/web/src/export/pdf.js +149 -0
  56. zero2geospatial3d/web/src/export/snapshot.js +234 -0
  57. zero2geospatial3d/web/src/export/snapshot_scene.js +150 -0
  58. zero2geospatial3d/web/src/main.js +75 -0
  59. zero2geospatial3d/web/src/scene/objects.js +645 -0
  60. zero2geospatial3d/web/src/scene/overlay.js +145 -0
  61. zero2geospatial3d/web/src/scene/terrain.js +546 -0
  62. zero2geospatial3d/web/src/scene/viewer.js +275 -0
  63. zero2geospatial3d/web/src/scene/wind_streamlines.js +336 -0
  64. zero2geospatial3d/web/src/style.css +225 -0
  65. zero2geospatial3d/web/src/ui/building_report.js +404 -0
  66. zero2geospatial3d/web/src/ui/charts.js +441 -0
  67. zero2geospatial3d/web/src/ui/colorize.js +101 -0
  68. zero2geospatial3d/web/src/ui/compare.js +362 -0
  69. zero2geospatial3d/web/src/ui/dom.js +104 -0
  70. zero2geospatial3d/web/src/ui/panel_analyze.js +742 -0
  71. zero2geospatial3d/web/src/ui/panel_compare.js +294 -0
  72. zero2geospatial3d/web/src/ui/panel_layers.js +172 -0
  73. zero2geospatial3d/web/src/ui/panel_terrain.js +277 -0
  74. zero2geospatial3d/web/src/ui/qgis_link.js +127 -0
  75. zero2geospatial3d/web/src/ui/sunclock.js +45 -0
  76. zero2geospatial3d/web/src/ui/tools.js +197 -0
  77. zero2geospatial3d/web/src/ui/viewtools.js +173 -0
  78. zero2geospatial3d/web/vendor/three/addons/controls/OrbitControls.js +1417 -0
  79. zero2geospatial3d/web/vendor/three/addons/exporters/GLTFExporter.js +3309 -0
  80. zero2geospatial3d/web/vendor/three/addons/utils/TextureUtils.js +98 -0
  81. zero2geospatial3d/web/vendor/three/three.module.js +53044 -0
  82. zero2geospatial3d/wind.py +178 -0
  83. zero2geospatial3d_sdk-0.1.0.dist-info/METADATA +219 -0
  84. zero2geospatial3d_sdk-0.1.0.dist-info/RECORD +89 -0
  85. zero2geospatial3d_sdk-0.1.0.dist-info/WHEEL +5 -0
  86. zero2geospatial3d_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  87. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  88. zero2geospatial3d_sdk-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +52 -0
  89. zero2geospatial3d_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,181 @@
1
+ // @ts-check
2
+ // NSGA-II (Deb, Pratap, Agarwal and Meyarivan 2002, "A fast and elitist
3
+ // multiobjective genetic algorithm: NSGA-II", IEEE Trans. Evol. Comput.
4
+ // 6(2):182-197, doi:10.1109/4235.996017) with constraint domination,
5
+ // simulated binary crossover and polynomial mutation. All objectives are
6
+ // minimised. Seeded, so a run can be repeated exactly.
7
+
8
+ /** mulberry32: small, fast, seedable PRNG returning [0, 1). */
9
+ export function makeRng(seed = 1) {
10
+ let a = seed >>> 0;
11
+ return function rng() {
12
+ a = (a + 0x6d2b79f5) >>> 0;
13
+ let t = a;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ }
19
+
20
+ /** a dominates b (constraint domination: feasibility first). */
21
+ export function dominates(a, b) {
22
+ if (a.violation > 0 || b.violation > 0) return a.violation < b.violation;
23
+ let better = false;
24
+ for (let i = 0; i < a.objectives.length; i++) {
25
+ if (a.objectives[i] > b.objectives[i]) return false;
26
+ if (a.objectives[i] < b.objectives[i]) better = true;
27
+ }
28
+ return better;
29
+ }
30
+
31
+ /** Fast non-dominated sort: array of fronts (arrays of indices); sets .rank. */
32
+ export function nonDominatedSort(pop) {
33
+ const n = pop.length;
34
+ const dominatedBy = Array.from({ length: n }, () => []);
35
+ const count = new Array(n).fill(0);
36
+ const fronts = [[]];
37
+ for (let p = 0; p < n; p++) {
38
+ for (let q = 0; q < n; q++) {
39
+ if (p === q) continue;
40
+ if (dominates(pop[p], pop[q])) dominatedBy[p].push(q);
41
+ else if (dominates(pop[q], pop[p])) count[p]++;
42
+ }
43
+ if (count[p] === 0) { pop[p].rank = 0; fronts[0].push(p); }
44
+ }
45
+ let i = 0;
46
+ while (fronts[i].length) {
47
+ const next = [];
48
+ for (const p of fronts[i]) {
49
+ for (const q of dominatedBy[p]) {
50
+ if (--count[q] === 0) { pop[q].rank = i + 1; next.push(q); }
51
+ }
52
+ }
53
+ i++;
54
+ fronts.push(next);
55
+ }
56
+ fronts.pop();
57
+ return fronts;
58
+ }
59
+
60
+ /** Crowding distance within one front; sets .crowding. */
61
+ export function crowdingDistance(pop, front) {
62
+ for (const i of front) pop[i].crowding = 0;
63
+ if (front.length <= 2) { for (const i of front) pop[i].crowding = Infinity; return; }
64
+ const m = pop[front[0]].objectives.length;
65
+ for (let k = 0; k < m; k++) {
66
+ const sorted = [...front].sort((a, b) => pop[a].objectives[k] - pop[b].objectives[k]);
67
+ const lo = pop[sorted[0]].objectives[k];
68
+ const hi = pop[sorted[sorted.length - 1]].objectives[k];
69
+ pop[sorted[0]].crowding = Infinity;
70
+ pop[sorted[sorted.length - 1]].crowding = Infinity;
71
+ if (hi - lo <= 0) continue;
72
+ for (let j = 1; j < sorted.length - 1; j++) {
73
+ pop[sorted[j]].crowding += (pop[sorted[j + 1]].objectives[k] - pop[sorted[j - 1]].objectives[k]) / (hi - lo);
74
+ }
75
+ }
76
+ }
77
+
78
+ function better(a, b) {
79
+ if (a.rank !== b.rank) return a.rank < b.rank;
80
+ return a.crowding > b.crowding;
81
+ }
82
+
83
+ function sbx(rng, x1, x2, lo, hi, eta) {
84
+ if (rng() > 0.5 || Math.abs(x1 - x2) < 1e-12) return [x1, x2];
85
+ const u = rng();
86
+ const beta = u <= 0.5 ? Math.pow(2 * u, 1 / (eta + 1)) : Math.pow(1 / (2 * (1 - u)), 1 / (eta + 1));
87
+ const c1 = 0.5 * ((1 + beta) * x1 + (1 - beta) * x2);
88
+ const c2 = 0.5 * ((1 - beta) * x1 + (1 + beta) * x2);
89
+ return [Math.min(hi, Math.max(lo, c1)), Math.min(hi, Math.max(lo, c2))];
90
+ }
91
+
92
+ function polyMutate(rng, x, lo, hi, eta) {
93
+ const span = hi - lo;
94
+ if (span <= 0) return x;
95
+ const u = rng();
96
+ const delta = u < 0.5 ? Math.pow(2 * u, 1 / (eta + 1)) - 1 : 1 - Math.pow(2 * (1 - u), 1 / (eta + 1));
97
+ return Math.min(hi, Math.max(lo, x + delta * span));
98
+ }
99
+
100
+ /**
101
+ * Run NSGA-II.
102
+ * @param {object} problem
103
+ * @param {number[]} problem.lower
104
+ * @param {number[]} problem.upper
105
+ * @param {boolean} [problem.integer] round every variable
106
+ * @param {(x: number[]) => Promise<{ objectives: number[], violation?: number }> | { objectives: number[], violation?: number }} problem.evaluate
107
+ * @param {number[][]} [problem.seeds] starting solutions (e.g. the current design)
108
+ * @param {object} [opts]
109
+ * @returns {Promise<{ population: any[], front: any[], evaluations: number, history: any[] }>}
110
+ */
111
+ export async function nsga2(problem, { populationSize = 24, generations = 20, seed = 1, etaC = 15, etaM = 20, crossoverProb = 0.9, onGeneration = null, shouldStop = null } = {}) {
112
+ const rng = makeRng(seed);
113
+ const n = problem.lower.length;
114
+ const pm = 1 / Math.max(1, n);
115
+ const round = (x) => (problem.integer ? x.map((v, i) => Math.min(problem.upper[i], Math.max(problem.lower[i], Math.round(v)))) : x);
116
+ const cache = new Map();
117
+ let evaluations = 0;
118
+ const evaluate = async (x) => {
119
+ const key = x.join(',');
120
+ if (cache.has(key)) return cache.get(key);
121
+ const r = await problem.evaluate(x);
122
+ evaluations++;
123
+ const ind = { x, objectives: r.objectives, violation: Math.max(0, r.violation || 0), extra: r.extra };
124
+ cache.set(key, ind);
125
+ return { ...ind };
126
+ };
127
+ let pop = [];
128
+ for (const s of problem.seeds || []) if (pop.length < populationSize) pop.push(await evaluate(round(s)));
129
+ while (pop.length < populationSize) {
130
+ const x = problem.lower.map((lo, i) => lo + rng() * (problem.upper[i] - lo));
131
+ pop.push(await evaluate(round(x)));
132
+ }
133
+ const history = [];
134
+ const rankAll = (p) => { const fronts = nonDominatedSort(p); for (const f of fronts) crowdingDistance(p, f); return fronts; };
135
+ rankAll(pop);
136
+ for (let g = 0; g < generations; g++) {
137
+ if (shouldStop && shouldStop()) break;
138
+ const tournament = () => { const a = pop[Math.floor(rng() * pop.length)]; const b = pop[Math.floor(rng() * pop.length)]; return better(a, b) ? a : b; };
139
+ const children = [];
140
+ while (children.length < populationSize) {
141
+ const p1 = tournament().x;
142
+ const p2 = tournament().x;
143
+ const c1 = [...p1];
144
+ const c2 = [...p2];
145
+ if (rng() < crossoverProb) {
146
+ for (let i = 0; i < n; i++) [c1[i], c2[i]] = sbx(rng, p1[i], p2[i], problem.lower[i], problem.upper[i], etaC);
147
+ }
148
+ for (let i = 0; i < n; i++) {
149
+ if (rng() < pm) c1[i] = polyMutate(rng, c1[i], problem.lower[i], problem.upper[i], etaM);
150
+ if (rng() < pm) c2[i] = polyMutate(rng, c2[i], problem.lower[i], problem.upper[i], etaM);
151
+ }
152
+ children.push(await evaluate(round(c1)));
153
+ if (children.length < populationSize) children.push(await evaluate(round(c2)));
154
+ }
155
+ // Copies of a parent (no crossover, no mutation) would crowd the front
156
+ // with identical points; keep one of each unless that leaves too few.
157
+ const seen = new Set();
158
+ const unique = [];
159
+ const dupes = [];
160
+ for (const ind of pop.concat(children)) {
161
+ const key = ind.x.join(',');
162
+ if (seen.has(key)) dupes.push(ind); else { seen.add(key); unique.push(ind); }
163
+ }
164
+ const merged = unique.length >= populationSize ? unique : unique.concat(dupes.slice(0, populationSize - unique.length));
165
+ const fronts = rankAll(merged);
166
+ const next = [];
167
+ for (const f of fronts) {
168
+ if (next.length + f.length <= populationSize) { for (const i of f) next.push(merged[i]); continue; }
169
+ const sorted = [...f].sort((a, b) => merged[b].crowding - merged[a].crowding);
170
+ for (const i of sorted.slice(0, populationSize - next.length)) next.push(merged[i]);
171
+ break;
172
+ }
173
+ pop = next;
174
+ rankAll(pop);
175
+ const front = pop.filter((p) => p.rank === 0 && p.violation === 0);
176
+ history.push({ generation: g + 1, evaluations, frontSize: front.length });
177
+ if (onGeneration) await onGeneration({ generation: g + 1, generations, population: pop, front, evaluations });
178
+ }
179
+ const front = pop.filter((p) => p.rank === 0);
180
+ return { population: pop, front, evaluations, history };
181
+ }
@@ -0,0 +1,60 @@
1
+ // @ts-check
2
+ // Walls built against another building (party walls) have no windows and see
3
+ // no sky: a point just in front of such a wall lies inside the other
4
+ // building's footprint, between its bottom and its roof. Above a lower
5
+ // neighbour the same wall is open again. A grid of footprint boxes keeps the
6
+ // test to the few buildings near each point.
7
+
8
+ function inside(x, y, ring) {
9
+ let odd = false;
10
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
11
+ const [xi, yi] = ring[i], [xj, yj] = ring[j];
12
+ if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) odd = !odd;
13
+ }
14
+ return odd;
15
+ }
16
+
17
+ /**
18
+ * @param {{ id: number, rings: number[][][], bottom: number, top: number }[]} buildings
19
+ * outer rings in local metres, absolute bottom and roof elevations
20
+ * @param {number} [cell] size of the index cells (m)
21
+ */
22
+ export function coverIndex(buildings, cell = 25) {
23
+ const cells = new Map();
24
+ const key = (cx, cy) => `${cx},${cy}`;
25
+ buildings.forEach((b, k) => {
26
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
27
+ for (const ring of b.rings) for (const [x, y] of ring) { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); }
28
+ if (!Number.isFinite(minX)) return;
29
+ for (let cx = Math.floor(minX / cell); cx <= Math.floor(maxX / cell); cx++) {
30
+ for (let cy = Math.floor(minY / cell); cy <= Math.floor(maxY / cell); cy++) {
31
+ const list = cells.get(key(cx, cy));
32
+ if (list) list.push(k); else cells.set(key(cx, cy), [k]);
33
+ }
34
+ }
35
+ });
36
+ return { buildings, cells, cell, key };
37
+ }
38
+
39
+ /**
40
+ * Height spans [bottom, top] of the buildings (other than `ownId`) whose
41
+ * footprint holds the point; empty when the point is in the open.
42
+ * @returns {number[][]}
43
+ */
44
+ export function coverAt(index, x, y, ownId) {
45
+ const list = index.cells.get(index.key(Math.floor(x / index.cell), Math.floor(y / index.cell)));
46
+ if (!list) return [];
47
+ const out = [];
48
+ for (const k of list) {
49
+ const b = index.buildings[k];
50
+ if (b.id === ownId) continue;
51
+ if (b.rings.some((ring) => inside(x, y, ring))) out.push([b.bottom, b.top]);
52
+ }
53
+ return out;
54
+ }
55
+
56
+ /** Whether a wall point at absolute height z, with these spans in front of it, is open to the air. */
57
+ export function isOpen(z, spans) {
58
+ for (const [bottom, top] of spans) if (z >= bottom && z < top) return false;
59
+ return true;
60
+ }
@@ -0,0 +1,127 @@
1
+ // @ts-check
2
+ // Road line sources shared by the noise (acoustics.js) and traffic
3
+ // dispersion (dispersion.js) models: given polylines sampled into points that
4
+ // carry their share of road length times relative traffic, or, without
5
+ // roads, street centrelines found between the buildings.
6
+
7
+ /** Relative traffic of the streets found when no roads are given: a
8
+ * residential street, as for OSM highway=residential (osm_tags.py). */
9
+ export const STREET_TRAFFIC = 0.4;
10
+
11
+ /**
12
+ * Relative traffic of road polyline q: 1 without weights, 0 for a road
13
+ * without traffic, never negative.
14
+ * @param {ArrayLike<number>|null} weights
15
+ * @param {number} q
16
+ */
17
+ export function roadWeight(weights, q) {
18
+ const w = weights ? Number(weights[q]) : 1;
19
+ return Number.isFinite(w) ? Math.max(0, w) : 1;
20
+ }
21
+
22
+ /**
23
+ * Points along road polylines, about `step` metres apart. Each point's
24
+ * `length` is the road length it stands for times the road's traffic.
25
+ * `given` is true when at least one usable polyline was passed, even if
26
+ * all of them carry no traffic (those roads stay silent; no fallback).
27
+ * @param {Array<Array<[number, number]>>|null|undefined} roads
28
+ * @param {ArrayLike<number>|null} weights relative traffic per polyline (default 1)
29
+ * @param {number} step metres between samples
30
+ * @returns {{ sources: Array<{ x: number, y: number, length: number }>, given: boolean }}
31
+ */
32
+ export function sampleRoadLines(roads, weights, step) {
33
+ /** @type {Array<{ x: number, y: number, length: number }>} */
34
+ const sources = [];
35
+ let given = false;
36
+ if (!Array.isArray(roads)) return { sources, given };
37
+ for (let q = 0; q < roads.length; q++) {
38
+ const poly = roads[q];
39
+ if (!Array.isArray(poly) || poly.length < 2) continue;
40
+ given = true;
41
+ const w = roadWeight(weights, q);
42
+ if (w === 0) continue;
43
+ for (let p = 0; p < poly.length - 1; p++) {
44
+ const [x1, y1] = poly[p];
45
+ const [x2, y2] = poly[p + 1];
46
+ const len = Math.hypot(x2 - x1, y2 - y1);
47
+ if (!(len > 0)) continue;
48
+ const count = Math.max(1, Math.round(len / step));
49
+ const segLen = len / count;
50
+ for (let k = 0; k < count; k++) {
51
+ const t = (k + 0.5) / count;
52
+ sources.push({ x: x1 + t * (x2 - x1), y: y1 + t * (y2 - y1), length: segLen * w });
53
+ }
54
+ }
55
+ }
56
+ return { sources, given };
57
+ }
58
+
59
+ /**
60
+ * Street centrelines when no roads are given: the ridge of the distance to
61
+ * the nearest solid building (the open cells farthest from both sides of a
62
+ * street), for streets up to 2 * maxHalfWidth wide. One source per ridge
63
+ * cell, `cell` metres of residential street (STREET_TRAFFIC), so a street
64
+ * counts once and not once per lane of cells. Empty without buildings.
65
+ * @param {Float32Array} heights obstacle height per cell (0 = open)
66
+ * @param {Float32Array|null} porosity tree porosity per cell (> 0 = not solid)
67
+ * @param {number} cols
68
+ * @param {number} rows
69
+ * @param {number} cell metres per cell
70
+ * @param {number} minX x of the first column's centre
71
+ * @param {number} maxY y of the first row's centre
72
+ * @param {number} [maxHalfWidth] metres from a facade to the street centre
73
+ * @returns {Array<{ x: number, y: number, length: number }>}
74
+ */
75
+ export function streetCentrelines(heights, porosity, cols, rows, cell, minX, maxY, maxHalfWidth = 25) {
76
+ const n = cols * rows;
77
+ const INF = 1e9;
78
+ const dist = new Float32Array(n);
79
+ let hasSolid = false;
80
+ for (let i = 0; i < n; i++) {
81
+ if (heights[i] > 0 && !(porosity && porosity[i] > 0)) { dist[i] = 0; hasSolid = true; } else dist[i] = INF;
82
+ }
83
+ if (!hasSolid) return [];
84
+ // Two-pass chamfer distance (metres).
85
+ const a = cell, b = cell * Math.SQRT2;
86
+ for (let r = 0; r < rows; r++) {
87
+ for (let c = 0; c < cols; c++) {
88
+ const i = r * cols + c;
89
+ let v = dist[i];
90
+ if (c > 0) v = Math.min(v, dist[i - 1] + a);
91
+ if (r > 0) {
92
+ v = Math.min(v, dist[i - cols] + a);
93
+ if (c > 0) v = Math.min(v, dist[i - cols - 1] + b);
94
+ if (c < cols - 1) v = Math.min(v, dist[i - cols + 1] + b);
95
+ }
96
+ dist[i] = v;
97
+ }
98
+ }
99
+ for (let r = rows - 1; r >= 0; r--) {
100
+ for (let c = cols - 1; c >= 0; c--) {
101
+ const i = r * cols + c;
102
+ let v = dist[i];
103
+ if (c < cols - 1) v = Math.min(v, dist[i + 1] + a);
104
+ if (r < rows - 1) {
105
+ v = Math.min(v, dist[i + cols] + a);
106
+ if (c < cols - 1) v = Math.min(v, dist[i + cols + 1] + b);
107
+ if (c > 0) v = Math.min(v, dist[i + cols - 1] + b);
108
+ }
109
+ dist[i] = v;
110
+ }
111
+ }
112
+ /** @type {Array<{ x: number, y: number, length: number }>} */
113
+ const sources = [];
114
+ for (let r = 1; r < rows - 1; r++) {
115
+ for (let c = 1; c < cols - 1; c++) {
116
+ const i = r * cols + c;
117
+ const d = dist[i];
118
+ if (d < cell || d > maxHalfWidth) continue;
119
+ // A ridge across x or across y. Strict on one side only, so the
120
+ // two-cell ridge of an even-width street counts once.
121
+ const ridgeX = d >= dist[i - 1] && d > dist[i + 1];
122
+ const ridgeY = d >= dist[i - cols] && d > dist[i + cols];
123
+ if (ridgeX || ridgeY) sources.push({ x: minX + c * cell, y: maxY - r * cell, length: cell * STREET_TRAFFIC });
124
+ }
125
+ }
126
+ return sources;
127
+ }
@@ -0,0 +1,112 @@
1
+ // @ts-check
2
+ // Solar geometry (NOAA "General Solar Position Calculations"), sun-path
3
+ // sampling for one day, and sky sampling for sky view / diffuse light.
4
+ //
5
+ // Scene axes: +x east, +y up, -z north. Azimuth is clockwise from north.
6
+
7
+ const DEG = Math.PI / 180;
8
+
9
+ /** Day of year (1..366) for a calendar date. */
10
+ export function dayOfYear(year, month, day) {
11
+ const start = Date.UTC(year, 0, 1);
12
+ return Math.floor((Date.UTC(year, month - 1, day) - start) / 86400000) + 1;
13
+ }
14
+
15
+ export function isLeap(year) {
16
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
17
+ }
18
+
19
+ /**
20
+ * Sun position.
21
+ * @param {number} doy day of year (1-based)
22
+ * @param {number} hour local clock time in hours (0..24, fractional)
23
+ * @param {number} lat latitude, degrees north
24
+ * @param {number} lon longitude, degrees east
25
+ * @param {number} utcOffset time zone of the clock, hours east of UTC
26
+ * @param {number} [daysInYear]
27
+ * @returns {{ elevation: number, azimuth: number, declination: number, eqTime: number }}
28
+ * elevation and azimuth in radians (azimuth clockwise from north), eqTime minutes
29
+ */
30
+ export function solarPosition(doy, hour, lat, lon, utcOffset, daysInYear = 365) {
31
+ // The fractional year is defined on UTC time.
32
+ const g = (2 * Math.PI / daysInYear) * (doy - 1 + (hour - utcOffset - 12) / 24);
33
+ const eqTime = 229.18 * (0.000075 + 0.001868 * Math.cos(g) - 0.032077 * Math.sin(g)
34
+ - 0.014615 * Math.cos(2 * g) - 0.040849 * Math.sin(2 * g));
35
+ const decl = 0.006918 - 0.399912 * Math.cos(g) + 0.070257 * Math.sin(g)
36
+ - 0.006758 * Math.cos(2 * g) + 0.000907 * Math.sin(2 * g)
37
+ - 0.002697 * Math.cos(3 * g) + 0.00148 * Math.sin(3 * g);
38
+ const trueSolarMinutes = hour * 60 + eqTime + 4 * lon - 60 * utcOffset;
39
+ const ha = (trueSolarMinutes / 4 - 180) * DEG;
40
+ const phi = lat * DEG;
41
+ const sinEl = Math.sin(phi) * Math.sin(decl) + Math.cos(phi) * Math.cos(decl) * Math.cos(ha);
42
+ const elevation = Math.asin(Math.max(-1, Math.min(1, sinEl)));
43
+ let azimuth = Math.atan2(-Math.sin(ha) * Math.cos(decl),
44
+ Math.cos(phi) * Math.sin(decl) - Math.sin(phi) * Math.cos(decl) * Math.cos(ha));
45
+ if (azimuth < 0) azimuth += 2 * Math.PI;
46
+ return { elevation, azimuth, declination: decl, eqTime };
47
+ }
48
+
49
+ /** Unit vector towards the sun in scene axes. */
50
+ export function sunVector(elevation, azimuth) {
51
+ const c = Math.cos(elevation);
52
+ return [Math.sin(azimuth) * c, Math.sin(elevation), -Math.cos(azimuth) * c];
53
+ }
54
+
55
+ /** Clock hours of sunrise and sunset (elevation crosses 0), or null for polar day/night. */
56
+ export function sunriseSunset(doy, lat, lon, utcOffset) {
57
+ const above = (h) => solarPosition(doy, h, lat, lon, utcOffset).elevation > 0;
58
+ const step = 0.25;
59
+ let rise = null;
60
+ let set = null;
61
+ for (let h = 0; h < 24; h += step) {
62
+ const a = above(h);
63
+ const b = above(h + step);
64
+ if (!a && b && rise === null) rise = bisect(above, h, h + step, false);
65
+ if (a && !b) set = bisect(above, h, h + step, true);
66
+ }
67
+ return { sunrise: rise, sunset: set };
68
+ }
69
+
70
+ function bisect(above, lo, hi, startAbove) {
71
+ for (let i = 0; i < 30; i++) {
72
+ const mid = (lo + hi) / 2;
73
+ if (above(mid) === startAbove) lo = mid; else hi = mid;
74
+ }
75
+ return (lo + hi) / 2;
76
+ }
77
+
78
+ /**
79
+ * Sun directions through one day, every stepMinutes between startHour and
80
+ * endHour while the sun is above minElevationDeg. Each sample is weighted
81
+ * with its duration in hours, so the weights of unobstructed samples sum to
82
+ * sun hours.
83
+ */
84
+ export function sunPath(doy, lat, lon, utcOffset, { startHour = 0, endHour = 24, stepMinutes = 10, minElevationDeg = 0.5 } = {}) {
85
+ const out = [];
86
+ const step = stepMinutes / 60;
87
+ for (let h = startHour + step / 2; h < endHour; h += step) {
88
+ const p = solarPosition(doy, h, lat, lon, utcOffset);
89
+ if (p.elevation < minElevationDeg * DEG) continue;
90
+ out.push({ dir: sunVector(p.elevation, p.azimuth), weight: step, hour: h, elevation: p.elevation, azimuth: p.azimuth });
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Quasi-uniform directions over the upper hemisphere (Fibonacci lattice),
97
+ * each standing for an equal solid angle 2*pi/n. With the shader's cosine
98
+ * factor max(0, n.d) and weight 2/n, the sum is the sky view factor of a
99
+ * surface with normal n: 1 for an open horizontal surface, 0.5 for an open
100
+ * wall.
101
+ */
102
+ export function skyHemisphere(n = 192) {
103
+ const out = [];
104
+ const golden = Math.PI * (3 - Math.sqrt(5));
105
+ for (let i = 0; i < n; i++) {
106
+ const y = 1 - (i + 0.5) / n; // uniform in cos(zenith) = uniform solid angle
107
+ const r = Math.sqrt(Math.max(0, 1 - y * y));
108
+ const a = i * golden;
109
+ out.push({ dir: [Math.cos(a) * r, y, Math.sin(a) * r], weight: 2 / n });
110
+ }
111
+ return out;
112
+ }