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 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
 
@@ -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.99.2",
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({ contributions, z, x, y, size, tiles, codec, signal });
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 || !masksAnything(recipe)) return;
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 ||