@unmap/elevation 0.31.0 → 0.32.0
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.
- package/README.md +22 -0
- package/dist/view.d.ts +150 -0
- package/dist/view.js +205 -0
- package/dist/view.js.map +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -24,3 +24,25 @@ throws status `503`, code `service_unavailable`, without falling back to another
|
|
|
24
24
|
|
|
25
25
|
`@unmap/sdk` exposes the same object as `unmap.elevation`. Full documentation:
|
|
26
26
|
<https://unmap.dev/docs/apis/elevation>.
|
|
27
|
+
|
|
28
|
+
## Sight lines
|
|
29
|
+
|
|
30
|
+
`@unmap/elevation/view` is the model the registry's `view-explorer` block runs: what a window can
|
|
31
|
+
see over the terrain tiles, with the Earth's curve and standard refraction. It is pure functions;
|
|
32
|
+
you supply the tile loader, so the browser decodes with `OffscreenCanvas` and Node with an image
|
|
33
|
+
library. Take the tile template from a `terrain=on` style's `raster-dem` source.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { fanRays, groundHeights, readRay, viewShare } from "@unmap/elevation/view";
|
|
37
|
+
|
|
38
|
+
const home: [number, number] = [-114.2005, 51.0575];
|
|
39
|
+
const rays = fanRays(home, { from: 200, to: 305, step: 3 }, 130_000, 72);
|
|
40
|
+
const { ground, perRay } = await groundHeights(home, rays, loadTerrainTile);
|
|
41
|
+
const results = rays.map((ray, i) => readRay(ray, perRay[i]!, ground!, ground! + 4.5));
|
|
42
|
+
const share = viewShare(results); // 0 to 1 of the fan that reaches the mountains
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each tile is one billed `/terrain` call and is loaded once per `groundHeights` call however many
|
|
46
|
+
samples fall in it; cache tiles in your loader to share them across homes. The terrain is bare
|
|
47
|
+
ground in most Canadian cities, so buildings and trees do not block a view unless you add their
|
|
48
|
+
heights to the samples.
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sight lines over unmap's terrain tiles: what a window can see, and what is in its way.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, no network and no decoder. A caller hands `groundHeights` a `TileLoader` that
|
|
5
|
+
* fetches `/terrain/{release}/{z}/{x}/{y}.webp` (the `raster-dem` template a `terrain=on` style
|
|
6
|
+
* carries) and decodes it to RGBA its own way: an `OffscreenCanvas` in the browser, an image
|
|
7
|
+
* library in Node. Every tile is one billed call and is loaded once however many samples fall in
|
|
8
|
+
* it, so a batch over many homes shares its tiles.
|
|
9
|
+
*
|
|
10
|
+
* The heights are the terrain release's: bare ground in most Canadian cities, so hills block a
|
|
11
|
+
* view and houses, towers and trees do not, unless the caller adds them to the heights it reads.
|
|
12
|
+
*/
|
|
13
|
+
type LngLat = [number, number];
|
|
14
|
+
/** An arc of azimuths, in degrees clockwise from north, `from` to `to` inclusive, every `step`. */
|
|
15
|
+
interface Fan {
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
step: number;
|
|
19
|
+
}
|
|
20
|
+
/** One sight line, sampled: metres from the home, and the ground there (null where the model has none). */
|
|
21
|
+
interface ViewRay {
|
|
22
|
+
azimuth: number;
|
|
23
|
+
distances: number[];
|
|
24
|
+
points: LngLat[];
|
|
25
|
+
}
|
|
26
|
+
/** What one sight line of a fan sees. */
|
|
27
|
+
interface RayResult {
|
|
28
|
+
azimuth: number;
|
|
29
|
+
/** True when the skyline in this direction is mountains, not a nearer hill. */
|
|
30
|
+
clear: boolean;
|
|
31
|
+
/** The skyline point: where the line from the window grazes the ground last. */
|
|
32
|
+
horizon: {
|
|
33
|
+
lngLat: LngLat;
|
|
34
|
+
distance: number;
|
|
35
|
+
height: number;
|
|
36
|
+
angle: number;
|
|
37
|
+
} | null;
|
|
38
|
+
/** The steepest angle up to any mountain on this line, seen or not, for the skyline's ghost. */
|
|
39
|
+
mountainAngle: number | null;
|
|
40
|
+
}
|
|
41
|
+
/** Whether a single target (a summit, the downtown towers) can be seen, and what is in the way. */
|
|
42
|
+
interface TargetResult {
|
|
43
|
+
visible: boolean;
|
|
44
|
+
/** The angle up to the target, in degrees above the level line from the window. */
|
|
45
|
+
angle: number;
|
|
46
|
+
distance: number;
|
|
47
|
+
/** The nearer ground that rises highest in the view, and so blocks the target when it is hidden. */
|
|
48
|
+
blocker: {
|
|
49
|
+
lngLat: LngLat;
|
|
50
|
+
distance: number;
|
|
51
|
+
height: number;
|
|
52
|
+
} | null;
|
|
53
|
+
}
|
|
54
|
+
/** The first sample out from the window, in metres: the next lot over. */
|
|
55
|
+
declare const NEAREST = 40;
|
|
56
|
+
/** Ground this far above the home's own counts as mountain. Calgary's highest hills stop well short of it. */
|
|
57
|
+
declare const MOUNTAIN_RELIEF = 600;
|
|
58
|
+
/**
|
|
59
|
+
* The terrain zoom each sample is read at, by its distance from the home: the finest tiles where a
|
|
60
|
+
* neighbour's rise matters, coarser ones in the mountains, where only the summits do. A z9 tile is
|
|
61
|
+
* about 100 m a pixel at Calgary's latitude, plenty for a peak 100 km off.
|
|
62
|
+
*/
|
|
63
|
+
declare const ZOOM_BANDS: Array<{
|
|
64
|
+
within: number;
|
|
65
|
+
zoom: number;
|
|
66
|
+
}>;
|
|
67
|
+
/** Standard atmospheric refraction: light bends a little with the curve, so a little less is hidden. */
|
|
68
|
+
declare const REFRACTION = 0.13;
|
|
69
|
+
/** Great-circle distance in metres. */
|
|
70
|
+
declare function distanceMetres(a: LngLat, b: LngLat): number;
|
|
71
|
+
/** The initial bearing from `a` to `b`, in degrees clockwise from north, 0 to 360. */
|
|
72
|
+
declare function bearingDegrees(a: LngLat, b: LngLat): number;
|
|
73
|
+
/** The point `metres` from `origin` along `azimuth`, on the sphere, rounded to five decimals (about a metre). */
|
|
74
|
+
declare function destination(origin: LngLat, azimuth: number, metres: number): LngLat;
|
|
75
|
+
/**
|
|
76
|
+
* `count` distances from `near` to `far`, each a fixed ratio past the last: about 100 m apart a
|
|
77
|
+
* kilometre out, a few kilometres apart in the mountains, where only the summits matter.
|
|
78
|
+
*/
|
|
79
|
+
declare function sampleDistances(count: number, far: number, near?: number): number[];
|
|
80
|
+
/** A sight line from `origin` along `azimuth`, sampled at `distances`. */
|
|
81
|
+
declare function sightRay(origin: LngLat, azimuth: number, distances: number[]): ViewRay;
|
|
82
|
+
/** The fan's azimuths, inclusive of both ends. */
|
|
83
|
+
declare function fanAzimuths(fan: Fan): number[];
|
|
84
|
+
/** The fan's sight lines, every one `reach` metres long and sampled `samples` times. */
|
|
85
|
+
declare function fanRays(origin: LngLat, fan: Fan, reach: number, samples: number): ViewRay[];
|
|
86
|
+
/** A sight line ending exactly on `target`. */
|
|
87
|
+
declare function targetRay(origin: LngLat, target: LngLat, samples: number): ViewRay;
|
|
88
|
+
/**
|
|
89
|
+
* How far the ground at `metres` sits below the level line from the window, from the Earth's
|
|
90
|
+
* curve less the share refraction gives back: about 680 m at 100 km.
|
|
91
|
+
*/
|
|
92
|
+
declare function curveDrop(metres: number, refraction?: number): number;
|
|
93
|
+
/** The angle up (or down) from the window to ground of `height` at `metres`, in degrees. */
|
|
94
|
+
declare function sightAngle(eyeHeight: number, metres: number, height: number): number;
|
|
95
|
+
/**
|
|
96
|
+
* What a sight line of a fan sees from a window at `eyeHeight` (metres above sea level) over
|
|
97
|
+
* ground at `groundHeight`. The skyline point is the sample at the steepest angle up; the line is
|
|
98
|
+
* clear when that sample is mountain, at least `relief` above the home's own ground.
|
|
99
|
+
*/
|
|
100
|
+
declare function readRay(ray: ViewRay, heights: (number | null)[], groundHeight: number, eyeHeight: number, relief?: number): RayResult;
|
|
101
|
+
/**
|
|
102
|
+
* Whether the last sample of `ray` (a summit, or the towers `targetLift` metres above their
|
|
103
|
+
* street) can be seen: nothing nearer rises to a steeper angle. Within a hundredth of a degree
|
|
104
|
+
* counts as seen, the model's own half-metre steps.
|
|
105
|
+
*/
|
|
106
|
+
declare function readTarget(ray: ViewRay, heights: (number | null)[], eyeHeight: number, targetLift?: number): TargetResult | null;
|
|
107
|
+
/** The terrain zoom to read a sample at, by its distance from the home. */
|
|
108
|
+
declare function zoomFor(metres: number): number;
|
|
109
|
+
/** A terrain tile, decoded: its RGBA pixels, row by row, `size` on a side. */
|
|
110
|
+
interface TerrainTile {
|
|
111
|
+
size: number;
|
|
112
|
+
data: ArrayLike<number>;
|
|
113
|
+
}
|
|
114
|
+
/** Terrarium: `elevation = R * 256 + G + B / 256 - 32768`, the encoding unmap's terrain uses. */
|
|
115
|
+
declare function terrarium(r: number, g: number, b: number): number;
|
|
116
|
+
/** The tile a coordinate falls in at zoom `z`, and its fractional pixel inside a `size` tile. */
|
|
117
|
+
declare function tilePixel(lng: number, lat: number, z: number, size: number): {
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
px: number;
|
|
121
|
+
py: number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The height at a pixel position, bilinear between the four pixel centres around it. At a tile's
|
|
125
|
+
* edge the nearest pixels stand in for the neighbour's: a few metres of error, never a hole.
|
|
126
|
+
*/
|
|
127
|
+
declare function sampleTile(tile: TerrainTile, px: number, py: number): number;
|
|
128
|
+
/** Loads one terrain tile, null where the terrain has none. */
|
|
129
|
+
type TileLoader = (z: number, x: number, y: number) => Promise<TerrainTile | null>;
|
|
130
|
+
/**
|
|
131
|
+
* The ground under every sample of every ray, each read at its distance's zoom. Each tile is
|
|
132
|
+
* loaded once however many samples fall in it, all of them at once. `home` is read at the finest
|
|
133
|
+
* zoom and answered first.
|
|
134
|
+
*/
|
|
135
|
+
declare function groundHeights(home: LngLat, rays: ViewRay[], load: TileLoader): Promise<{
|
|
136
|
+
ground: number | null;
|
|
137
|
+
perRay: (number | null)[][];
|
|
138
|
+
}>;
|
|
139
|
+
/** The share of the fan that reaches the mountains, 0 to 1. */
|
|
140
|
+
declare function viewShare(results: RayResult[]): number;
|
|
141
|
+
/**
|
|
142
|
+
* The widest run of blocked lines in the fan: its middle azimuth, and the median distance to
|
|
143
|
+
* what blocks it. Null when nothing is blocked.
|
|
144
|
+
*/
|
|
145
|
+
declare function widestBlock(results: RayResult[]): {
|
|
146
|
+
azimuth: number;
|
|
147
|
+
distance: number;
|
|
148
|
+
} | null;
|
|
149
|
+
|
|
150
|
+
export { type Fan, MOUNTAIN_RELIEF, NEAREST, REFRACTION, type RayResult, type TargetResult, type TerrainTile, type TileLoader, type ViewRay, ZOOM_BANDS, bearingDegrees, curveDrop, destination, distanceMetres, fanAzimuths, fanRays, groundHeights, readRay, readTarget, sampleDistances, sampleTile, sightAngle, sightRay, targetRay, terrarium, tilePixel, viewShare, widestBlock, zoomFor };
|
package/dist/view.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/view.ts
|
|
2
|
+
var NEAREST = 40;
|
|
3
|
+
var MOUNTAIN_RELIEF = 600;
|
|
4
|
+
var ZOOM_BANDS = [
|
|
5
|
+
{ within: 4e3, zoom: 12 },
|
|
6
|
+
{ within: 12e3, zoom: 11 },
|
|
7
|
+
{ within: 35e3, zoom: 10 },
|
|
8
|
+
{ within: Infinity, zoom: 9 }
|
|
9
|
+
];
|
|
10
|
+
var EARTH_RADIUS_M = 63710088e-1;
|
|
11
|
+
var REFRACTION = 0.13;
|
|
12
|
+
var toRadians = (degrees) => degrees * Math.PI / 180;
|
|
13
|
+
var toDegrees = (radians) => radians * 180 / Math.PI;
|
|
14
|
+
function distanceMetres(a, b) {
|
|
15
|
+
const dLat = toRadians(b[1] - a[1]);
|
|
16
|
+
const dLng = toRadians(b[0] - a[0]);
|
|
17
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRadians(a[1])) * Math.cos(toRadians(b[1])) * Math.sin(dLng / 2) ** 2;
|
|
18
|
+
return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
19
|
+
}
|
|
20
|
+
function bearingDegrees(a, b) {
|
|
21
|
+
const \u03C61 = toRadians(a[1]);
|
|
22
|
+
const \u03C62 = toRadians(b[1]);
|
|
23
|
+
const \u0394\u03BB = toRadians(b[0] - a[0]);
|
|
24
|
+
const y = Math.sin(\u0394\u03BB) * Math.cos(\u03C62);
|
|
25
|
+
const x = Math.cos(\u03C61) * Math.sin(\u03C62) - Math.sin(\u03C61) * Math.cos(\u03C62) * Math.cos(\u0394\u03BB);
|
|
26
|
+
return (toDegrees(Math.atan2(y, x)) + 360) % 360;
|
|
27
|
+
}
|
|
28
|
+
function destination(origin, azimuth, metres) {
|
|
29
|
+
const \u03B4 = metres / EARTH_RADIUS_M;
|
|
30
|
+
const \u03B8 = toRadians(azimuth);
|
|
31
|
+
const \u03C61 = toRadians(origin[1]);
|
|
32
|
+
const \u03BB1 = toRadians(origin[0]);
|
|
33
|
+
const \u03C62 = Math.asin(Math.sin(\u03C61) * Math.cos(\u03B4) + Math.cos(\u03C61) * Math.sin(\u03B4) * Math.cos(\u03B8));
|
|
34
|
+
const \u03BB2 = \u03BB1 + Math.atan2(Math.sin(\u03B8) * Math.sin(\u03B4) * Math.cos(\u03C61), Math.cos(\u03B4) - Math.sin(\u03C61) * Math.sin(\u03C62));
|
|
35
|
+
return [round5(toDegrees(\u03BB2)), round5(toDegrees(\u03C62))];
|
|
36
|
+
}
|
|
37
|
+
function round5(value) {
|
|
38
|
+
return Math.round(value * 1e5) / 1e5;
|
|
39
|
+
}
|
|
40
|
+
function sampleDistances(count, far, near = NEAREST) {
|
|
41
|
+
if (count <= 1) return [far];
|
|
42
|
+
const ratio = (far / near) ** (1 / (count - 1));
|
|
43
|
+
return Array.from({ length: count }, (_value, index) => index === count - 1 ? far : near * ratio ** index);
|
|
44
|
+
}
|
|
45
|
+
function sightRay(origin, azimuth, distances) {
|
|
46
|
+
return { azimuth, distances, points: distances.map((distance) => destination(origin, azimuth, distance)) };
|
|
47
|
+
}
|
|
48
|
+
function fanAzimuths(fan) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (let azimuth = fan.from; azimuth <= fan.to + 1e-9; azimuth += fan.step) out.push(azimuth);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function fanRays(origin, fan, reach, samples) {
|
|
54
|
+
const distances = sampleDistances(samples, reach);
|
|
55
|
+
return fanAzimuths(fan).map((azimuth) => sightRay(origin, azimuth, distances));
|
|
56
|
+
}
|
|
57
|
+
function targetRay(origin, target, samples) {
|
|
58
|
+
const ray = sightRay(origin, bearingDegrees(origin, target), sampleDistances(samples, distanceMetres(origin, target), Math.min(NEAREST, distanceMetres(origin, target) / samples)));
|
|
59
|
+
ray.points[ray.points.length - 1] = [round5(target[0]), round5(target[1])];
|
|
60
|
+
return ray;
|
|
61
|
+
}
|
|
62
|
+
function curveDrop(metres, refraction = REFRACTION) {
|
|
63
|
+
return metres * metres / (2 * EARTH_RADIUS_M) * (1 - refraction);
|
|
64
|
+
}
|
|
65
|
+
function sightAngle(eyeHeight, metres, height) {
|
|
66
|
+
return toDegrees(Math.atan2(height - curveDrop(metres) - eyeHeight, metres));
|
|
67
|
+
}
|
|
68
|
+
function readRay(ray, heights, groundHeight, eyeHeight, relief = MOUNTAIN_RELIEF) {
|
|
69
|
+
let best = -Infinity;
|
|
70
|
+
let at = -1;
|
|
71
|
+
let mountainAngle = null;
|
|
72
|
+
for (let index = 0; index < ray.distances.length; index++) {
|
|
73
|
+
const height2 = heights[index];
|
|
74
|
+
if (height2 === null || height2 === void 0) continue;
|
|
75
|
+
const angle = sightAngle(eyeHeight, ray.distances[index], height2);
|
|
76
|
+
if (angle > best) {
|
|
77
|
+
best = angle;
|
|
78
|
+
at = index;
|
|
79
|
+
}
|
|
80
|
+
if (height2 >= groundHeight + relief && (mountainAngle === null || angle > mountainAngle)) mountainAngle = angle;
|
|
81
|
+
}
|
|
82
|
+
if (at < 0) return { azimuth: ray.azimuth, clear: false, horizon: null, mountainAngle: null };
|
|
83
|
+
const height = heights[at];
|
|
84
|
+
return {
|
|
85
|
+
azimuth: ray.azimuth,
|
|
86
|
+
clear: height >= groundHeight + relief,
|
|
87
|
+
horizon: { lngLat: ray.points[at], distance: ray.distances[at], height, angle: best },
|
|
88
|
+
mountainAngle
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function readTarget(ray, heights, eyeHeight, targetLift = 0) {
|
|
92
|
+
const last = ray.distances.length - 1;
|
|
93
|
+
const ground = heights[last];
|
|
94
|
+
if (ground === null || ground === void 0) return null;
|
|
95
|
+
const distance = ray.distances[last];
|
|
96
|
+
const angle = sightAngle(eyeHeight, distance, ground + targetLift);
|
|
97
|
+
let best = -Infinity;
|
|
98
|
+
let blocker = null;
|
|
99
|
+
for (let index = 0; index < last; index++) {
|
|
100
|
+
const height = heights[index];
|
|
101
|
+
if (height === null || height === void 0) continue;
|
|
102
|
+
const nearer = sightAngle(eyeHeight, ray.distances[index], height);
|
|
103
|
+
if (nearer > best) {
|
|
104
|
+
best = nearer;
|
|
105
|
+
blocker = { lngLat: ray.points[index], distance: ray.distances[index], height };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { visible: angle >= best - 0.01, angle, distance, blocker };
|
|
109
|
+
}
|
|
110
|
+
function zoomFor(metres) {
|
|
111
|
+
return ZOOM_BANDS.find((band) => metres < band.within).zoom;
|
|
112
|
+
}
|
|
113
|
+
function terrarium(r, g, b) {
|
|
114
|
+
return r * 256 + g + b / 256 - 32768;
|
|
115
|
+
}
|
|
116
|
+
function tilePixel(lng, lat, z, size) {
|
|
117
|
+
const n = 2 ** z;
|
|
118
|
+
const gx = (lng + 180) / 360 * n;
|
|
119
|
+
const gy = (1 - Math.asinh(Math.tan(toRadians(lat))) / Math.PI) / 2 * n;
|
|
120
|
+
const x = Math.floor(gx);
|
|
121
|
+
const y = Math.floor(gy);
|
|
122
|
+
return { x, y, px: (gx - x) * size, py: (gy - y) * size };
|
|
123
|
+
}
|
|
124
|
+
function sampleTile(tile, px, py) {
|
|
125
|
+
const size = tile.size;
|
|
126
|
+
const fx = Math.min(Math.max(px - 0.5, 0), size - 1);
|
|
127
|
+
const fy = Math.min(Math.max(py - 0.5, 0), size - 1);
|
|
128
|
+
const x0 = Math.floor(fx);
|
|
129
|
+
const y0 = Math.floor(fy);
|
|
130
|
+
const x1 = Math.min(x0 + 1, size - 1);
|
|
131
|
+
const y1 = Math.min(y0 + 1, size - 1);
|
|
132
|
+
const at = (x, y) => {
|
|
133
|
+
const i = (y * size + x) * 4;
|
|
134
|
+
return terrarium(tile.data[i], tile.data[i + 1], tile.data[i + 2]);
|
|
135
|
+
};
|
|
136
|
+
const tx = fx - x0;
|
|
137
|
+
const ty = fy - y0;
|
|
138
|
+
const top = at(x0, y0) * (1 - tx) + at(x1, y0) * tx;
|
|
139
|
+
const bottom = at(x0, y1) * (1 - tx) + at(x1, y1) * tx;
|
|
140
|
+
return top * (1 - ty) + bottom * ty;
|
|
141
|
+
}
|
|
142
|
+
async function groundHeights(home, rays, load) {
|
|
143
|
+
const wanted = new globalThis.Map();
|
|
144
|
+
const read = (lngLat, metres) => {
|
|
145
|
+
const z = zoomFor(metres);
|
|
146
|
+
const at = tilePixel(lngLat[0], lngLat[1], z, 512);
|
|
147
|
+
const id = `${z}/${at.x}/${at.y}`;
|
|
148
|
+
if (!wanted.has(id)) wanted.set(id, load(z, at.x, at.y));
|
|
149
|
+
return { id, at };
|
|
150
|
+
};
|
|
151
|
+
const homeRead = read(home, 0);
|
|
152
|
+
const rayReads = rays.map((ray) => ray.points.map((point, index) => read(point, ray.distances[index])));
|
|
153
|
+
const ids = [...wanted.keys()];
|
|
154
|
+
const loaded = await Promise.all(ids.map((id) => wanted.get(id)));
|
|
155
|
+
const tiles = new globalThis.Map(ids.map((id, index) => [id, loaded[index]]));
|
|
156
|
+
const height = ({ id, at }) => {
|
|
157
|
+
const tile = tiles.get(id);
|
|
158
|
+
if (!tile) return null;
|
|
159
|
+
const scale = tile.size / 512;
|
|
160
|
+
return sampleTile(tile, at.px * scale, at.py * scale);
|
|
161
|
+
};
|
|
162
|
+
return { ground: height(homeRead), perRay: rayReads.map((reads) => reads.map(height)) };
|
|
163
|
+
}
|
|
164
|
+
function viewShare(results) {
|
|
165
|
+
const read = results.filter((result) => result.horizon !== null);
|
|
166
|
+
return read.length === 0 ? 0 : read.filter((result) => result.clear).length / read.length;
|
|
167
|
+
}
|
|
168
|
+
function widestBlock(results) {
|
|
169
|
+
let best = [];
|
|
170
|
+
let run = [];
|
|
171
|
+
for (const result of results) {
|
|
172
|
+
if (result.horizon && !result.clear) run.push(result);
|
|
173
|
+
else run = [];
|
|
174
|
+
if (run.length > best.length) best = [...run];
|
|
175
|
+
}
|
|
176
|
+
if (best.length === 0) return null;
|
|
177
|
+
const distances = best.map((result) => result.horizon.distance).sort((a, b) => a - b);
|
|
178
|
+
return { azimuth: (best[0].azimuth + best[best.length - 1].azimuth) / 2, distance: distances[Math.floor(distances.length / 2)] };
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
MOUNTAIN_RELIEF,
|
|
182
|
+
NEAREST,
|
|
183
|
+
REFRACTION,
|
|
184
|
+
ZOOM_BANDS,
|
|
185
|
+
bearingDegrees,
|
|
186
|
+
curveDrop,
|
|
187
|
+
destination,
|
|
188
|
+
distanceMetres,
|
|
189
|
+
fanAzimuths,
|
|
190
|
+
fanRays,
|
|
191
|
+
groundHeights,
|
|
192
|
+
readRay,
|
|
193
|
+
readTarget,
|
|
194
|
+
sampleDistances,
|
|
195
|
+
sampleTile,
|
|
196
|
+
sightAngle,
|
|
197
|
+
sightRay,
|
|
198
|
+
targetRay,
|
|
199
|
+
terrarium,
|
|
200
|
+
tilePixel,
|
|
201
|
+
viewShare,
|
|
202
|
+
widestBlock,
|
|
203
|
+
zoomFor
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=view.js.map
|
package/dist/view.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/view.ts"],"sourcesContent":["/**\n * Sight lines over unmap's terrain tiles: what a window can see, and what is in its way.\n *\n * Pure functions, no network and no decoder. A caller hands `groundHeights` a `TileLoader` that\n * fetches `/terrain/{release}/{z}/{x}/{y}.webp` (the `raster-dem` template a `terrain=on` style\n * carries) and decodes it to RGBA its own way: an `OffscreenCanvas` in the browser, an image\n * library in Node. Every tile is one billed call and is loaded once however many samples fall in\n * it, so a batch over many homes shares its tiles.\n *\n * The heights are the terrain release's: bare ground in most Canadian cities, so hills block a\n * view and houses, towers and trees do not, unless the caller adds them to the heights it reads.\n */\n\ntype LngLat = [number, number];\n\n/** An arc of azimuths, in degrees clockwise from north, `from` to `to` inclusive, every `step`. */\nexport interface Fan {\n from: number;\n to: number;\n step: number;\n}\n\n/** One sight line, sampled: metres from the home, and the ground there (null where the model has none). */\nexport interface ViewRay {\n azimuth: number;\n distances: number[];\n points: LngLat[];\n}\n\n/** What one sight line of a fan sees. */\nexport interface RayResult {\n azimuth: number;\n /** True when the skyline in this direction is mountains, not a nearer hill. */\n clear: boolean;\n /** The skyline point: where the line from the window grazes the ground last. */\n horizon: { lngLat: LngLat; distance: number; height: number; angle: number } | null;\n /** The steepest angle up to any mountain on this line, seen or not, for the skyline's ghost. */\n mountainAngle: number | null;\n}\n\n/** Whether a single target (a summit, the downtown towers) can be seen, and what is in the way. */\nexport interface TargetResult {\n visible: boolean;\n /** The angle up to the target, in degrees above the level line from the window. */\n angle: number;\n distance: number;\n /** The nearer ground that rises highest in the view, and so blocks the target when it is hidden. */\n blocker: { lngLat: LngLat; distance: number; height: number } | null;\n}\n\n/** The first sample out from the window, in metres: the next lot over. */\nexport const NEAREST = 40;\n\n/** Ground this far above the home's own counts as mountain. Calgary's highest hills stop well short of it. */\nexport const MOUNTAIN_RELIEF = 600;\n\n/**\n * The terrain zoom each sample is read at, by its distance from the home: the finest tiles where a\n * neighbour's rise matters, coarser ones in the mountains, where only the summits do. A z9 tile is\n * about 100 m a pixel at Calgary's latitude, plenty for a peak 100 km off.\n */\nexport const ZOOM_BANDS: Array<{ within: number; zoom: number }> = [\n { within: 4_000, zoom: 12 },\n { within: 12_000, zoom: 11 },\n { within: 35_000, zoom: 10 },\n { within: Infinity, zoom: 9 },\n];\n\nconst EARTH_RADIUS_M = 6_371_008.8;\n/** Standard atmospheric refraction: light bends a little with the curve, so a little less is hidden. */\nexport const REFRACTION = 0.13;\n\nconst toRadians = (degrees: number) => (degrees * Math.PI) / 180;\nconst toDegrees = (radians: number) => (radians * 180) / Math.PI;\n\n/** Great-circle distance in metres. */\nexport function distanceMetres(a: LngLat, b: LngLat): number {\n const dLat = toRadians(b[1] - a[1]);\n const dLng = toRadians(b[0] - a[0]);\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRadians(a[1])) * Math.cos(toRadians(b[1])) * Math.sin(dLng / 2) ** 2;\n return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));\n}\n\n/** The initial bearing from `a` to `b`, in degrees clockwise from north, 0 to 360. */\nexport function bearingDegrees(a: LngLat, b: LngLat): number {\n const φ1 = toRadians(a[1]);\n const φ2 = toRadians(b[1]);\n const Δλ = toRadians(b[0] - a[0]);\n const y = Math.sin(Δλ) * Math.cos(φ2);\n const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);\n return (toDegrees(Math.atan2(y, x)) + 360) % 360;\n}\n\n/** The point `metres` from `origin` along `azimuth`, on the sphere, rounded to five decimals (about a metre). */\nexport function destination(origin: LngLat, azimuth: number, metres: number): LngLat {\n const δ = metres / EARTH_RADIUS_M;\n const θ = toRadians(azimuth);\n const φ1 = toRadians(origin[1]);\n const λ1 = toRadians(origin[0]);\n const φ2 = Math.asin(Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(θ));\n const λ2 = λ1 + Math.atan2(Math.sin(θ) * Math.sin(δ) * Math.cos(φ1), Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2));\n return [round5(toDegrees(λ2)), round5(toDegrees(φ2))];\n}\n\nfunction round5(value: number): number {\n return Math.round(value * 1e5) / 1e5;\n}\n\n/**\n * `count` distances from `near` to `far`, each a fixed ratio past the last: about 100 m apart a\n * kilometre out, a few kilometres apart in the mountains, where only the summits matter.\n */\nexport function sampleDistances(count: number, far: number, near = NEAREST): number[] {\n if (count <= 1) return [far];\n const ratio = (far / near) ** (1 / (count - 1));\n return Array.from({ length: count }, (_value, index) => (index === count - 1 ? far : near * ratio ** index));\n}\n\n/** A sight line from `origin` along `azimuth`, sampled at `distances`. */\nexport function sightRay(origin: LngLat, azimuth: number, distances: number[]): ViewRay {\n return { azimuth, distances, points: distances.map((distance) => destination(origin, azimuth, distance)) };\n}\n\n/** The fan's azimuths, inclusive of both ends. */\nexport function fanAzimuths(fan: Fan): number[] {\n const out: number[] = [];\n for (let azimuth = fan.from; azimuth <= fan.to + 1e-9; azimuth += fan.step) out.push(azimuth);\n return out;\n}\n\n/** The fan's sight lines, every one `reach` metres long and sampled `samples` times. */\nexport function fanRays(origin: LngLat, fan: Fan, reach: number, samples: number): ViewRay[] {\n const distances = sampleDistances(samples, reach);\n return fanAzimuths(fan).map((azimuth) => sightRay(origin, azimuth, distances));\n}\n\n/** A sight line ending exactly on `target`. */\nexport function targetRay(origin: LngLat, target: LngLat, samples: number): ViewRay {\n const ray = sightRay(origin, bearingDegrees(origin, target), sampleDistances(samples, distanceMetres(origin, target), Math.min(NEAREST, distanceMetres(origin, target) / samples)));\n ray.points[ray.points.length - 1] = [round5(target[0]), round5(target[1])];\n return ray;\n}\n\n/**\n * How far the ground at `metres` sits below the level line from the window, from the Earth's\n * curve less the share refraction gives back: about 680 m at 100 km.\n */\nexport function curveDrop(metres: number, refraction = REFRACTION): number {\n return ((metres * metres) / (2 * EARTH_RADIUS_M)) * (1 - refraction);\n}\n\n/** The angle up (or down) from the window to ground of `height` at `metres`, in degrees. */\nexport function sightAngle(eyeHeight: number, metres: number, height: number): number {\n return toDegrees(Math.atan2(height - curveDrop(metres) - eyeHeight, metres));\n}\n\n/**\n * What a sight line of a fan sees from a window at `eyeHeight` (metres above sea level) over\n * ground at `groundHeight`. The skyline point is the sample at the steepest angle up; the line is\n * clear when that sample is mountain, at least `relief` above the home's own ground.\n */\nexport function readRay(ray: ViewRay, heights: (number | null)[], groundHeight: number, eyeHeight: number, relief = MOUNTAIN_RELIEF): RayResult {\n let best = -Infinity;\n let at = -1;\n let mountainAngle: number | null = null;\n for (let index = 0; index < ray.distances.length; index++) {\n const height = heights[index];\n if (height === null || height === undefined) continue;\n const angle = sightAngle(eyeHeight, ray.distances[index]!, height);\n if (angle > best) {\n best = angle;\n at = index;\n }\n if (height >= groundHeight + relief && (mountainAngle === null || angle > mountainAngle)) mountainAngle = angle;\n }\n if (at < 0) return { azimuth: ray.azimuth, clear: false, horizon: null, mountainAngle: null };\n const height = heights[at]!;\n return {\n azimuth: ray.azimuth,\n clear: height >= groundHeight + relief,\n horizon: { lngLat: ray.points[at]!, distance: ray.distances[at]!, height, angle: best },\n mountainAngle,\n };\n}\n\n/**\n * Whether the last sample of `ray` (a summit, or the towers `targetLift` metres above their\n * street) can be seen: nothing nearer rises to a steeper angle. Within a hundredth of a degree\n * counts as seen, the model's own half-metre steps.\n */\nexport function readTarget(ray: ViewRay, heights: (number | null)[], eyeHeight: number, targetLift = 0): TargetResult | null {\n const last = ray.distances.length - 1;\n const ground = heights[last];\n if (ground === null || ground === undefined) return null;\n const distance = ray.distances[last]!;\n const angle = sightAngle(eyeHeight, distance, ground + targetLift);\n let best = -Infinity;\n let blocker: TargetResult[\"blocker\"] = null;\n for (let index = 0; index < last; index++) {\n const height = heights[index];\n if (height === null || height === undefined) continue;\n const nearer = sightAngle(eyeHeight, ray.distances[index]!, height);\n if (nearer > best) {\n best = nearer;\n blocker = { lngLat: ray.points[index]!, distance: ray.distances[index]!, height };\n }\n }\n return { visible: angle >= best - 0.01, angle, distance, blocker };\n}\n\n/** The terrain zoom to read a sample at, by its distance from the home. */\nexport function zoomFor(metres: number): number {\n return ZOOM_BANDS.find((band) => metres < band.within)!.zoom;\n}\n\n/** A terrain tile, decoded: its RGBA pixels, row by row, `size` on a side. */\nexport interface TerrainTile {\n size: number;\n data: ArrayLike<number>;\n}\n\n/** Terrarium: `elevation = R * 256 + G + B / 256 - 32768`, the encoding unmap's terrain uses. */\nexport function terrarium(r: number, g: number, b: number): number {\n return r * 256 + g + b / 256 - 32768;\n}\n\n/** The tile a coordinate falls in at zoom `z`, and its fractional pixel inside a `size` tile. */\nexport function tilePixel(lng: number, lat: number, z: number, size: number): { x: number; y: number; px: number; py: number } {\n const n = 2 ** z;\n const gx = ((lng + 180) / 360) * n;\n const gy = ((1 - Math.asinh(Math.tan(toRadians(lat))) / Math.PI) / 2) * n;\n const x = Math.floor(gx);\n const y = Math.floor(gy);\n return { x, y, px: (gx - x) * size, py: (gy - y) * size };\n}\n\n/**\n * The height at a pixel position, bilinear between the four pixel centres around it. At a tile's\n * edge the nearest pixels stand in for the neighbour's: a few metres of error, never a hole.\n */\nexport function sampleTile(tile: TerrainTile, px: number, py: number): number {\n const size = tile.size;\n const fx = Math.min(Math.max(px - 0.5, 0), size - 1);\n const fy = Math.min(Math.max(py - 0.5, 0), size - 1);\n const x0 = Math.floor(fx);\n const y0 = Math.floor(fy);\n const x1 = Math.min(x0 + 1, size - 1);\n const y1 = Math.min(y0 + 1, size - 1);\n const at = (x: number, y: number) => {\n const i = (y * size + x) * 4;\n return terrarium(tile.data[i]!, tile.data[i + 1]!, tile.data[i + 2]!);\n };\n const tx = fx - x0;\n const ty = fy - y0;\n const top = at(x0, y0) * (1 - tx) + at(x1, y0) * tx;\n const bottom = at(x0, y1) * (1 - tx) + at(x1, y1) * tx;\n return top * (1 - ty) + bottom * ty;\n}\n\n/** Loads one terrain tile, null where the terrain has none. */\nexport type TileLoader = (z: number, x: number, y: number) => Promise<TerrainTile | null>;\n\n/**\n * The ground under every sample of every ray, each read at its distance's zoom. Each tile is\n * loaded once however many samples fall in it, all of them at once. `home` is read at the finest\n * zoom and answered first.\n */\nexport async function groundHeights(home: LngLat, rays: ViewRay[], load: TileLoader): Promise<{ ground: number | null; perRay: (number | null)[][] }> {\n const wanted = new globalThis.Map<string, Promise<TerrainTile | null>>();\n const read = (lngLat: LngLat, metres: number) => {\n const z = zoomFor(metres);\n const at = tilePixel(lngLat[0], lngLat[1], z, 512);\n const id = `${z}/${at.x}/${at.y}`;\n if (!wanted.has(id)) wanted.set(id, load(z, at.x, at.y));\n return { id, at };\n };\n const homeRead = read(home, 0);\n const rayReads = rays.map((ray) => ray.points.map((point, index) => read(point, ray.distances[index]!)));\n const ids = [...wanted.keys()];\n const loaded = await Promise.all(ids.map((id) => wanted.get(id)!));\n const tiles = new globalThis.Map(ids.map((id, index) => [id, loaded[index]!]));\n const height = ({ id, at }: { id: string; at: { px: number; py: number } }) => {\n const tile = tiles.get(id);\n if (!tile) return null;\n const scale = tile.size / 512;\n return sampleTile(tile, at.px * scale, at.py * scale);\n };\n return { ground: height(homeRead), perRay: rayReads.map((reads) => reads.map(height)) };\n}\n\n/** The share of the fan that reaches the mountains, 0 to 1. */\nexport function viewShare(results: RayResult[]): number {\n const read = results.filter((result) => result.horizon !== null);\n return read.length === 0 ? 0 : read.filter((result) => result.clear).length / read.length;\n}\n\n/**\n * The widest run of blocked lines in the fan: its middle azimuth, and the median distance to\n * what blocks it. Null when nothing is blocked.\n */\nexport function widestBlock(results: RayResult[]): { azimuth: number; distance: number } | null {\n let best: RayResult[] = [];\n let run: RayResult[] = [];\n for (const result of results) {\n if (result.horizon && !result.clear) run.push(result);\n else run = [];\n if (run.length > best.length) best = [...run];\n }\n if (best.length === 0) return null;\n const distances = best.map((result) => result.horizon!.distance).sort((a, b) => a - b);\n return { azimuth: (best[0]!.azimuth + best[best.length - 1]!.azimuth) / 2, distance: distances[Math.floor(distances.length / 2)]! };\n}\n"],"mappings":"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","names":["height"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unmap/elevation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Typed client for the unmap DEM elevation API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./view": {
|
|
32
|
+
"types": "./dist/view.d.ts",
|
|
33
|
+
"import": "./dist/view.js"
|
|
30
34
|
}
|
|
31
35
|
},
|
|
32
36
|
"files": [
|
|
@@ -37,12 +41,12 @@
|
|
|
37
41
|
"access": "public"
|
|
38
42
|
},
|
|
39
43
|
"dependencies": {
|
|
40
|
-
"@unmap/core": "0.
|
|
44
|
+
"@unmap/core": "0.32.0"
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|
|
43
47
|
"tsup": "^8.3.0",
|
|
44
48
|
"typescript": "^5.6.0",
|
|
45
|
-
"vitest": "^
|
|
49
|
+
"vitest": "^4.1.12"
|
|
46
50
|
},
|
|
47
51
|
"scripts": {
|
|
48
52
|
"build": "tsup",
|