pmtiles-swarm 0.99.2 → 0.100.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 +26 -0
- package/docs/tile-stacks.md +22 -0
- package/package.json +1 -1
- package/src/elevation.js +41 -0
- package/src/stack-tile.js +69 -3
- package/src/stacks.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.100.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **A nested stack takes `maskColors`, and fades into what is under it.** Two things a stack could
|
|
13
|
+
not do that every other kind of source could, both for the same reason: it is evaluated rather
|
|
14
|
+
than stored, so it has no bytes. That is a fact about storage, not about meaning, and it was
|
|
15
|
+
showing up in the recipe as a source with a different set of options.
|
|
16
|
+
|
|
17
|
+
A colour is now decoded into the height it names, under the encoding the inner stack packs its
|
|
18
|
+
own output in, and masked as a height — through `decodeHeights` rather than arithmetic written a
|
|
19
|
+
second time. `maskColors: ["#0186a0"]` and `maskValues: [0]` say the same thing to a
|
|
20
|
+
mapbox-encoded stack, so a source keeps its mask when it is swapped between an archive and a
|
|
21
|
+
stack. `encoding`, `baseVal`, `interval` and the custom factors stay refused: those describe how
|
|
22
|
+
to unpack channels into a number, and the number arrived already made.
|
|
23
|
+
|
|
24
|
+
Feathering a nested source now works rather than silently doing nothing. A ramp is measured in
|
|
25
|
+
pixels and the pixels that say how far a hole reaches are partly in the next tile — which for an
|
|
26
|
+
archive means reading its parent and for a stack means evaluating it again, which was never
|
|
27
|
+
implemented. Validation also stopped asking a nested source for a mask before it would accept a
|
|
28
|
+
feather: its holes are already an edge, and the mask being demanded would have made a second one.
|
|
29
|
+
|
|
30
|
+
The representation is unchanged. A nested stack is still merged as heights, because a hole is
|
|
31
|
+
`NaN` and no encoding has one — encoding it would turn every hole into a sentinel the recipe
|
|
32
|
+
above had to mask back out, which is the problem nesting avoids by construction.
|
|
33
|
+
|
|
34
|
+
### 🐞 Bug fixes
|
|
35
|
+
|
|
10
36
|
## 0.99.2
|
|
11
37
|
### ✨ Features and improvements
|
|
12
38
|
|
package/docs/tile-stacks.md
CHANGED
|
@@ -55,6 +55,7 @@ of its parts.
|
|
|
55
55
|
- [What a mask has to match](#what-a-mask-has-to-match)
|
|
56
56
|
- [Feathering a seam](#feathering-a-seam)
|
|
57
57
|
- [A stack as a source](#a-stack-as-a-source)
|
|
58
|
+
- [A colour is a height said another way](#a-colour-is-a-height-said-another-way)
|
|
58
59
|
- [A source read straight from a URL](#a-source-read-straight-from-a-url)
|
|
59
60
|
- [Importing a list of URLs](#importing-a-list-of-urls)
|
|
60
61
|
- [Exporting on a schedule](#exporting-on-a-schedule)
|
|
@@ -1539,6 +1540,27 @@ height mask has to round; a stack that was never stored has no such channels to
|
|
|
1539
1540
|
compare, and masking the heights it decoded to would be a different operation
|
|
1540
1541
|
wearing the same name.
|
|
1541
1542
|
|
|
1543
|
+
### A colour is a height said another way
|
|
1544
|
+
|
|
1545
|
+
`maskColors` compares the bytes a source was stored as, and a nested stack has
|
|
1546
|
+
none — it is evaluated, so what arrives is already metres. Refusing the field
|
|
1547
|
+
there would leave one kind of source unable to say a thing every other kind can,
|
|
1548
|
+
for a reason that is about storage rather than about meaning.
|
|
1549
|
+
|
|
1550
|
+
So the colour is decoded into the height it names, under the encoding the inner
|
|
1551
|
+
stack packs its own output in, and masked as a height. Through `decodeHeights`
|
|
1552
|
+
rather than arithmetic written a second time: it is the same question the merge
|
|
1553
|
+
asks of every pixel, and a second copy of it is a second thing to keep in step
|
|
1554
|
+
with `terrarium` and the custom factors.
|
|
1555
|
+
|
|
1556
|
+
`maskColors: ["#0186a0"]` and `maskValues: [0]` therefore say the same thing to
|
|
1557
|
+
a mapbox-encoded stack, which is what lets a source keep its mask when it is
|
|
1558
|
+
swapped between an archive and a stack.
|
|
1559
|
+
|
|
1560
|
+
What stays refused is the rest of that family — `encoding`, `baseVal`,
|
|
1561
|
+
`interval` and the custom factors. Those describe how to unpack channels into a
|
|
1562
|
+
number, and there are no channels: the number arrived already made.
|
|
1563
|
+
|
|
1542
1564
|
### Nothing passes through
|
|
1543
1565
|
|
|
1544
1566
|
The short-circuit that hands back a source's own bytes cannot apply: there are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.100.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
|
@@ -187,6 +187,42 @@ export function rangesOf(maskRange) {
|
|
|
187
187
|
return out;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The heights a list of colours stands for, under one encoding.
|
|
192
|
+
*
|
|
193
|
+
* `maskColors` compares the bytes a source was stored as, which a nested stack
|
|
194
|
+
* has none of: it is evaluated, so what arrives is already metres. Rather than
|
|
195
|
+
* refuse the field there -- leaving one kind of source unable to say a thing
|
|
196
|
+
* every other kind can -- the colour is decoded into the height it names, and
|
|
197
|
+
* the mask is applied as a height like any other.
|
|
198
|
+
*
|
|
199
|
+
* Through `decodeHeights` rather than arithmetic written again here. It is the
|
|
200
|
+
* same question the merge asks of every pixel, and a second implementation of
|
|
201
|
+
* it is a second thing to keep in step with `terrarium` and the custom factors.
|
|
202
|
+
* @param {Array<string|number[]>} [colours] - What the recipe named.
|
|
203
|
+
* @param {object} [encoding] - `encoding`, and the factors a custom one needs.
|
|
204
|
+
* @returns {number[]|undefined} - Those heights, or undefined for no colours.
|
|
205
|
+
*/
|
|
206
|
+
export function heightsForColors(colours, encoding = {}) {
|
|
207
|
+
const packed = (colours ?? [])
|
|
208
|
+
.map((colour) => parseColor(colour))
|
|
209
|
+
.filter((value) => value !== null);
|
|
210
|
+
if (!packed.length) return undefined;
|
|
211
|
+
|
|
212
|
+
const data = new Uint8Array(packed.length * 3);
|
|
213
|
+
packed.forEach((value, i) => {
|
|
214
|
+
data[i * 3] = (value >> 16) & 255;
|
|
215
|
+
data[i * 3 + 1] = (value >> 8) & 255;
|
|
216
|
+
data[i * 3 + 2] = value & 255;
|
|
217
|
+
});
|
|
218
|
+
return [
|
|
219
|
+
...decodeHeights(
|
|
220
|
+
{ data, channels: 3, width: packed.length, height: 1 },
|
|
221
|
+
encoding,
|
|
222
|
+
),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
|
|
190
226
|
/**
|
|
191
227
|
* Blanks every height inside a band.
|
|
192
228
|
*
|
|
@@ -719,6 +755,11 @@ export function mergeElevation(contributions, options) {
|
|
|
719
755
|
maskHeights(heights, source.maskValues);
|
|
720
756
|
if (contribution.raster) {
|
|
721
757
|
maskColors(heights, contribution.raster, source.maskColors);
|
|
758
|
+
} else {
|
|
759
|
+
// A nested stack has no bytes to compare a colour against, so the
|
|
760
|
+
// colours it named were decoded into the heights they stand for before
|
|
761
|
+
// this. Same field, same meaning, asked of what there is.
|
|
762
|
+
maskHeights(heights, contribution.colorHeights);
|
|
722
763
|
}
|
|
723
764
|
// A band as well as, not instead of: a source may have a sentinel it names
|
|
724
765
|
// exactly and a range of ground it does not want either.
|
package/src/stack-tile.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
decodeHeights,
|
|
4
4
|
encodeHeights,
|
|
5
5
|
fillNodata,
|
|
6
|
+
heightsForColors,
|
|
6
7
|
maskColors,
|
|
7
8
|
maskHeights,
|
|
8
9
|
maskRanges,
|
|
@@ -504,6 +505,19 @@ async function gather({
|
|
|
504
505
|
source: read.source.source,
|
|
505
506
|
heights: inner.heights,
|
|
506
507
|
width: inner.width,
|
|
508
|
+
// Kept so a feather can measure its ramp past this tile's border. The
|
|
509
|
+
// heights above are this tile only; where the hole continues is a
|
|
510
|
+
// question only the parent can answer, and for a stack that means
|
|
511
|
+
// evaluating it again rather than reading bytes.
|
|
512
|
+
nested: read.source.nested,
|
|
513
|
+
// A colour named against something that was never stored as pixels.
|
|
514
|
+
// Read as the height it stands for, under the encoding the inner
|
|
515
|
+
// stack packs its own output in -- which is the encoding those bytes
|
|
516
|
+
// would have been, had there been any.
|
|
517
|
+
colorHeights: heightsForColors(
|
|
518
|
+
read.source.source?.maskColors,
|
|
519
|
+
read.source.nested?.stack?.output ?? {},
|
|
520
|
+
),
|
|
507
521
|
});
|
|
508
522
|
continue;
|
|
509
523
|
}
|
|
@@ -598,7 +612,17 @@ async function gather({
|
|
|
598
612
|
});
|
|
599
613
|
}
|
|
600
614
|
|
|
601
|
-
await readMaskEdges({
|
|
615
|
+
await readMaskEdges({
|
|
616
|
+
contributions,
|
|
617
|
+
z,
|
|
618
|
+
x,
|
|
619
|
+
y,
|
|
620
|
+
size,
|
|
621
|
+
tiles,
|
|
622
|
+
codec,
|
|
623
|
+
signal,
|
|
624
|
+
cutlines,
|
|
625
|
+
});
|
|
602
626
|
return { contributors, contributions };
|
|
603
627
|
}
|
|
604
628
|
|
|
@@ -640,6 +664,7 @@ async function readMaskEdges({
|
|
|
640
664
|
tiles,
|
|
641
665
|
codec,
|
|
642
666
|
signal,
|
|
667
|
+
cutlines,
|
|
643
668
|
}) {
|
|
644
669
|
if (!codec) return;
|
|
645
670
|
await Promise.all(
|
|
@@ -648,9 +673,13 @@ async function readMaskEdges({
|
|
|
648
673
|
const recipe = contribution.source ?? {};
|
|
649
674
|
// On the source's own grid rather than the output's, because that is
|
|
650
675
|
// what the ramp will be measured on.
|
|
651
|
-
const grid = contribution.raster?.width ?? size;
|
|
676
|
+
const grid = contribution.raster?.width ?? contribution.width ?? size;
|
|
652
677
|
const feather = featherFor(recipe, { z, y, size: grid });
|
|
653
|
-
if (!feather
|
|
678
|
+
if (!feather) return;
|
|
679
|
+
// A nested stack arrives with its holes already in it -- they are where
|
|
680
|
+
// its own sources stopped -- so it has an edge whether or not the recipe
|
|
681
|
+
// above it masks anything. Everything else needs a mask to have made one.
|
|
682
|
+
if (!contribution.nested && !masksAnything(recipe)) return;
|
|
654
683
|
const layout = parentsFor({ z, x, y }, grid, feather);
|
|
655
684
|
if (!layout) return;
|
|
656
685
|
|
|
@@ -658,6 +687,43 @@ async function readMaskEdges({
|
|
|
658
687
|
let parentSize = grid;
|
|
659
688
|
await Promise.all(
|
|
660
689
|
layout.tiles.map(async (parent) => {
|
|
690
|
+
// A nested stack has no bytes to read: what its holes are is the
|
|
691
|
+
// answer to evaluating it, so it is evaluated. One level up and at
|
|
692
|
+
// most four tiles, and only for a source that actually fades --
|
|
693
|
+
// which is what keeps this from being the whole recipe again on
|
|
694
|
+
// every tile.
|
|
695
|
+
if (contribution.nested) {
|
|
696
|
+
const inner = await stackHeights({
|
|
697
|
+
resolved: contribution.nested,
|
|
698
|
+
z: parent.z,
|
|
699
|
+
x: parent.x,
|
|
700
|
+
y: parent.y,
|
|
701
|
+
tiles,
|
|
702
|
+
codec,
|
|
703
|
+
signal,
|
|
704
|
+
size: grid,
|
|
705
|
+
cutlines,
|
|
706
|
+
}).catch(() => null);
|
|
707
|
+
if (!inner?.heights) return;
|
|
708
|
+
|
|
709
|
+
// The masks the recipe above applies to it, on a copy: the ramp
|
|
710
|
+
// has to be measured against the same idea of a hole the merge
|
|
711
|
+
// will use, and stackHeights hands back an array we do not own.
|
|
712
|
+
// Colours included, as the heights they were read into.
|
|
713
|
+
const heights = Float32Array.from(inner.heights);
|
|
714
|
+
maskHeights(heights, recipe.maskValues);
|
|
715
|
+
maskHeights(heights, contribution.colorHeights);
|
|
716
|
+
maskRanges(heights, recipe.maskRange);
|
|
717
|
+
|
|
718
|
+
const flags = new Uint8Array(heights.length);
|
|
719
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
720
|
+
flags[i] = Number.isNaN(heights[i]) ? 0 : 1;
|
|
721
|
+
}
|
|
722
|
+
parentSize = inner.width;
|
|
723
|
+
known.set(`${parent.column},${parent.row}`, flags);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
661
727
|
const tile = await (
|
|
662
728
|
contribution.remote
|
|
663
729
|
? tiles.getRemoteTile(
|
package/src/stacks.js
CHANGED
|
@@ -132,7 +132,6 @@ const NOT_ON_A_NESTED_SOURCE = Object.freeze([
|
|
|
132
132
|
'greenFactor',
|
|
133
133
|
'blueFactor',
|
|
134
134
|
'baseShift',
|
|
135
|
-
'maskColors',
|
|
136
135
|
]);
|
|
137
136
|
|
|
138
137
|
/**
|
|
@@ -292,7 +291,13 @@ export function validateStack(stack) {
|
|
|
292
291
|
// a source that blends into what is under it, and it does nothing at all.
|
|
293
292
|
// A mask is an edge as much as a cutline is -- the hole it leaves is where
|
|
294
293
|
// most of these recipes actually stop.
|
|
294
|
+
//
|
|
295
|
+
// A nested stack always has one. Its holes are where its own sources
|
|
296
|
+
// stopped, and it hands them up unfilled precisely so the recipe above can
|
|
297
|
+
// show through -- so requiring a mask here asked for a second edge on top
|
|
298
|
+
// of the one it already brought.
|
|
295
299
|
const fades =
|
|
300
|
+
source?.stack ||
|
|
296
301
|
source?.cutline ||
|
|
297
302
|
source?.bounds ||
|
|
298
303
|
source?.maskValues?.length ||
|