pmtiles-swarm 0.73.0 β 0.74.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/CHANGELOG.md +46 -0
- package/docs/tile-stacks.md +52 -17
- package/package.json +1 -1
- package/src/elevation.js +42 -0
- package/src/mask-edge.js +118 -0
- package/src/stack-tile.js +95 -0
- package/src/stacks.js +9 -2
- package/src/web/index.html +19 -19
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.74.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **The holes a mask leaves are feathered now, not just a cutline's edge.** `feather` faded a source
|
|
13
|
+
in at its `cutline` or `bounds` and did nothing about `maskValues` or `maskColors` - which is
|
|
14
|
+
where most of these recipes actually stop, and why the field looked like it did nothing.
|
|
15
|
+
|
|
16
|
+
A tile cannot measure that edge alone: the hole may continue past its border and nothing inside it
|
|
17
|
+
says so. Carrying the edge outward, which is the obvious answer, changes the seam by nothing at
|
|
18
|
+
all - measured across four coastline geometries, to the metre - because it only adds holes where
|
|
19
|
+
the edge pixel is already one and the distance is already zero. The tile that gets it wrong is
|
|
20
|
+
the tile with no hole in it, and no rule applied to its own pixels can invent one.
|
|
21
|
+
|
|
22
|
+
So the mask is read from the source's parent, which covers this tile and its three siblings and
|
|
23
|
+
sees past every one of their borders. Four parents cover any tile's surroundings, each is shared
|
|
24
|
+
by four children, and they are read only for a source that both masks and fades. The parent
|
|
25
|
+
supplies the border; the middle is the tile's own pixels, because a ramp measured entirely against
|
|
26
|
+
half-resolution pixels climbs in two-pixel steps, which is the terracing the fade exists to
|
|
27
|
+
remove. A 1000 m drop over a 16 px fade, stepping along the edge two tiles share:
|
|
28
|
+
|
|
29
|
+
coastline crossing at 45Β° 1000 m unfeathered -> 63 m
|
|
30
|
+
crossing steeply 1000 m -> 0 m
|
|
31
|
+
parallel, just past the seam 0 m -> 125 m
|
|
32
|
+
parallel, right on the seam 0 m -> 125 m
|
|
33
|
+
|
|
34
|
+
What is left is bounded by the parent's resolution rather than by the height difference - two
|
|
35
|
+
steps of the ramp, or `2 / feather` of the drop - and shrinks as the feather widens.
|
|
36
|
+
|
|
37
|
+
The field is on every source in the editor now rather than appearing only once a clip is set,
|
|
38
|
+
with a note where a source neither masks nor clips and it would do nothing.
|
|
39
|
+
|
|
40
|
+
## 0.73.1
|
|
41
|
+
### π Bug fixes
|
|
42
|
+
- **The console could not start at all in 0.73.0.** A string literal in the export dialog's output
|
|
43
|
+
summary lost its escape, so the console's module failed to parse - and a module that does not
|
|
44
|
+
parse does not run one line of itself. Every page rendered its static markup and then sat on
|
|
45
|
+
"connectingβ¦" for ever, with one `SyntaxError` in the browser console and nothing anywhere else.
|
|
46
|
+
|
|
47
|
+
Nothing was in a position to catch it. eslint does not read HTML, prettier reports the file clean
|
|
48
|
+
whether or not the script inside it is valid, and the tests that do read that script match
|
|
49
|
+
patterns in its text rather than running it. So 320 KB of JavaScript shipped without anything
|
|
50
|
+
having asked whether it parses.
|
|
51
|
+
|
|
52
|
+
`test/web-parse.test.js` now hands every inline script in every page to `node --check` - the same
|
|
53
|
+
engine that will run it, so there is no second grammar to disagree with. Verified against this
|
|
54
|
+
exact fault: the test fails on it and names the line, where prettier and eslint both pass.
|
|
55
|
+
|
|
10
56
|
## 0.73.0
|
|
11
57
|
### β¨ Features and improvements
|
|
12
58
|
- **A source can fade in at the edge of its shape instead of stopping dead.** Where a
|
package/docs/tile-stacks.md
CHANGED
|
@@ -1114,29 +1114,64 @@ The offline merge still has this. `scipy.ndimage.gaussian_filter` defaults to
|
|
|
1114
1114
|
`mode='reflect'` and is handed one tile's array, so an archive built with a
|
|
1115
1115
|
sigma carries the seam.
|
|
1116
1116
|
|
|
1117
|
-
### Where the margin comes from for
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
the
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1117
|
+
### Where the margin comes from for a mask edge
|
|
1118
|
+
|
|
1119
|
+
A cutline is known in full, so the ramp beside a tile costs a few extra
|
|
1120
|
+
rasterised rows. A mask edge is not: it lives in the source's own decoded
|
|
1121
|
+
pixels, and a tile cannot see whether the hole continues past its own border.
|
|
1122
|
+
|
|
1123
|
+
That is not a boundary condition to be chosen well, which is the first thing
|
|
1124
|
+
worth writing down because it looks like one. Carrying the edge outward β the
|
|
1125
|
+
obvious fix β adds holes only where the edge pixel is already a hole, and there
|
|
1126
|
+
the distance is already zero. Measured against four coastline geometries it
|
|
1127
|
+
changes the seam by nothing at all, to the metre. The tile that gets it wrong is
|
|
1128
|
+
the tile with **no hole in it**, and no rule applied to its own pixels can
|
|
1129
|
+
invent one.
|
|
1130
|
+
|
|
1131
|
+
So the mask is read from the source's **parent**, which covers this tile and its
|
|
1132
|
+
three siblings and therefore sees past every one of their borders. Four parents
|
|
1133
|
+
cover any tile's surroundings, each is shared by four children, and they are
|
|
1134
|
+
read only for a source that both masks and fades.
|
|
1135
|
+
|
|
1136
|
+
The parent supplies the border only. Its pixels are half this tile's, so a ramp
|
|
1137
|
+
measured entirely against them climbs in two-pixel steps β terracing, which is
|
|
1138
|
+
the artefact the fade exists to remove β and the middle is overwritten with the
|
|
1139
|
+
tile's own pixels, which are exact.
|
|
1140
|
+
|
|
1141
|
+
A 1000 m drop faded over 16 px, measuring the step along the edge two tiles
|
|
1142
|
+
share:
|
|
1143
|
+
|
|
1144
|
+
| coastline at the seam | no fade | tile alone | with parents |
|
|
1145
|
+
| ---------------------------- | ------- | ---------- | ------------ |
|
|
1146
|
+
| crossing at 45Β° | 1000 m | 1000 m | 63 m |
|
|
1147
|
+
| crossing steeply | 1000 m | 938 m | 0 m |
|
|
1148
|
+
| parallel, just past the seam | 0 m | 438 m | 125 m |
|
|
1149
|
+
| parallel, on the seam | 0 m | 938 m | 125 m |
|
|
1150
|
+
|
|
1151
|
+
"tile alone" is the version that measures inside the tile and is why it is not
|
|
1152
|
+
the version that shipped: it moves the wall off the coastline, where it is at
|
|
1153
|
+
least geographically meaningful, and onto the tile grid.
|
|
1154
|
+
|
|
1155
|
+
What is left is bounded by the parent's resolution rather than by the height
|
|
1156
|
+
difference β two steps of the ramp, or `2 / feather` of the drop, and it shrinks
|
|
1157
|
+
as the feather widens. Reading the eight neighbours at native resolution instead
|
|
1158
|
+
would make it exact, at nine times the reads and decodes for that source; the
|
|
1159
|
+
extra precision is finer than a sixteen-pixel ramp can show.
|
|
1160
|
+
|
|
1161
|
+
**A source that vanishes at a tile edge is still open.** A sparse archive has no
|
|
1162
|
+
tile outside its extent, so the seam lands exactly on a tile boundary. It is the
|
|
1163
|
+
cheapest of the three to detect β whether a neighbour exists is a directory
|
|
1164
|
+
lookup rather than a decode β and it is not done.
|
|
1131
1165
|
|
|
1132
1166
|
### What it does
|
|
1133
1167
|
|
|
1134
1168
|
`feather` on a source, in pixels, `0` and absent meaning the edge is a switch as
|
|
1135
|
-
before. It fades that source in over that many pixels measured inward from
|
|
1136
|
-
|
|
1169
|
+
before. It fades that source in over that many pixels measured inward from
|
|
1170
|
+
wherever it stops β the holes `maskValues` and `maskColors` leave, and the edge
|
|
1171
|
+
of a `cutline` or `bounds`:
|
|
1137
1172
|
|
|
1138
1173
|
```json
|
|
1139
|
-
{ "archive": "swissalti", "
|
|
1174
|
+
{ "archive": "swissalti", "maskValues": [0], "feather": 16 }
|
|
1140
1175
|
```
|
|
1141
1176
|
|
|
1142
1177
|
The step left at the seam is the height difference divided by the feather, which
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/elevation.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cropMask, featherMask } from './cutline.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Merging terrain tiles, in metres rather than in pixels.
|
|
3
5
|
*
|
|
@@ -691,6 +693,46 @@ export function mergeElevation(contributions, options) {
|
|
|
691
693
|
if (contribution.coverage[i] <= 0) heights[i] = Number.NaN;
|
|
692
694
|
}
|
|
693
695
|
}
|
|
696
|
+
|
|
697
|
+
// The edge a mask left, which is the one most of these recipes have.
|
|
698
|
+
// Measured on a grid that reaches past this tile, because the pixels that
|
|
699
|
+
// decide how far a hole is may be in the next one -- `gather` reads the
|
|
700
|
+
// source's parents for exactly this. Without them there is nothing to
|
|
701
|
+
// measure against and the edge stays as it was, rather than being faded
|
|
702
|
+
// against a hole this tile cannot see.
|
|
703
|
+
if (contribution.neighbourhood) {
|
|
704
|
+
const { known, margin } = contribution.neighbourhood;
|
|
705
|
+
const side = size + margin * 2;
|
|
706
|
+
|
|
707
|
+
// The parent is half this tile's resolution, so a ramp measured entirely
|
|
708
|
+
// against it climbs in two-pixel steps -- terracing, which is the
|
|
709
|
+
// artefact this whole feature exists to remove. So the parent supplies
|
|
710
|
+
// the border only, and the middle is overwritten with this tile's own
|
|
711
|
+
// pixels, which are exact. Distances inside the tile are what the ramp
|
|
712
|
+
// is mostly made of; only the last few pixels of its reach are
|
|
713
|
+
// approximated, and there a pixel of error is a pixel of a sixteen-pixel
|
|
714
|
+
// fade.
|
|
715
|
+
const grid = Uint8Array.from(known);
|
|
716
|
+
for (let row = 0; row < size; row += 1) {
|
|
717
|
+
const from = row * size;
|
|
718
|
+
const to = (row + margin) * side + margin;
|
|
719
|
+
for (let column = 0; column < size; column += 1) {
|
|
720
|
+
grid[to + column] = Number.isNaN(heights[from + column]) ? 0 : 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const ramp = cropMask(featherMask(grid, side, margin), size, margin);
|
|
725
|
+
// The narrower of the two where a cutline has ramped as well: each is a
|
|
726
|
+
// separate statement about how much of this source belongs here, and
|
|
727
|
+
// both have to hold.
|
|
728
|
+
if (contribution.coverage) {
|
|
729
|
+
for (let i = 0; i < ramp.length; i += 1) {
|
|
730
|
+
ramp[i] = Math.min(ramp[i], contribution.coverage[i]);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
contribution.coverage = ramp;
|
|
734
|
+
}
|
|
735
|
+
|
|
694
736
|
return heights;
|
|
695
737
|
});
|
|
696
738
|
|
package/src/mask-edge.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edge a mask leaves, measured across the tile boundary.
|
|
3
|
+
*
|
|
4
|
+
* `maskValues` and `maskColors` turn a source's nodata into holes, and where a
|
|
5
|
+
* high-resolution source ends the merge steps straight down to whatever is
|
|
6
|
+
* underneath. Feathering that edge means knowing how far each pixel is from
|
|
7
|
+
* the hole β and a tile cannot answer that on its own, because the hole may
|
|
8
|
+
* continue past its border and nothing inside the tile says so.
|
|
9
|
+
*
|
|
10
|
+
* That is not a boundary condition to be chosen well. Carrying the edge
|
|
11
|
+
* outward adds holes only where the edge pixel is already one, where the
|
|
12
|
+
* distance is already zero, so it changes nothing β measured, to the metre.
|
|
13
|
+
* The tile that gets it wrong is the tile with no hole in it at all, and no
|
|
14
|
+
* rule applied to its own pixels can invent one.
|
|
15
|
+
*
|
|
16
|
+
* So the mask is read from the source's **parent**, which covers this tile and
|
|
17
|
+
* its three siblings and therefore sees past every one of their borders. Four
|
|
18
|
+
* parents cover any tile's surroundings, and each is shared by four children.
|
|
19
|
+
*
|
|
20
|
+
* The parent supplies the border only. Its pixels are half this tile's, so a
|
|
21
|
+
* ramp measured entirely against them climbs in two-pixel steps -- terracing,
|
|
22
|
+
* which is the artefact the fade exists to remove -- and the middle is
|
|
23
|
+
* overwritten with the tile's own. What is left is a seam bounded by the
|
|
24
|
+
* parent's resolution rather than by the height difference: two steps of the
|
|
25
|
+
* ramp, against the whole drop unfeathered. See docs/tile-stacks.md --
|
|
26
|
+
* "Feathering a seam".
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Which parent tiles cover this tile and the border around it.
|
|
31
|
+
*
|
|
32
|
+
* At most four: the tile occupies one quadrant of its parent, so a border
|
|
33
|
+
* around it reaches past two of that parent's sides and into the two
|
|
34
|
+
* neighbours and the one diagonal.
|
|
35
|
+
* @param {object} tile - z, x, y of the tile being built.
|
|
36
|
+
* @param {number} size - Pixels per side of that tile.
|
|
37
|
+
* @param {number} feather - The border wanted, in those pixels.
|
|
38
|
+
* @returns {object|null} - `{z, tiles, west, north, per}`, or null at z0.
|
|
39
|
+
*/
|
|
40
|
+
export function parentsFor(tile, size, feather) {
|
|
41
|
+
const z = tile.z - 1;
|
|
42
|
+
if (z < 0 || !(feather > 0)) return null;
|
|
43
|
+
|
|
44
|
+
// In units of one parent tile: this tile is half of one a side, and the
|
|
45
|
+
// border is a fraction of that again.
|
|
46
|
+
const per = feather / (2 * size);
|
|
47
|
+
const west = tile.x / 2 - per;
|
|
48
|
+
const east = (tile.x + 1) / 2 + per;
|
|
49
|
+
const north = tile.y / 2 - per;
|
|
50
|
+
const south = (tile.y + 1) / 2 + per;
|
|
51
|
+
|
|
52
|
+
const span = 2 ** z;
|
|
53
|
+
const tiles = [];
|
|
54
|
+
for (let row = Math.floor(north); row <= Math.floor(south); row += 1) {
|
|
55
|
+
for (
|
|
56
|
+
let column = Math.floor(west);
|
|
57
|
+
column <= Math.floor(east);
|
|
58
|
+
column += 1
|
|
59
|
+
) {
|
|
60
|
+
// Off the top or bottom of the world there is no tile and never was.
|
|
61
|
+
// Off the side there is: the map wraps, and a source that covers the
|
|
62
|
+
// antimeridian covers both halves of it.
|
|
63
|
+
if (row < 0 || row >= span) continue;
|
|
64
|
+
tiles.push({
|
|
65
|
+
z,
|
|
66
|
+
x: ((column % span) + span) % span,
|
|
67
|
+
y: row,
|
|
68
|
+
column,
|
|
69
|
+
row,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { z, tiles, west, north, per };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A padded grid saying where the source has data, at the output's resolution.
|
|
78
|
+
*
|
|
79
|
+
* Sampled nearest from the parents rather than interpolated: this is a yes or
|
|
80
|
+
* no about each pixel, and a half-yes between a hole and the ground beside it
|
|
81
|
+
* is not a smaller hole, it is a wrong answer about which one the pixel is.
|
|
82
|
+
* @param {object} layout - What `parentsFor` returned.
|
|
83
|
+
* @param {Map} known - Parent key `"x,y"` to a Uint8Array, 1 where it has data.
|
|
84
|
+
* @param {number} size - Pixels per side of the tile being built.
|
|
85
|
+
* @param {number} feather - The border, in those pixels.
|
|
86
|
+
* @param {number} parentSize - Pixels per side of a parent.
|
|
87
|
+
* @returns {Uint8Array} - `size + 2 * feather` a side, 1 where there is data.
|
|
88
|
+
*/
|
|
89
|
+
export function paddedKnown(layout, known, size, feather, parentSize) {
|
|
90
|
+
const side = size + feather * 2;
|
|
91
|
+
const out = new Uint8Array(side * side);
|
|
92
|
+
// One output pixel is this much of a parent tile.
|
|
93
|
+
const step = 1 / (2 * size);
|
|
94
|
+
|
|
95
|
+
for (let row = 0; row < side; row += 1) {
|
|
96
|
+
// The middle of the pixel, in parent-tile units.
|
|
97
|
+
const y = layout.north + (row + 0.5) * step;
|
|
98
|
+
const parentRow = Math.floor(y);
|
|
99
|
+
for (let column = 0; column < side; column += 1) {
|
|
100
|
+
const x = layout.west + (column + 0.5) * step;
|
|
101
|
+
const parentColumn = Math.floor(x);
|
|
102
|
+
const grid = known.get(`${parentColumn},${parentRow}`);
|
|
103
|
+
// A parent nothing could be read from is ground this source does not
|
|
104
|
+
// cover, which is what a hole means anyway.
|
|
105
|
+
if (!grid) continue;
|
|
106
|
+
const px = Math.min(
|
|
107
|
+
parentSize - 1,
|
|
108
|
+
Math.floor((x - parentColumn) * parentSize),
|
|
109
|
+
);
|
|
110
|
+
const py = Math.min(
|
|
111
|
+
parentSize - 1,
|
|
112
|
+
Math.floor((y - parentRow) * parentSize),
|
|
113
|
+
);
|
|
114
|
+
out[row * side + column] = grid[py * parentSize + px];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
package/src/stack-tile.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
assembleChildren,
|
|
3
|
+
decodeHeights,
|
|
3
4
|
encodeHeights,
|
|
4
5
|
fillNodata,
|
|
6
|
+
maskColors,
|
|
7
|
+
maskHeights,
|
|
5
8
|
mergeElevation,
|
|
6
9
|
} from './elevation.js';
|
|
10
|
+
import { paddedKnown, parentsFor } from './mask-edge.js';
|
|
7
11
|
import {
|
|
8
12
|
INSIDE,
|
|
9
13
|
OUTSIDE,
|
|
@@ -428,14 +432,105 @@ async function gather({ reads, z, x, y, tiles, codec, signal, size, rgba }) {
|
|
|
428
432
|
|
|
429
433
|
contributions.push({
|
|
430
434
|
source: read.source.source,
|
|
435
|
+
// Carried so a feathered source can go back for its parents, which is
|
|
436
|
+
// the one thing the merge needs that is not already in this raster.
|
|
437
|
+
infoHash: read.source.entry?.infoHash,
|
|
431
438
|
parentZ: read.found.parentZ,
|
|
432
439
|
raster,
|
|
433
440
|
});
|
|
434
441
|
}
|
|
435
442
|
|
|
443
|
+
await readMaskEdges({ contributions, z, x, y, size, tiles, codec, signal });
|
|
436
444
|
return { contributors, contributions };
|
|
437
445
|
}
|
|
438
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Whether a source's recipe leaves holes a feather would have to fade.
|
|
449
|
+
* @param {object} recipe - One source out of a recipe.
|
|
450
|
+
* @returns {boolean} - True when it masks anything.
|
|
451
|
+
*/
|
|
452
|
+
function masksAnything(recipe) {
|
|
453
|
+
return Boolean(recipe?.maskValues?.length || recipe?.maskColors?.length);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Reads the parents a feathered source needs to see past its own tile.
|
|
458
|
+
*
|
|
459
|
+
* Only for a source that both fades and masks. A cutline's ramp is measured
|
|
460
|
+
* from a shape this node holds in full and needs nothing read; a mask's is
|
|
461
|
+
* measured from pixels, and the pixels that decide it are partly in the
|
|
462
|
+
* neighbouring tiles. See src/mask-edge.js for why the parent rather than
|
|
463
|
+
* those neighbours.
|
|
464
|
+
*
|
|
465
|
+
* A parent that cannot be read is left out rather than failing the tile. It
|
|
466
|
+
* means the ramp is measured against less than it could be, which is a slightly
|
|
467
|
+
* wrong edge -- and a tile that will not draw at all is worse than one whose
|
|
468
|
+
* coastline fades over fifteen pixels instead of sixteen.
|
|
469
|
+
* @param {object} options - The contributions, where they are, and what to read with.
|
|
470
|
+
* @returns {Promise<void>} - Resolves once every feathered source has its grid.
|
|
471
|
+
*/
|
|
472
|
+
async function readMaskEdges({
|
|
473
|
+
contributions,
|
|
474
|
+
z,
|
|
475
|
+
x,
|
|
476
|
+
y,
|
|
477
|
+
size,
|
|
478
|
+
tiles,
|
|
479
|
+
codec,
|
|
480
|
+
signal,
|
|
481
|
+
}) {
|
|
482
|
+
if (!codec) return;
|
|
483
|
+
await Promise.all(
|
|
484
|
+
contributions.map(async (contribution) => {
|
|
485
|
+
if (!contribution) return;
|
|
486
|
+
const recipe = contribution.source ?? {};
|
|
487
|
+
const feather = featherFor(recipe);
|
|
488
|
+
if (!feather || !masksAnything(recipe)) return;
|
|
489
|
+
|
|
490
|
+
const grid = contribution.raster?.width ?? size;
|
|
491
|
+
const layout = parentsFor({ z, x, y }, grid, feather);
|
|
492
|
+
if (!layout) return;
|
|
493
|
+
|
|
494
|
+
const known = new Map();
|
|
495
|
+
let parentSize = grid;
|
|
496
|
+
await Promise.all(
|
|
497
|
+
layout.tiles.map(async (parent) => {
|
|
498
|
+
const tile = await tiles
|
|
499
|
+
.getTile(contribution.infoHash, parent.z, parent.x, parent.y, {
|
|
500
|
+
signal,
|
|
501
|
+
})
|
|
502
|
+
.catch(() => null);
|
|
503
|
+
if (!tile?.data) return;
|
|
504
|
+
const raster = await codec
|
|
505
|
+
.decode(Buffer.from(tile.data), { channels: 3 })
|
|
506
|
+
.catch(() => null);
|
|
507
|
+
if (!raster) return;
|
|
508
|
+
|
|
509
|
+
// The same two masks the merge applies, asked of the parent. A ramp
|
|
510
|
+
// measured against a different idea of where the holes are would fade
|
|
511
|
+
// toward ground that is not a hole.
|
|
512
|
+
const heights = decodeHeights(raster, recipe);
|
|
513
|
+
maskHeights(heights, recipe.maskValues);
|
|
514
|
+
maskColors(heights, raster, recipe.maskColors);
|
|
515
|
+
|
|
516
|
+
const flags = new Uint8Array(heights.length);
|
|
517
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
518
|
+
flags[i] = Number.isNaN(heights[i]) ? 0 : 1;
|
|
519
|
+
}
|
|
520
|
+
parentSize = raster.width;
|
|
521
|
+
known.set(`${parent.column},${parent.row}`, flags);
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (known.size === 0) return;
|
|
526
|
+
contribution.neighbourhood = {
|
|
527
|
+
known: paddedKnown(layout, known, grid, feather, parentSize),
|
|
528
|
+
margin: feather,
|
|
529
|
+
};
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
439
534
|
/**
|
|
440
535
|
* Merges what the sources gave into one encoded tile.
|
|
441
536
|
* @param {object} options - Everything the merge needs.
|
package/src/stacks.js
CHANGED
|
@@ -137,9 +137,16 @@ export function validateStack(stack) {
|
|
|
137
137
|
}
|
|
138
138
|
// A fade with no edge to fade at. Refused rather than ignored: it reads as
|
|
139
139
|
// a source that blends into what is under it, and it does nothing at all.
|
|
140
|
-
|
|
140
|
+
// A mask is an edge as much as a cutline is -- the hole it leaves is where
|
|
141
|
+
// most of these recipes actually stop.
|
|
142
|
+
const fades =
|
|
143
|
+
source?.cutline ||
|
|
144
|
+
source?.bounds ||
|
|
145
|
+
source?.maskValues?.length ||
|
|
146
|
+
source?.maskColors?.length;
|
|
147
|
+
if (source?.feather && !fades) {
|
|
141
148
|
problems.push(
|
|
142
|
-
`sources[${index}].feather needs a cutline or
|
|
149
|
+
`sources[${index}].feather needs a cutline, bounds, or a mask to fade at`,
|
|
143
150
|
);
|
|
144
151
|
}
|
|
145
152
|
if (source?.encoding === 'custom' && !hasUsableEncoding(source)) {
|
package/src/web/index.html
CHANGED
|
@@ -7672,19 +7672,23 @@ Every piece is hashed against the ` +
|
|
|
7672
7672
|
</label>`
|
|
7673
7673
|
: ''
|
|
7674
7674
|
}
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7675
|
+
<label class="choice"
|
|
7676
|
+
title="Fade this source in over this many pixels wherever it stops β the holes a mask leaves, and the edge of a cutline or box. Where a high-resolution source meets a coarser one the two disagree, by their survey and by their vertical datum, and the step between them reads as a wall under a hillshade. The drop is divided by this: 40 m across an 8 px fade is 5 m a pixel. 0 is a hard edge.">
|
|
7677
|
+
Fade in
|
|
7678
|
+
<input type="number" min="0" max="64" step="1" style="width:4.5rem"
|
|
7679
|
+
placeholder="0"
|
|
7680
|
+
data-stack-field="feather" data-stack-index="${index}"
|
|
7681
|
+
value="${source.feather ?? ''}" />
|
|
7682
|
+
<span class="sub">px${
|
|
7683
|
+
source.feather &&
|
|
7684
|
+
!source.cutline &&
|
|
7685
|
+
!source.bounds &&
|
|
7686
|
+
!(source.maskValues ?? []).length &&
|
|
7687
|
+
!(source.maskColors ?? []).length
|
|
7688
|
+
? ' β nothing to fade at yet: mask something, or clip it'
|
|
7689
|
+
: ''
|
|
7690
|
+
}</span>
|
|
7691
|
+
</label>
|
|
7688
7692
|
${
|
|
7689
7693
|
source.cutline && !(stackCutlines ?? []).includes(source.cutline)
|
|
7690
7694
|
? `<span class="bad">no cutline called
|
|
@@ -7767,10 +7771,6 @@ Every piece is hashed against the ` +
|
|
|
7767
7771
|
if (source) {
|
|
7768
7772
|
delete source.cutline;
|
|
7769
7773
|
delete source.bounds;
|
|
7770
|
-
// A fade needs an edge to fade at, and the recipe validation says
|
|
7771
|
-
// so -- leaving it behind would make the source unsaveable with
|
|
7772
|
-
// nothing on screen to explain why.
|
|
7773
|
-
if (!event.target.value) delete source.feather;
|
|
7774
7774
|
if (event.target.value === '__bounds') {
|
|
7775
7775
|
source.bounds = [-180, -85, 180, 85];
|
|
7776
7776
|
} else if (event.target.value) {
|
|
@@ -7946,9 +7946,9 @@ Every piece is hashed against the ` +
|
|
|
7946
7946
|
const output = stack?.output ?? {};
|
|
7947
7947
|
const rgba = stack?.space === 'rgba';
|
|
7948
7948
|
const parts = [
|
|
7949
|
-
rgba ? 'rgba' : (output.encoding ??
|
|
7949
|
+
rgba ? 'rgba' : (output.encoding ?? "the sources' encoding"),
|
|
7950
7950
|
output.format ?? "the sources' format",
|
|
7951
|
-
output.tileSize ? `${output.tileSize} px` :
|
|
7951
|
+
output.tileSize ? `${output.tileSize} px` : "the sources' tile size",
|
|
7952
7952
|
];
|
|
7953
7953
|
if (!rgba) {
|
|
7954
7954
|
parts.push(`${stack?.resampling ?? 'bilinear'} resampling`);
|