bruce-cesium 7.2.1 → 7.2.3

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.
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, Color, ColorMaterialProperty, Entity, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, OrthographicFrustum, EasingFunction, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Intersect, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, CustomDataSource, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Carto, Geometry as Geometry$1, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, Bounds, Color as Color$1, DelayQueue, DataLab, BruceApi, EntityRelation, AccountConcept, RecordChangeFeed, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, Math as Math$1, Cartesian3, JulianDate, Quaternion, Transforms, HeadingPitchRoll, Matrix4, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, DistanceDisplayCondition, ArcType, CornerType, ConstantPositionProperty, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, Intersect, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, OrthographicFrustum, EasingFunction, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, CustomDataSource, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Carto, Geometry as Geometry$1, Bounds, Color as Color$1, Api, BruceEvent, EntityCoords, MenuItem, Tileset, ProjectView, ProjectViewBookmark, ZoomControl, DelayQueue, DataLab, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -9054,7 +9054,8 @@ var VisualsRegister;
9054
9054
  entityId: rego.entityId,
9055
9055
  menuItemId: rego.menuItemId,
9056
9056
  doUpdate: false,
9057
- requestRender: false
9057
+ requestRender: false,
9058
+ keepVisual: rego.visual
9058
9059
  });
9059
9060
  }
9060
9061
  const entityId = rego.entityId;
@@ -9347,7 +9348,9 @@ var VisualsRegister;
9347
9348
  EntityLabel.Detatch({
9348
9349
  rego
9349
9350
  });
9350
- if (doRemove != false) {
9351
+ // Never tear down the graphic the caller is registering: the renderers hand back
9352
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9353
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9351
9354
  removeEntity(this.viewer, rego.visual);
9352
9355
  }
9353
9356
  rego.visual = null;
@@ -9401,7 +9404,9 @@ var VisualsRegister;
9401
9404
  EntityLabel.Detatch({
9402
9405
  rego
9403
9406
  });
9404
- if (doRemove != false) {
9407
+ // Never tear down the graphic the caller is registering: the renderers hand back
9408
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9409
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9405
9410
  removeEntity(this.viewer, rego.visual);
9406
9411
  }
9407
9412
  rego.visual = null;
@@ -40283,6 +40288,107 @@ function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
40283
40288
  data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
40284
40289
  }
40285
40290
  }
40291
+ /**
40292
+ * The colour a ramp gives a normalised value.
40293
+ * @param sorted stops in ascending position, normalised into the texture's value range
40294
+ */
40295
+ function SampleRamp(sorted, t) {
40296
+ let upper = 0;
40297
+ while (upper < sorted.length && sorted[upper].position < t) {
40298
+ upper++;
40299
+ }
40300
+ if (upper === 0) {
40301
+ return sorted[0].color;
40302
+ }
40303
+ if (upper >= sorted.length) {
40304
+ return sorted[sorted.length - 1].color;
40305
+ }
40306
+ const a = sorted[upper - 1];
40307
+ const b = sorted[upper];
40308
+ const span = b.position - a.position;
40309
+ // Equal positions are a hard step rather than a division by zero, which is how a band
40310
+ // gets a crisp edge instead of a gradient into its neighbour.
40311
+ const f = span > 0 ? (t - a.position) / span : 1;
40312
+ return {
40313
+ red: a.color.red + (b.color.red - a.color.red) * f,
40314
+ green: a.color.green + (b.color.green - a.color.green) * f,
40315
+ blue: a.color.blue + (b.color.blue - a.color.blue) * f,
40316
+ alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
40317
+ };
40318
+ }
40319
+ /**
40320
+ * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
40321
+ * @param steps entries in the lookup, each covering an equal slice of the value range
40322
+ */
40323
+ function RampLookup(stops, low, high, steps = 256) {
40324
+ const sorted = stops && stops.length > 0
40325
+ ? stops.slice().sort((a, b) => a.position - b.position)
40326
+ : null;
40327
+ const out = new Uint8Array(steps * 4);
40328
+ for (let i = 0; i < steps; i++) {
40329
+ const t = steps > 1 ? i / (steps - 1) : 0;
40330
+ const color = sorted
40331
+ ? SampleRamp(sorted, t)
40332
+ : {
40333
+ red: low.red + (high.red - low.red) * t,
40334
+ green: low.green + (high.green - low.green) * t,
40335
+ blue: low.blue + (high.blue - low.blue) * t,
40336
+ alpha: low.alpha + (high.alpha - low.alpha) * t
40337
+ };
40338
+ out[i * 4] = Math.round(color.red);
40339
+ out[i * 4 + 1] = Math.round(color.green);
40340
+ out[i * 4 + 2] = Math.round(color.blue);
40341
+ out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
40342
+ }
40343
+ return out;
40344
+ }
40345
+ function ApplyGradientStops(imageData, stops) {
40346
+ if (!stops || stops.length === 0) {
40347
+ return;
40348
+ }
40349
+ const sorted = stops.slice().sort((a, b) => a.position - b.position);
40350
+ const data = imageData.data;
40351
+ for (let i = 0; i < data.length; i += 4) {
40352
+ const t = data[i] / 255;
40353
+ const color = SampleRamp(sorted, t);
40354
+ data[i] = Math.round(color.red);
40355
+ data[i + 1] = Math.round(color.green);
40356
+ data[i + 2] = Math.round(color.blue);
40357
+ const srcAlpha = data[i + 3] / 255;
40358
+ data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
40359
+ }
40360
+ }
40361
+ /**
40362
+ * Draws the texture's own cell grid into the tinted pixels.
40363
+ *
40364
+ * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
40365
+ * no per-frame work as the camera moves.
40366
+ */
40367
+ function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
40368
+ if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
40369
+ return;
40370
+ }
40371
+ const { width, height, data } = imageData;
40372
+ const edge = Math.max(1, Math.round(widthTexels));
40373
+ for (let y = 0; y < height; y++) {
40374
+ const onRow = (y % cellTexels) < edge;
40375
+ for (let x = 0; x < width; x++) {
40376
+ if (!onRow && (x % cellTexels) >= edge) {
40377
+ continue;
40378
+ }
40379
+ const i = (y * width + x) * 4;
40380
+ // Only where something is already drawn, so a border cannot invent coverage the data
40381
+ // does not have.
40382
+ if (data[i + 3] === 0) {
40383
+ continue;
40384
+ }
40385
+ const a = color.alpha;
40386
+ data[i] = Math.round(data[i] * (1 - a) + color.red * a);
40387
+ data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
40388
+ data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
40389
+ }
40390
+ }
40391
+ }
40286
40392
  function loadImage(src) {
40287
40393
  return new Promise((res, rej) => {
40288
40394
  const image = new Image();
@@ -40294,26 +40400,317 @@ function loadImage(src) {
40294
40400
 
40295
40401
  var TextureFrameSeriesAnimator;
40296
40402
  (function (TextureFrameSeriesAnimator) {
40403
+ TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
40404
+ // The only generation this renderer draws. Anything else is refused rather than half read: the
40405
+ // shapes differ enough that a partial reading produces a plausible wrong surface, which is worse
40406
+ // than an empty one.
40407
+ TextureFrameSeriesAnimator.SUPPORTED_VERSION = 3;
40408
+ // Value in R at 8 bits, or high in R and low in G at 16.
40409
+ TextureFrameSeriesAnimator.FORMAT_U8 = "u8";
40410
+ TextureFrameSeriesAnimator.FORMAT_RG16 = "rg16";
40411
+ // Declared as a field with one value, so a second mode can be added later without an absent
40412
+ // Encoding having to mean anything.
40413
+ TextureFrameSeriesAnimator.ENCODING_ABSOLUTE = "absolute";
40414
+ TextureFrameSeriesAnimator.LAYER_VALUE = "Value";
40415
+ TextureFrameSeriesAnimator.LAYER_VALUE_MIN = "ValueMin";
40416
+ TextureFrameSeriesAnimator.LAYER_VALUE_MAX = "ValueMax";
40417
+ TextureFrameSeriesAnimator.LAYER_BASELINE = "Baseline";
40418
+ // The per-texel correction that was applied, published only on an archive that was transformed.
40419
+ TextureFrameSeriesAnimator.LAYER_VALUE_SHIFT = "ValueShift";
40420
+ /**
40421
+ * Turns a published value map into the flat shape this renderer works in, or null when it cannot be drawn.
40422
+ * @param published the archive's Data.generation
40423
+ */
40424
+ function Adapt(published) {
40425
+ if (!published || published.Version !== TextureFrameSeriesAnimator.SUPPORTED_VERSION) {
40426
+ const found = published && published.Version != null ? published.Version : "none";
40427
+ const at = published && published.Identity ? published.Identity.Generated : undefined;
40428
+ // In theory should be no files.
40429
+ console.warn("bruce-cesium: value map version " + found + " is not supported (this build reads "
40430
+ + TextureFrameSeriesAnimator.SUPPORTED_VERSION + "), so nothing will be drawn for it."
40431
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40432
+ return null;
40433
+ }
40434
+ const geometry = published.Geometry || {};
40435
+ const tiles = published.Tiles || [];
40436
+ if (geometry.Layout !== TextureFrameSeriesAnimator.LAYOUT_TILES || tiles.length === 0) {
40437
+ const at = published.Identity ? published.Identity.Generated : undefined;
40438
+ console.warn("bruce-cesium: value map declares no tiles, so nothing will be drawn for it."
40439
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40440
+ return null;
40441
+ }
40442
+ const value = published.Value || {};
40443
+ if (value.Encoding && value.Encoding !== TextureFrameSeriesAnimator.ENCODING_ABSOLUTE) {
40444
+ const at = published.Identity ? published.Identity.Generated : undefined;
40445
+ console.warn("bruce-cesium: value map encoding " + value.Encoding + " is not supported"
40446
+ + " (this build reads " + TextureFrameSeriesAnimator.ENCODING_ABSOLUTE + "), so nothing will be drawn for it."
40447
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40448
+ return null;
40449
+ }
40450
+ const series = published.Series || {};
40451
+ const layerRange = declaredRange(published, TextureFrameSeriesAnimator.LAYER_VALUE_MIN);
40452
+ const flat = {
40453
+ Frames: [],
40454
+ ValueMin: value.Min,
40455
+ ValueMax: value.Max,
40456
+ Format: value.Format || TextureFrameSeriesAnimator.FORMAT_U8,
40457
+ Encoding: value.Encoding || TextureFrameSeriesAnimator.ENCODING_ABSOLUTE,
40458
+ Units: value.Units,
40459
+ West: geometry.West,
40460
+ East: geometry.East,
40461
+ South: geometry.South,
40462
+ North: geometry.North,
40463
+ ResolutionX: geometry.ResolutionX,
40464
+ ResolutionY: geometry.ResolutionY,
40465
+ PixelPlacement: geometry.PixelPlacement,
40466
+ Times: series.Times,
40467
+ ExtremesValueMin: layerRange ? layerRange.min : undefined,
40468
+ ExtremesValueMax: layerRange ? layerRange.max : undefined,
40469
+ // The delivered values are what gets drawn. What the source called them, and any shift
40470
+ // applied on the way, travel for labels and analysis rather than for placement.
40471
+ SourceVerticalDatum: published.ValueSource ? published.ValueSource.VerticalDatum : undefined,
40472
+ Transform: published.ValueSource ? published.ValueSource.Transform : undefined,
40473
+ SourceUnits: published.ValueSource ? published.ValueSource.Units : undefined,
40474
+ SourceValueMin: published.ValueSource ? published.ValueSource.Min : undefined,
40475
+ SourceValueMax: published.ValueSource ? published.ValueSource.Max : undefined
40476
+ };
40477
+ flat.Tiles = tiles.map((tile) => adaptTile(tile, series.Times));
40478
+ // The coarsest tile covers the whole extent, so it is what a caller wanting one time list reads.
40479
+ flat.Frames = flat.Tiles[0].Frames;
40480
+ warnIfUnshifted(published, flat);
40481
+ return flat;
40482
+ }
40483
+ TextureFrameSeriesAnimator.Adapt = Adapt;
40484
+ /*
40485
+ * Says so when an archive's heights are not on the ellipsoid they will be drawn against.
40486
+ */
40487
+ function warnIfUnshifted(published, flat) {
40488
+ const datum = flat.SourceVerticalDatum;
40489
+ if (!datum || datum === TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL || flat.Transform) {
40490
+ return;
40491
+ }
40492
+ const at = published.Identity ? published.Identity.Generated : undefined;
40493
+ console.warn("bruce-cesium: value map heights are in " + datum + " and report no transform, so"
40494
+ + " they are not ellipsoidal and will draw at the wrong height when placed absolutely."
40495
+ + (at ? " The archive was generated at " + at + " and needs regenerating with the shift"
40496
+ + " applied." : ""));
40497
+ }
40498
+ /*
40499
+ * The Min and Max a named layer declares, since a statistics layer is encoded over the true data
40500
+ * range rather than over the frames' own.
40501
+ */
40502
+ function declaredRange(published, name) {
40503
+ const layers = published.Layers || [];
40504
+ for (const layer of layers) {
40505
+ if (layer.Name === name && typeof layer.Min === "number" && typeof layer.Max === "number") {
40506
+ return { min: layer.Min, max: layer.Max };
40507
+ }
40508
+ }
40509
+ return null;
40510
+ }
40511
+ /*
40512
+ * One tile, with its per-time value rasters lined up against the shared time list.
40513
+ */
40514
+ function adaptTile(tile, times) {
40515
+ const layers = tile.Layers || {};
40516
+ const perTime = layers[TextureFrameSeriesAnimator.LAYER_VALUE] || [];
40517
+ const frames = perTime.map((entry, index) => ({
40518
+ Timestamp: times && times[index] ? times[index] : "",
40519
+ ByteOffset: entry.Bytes.Offset,
40520
+ ByteLength: entry.Bytes.Length
40521
+ }));
40522
+ return {
40523
+ Level: tile.Level,
40524
+ X: tile.X,
40525
+ Y: tile.Y,
40526
+ West: tile.West,
40527
+ East: tile.East,
40528
+ South: tile.South,
40529
+ North: tile.North,
40530
+ TexelMetres: tile.TexelMetres,
40531
+ ResolutionX: tile.ResolutionX,
40532
+ ResolutionY: tile.ResolutionY,
40533
+ Frames: frames,
40534
+ Floor: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MIN]),
40535
+ Ceiling: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MAX]),
40536
+ BaselineMask: single(layers[TextureFrameSeriesAnimator.LAYER_BASELINE])
40537
+ };
40538
+ }
40539
+ function single(layer) {
40540
+ if (!layer || !layer.Bytes) {
40541
+ return undefined;
40542
+ }
40543
+ return { ByteOffset: layer.Bytes.Offset, ByteLength: layer.Bytes.Length };
40544
+ }
40545
+ /**
40546
+ * How to turn a delivered value into the number the source published, or null when the archive
40547
+ * does not say enough to do it.
40548
+ * @param metadata the archive's Data.generation
40549
+ */
40550
+ function SourceMap(metadata) {
40551
+ if (!metadata) {
40552
+ return null;
40553
+ }
40554
+ const { ValueMin, ValueMax, SourceValueMin, SourceValueMax } = metadata;
40555
+ if (typeof ValueMin !== "number" || typeof ValueMax !== "number"
40556
+ || typeof SourceValueMin !== "number" || typeof SourceValueMax !== "number") {
40557
+ return null;
40558
+ }
40559
+ const delivered = ValueMax - ValueMin;
40560
+ // A single delivered value describes no line, so all that can be recovered is where it sits.
40561
+ const scale = delivered !== 0 ? (SourceValueMax - SourceValueMin) / delivered : 1;
40562
+ return { scale, offset: SourceValueMin - ValueMin * scale };
40563
+ }
40564
+ TextureFrameSeriesAnimator.SourceMap = SourceMap;
40565
+ /**
40566
+ * A delivered value in the source's own terms, or unchanged when it cannot be turned back.
40567
+ * @param map from SourceMap(), null when the archive does not say enough
40568
+ */
40569
+ function ToSource(map, value) {
40570
+ return map ? value * map.scale + map.offset : value;
40571
+ }
40572
+ TextureFrameSeriesAnimator.ToSource = ToSource;
40573
+ /**
40574
+ * Whether the archive's frames carry a 16 bit value packed across R and G.
40575
+ *
40576
+ * Every consumer that reads a texel has to ask: taking R alone from a packed frame yields a
40577
+ * plausible surface quantised to 255 steps of the full range rather than an obvious failure.
40578
+ * @param metadata the archive's Data.generation
40579
+ */
40580
+ function IsPackedValue(metadata) {
40581
+ return Boolean(metadata && metadata.Format === TextureFrameSeriesAnimator.FORMAT_RG16);
40582
+ }
40583
+ TextureFrameSeriesAnimator.IsPackedValue = IsPackedValue;
40584
+ /**
40585
+ * The normalised 0 to 1 value at a texel, from whichever format the archive packed it in.
40586
+ * @param metadata the archive's Data.generation
40587
+ * @param pixels untinted RGBA value pixels
40588
+ * @param at index of the texel's red channel
40589
+ */
40590
+ function NormalisedAt(metadata, pixels, at) {
40591
+ if (IsPackedValue(metadata)) {
40592
+ return (pixels[at] * 256 + pixels[at + 1]) / 65535;
40593
+ }
40594
+ return pixels[at] / 255;
40595
+ }
40596
+ TextureFrameSeriesAnimator.NormalisedAt = NormalisedAt;
40597
+ // Values already sit on the ellipsoid Cesium measures against, so nothing is added to place them.
40598
+ TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL = "WGS84";
40599
+ /**
40600
+ * Whether the values are heights against a vertical datum rather than a thickness.
40601
+ *
40602
+ * A thickness rises from wherever the polygon sits and a height already knows where it belongs, so
40603
+ * this is what decides whether the polygon's own altitude may be added underneath it.
40604
+ * @param metadata the archive's Data.generation
40605
+ */
40606
+ function IsDatumHeight(metadata) {
40607
+ return Boolean(metadata && metadata.SourceVerticalDatum);
40608
+ }
40609
+ TextureFrameSeriesAnimator.IsDatumHeight = IsDatumHeight;
40610
+ /**
40611
+ * The archive's tiles, which Adapt() guarantees are present.
40612
+ * @param metadata the archive's Data.generation
40613
+ */
40614
+ function TilesOf(metadata) {
40615
+ return (metadata && metadata.Tiles) || [];
40616
+ }
40617
+ TextureFrameSeriesAnimator.TilesOf = TilesOf;
40618
+ /**
40619
+ * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
40620
+ *
40621
+ * Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
40622
+ * actually has, and the archive's own published range is what makes that expressible.
40623
+ * @param metadata the archive's Data.generation
40624
+ * @param value in the attribute's units
40625
+ */
40626
+ function NormalisePosition(metadata, value) {
40627
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
40628
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
40629
+ const span = hi - lo;
40630
+ if (!(span > 0)) {
40631
+ return 0;
40632
+ }
40633
+ return Math.min(1, Math.max(0, (value - lo) / span));
40634
+ }
40635
+ TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
40297
40636
  /**
40298
40637
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
40299
40638
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
40300
- * @param data ClientFile.Data (the `IFile.Data` field), as returned by ClientFile.Get().
40639
+ * @param data ClientFile.Data
40301
40640
  */
40302
40641
  function IsFrameArchiveMetadata(data) {
40303
- return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
40642
+ const generation = data && data.generation;
40643
+ if (!generation) {
40644
+ return false;
40645
+ }
40646
+ return (typeof generation.Version === "number" ||
40647
+ (Array.isArray(generation.Frames) && generation.Frames.length > 0));
40304
40648
  }
40305
40649
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
40306
- // How far covered values bleed outward into uncovered texels before the GPU samples them.
40650
+ // First resolvable static number out of a calculator field list, since that is all a border needs.
40651
+ function resolveNumber(fields) {
40652
+ if (!fields) {
40653
+ return 0;
40654
+ }
40655
+ for (const field of fields) {
40656
+ const value = Number(field && field.value);
40657
+ if (Number.isFinite(value)) {
40658
+ return value;
40659
+ }
40660
+ }
40661
+ return 0;
40662
+ }
40663
+ // First resolvable static colour out of a calculator field list.
40664
+ function resolveColor(fields) {
40665
+ if (!fields) {
40666
+ return null;
40667
+ }
40668
+ for (const field of fields) {
40669
+ const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
40670
+ ? Color$1.ColorFromStr(field.value)
40671
+ : null;
40672
+ if (parsed) {
40673
+ return parsed;
40674
+ }
40675
+ }
40676
+ return null;
40677
+ }
40678
+ /*
40679
+ * Everything about an Animator that a style can change, as a comparable string.
40680
+ */
40681
+ function AppearanceSignature(options) {
40682
+ var _a, _b, _c;
40683
+ return JSON.stringify([
40684
+ (_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
40685
+ (_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
40686
+ (_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
40687
+ Boolean(options.maskBaseline)
40688
+ ]);
40689
+ }
40690
+ TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
40691
+ const MAX_COMPOSITE_TEXELS = 2048;
40307
40692
  const VALUE_DILATE_PASSES = 2;
40308
40693
  /*
40309
- * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
40694
+ * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
40310
40695
  *
40311
- * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
40312
- * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
40313
- * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
40314
- * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
40696
+ * All three channels, since a ramp is free to read any of them and a leftover low byte in G would
40697
+ * show up as a green cast on the one that reads them all.
40698
+ */
40699
+ function flattenPacked(pixels, packed) {
40700
+ if (!packed) {
40701
+ return;
40702
+ }
40703
+ for (let i = 0; i < pixels.length; i += 4) {
40704
+ const grey = Math.round((pixels[i] * 256 + pixels[i + 1]) / 65535 * 255);
40705
+ pixels[i] = grey;
40706
+ pixels[i + 1] = grey;
40707
+ pixels[i + 2] = grey;
40708
+ }
40709
+ }
40710
+ /*
40711
+ * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
40315
40712
  */
40316
- function dilateValues(value, width, height, passes) {
40713
+ function dilateValues(value, width, height, passes, packed) {
40317
40714
  const texels = width * height;
40318
40715
  const filled = new Uint8Array(texels);
40319
40716
  for (let p = 0; p < texels; p++) {
@@ -40337,15 +40734,25 @@ var TextureFrameSeriesAnimator;
40337
40734
  if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
40338
40735
  continue;
40339
40736
  }
40340
- sum += source[(ny * width + nx) * 4];
40737
+ const at = (ny * width + nx) * 4;
40738
+ // Averaged as a value rather than per byte: averaging a packed low byte
40739
+ // on its own wraps at every 256th step and speckles the bled edge.
40740
+ sum += packed ? source[at] * 256 + source[at + 1] : source[at];
40341
40741
  hits++;
40342
40742
  }
40343
40743
  }
40344
40744
  if (hits > 0) {
40345
40745
  const v = Math.round(sum / hits);
40346
- value[p * 4] = v;
40347
- value[p * 4 + 1] = v;
40348
- value[p * 4 + 2] = v;
40746
+ if (packed) {
40747
+ value[p * 4] = (v >> 8) & 255;
40748
+ value[p * 4 + 1] = v & 255;
40749
+ value[p * 4 + 2] = 0;
40750
+ }
40751
+ else {
40752
+ value[p * 4] = v;
40753
+ value[p * 4 + 1] = v;
40754
+ value[p * 4 + 2] = v;
40755
+ }
40349
40756
  filled[p] = 1;
40350
40757
  }
40351
40758
  }
@@ -40357,7 +40764,7 @@ var TextureFrameSeriesAnimator;
40357
40764
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
40358
40765
  class Animator {
40359
40766
  constructor(options) {
40360
- var _a, _b;
40767
+ var _a, _b, _c;
40361
40768
  this.removeOnTick = null;
40362
40769
  this.removeCrossfadeTick = null;
40363
40770
  this.disposed = false;
@@ -40385,6 +40792,9 @@ var TextureFrameSeriesAnimator;
40385
40792
  this.poolIdx = 0;
40386
40793
  this.valueDims = null;
40387
40794
  this.presentVersion = 0;
40795
+ this.extremesLoad = null;
40796
+ this.floorPixels = null;
40797
+ this.ceilingPixels = null;
40388
40798
  if (!options.entity.polygon) {
40389
40799
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
40390
40800
  }
@@ -40396,16 +40806,40 @@ var TextureFrameSeriesAnimator;
40396
40806
  this.archiveUrl = options.archiveUrl;
40397
40807
  this.frames = options.frames;
40398
40808
  this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
40809
+ this.appearance = AppearanceSignature(options);
40399
40810
  const mask = options.textureColorMask;
40400
40811
  this.lowColor = (mask && Color$1.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
40401
40812
  this.highColor = (mask && Color$1.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
40813
+ // Positions are authored in the attribute's units and normalised once here, so the
40814
+ // per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
40815
+ const points = mask && mask.points;
40816
+ this.rampStops = (points && points.length > 0)
40817
+ ? points.map((p) => ({
40818
+ position: NormalisePosition(options.metadata, p.position),
40819
+ color: Color$1.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
40820
+ }))
40821
+ : null;
40822
+ const border = options.cellBorder;
40823
+ const borderWidth = border ? resolveNumber(border.width) : 0;
40824
+ const borderColor = border ? resolveColor(border.color) : null;
40825
+ // Zero width or a fully transparent colour means no grid, matching how the polygon's own
40826
+ // outline behaves, so an existing style that wants no border keeps getting none.
40827
+ this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
40828
+ ? {
40829
+ cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
40830
+ widthTexels: borderWidth,
40831
+ color: borderColor
40832
+ }
40833
+ : null;
40402
40834
  this.frameDates = this.frames.map((f) => JulianDate.fromIso8601(f.Timestamp));
40403
40835
  this.frameCache = new Array(this.frames.length).fill(null);
40404
40836
  this.frameDims = new Array(this.frames.length).fill(null);
40405
40837
  this.valueCache = new Array(this.frames.length).fill(null);
40406
40838
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
40407
- this.baselineMaskEntry = options.baselineMask || null;
40408
- this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
40839
+ this.metadata = options.metadata;
40840
+ this.drapeExtent = options.drapeExtent;
40841
+ this.tiles = TilesOf(options.metadata);
40842
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
40409
40843
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
40410
40844
  this.driveMaterial = options.driveMaterial !== false;
40411
40845
  this.originalMaterial = options.entity.polygon.material;
@@ -40441,6 +40875,10 @@ var TextureFrameSeriesAnimator;
40441
40875
  IsDisposed() {
40442
40876
  return this.disposed;
40443
40877
  }
40878
+ // What this Animator was built to look like, for deciding whether it can be reused.
40879
+ GetAppearanceSignature() {
40880
+ return this.appearance;
40881
+ }
40444
40882
  /**
40445
40883
  * The archive URL this instance was constructed with,
40446
40884
  * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
@@ -40455,6 +40893,15 @@ var TextureFrameSeriesAnimator;
40455
40893
  GetImageProperty() {
40456
40894
  return this.imageProperty;
40457
40895
  }
40896
+ /**
40897
+ * The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
40898
+ *
40899
+ * Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
40900
+ * reading and still be painted nothing, which is exactly what a hidden floor band does.
40901
+ */
40902
+ GetDisplayedPixels() {
40903
+ return this.displayedPixels;
40904
+ }
40458
40905
  /**
40459
40906
  * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
40460
40907
  * or null unless the instance was constructed with produceValueCanvas.
@@ -40567,10 +41014,20 @@ var TextureFrameSeriesAnimator;
40567
41014
  });
40568
41015
  }
40569
41016
  async fetchAndTint(idx) {
40570
- const entry = this.frames[idx];
40571
- const start = entry.ByteOffset;
40572
- const end = entry.ByteOffset + entry.ByteLength - 1;
40573
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
41017
+ const tiles = this.tiles;
41018
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
41019
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
41020
+ let lo = Number.MAX_SAFE_INTEGER;
41021
+ let hi = 0;
41022
+ for (const tile of tiles) {
41023
+ const at = tile.Frames[idx];
41024
+ if (!at) {
41025
+ continue;
41026
+ }
41027
+ lo = Math.min(lo, at.ByteOffset);
41028
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41029
+ }
41030
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
40574
41031
  const buffer = await response.arrayBuffer();
40575
41032
  if (this.disposed) {
40576
41033
  return;
@@ -40581,7 +41038,7 @@ var TextureFrameSeriesAnimator;
40581
41038
  if (this.disposed) {
40582
41039
  return;
40583
41040
  }
40584
- const decoded = await this.decodeAndTint(buffer);
41041
+ const decoded = await this.composeTiles(tiles, idx, buffer, lo);
40585
41042
  if (this.disposed) {
40586
41043
  return;
40587
41044
  }
@@ -40592,39 +41049,292 @@ var TextureFrameSeriesAnimator;
40592
41049
  this.beginCrossfadeTo(idx);
40593
41050
  }
40594
41051
  }
40595
- /**
40596
- * Decodes one frame's raw PNG bytes (as returned by a Range GET) and applies the grayscale color mask.
41052
+ /*
41053
+ * Draws every tile of one frame into a single raster covering the drape extent.
40597
41054
  */
40598
- async decodeAndTint(buffer) {
40599
- const blob = new Blob([buffer], { type: "image/png" });
40600
- const objectUrl = URL.createObjectURL(blob);
40601
- let image;
40602
- try {
40603
- image = await loadImage(objectUrl);
40604
- }
40605
- finally {
40606
- URL.revokeObjectURL(objectUrl);
40607
- }
41055
+ async composeTiles(tiles, idx, buffer, spanStart) {
41056
+ const target = this.compositeExtent();
41057
+ const size = this.compositeSize(tiles, target);
40608
41058
  const canvas = document.createElement("canvas");
40609
- canvas.width = image.width;
40610
- canvas.height = image.height;
41059
+ canvas.width = size.width;
41060
+ canvas.height = size.height;
40611
41061
  const ctx = canvas.getContext("2d");
40612
- ctx.drawImage(image, 0, 0);
40613
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
41062
+ ctx.imageSmoothingEnabled = false;
41063
+ ctx.clearRect(0, 0, size.width, size.height);
41064
+ const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
41065
+ for (let i = 0; i < tiles.length; i++) {
41066
+ const image = images[i];
41067
+ if (!image || this.disposed) {
41068
+ continue;
41069
+ }
41070
+ const at = this.tileRect(tiles[i], target, size);
41071
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
41072
+ }
41073
+ return this.tint(ctx.getImageData(0, 0, size.width, size.height));
41074
+ }
41075
+ /*
41076
+ * Where a composited frame is draped, which is the polygon's rectangle when there is one.
41077
+ */
41078
+ compositeExtent() {
41079
+ if (this.drapeExtent) {
41080
+ return this.drapeExtent;
41081
+ }
41082
+ const m = this.metadata;
41083
+ return {
41084
+ West: m && m.West != null ? m.West : -180,
41085
+ East: m && m.East != null ? m.East : 180,
41086
+ South: m && m.South != null ? m.South : -90,
41087
+ North: m && m.North != null ? m.North : 90
41088
+ };
41089
+ }
41090
+ /*
41091
+ * Composite resolution, capped by what can be uploaded as a texture every frame.
41092
+ */
41093
+ compositeSize(tiles, target) {
41094
+ let finest = Number.MAX_VALUE;
41095
+ for (const tile of tiles) {
41096
+ if (tile.TexelMetres > 0) {
41097
+ finest = Math.min(finest, tile.TexelMetres);
41098
+ }
41099
+ }
41100
+ if (!(finest > 0) || finest === Number.MAX_VALUE) {
41101
+ finest = 1;
41102
+ }
41103
+ const mid = (target.South + target.North) / 2 * Math.PI / 180;
41104
+ const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
41105
+ const heightM = Math.max((target.North - target.South) * 110574, 1);
41106
+ let width = Math.ceil(widthM / finest);
41107
+ let height = Math.ceil(heightM / finest);
41108
+ const longest = Math.max(width, height);
41109
+ if (longest > MAX_COMPOSITE_TEXELS) {
41110
+ const shrink = MAX_COMPOSITE_TEXELS / longest;
41111
+ width = Math.max(1, Math.round(width * shrink));
41112
+ height = Math.max(1, Math.round(height * shrink));
41113
+ }
41114
+ return { width: Math.max(1, width), height: Math.max(1, height) };
41115
+ }
41116
+ /*
41117
+ * Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
41118
+ */
41119
+ tint(imageData) {
41120
+ const canvas = { width: imageData.width, height: imageData.height };
40614
41121
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
40615
41122
  // recovered from the tinted pixels afterwards.
40616
41123
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
40617
- ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
41124
+ // The ramp indexes on R as an 8 bit grey, so a packed frame is flattened to one here
41125
+ // rather than teaching every ramp function a second format.
41126
+ // The copy above keeps the packed pair, which is what displacement and labels read.
41127
+ flattenPacked(imageData.data, IsPackedValue(this.metadata));
41128
+ // Stops win when supplied: they can express a band and a hidden floor, which a two
41129
+ // colour ramp cannot. The pair stays the fallback so an older style still draws.
41130
+ if (this.rampStops && this.rampStops.length > 0) {
41131
+ ApplyGradientStops(imageData, this.rampStops);
41132
+ }
41133
+ else {
41134
+ ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
41135
+ }
41136
+ if (this.cellBorder) {
41137
+ ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
41138
+ }
40618
41139
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
40619
41140
  if (valuePixels) {
40620
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
41141
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
40621
41142
  }
40622
41143
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
40623
41144
  }
41145
+ /*
41146
+ * Paints one frame's composited pixels onto a canvas.
41147
+ */
41148
+ writeInto(canvas, pixels, dims) {
41149
+ canvas.width = dims.width;
41150
+ canvas.height = dims.height;
41151
+ const ctx = canvas.getContext("2d");
41152
+ const image = ctx.createImageData(dims.width, dims.height);
41153
+ image.data.set(pixels);
41154
+ ctx.putImageData(image, 0, 0);
41155
+ }
41156
+ /**
41157
+ * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
41158
+ */
41159
+ GetExtremes() {
41160
+ return { floor: this.floorPixels, ceiling: this.ceilingPixels };
41161
+ }
41162
+ /**
41163
+ * Fetches and decodes the floor and ceiling rasters, at most once.
41164
+ */
41165
+ EnsureExtremes() {
41166
+ if (!this.extremesLoad) {
41167
+ this.extremesLoad = this.loadTiledExtremes();
41168
+ }
41169
+ return this.extremesLoad;
41170
+ }
41171
+ /*
41172
+ * Composites each tile's baseline mask into one covering the composited frames.
41173
+ */
41174
+ async loadTiledBaselineMask() {
41175
+ const tiles = this.tiles;
41176
+ const entries = tiles.map((tile) => tile.BaselineMask);
41177
+ let lo = Number.MAX_SAFE_INTEGER;
41178
+ let hi = 0;
41179
+ for (const at of entries) {
41180
+ if (!at) {
41181
+ continue;
41182
+ }
41183
+ lo = Math.min(lo, at.ByteOffset);
41184
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41185
+ }
41186
+ if (!(hi > lo)) {
41187
+ return;
41188
+ }
41189
+ try {
41190
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41191
+ const block = await response.arrayBuffer();
41192
+ if (this.disposed) {
41193
+ return;
41194
+ }
41195
+ const images = await this.decodeSlices(block, lo, entries);
41196
+ if (this.disposed) {
41197
+ return;
41198
+ }
41199
+ const target = this.compositeExtent();
41200
+ const size = this.compositeSize(tiles, target);
41201
+ const canvas = document.createElement("canvas");
41202
+ canvas.width = size.width;
41203
+ canvas.height = size.height;
41204
+ const ctx = canvas.getContext("2d");
41205
+ ctx.imageSmoothingEnabled = false;
41206
+ ctx.clearRect(0, 0, size.width, size.height);
41207
+ for (let i = 0; i < tiles.length; i++) {
41208
+ if (!images[i]) {
41209
+ continue;
41210
+ }
41211
+ const at = this.tileRect(tiles[i], target, size);
41212
+ ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
41213
+ }
41214
+ const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
41215
+ const flags = new Uint8Array(size.width * size.height);
41216
+ for (let p = 0; p < flags.length; p++) {
41217
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
41218
+ }
41219
+ this.baselineFlags = flags;
41220
+ this.baselineDims = { width: size.width, height: size.height };
41221
+ }
41222
+ catch (e) {
41223
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
41224
+ }
41225
+ }
41226
+ /*
41227
+ * Composites each tile's floor and ceiling into rasters matching the composited frames.
41228
+ *
41229
+ * A label indexes them by the same texel as the value canvas, so they have to be laid out the
41230
+ * same way rather than fetched as whole-extent rasters.
41231
+ */
41232
+ async loadTiledExtremes() {
41233
+ const tiles = this.tiles;
41234
+ const target = this.compositeExtent();
41235
+ const size = this.compositeSize(tiles, target);
41236
+ // One request for the whole extremes block. They sit contiguously after the frames, so
41237
+ // fetching per tile would be 164 round trips for an 82 tile pyramid.
41238
+ let lo = Number.MAX_SAFE_INTEGER;
41239
+ let hi = 0;
41240
+ for (const tile of tiles) {
41241
+ for (const at of [tile.Floor, tile.Ceiling]) {
41242
+ if (!at) {
41243
+ continue;
41244
+ }
41245
+ lo = Math.min(lo, at.ByteOffset);
41246
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41247
+ }
41248
+ }
41249
+ if (!(hi > lo)) {
41250
+ return;
41251
+ }
41252
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41253
+ const block = await response.arrayBuffer();
41254
+ if (this.disposed) {
41255
+ return;
41256
+ }
41257
+ const draw = async (pick) => {
41258
+ const images = await this.decodeSlices(block, lo, tiles.map(pick));
41259
+ if (this.disposed) {
41260
+ return null;
41261
+ }
41262
+ const canvas = document.createElement("canvas");
41263
+ canvas.width = size.width;
41264
+ canvas.height = size.height;
41265
+ const ctx = canvas.getContext("2d");
41266
+ ctx.imageSmoothingEnabled = false;
41267
+ ctx.clearRect(0, 0, size.width, size.height);
41268
+ let drew = false;
41269
+ for (let i = 0; i < tiles.length; i++) {
41270
+ const image = images[i];
41271
+ if (!image) {
41272
+ continue;
41273
+ }
41274
+ const at = this.tileRect(tiles[i], target, size);
41275
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
41276
+ drew = true;
41277
+ }
41278
+ return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
41279
+ };
41280
+ try {
41281
+ this.floorPixels = await draw((tile) => tile.Floor);
41282
+ this.ceilingPixels = await draw((tile) => tile.Ceiling);
41283
+ }
41284
+ catch (e) {
41285
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
41286
+ }
41287
+ }
41288
+ /*
41289
+ * Where one tile lands on the composite, snapped to whole pixels.
41290
+ *
41291
+ * Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
41292
+ * a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
41293
+ * background shows between them.
41294
+ */
41295
+ tileRect(tile, target, size) {
41296
+ const spanLon = target.East - target.West;
41297
+ const spanLat = target.North - target.South;
41298
+ const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
41299
+ const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
41300
+ const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
41301
+ const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
41302
+ return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
41303
+ }
41304
+ /*
41305
+ * Decodes many tile images at once.
41306
+ *
41307
+ * Serially awaiting each decode makes a frame cost as many round trips through the image
41308
+ * decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
41309
+ */
41310
+ decodeSlices(block, base, entries) {
41311
+ return Promise.all(entries.map(async (at) => {
41312
+ if (!at) {
41313
+ return null;
41314
+ }
41315
+ const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
41316
+ const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
41317
+ try {
41318
+ return await loadImage(objectUrl);
41319
+ }
41320
+ catch {
41321
+ return null;
41322
+ }
41323
+ finally {
41324
+ URL.revokeObjectURL(objectUrl);
41325
+ }
41326
+ }));
41327
+ }
40624
41328
  /**
40625
41329
  * Fetches and decodes the archive's static baseline mask, at most once.
40626
41330
  */
40627
41331
  ensureBaselineMask() {
41332
+ if (this.maskBaseline && this.tiles) {
41333
+ if (!this.baselineLoad) {
41334
+ this.baselineLoad = this.loadTiledBaselineMask();
41335
+ }
41336
+ return this.baselineLoad;
41337
+ }
40628
41338
  if (!this.maskBaseline || !this.baselineMaskEntry) {
40629
41339
  return Promise.resolve();
40630
41340
  }
@@ -40737,24 +41447,17 @@ var TextureFrameSeriesAnimator;
40737
41447
  presentPixels(pixels, dims, valuePixels) {
40738
41448
  this.poolIdx = 1 - this.poolIdx;
40739
41449
  const target = this.pool[this.poolIdx];
40740
- target.width = dims.width;
40741
- target.height = dims.height;
40742
- const ctx = target.getContext("2d");
40743
- const imgData = ctx.createImageData(dims.width, dims.height);
40744
- imgData.data.set(pixels);
40745
- ctx.putImageData(imgData, 0, 0);
41450
+ this.writeInto(target, pixels, dims);
40746
41451
  this.displayedPixels = pixels;
40747
41452
  if (this.valueCanvas && valuePixels) {
40748
- this.valueCanvas.width = dims.width;
40749
- this.valueCanvas.height = dims.height;
40750
- const valueCtx = this.valueCanvas.getContext("2d");
40751
- const valueImgData = valueCtx.createImageData(dims.width, dims.height);
40752
- valueImgData.data.set(valuePixels);
40753
- valueCtx.putImageData(valueImgData, 0, 0);
41453
+ this.writeInto(this.valueCanvas, valuePixels, dims);
40754
41454
  this.displayedValuePixels = valuePixels;
40755
41455
  this.valueDims = dims;
40756
41456
  }
40757
41457
  this.presentVersion++;
41458
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
41459
+ this.viewer.scene.requestRender();
41460
+ }
40758
41461
  }
40759
41462
  }
40760
41463
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -40766,6 +41469,730 @@ var TextureFrameSeriesAnimator;
40766
41469
  }
40767
41470
  })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
40768
41471
 
41472
+ /**
41473
+ * Prints a value texture's own numbers over the cells they belong to.
41474
+ */
41475
+ var TextureValueLabels;
41476
+ (function (TextureValueLabels) {
41477
+ // Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
41478
+ // distant view readable.
41479
+ const DEFAULT_MIN_CELL_PIXELS = 0;
41480
+ const MIN_VISIBLE_PIXELS = 60;
41481
+ const MAX_STRIDE = 512;
41482
+ const STRIDE_HYSTERESIS = 1.4;
41483
+ const DEFAULT_MIN_SPACING_PIXELS = 78;
41484
+ const DEFAULT_MAX_LABELS = 240;
41485
+ const LABEL_RADIUS = 4;
41486
+ const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
41487
+ // Clear space either side of a label box, so neighbours read as separate.
41488
+ const LABEL_GAP_PIXELS = 16;
41489
+ const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
41490
+ // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
41491
+ // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
41492
+ const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
41493
+ const DEFAULT_GRID_WIDTH_PIXELS = 1;
41494
+ // Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
41495
+ // whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
41496
+ // too tightly made the grid depend on which tile happened to be walked first.
41497
+ const MAX_GRID_CELLS = 40000;
41498
+ // Probes per side when hunting a labellable cell inside one lattice block.
41499
+ const MAX_LABEL_PROBES_PER_SIDE = 8;
41500
+ const TILE_OUTLINE_ALPHA = 0.45;
41501
+ // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
41502
+ const MAX_GRID_STRIDE = 256;
41503
+ const LINE_HEIGHT = 13;
41504
+ /**
41505
+ * A clip outline from a projected ring, or null when the ring cannot be drawn.
41506
+ * @param projected one entry per ring vertex, null where the vertex has no window coordinate
41507
+ */
41508
+ function ClipOutline(projected) {
41509
+ if (projected.length < 3) {
41510
+ return null;
41511
+ }
41512
+ const out = [];
41513
+ for (const at of projected) {
41514
+ if (!at) {
41515
+ return null;
41516
+ }
41517
+ out.push(at);
41518
+ }
41519
+ return out;
41520
+ }
41521
+ TextureValueLabels.ClipOutline = ClipOutline;
41522
+ /**
41523
+ * One cell's numbers as a label should print them, or null where the texture has no data there.
41524
+ * @param params at is the index of the cell's red channel. sourceMap is derived from the metadata
41525
+ * when omitted, which a caller reading many cells should hoist rather than repeat.
41526
+ */
41527
+ function ReadingAt(params) {
41528
+ const { metadata, source, floor, ceiling, at } = params;
41529
+ if (source.data[at + 3] < 128) {
41530
+ return null;
41531
+ }
41532
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
41533
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
41534
+ const normalised = TextureFrameSeriesAnimator.NormalisedAt(metadata, source.data, at);
41535
+ const toSource = params.sourceMap !== undefined
41536
+ ? params.sourceMap
41537
+ : TextureFrameSeriesAnimator.SourceMap(metadata);
41538
+ const inSource = (value) => TextureFrameSeriesAnimator.ToSource(toSource, value);
41539
+ const reading = { value: inSource(lo + normalised * (hi - lo)) };
41540
+ const eLo = metadata && metadata.ExtremesValueMin;
41541
+ const eHi = metadata && metadata.ExtremesValueMax;
41542
+ if (typeof eLo === "number" && typeof eHi === "number") {
41543
+ if (floor) {
41544
+ reading.floor = inSource(eLo + decode16(floor.data, at) * (eHi - eLo));
41545
+ }
41546
+ if (ceiling) {
41547
+ reading.ceiling = inSource(eLo + decode16(ceiling.data, at) * (eHi - eLo));
41548
+ }
41549
+ }
41550
+ return reading;
41551
+ }
41552
+ TextureValueLabels.ReadingAt = ReadingAt;
41553
+ /**
41554
+ * The unit a label's value line should name, or undefined when the archive states none.
41555
+ * @param metadata the archive's Data.generation
41556
+ */
41557
+ function LabelUnits(metadata) {
41558
+ if (!metadata) {
41559
+ return undefined;
41560
+ }
41561
+ const recovered = TextureFrameSeriesAnimator.SourceMap(metadata);
41562
+ return (recovered && metadata.SourceUnits) || metadata.Units;
41563
+ }
41564
+ TextureValueLabels.LabelUnits = LabelUnits;
41565
+ /**
41566
+ * How many cells apart labels should sit, as a power of two.
41567
+ * @param options cellPixels is one cell's size on screen, held is the stride already in use
41568
+ */
41569
+ function ChooseLabelStride(options) {
41570
+ const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
41571
+ const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
41572
+ && Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
41573
+ let stride = 1;
41574
+ while (stride < MAX_STRIDE && !fits(stride, 1)) {
41575
+ stride *= 2;
41576
+ }
41577
+ const held = options.held;
41578
+ if (held && fits(held, STRIDE_HYSTERESIS)) {
41579
+ if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
41580
+ return held;
41581
+ }
41582
+ if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
41583
+ return held;
41584
+ }
41585
+ }
41586
+ return stride;
41587
+ }
41588
+ TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
41589
+ class Labels {
41590
+ constructor(options) {
41591
+ this.sourceMap = null;
41592
+ this.source = null;
41593
+ this.painted = null;
41594
+ this.floor = null;
41595
+ this.ceiling = null;
41596
+ this.disposed = false;
41597
+ this.drawn = 0;
41598
+ this.stride = 0;
41599
+ this.viewer = options.viewer;
41600
+ this.extent = options.extent;
41601
+ this.settings = options.settings || {};
41602
+ this.metadata = options.metadata;
41603
+ // Once per overlay rather than once per cell: a label pass probes thousands of texels
41604
+ // looking for one worth labelling, and the map cannot change while the metadata does not.
41605
+ this.sourceMap = TextureFrameSeriesAnimator.SourceMap(options.metadata);
41606
+ this.tiles = options.tiles;
41607
+ this.clipRing = options.clipRing;
41608
+ this.canvas = document.createElement("canvas");
41609
+ this.canvas.style.position = "absolute";
41610
+ this.canvas.style.inset = "0";
41611
+ this.canvas.style.pointerEvents = "none";
41612
+ const parent = this.viewer.canvas.parentElement;
41613
+ if (parent) {
41614
+ parent.appendChild(this.canvas);
41615
+ }
41616
+ this.ctx = this.canvas.getContext("2d");
41617
+ }
41618
+ SetExtent(extent) {
41619
+ this.extent = extent;
41620
+ }
41621
+ /**
41622
+ * The current frame's decoded values, as the animator's value canvas already provides them.
41623
+ * @param source value pixels, one texel per cell
41624
+ */
41625
+ SetSource(source) {
41626
+ this.source = source;
41627
+ }
41628
+ /**
41629
+ * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
41630
+ * @param painted RGBA of the presented frame, or null to fall back to coverage alone
41631
+ */
41632
+ SetPainted(painted) {
41633
+ this.painted = painted;
41634
+ }
41635
+ /**
41636
+ * The archive's per-cell extremes, when it publishes them. Without these a label can still
41637
+ * print the current reading, just not the range behind it.
41638
+ */
41639
+ SetExtremes(floor, ceiling) {
41640
+ this.floor = floor;
41641
+ this.ceiling = ceiling;
41642
+ }
41643
+ GetDrawnCount() {
41644
+ return this.drawn;
41645
+ }
41646
+ Clear() {
41647
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
41648
+ this.drawn = 0;
41649
+ }
41650
+ Dispose() {
41651
+ this.disposed = true;
41652
+ if (this.canvas.parentElement) {
41653
+ this.canvas.parentElement.removeChild(this.canvas);
41654
+ }
41655
+ }
41656
+ /**
41657
+ * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
41658
+ * count it considers is bounded by the view rather than by the texture.
41659
+ */
41660
+ Render() {
41661
+ var _a, _b, _c;
41662
+ if (this.disposed || !this.source) {
41663
+ return;
41664
+ }
41665
+ const view = this.viewer.canvas;
41666
+ if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
41667
+ this.canvas.width = view.clientWidth;
41668
+ this.canvas.height = view.clientHeight;
41669
+ }
41670
+ this.Clear();
41671
+ const cols = this.source.width;
41672
+ const rows = this.source.height;
41673
+ const window = this.visibleTexels(cols, rows);
41674
+ if (!window) {
41675
+ return;
41676
+ }
41677
+ const midCol = (window.col0 + window.col1) / 2;
41678
+ const midRow = (window.row0 + window.row1) / 2;
41679
+ const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
41680
+ const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
41681
+ if (!a || !b) {
41682
+ return;
41683
+ }
41684
+ const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
41685
+ // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
41686
+ // number no longer fits.
41687
+ this.drawGrid(window, cols, rows, cellPixels);
41688
+ if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
41689
+ return;
41690
+ }
41691
+ const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
41692
+ const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
41693
+ if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
41694
+ return;
41695
+ }
41696
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
41697
+ this.ctx.font = LABEL_FONT;
41698
+ this.ctx.textAlign = "center";
41699
+ const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
41700
+ this.stride = ChooseLabelStride({
41701
+ cellPixels,
41702
+ visibleCols: window.col1 - window.col0 + 1,
41703
+ visibleRows: window.row1 - window.row0 + 1,
41704
+ maxLabels,
41705
+ // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
41706
+ // one is, so changing what a label says cannot quietly start overlapping them.
41707
+ minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
41708
+ held: this.stride
41709
+ });
41710
+ const stride = this.stride;
41711
+ const firstRow = Math.floor(window.row0 / stride) * stride;
41712
+ const firstCol = Math.floor(window.col0 / stride) * stride;
41713
+ for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
41714
+ for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
41715
+ const found = this.readNear(col, row, cols, rows, stride);
41716
+ if (!found) {
41717
+ continue;
41718
+ }
41719
+ const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
41720
+ if (!point || point.x < 0 || point.y < 0
41721
+ || point.x > this.canvas.width || point.y > this.canvas.height) {
41722
+ continue;
41723
+ }
41724
+ this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
41725
+ }
41726
+ }
41727
+ }
41728
+ /*
41729
+ * Outlines each covered cell, from a projected lattice of its corners.
41730
+ */
41731
+ drawGrid(window, cols, rows, cellPixels) {
41732
+ var _a, _b;
41733
+ const color = this.settings.gridColor;
41734
+ if (!color) {
41735
+ return;
41736
+ }
41737
+ if (this.tiles && this.tiles.length) {
41738
+ this.ctx.save();
41739
+ this.applyRingClip();
41740
+ this.drawTileGrid(color);
41741
+ this.ctx.restore();
41742
+ return;
41743
+ }
41744
+ if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
41745
+ return;
41746
+ }
41747
+ // Corners are shared by four cells, so projecting the lattice once rather than per cell
41748
+ // cuts the work by about four and keeps neighbouring outlines exactly coincident.
41749
+ const wide = window.col1 - window.col0 + 1;
41750
+ const high = window.row1 - window.row0 + 1;
41751
+ if (wide * high > MAX_GRID_CELLS) {
41752
+ return;
41753
+ }
41754
+ const lattice = new Array((wide + 1) * (high + 1));
41755
+ for (let r = 0; r <= high; r++) {
41756
+ for (let c = 0; c <= wide; c++) {
41757
+ // Corners sit half a texel out from the centres lonAt/latAt return.
41758
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
41759
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
41760
+ lattice[r * (wide + 1) + c] = this.project(lon, lat);
41761
+ }
41762
+ }
41763
+ const path = new Path2D();
41764
+ let drew = false;
41765
+ for (let r = 0; r < high; r++) {
41766
+ for (let c = 0; c < wide; c++) {
41767
+ if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
41768
+ continue;
41769
+ }
41770
+ const tl = lattice[r * (wide + 1) + c];
41771
+ const tr = lattice[r * (wide + 1) + c + 1];
41772
+ const bl = lattice[(r + 1) * (wide + 1) + c];
41773
+ const br = lattice[(r + 1) * (wide + 1) + c + 1];
41774
+ if (!tl || !tr || !bl || !br) {
41775
+ continue;
41776
+ }
41777
+ path.moveTo(tl.x, tl.y);
41778
+ path.lineTo(tr.x, tr.y);
41779
+ path.lineTo(br.x, br.y);
41780
+ path.lineTo(bl.x, bl.y);
41781
+ path.closePath();
41782
+ drew = true;
41783
+ }
41784
+ }
41785
+ if (!drew) {
41786
+ return;
41787
+ }
41788
+ this.ctx.strokeStyle = color;
41789
+ this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41790
+ this.ctx.stroke(path);
41791
+ +" alpha" + this.ctx.globalAlpha;
41792
+ }
41793
+ /*
41794
+ * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
41795
+ */
41796
+ applyRingClip() {
41797
+ const ring = this.clipRing;
41798
+ if (!ring || ring.length < 3) {
41799
+ return;
41800
+ }
41801
+ const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
41802
+ if (!outline) {
41803
+ return;
41804
+ }
41805
+ const path = new Path2D();
41806
+ path.moveTo(outline[0].x, outline[0].y);
41807
+ for (let i = 1; i < outline.length; i++) {
41808
+ path.lineTo(outline[i].x, outline[i].y);
41809
+ }
41810
+ path.closePath();
41811
+ this.ctx.clip(path);
41812
+ }
41813
+ /*
41814
+ * The cell of whichever tile covers a point, as screen corners.
41815
+ */
41816
+ tileCellOutline(lon, lat) {
41817
+ for (const tile of this.tiles) {
41818
+ if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
41819
+ continue;
41820
+ }
41821
+ const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
41822
+ const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
41823
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41824
+ const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
41825
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41826
+ const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
41827
+ const corners = [
41828
+ this.project(w0, n0), this.project(w1, n0),
41829
+ this.project(w1, n1), this.project(w0, n1)
41830
+ ];
41831
+ return corners.every((q) => q) ? corners : null;
41832
+ }
41833
+ return null;
41834
+ }
41835
+ /*
41836
+ * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
41837
+ */
41838
+ drawTileGrid(color) {
41839
+ var _a, _b, _c;
41840
+ const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
41841
+ if (!view) {
41842
+ return;
41843
+ }
41844
+ const west = Math$1.toDegrees(view.west);
41845
+ const east = Math$1.toDegrees(view.east);
41846
+ const south = Math$1.toDegrees(view.south);
41847
+ const north = Math$1.toDegrees(view.north);
41848
+ const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
41849
+ const focus = this.groundAtScreenCentre();
41850
+ const path = new Path2D();
41851
+ // A tile too fine to rule, or one the budget cannot afford, contributes its own outline
41852
+ // instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
41853
+ const coarse = new Path2D();
41854
+ let drew = false;
41855
+ let drewCoarse = false;
41856
+ const outlineTile = (tile) => {
41857
+ const tl = this.project(tile.West, tile.North);
41858
+ const tr = this.project(tile.East, tile.North);
41859
+ const br = this.project(tile.East, tile.South);
41860
+ const bl = this.project(tile.West, tile.South);
41861
+ if (!tl || !tr || !br || !bl) {
41862
+ return;
41863
+ }
41864
+ coarse.moveTo(tl.x, tl.y);
41865
+ coarse.lineTo(tr.x, tr.y);
41866
+ coarse.lineTo(br.x, br.y);
41867
+ coarse.lineTo(bl.x, bl.y);
41868
+ coarse.closePath();
41869
+ drewCoarse = true;
41870
+ };
41871
+ // Planned for every visible tile BEFORE any is drawn.
41872
+ const planned = [];
41873
+ for (const tile of this.tiles) {
41874
+ if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
41875
+ continue;
41876
+ }
41877
+ const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
41878
+ / (tile.East - tile.West) * tile.ResolutionX));
41879
+ const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
41880
+ / (tile.East - tile.West) * tile.ResolutionX));
41881
+ const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
41882
+ / (tile.North - tile.South) * tile.ResolutionY));
41883
+ const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
41884
+ / (tile.North - tile.South) * tile.ResolutionY));
41885
+ if (c1 <= c0 || r1 <= r0) {
41886
+ continue;
41887
+ }
41888
+ const focusC = focus
41889
+ ? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
41890
+ : Math.floor((c0 + c1) / 2);
41891
+ const focusR = focus
41892
+ ? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
41893
+ : Math.floor((r0 + r1) / 2);
41894
+ const midC = Math.min(c1 - 1, Math.max(c0, focusC));
41895
+ const midR = Math.min(r1 - 1, Math.max(r0, focusR));
41896
+ const cellLon = (tile.East - tile.West) / tile.ResolutionX;
41897
+ const cellLat = (tile.North - tile.South) / tile.ResolutionY;
41898
+ const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
41899
+ const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
41900
+ if (!at || !next) {
41901
+ outlineTile(tile);
41902
+ continue;
41903
+ }
41904
+ const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
41905
+ let step = 1;
41906
+ while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
41907
+ step *= 2;
41908
+ }
41909
+ if (onScreen * step < minCell) {
41910
+ outlineTile(tile);
41911
+ continue;
41912
+ }
41913
+ planned.push({ tile, c0, c1, r0, r1, step });
41914
+ }
41915
+ // Coarsened together rather than dropped one by one, so a busy view thins out evenly.
41916
+ const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
41917
+ let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41918
+ while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
41919
+ for (const p of planned) {
41920
+ p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
41921
+ }
41922
+ total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41923
+ }
41924
+ for (const { tile, c0, c1, r0, r1, step } of planned) {
41925
+ // Anchored to the tile's own origin so lines stay on the same boundaries as the camera
41926
+ // moves, and so each step up is a subset of the one below it.
41927
+ const firstR = Math.floor(r0 / step) * step;
41928
+ const firstC = Math.floor(c0 / step) * step;
41929
+ for (let r = firstR; r < r1; r += step) {
41930
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41931
+ const n1 = tile.North
41932
+ - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
41933
+ for (let c = firstC; c < c1; c += step) {
41934
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41935
+ const w1 = tile.West
41936
+ + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
41937
+ if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
41938
+ continue;
41939
+ }
41940
+ const tl = this.project(w0, n0);
41941
+ const tr = this.project(w1, n0);
41942
+ const br = this.project(w1, n1);
41943
+ const bl = this.project(w0, n1);
41944
+ if (!tl || !tr || !br || !bl) {
41945
+ continue;
41946
+ }
41947
+ path.moveTo(tl.x, tl.y);
41948
+ path.lineTo(tr.x, tr.y);
41949
+ path.lineTo(br.x, br.y);
41950
+ path.lineTo(bl.x, bl.y);
41951
+ path.closePath();
41952
+ drew = true;
41953
+ }
41954
+ }
41955
+ }
41956
+ const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41957
+ if (drewCoarse) {
41958
+ this.ctx.save();
41959
+ // Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
41960
+ // rather than as a cell in its own right.
41961
+ this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
41962
+ this.ctx.strokeStyle = color;
41963
+ this.ctx.lineWidth = gridWidth;
41964
+ this.ctx.stroke(coarse);
41965
+ this.ctx.restore();
41966
+ }
41967
+ if (!drew) {
41968
+ return;
41969
+ }
41970
+ this.ctx.strokeStyle = color;
41971
+ this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
41972
+ this.ctx.stroke(path);
41973
+ }
41974
+ /*
41975
+ * Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
41976
+ */
41977
+ groundAtScreenCentre() {
41978
+ const scene = this.viewer.scene;
41979
+ const centre = new Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
41980
+ const hit = scene.camera.pickEllipsoid(centre, Ellipsoid.WGS84);
41981
+ if (!hit) {
41982
+ return null;
41983
+ }
41984
+ const carto = Cartographic.fromCartesian(hit);
41985
+ return {
41986
+ lon: Math$1.toDegrees(carto.longitude),
41987
+ lat: Math$1.toDegrees(carto.latitude)
41988
+ };
41989
+ }
41990
+ /*
41991
+ * Whether the composited raster holds data at a lon/lat.
41992
+ */
41993
+ coveredAt(lon, lat) {
41994
+ if (!this.source) {
41995
+ return false;
41996
+ }
41997
+ const cols = this.source.width;
41998
+ const rows = this.source.height;
41999
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
42000
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
42001
+ if (col < 0 || row < 0 || col >= cols || row >= rows) {
42002
+ return false;
42003
+ }
42004
+ const at = (row * cols + col) * 4 + 3;
42005
+ // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
42006
+ // ruling a grid over it draws detail where the picture shows none.
42007
+ if (this.painted && this.painted.length === cols * rows * 4) {
42008
+ return this.painted[at] > 0;
42009
+ }
42010
+ return this.source.data[at] >= 128;
42011
+ }
42012
+ isCovered(col, row, cols) {
42013
+ return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
42014
+ }
42015
+ /*
42016
+ * Width of the widest label this overlay could draw, in pixels.
42017
+ */
42018
+ widestLabel(parts) {
42019
+ const sample = [];
42020
+ if (parts.indexOf("value") >= 0) {
42021
+ sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
42022
+ }
42023
+ const range = [];
42024
+ if (parts.indexOf("floor") >= 0) {
42025
+ range.push("min: -000.00");
42026
+ }
42027
+ if (parts.indexOf("ceiling") >= 0) {
42028
+ range.push("max: -000.00");
42029
+ }
42030
+ if (range.length) {
42031
+ sample.push(range.join(" - "));
42032
+ }
42033
+ if (!sample.length) {
42034
+ return 0;
42035
+ }
42036
+ const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
42037
+ return widest + LABEL_GAP_PIXELS;
42038
+ }
42039
+ /*
42040
+ * The four screen corners of one cell, or null if any of them will not project.
42041
+ */
42042
+ cellOutline(col, row, cols, rows) {
42043
+ // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
42044
+ // uniform lattice over the tile grid, misaligned with it everywhere.
42045
+ if (this.tiles && this.tiles.length) {
42046
+ return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
42047
+ }
42048
+ const corners = [
42049
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
42050
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
42051
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
42052
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
42053
+ ];
42054
+ return corners.every((c) => c) ? corners : null;
42055
+ }
42056
+ drawLabel(point, reading, parts, outline) {
42057
+ var _a;
42058
+ const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
42059
+ const lines = [];
42060
+ if (parts.indexOf("value") >= 0) {
42061
+ lines.push(valueLine(reading.value.toFixed(dp), LabelUnits(this.metadata)));
42062
+ }
42063
+ // Named rather than a bare pair: two numbers under a third say nothing about which is
42064
+ // the series low and which is its high.
42065
+ const range = [];
42066
+ if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
42067
+ range.push(`min: ${reading.floor.toFixed(dp)}`);
42068
+ }
42069
+ if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
42070
+ range.push(`max: ${reading.ceiling.toFixed(dp)}`);
42071
+ }
42072
+ if (range.length) {
42073
+ lines.push(range.join(" - "));
42074
+ }
42075
+ if (!lines.length) {
42076
+ return;
42077
+ }
42078
+ if (outline) {
42079
+ this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
42080
+ this.ctx.lineWidth = 1.5;
42081
+ this.ctx.beginPath();
42082
+ this.ctx.moveTo(outline[0].x, outline[0].y);
42083
+ for (let i = 1; i < outline.length; i++) {
42084
+ this.ctx.lineTo(outline[i].x, outline[i].y);
42085
+ }
42086
+ this.ctx.closePath();
42087
+ this.ctx.stroke();
42088
+ }
42089
+ const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
42090
+ const height = lines.length * LINE_HEIGHT + 6;
42091
+ const top = point.y - height / 2;
42092
+ this.ctx.fillStyle = "rgba(16,16,14,0.72)";
42093
+ this.ctx.beginPath();
42094
+ this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
42095
+ this.ctx.fill();
42096
+ lines.forEach((line, i) => {
42097
+ this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
42098
+ this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
42099
+ });
42100
+ this.drawn++;
42101
+ }
42102
+ /*
42103
+ * The first cell with a reading at or near a lattice point, searched within its own block.
42104
+ */
42105
+ readNear(col, row, cols, rows, stride) {
42106
+ const direct = this.readAt(col, row, cols);
42107
+ if (direct && this.paintedAt(col, row, cols)) {
42108
+ return { col, row, reading: direct };
42109
+ }
42110
+ if (stride <= 1) {
42111
+ return null;
42112
+ }
42113
+ // Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
42114
+ // expensive part of a frame.
42115
+ const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
42116
+ for (let r = row; r < Math.min(rows, row + stride); r += step) {
42117
+ for (let c = col; c < Math.min(cols, col + stride); c += step) {
42118
+ const reading = this.readAt(c, r, cols);
42119
+ if (reading && this.paintedAt(c, r, cols)) {
42120
+ return { col: c, row: r, reading };
42121
+ }
42122
+ }
42123
+ }
42124
+ return null;
42125
+ }
42126
+ paintedAt(col, row, cols) {
42127
+ if (!this.painted) {
42128
+ return true;
42129
+ }
42130
+ const at = (row * cols + col) * 4 + 3;
42131
+ return this.painted[at] > 0;
42132
+ }
42133
+ // Decodes one cell, skipping anything the texture marks as having no data.
42134
+ readAt(col, row, cols) {
42135
+ if (!this.source) {
42136
+ return null;
42137
+ }
42138
+ return ReadingAt({
42139
+ metadata: this.metadata,
42140
+ source: this.source,
42141
+ floor: this.floor,
42142
+ ceiling: this.ceiling,
42143
+ at: (row * cols + col) * 4,
42144
+ sourceMap: this.sourceMap
42145
+ });
42146
+ }
42147
+ lonAt(col, cols) {
42148
+ return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
42149
+ }
42150
+ latAt(row, rows) {
42151
+ return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
42152
+ }
42153
+ // The texel range overlapping the camera's view, so an off-screen texture costs nothing.
42154
+ visibleTexels(cols, rows) {
42155
+ const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
42156
+ if (!view) {
42157
+ return null;
42158
+ }
42159
+ const west = Math.max(this.extent.West, Math$1.toDegrees(view.west));
42160
+ const east = Math.min(this.extent.East, Math$1.toDegrees(view.east));
42161
+ const south = Math.max(this.extent.South, Math$1.toDegrees(view.south));
42162
+ const north = Math.min(this.extent.North, Math$1.toDegrees(view.north));
42163
+ if (west >= east || south >= north) {
42164
+ return null;
42165
+ }
42166
+ const spanLon = this.extent.East - this.extent.West;
42167
+ const spanLat = this.extent.North - this.extent.South;
42168
+ return {
42169
+ col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
42170
+ col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
42171
+ row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
42172
+ row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
42173
+ };
42174
+ }
42175
+ project(lon, lat) {
42176
+ const world = Cartesian3.fromDegrees(lon, lat, 0);
42177
+ const transforms = SceneTransforms;
42178
+ // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
42179
+ const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
42180
+ return fn ? fn.call(transforms, this.viewer.scene, world) : null;
42181
+ }
42182
+ }
42183
+ TextureValueLabels.Labels = Labels;
42184
+ /*
42185
+ * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
42186
+ * label to say nothing, and the two lines are plainly the same quantity.
42187
+ */
42188
+ function valueLine(value, units) {
42189
+ return units ? `${value} (${units})` : value;
42190
+ }
42191
+ function decode16(data, i) {
42192
+ return (data[i] * 256 + data[i + 1]) / 65535;
42193
+ }
42194
+ })(TextureValueLabels || (TextureValueLabels = {}));
42195
+
40769
42196
  /**
40770
42197
  * Renders a value texture as a displaced, coloured surface.
40771
42198
  * The source canvas is expected as RGBA: value in R, coverage in A.
@@ -40786,8 +42213,6 @@ var DisplacedSurfacePrimitive;
40786
42213
  const SKIRT_FRACTION = 0.015;
40787
42214
  // Alpha below which a texel counts as no-data and is not drawn at all.
40788
42215
  const COVERAGE_CUTOFF = 0.35;
40789
- // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
40790
- const AUTO_RELIEF_FRACTION = 0.02;
40791
42216
  // Terrain is sampled on this grid and interpolated between samples.
40792
42217
  // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
40793
42218
  const GROUND_SAMPLES_PER_SIDE = 64;
@@ -40808,6 +42233,7 @@ in float skirt;
40808
42233
 
40809
42234
  uniform sampler2D u_valueTexture;
40810
42235
  uniform sampler2D u_groundTexture;
42236
+ uniform float u_valuePacked;
40811
42237
  uniform float u_valueMin;
40812
42238
  uniform float u_valueRange;
40813
42239
  uniform float u_exaggeration;
@@ -40845,18 +42271,37 @@ float sampleGround(vec2 uv) {
40845
42271
  return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
40846
42272
  }
40847
42273
 
42274
+ /*
42275
+ * The texel's normalised value, whichever way the archive packed it.
42276
+ *
42277
+ * A packed frame keeps the high byte in R, so reading R alone would quantise the whole range to 255
42278
+ * steps and draw a plausible wrong surface rather than an obviously broken one.
42279
+ *
42280
+ * Each channel is scaled and then added, rather than reassembled to a 0..65535 integer and divided.
42281
+ * That integer is outside the range mediump guarantees, and a fragment shader only gets highp where
42282
+ * the device offers it, so the obvious form loses the low byte on exactly the older mobile hardware
42283
+ * that Cesium falls back to mediump for.
42284
+ */
42285
+ float decodeValue(vec4 texel) {
42286
+ float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
42287
+ return mix(texel.r, packed16, u_valuePacked);
42288
+ }
42289
+
40848
42290
  void main() {
40849
42291
  vec4 texel = texture(u_valueTexture, st);
40850
42292
  v_st = st;
40851
- v_value = texel.r;
42293
+ v_value = decodeValue(texel);
40852
42294
  v_coverage = texel.a;
40853
42295
 
40854
- float metres = u_valueMin + texel.r * u_valueRange;
42296
+ // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
42297
+ // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
42298
+ // scaling that instead translates the whole sheet underground rather than spreading it out.
42299
+ float metres = u_valueMin + v_value * u_valueRange * u_exaggeration;
40855
42300
 
40856
42301
  // Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
40857
42302
  // is following the ground rather than absolutely positioned.
40858
42303
  float ground = sampleGround(st) * u_groundWeight;
40859
- float displacement = u_baseHeight + ground + metres * u_exaggeration * texel.a - skirt * u_skirtDepth;
42304
+ float displacement = u_baseHeight + ground + metres * texel.a - skirt * u_skirtDepth;
40860
42305
 
40861
42306
  // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
40862
42307
  // would be lost to float32 rounding at an earth radius.
@@ -40866,10 +42311,10 @@ void main() {
40866
42311
  `;
40867
42312
  const FRAGMENT_SHADER_GLSL300 = `
40868
42313
  uniform sampler2D u_valueTexture;
40869
- uniform vec4 u_lowColor;
40870
- uniform vec4 u_highColor;
42314
+ uniform sampler2D u_rampTexture;
40871
42315
  uniform float u_coverageCutoff;
40872
42316
  uniform vec2 u_texelSize;
42317
+ uniform float u_valuePacked;
40873
42318
  uniform float u_valueRange;
40874
42319
  uniform float u_exaggeration;
40875
42320
  uniform float u_metresPerTexel;
@@ -40878,18 +42323,34 @@ in vec2 v_st;
40878
42323
  in float v_value;
40879
42324
  in float v_coverage;
40880
42325
 
42326
+ /*
42327
+ * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
42328
+ * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
42329
+ */
42330
+ float valueAt(vec2 uv) {
42331
+ vec4 texel = texture(u_valueTexture, uv);
42332
+ float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
42333
+ return mix(texel.r, packed16, u_valuePacked);
42334
+ }
42335
+
40881
42336
  void main() {
40882
42337
  if (v_coverage < u_coverageCutoff) {
40883
42338
  discard;
40884
42339
  }
40885
- vec4 color = mix(u_lowColor, u_highColor, v_value);
42340
+ // The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
42341
+ // hidden floor reads the same here as it does on the flat drape.
42342
+ // Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
42343
+ vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
42344
+ if (color.a <= 0.0) {
42345
+ discard;
42346
+ }
40886
42347
 
40887
42348
  // Relief shading from the value gradient, so the displacement reads at a distance without
40888
42349
  // recomputing vertex normals every time the texture changes.
40889
- float left = texture(u_valueTexture, v_st - vec2(u_texelSize.x, 0.0)).r;
40890
- float right = texture(u_valueTexture, v_st + vec2(u_texelSize.x, 0.0)).r;
40891
- float down = texture(u_valueTexture, v_st - vec2(0.0, u_texelSize.y)).r;
40892
- float up = texture(u_valueTexture, v_st + vec2(0.0, u_texelSize.y)).r;
42350
+ float left = valueAt(v_st - vec2(u_texelSize.x, 0.0));
42351
+ float right = valueAt(v_st + vec2(u_texelSize.x, 0.0));
42352
+ float down = valueAt(v_st - vec2(0.0, u_texelSize.y));
42353
+ float up = valueAt(v_st + vec2(0.0, u_texelSize.y));
40893
42354
  float scale = u_valueRange * u_exaggeration;
40894
42355
  vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
40895
42356
  float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
@@ -41081,6 +42542,7 @@ void main() {
41081
42542
  this.show = true;
41082
42543
  this.valueSource = null;
41083
42544
  this.lastPresentVersion = -1;
42545
+ this.rampTexture = null;
41084
42546
  this.texture = null;
41085
42547
  this.textureDirty = true;
41086
42548
  this.shaderProgram = null;
@@ -41098,12 +42560,12 @@ void main() {
41098
42560
  this.source = options.source || null;
41099
42561
  this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
41100
42562
  this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
41101
- this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
42563
+ this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : 1;
41102
42564
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
42565
+ this.packedValue = Boolean(options.packedValue);
41103
42566
  this.pixelPlacement = options.pixelPlacement;
41104
42567
  this.tileSkirts = Boolean(options.tileSkirts);
41105
- this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
41106
- this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
42568
+ this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
41107
42569
  this.tiles = buildTiles(hasCoverage);
41108
42570
  }
41109
42571
  GetFollowsGround() {
@@ -41162,6 +42624,7 @@ void main() {
41162
42624
  return;
41163
42625
  }
41164
42626
  this.syncTexture(context);
42627
+ this.syncRampTexture(context);
41165
42628
  this.requestGround();
41166
42629
  this.syncGroundTexture(context);
41167
42630
  // A tile needs one mesh before it has a bounding sphere to measure against.
@@ -41200,6 +42663,10 @@ void main() {
41200
42663
  this.texture.destroy();
41201
42664
  this.texture = null;
41202
42665
  }
42666
+ if (this.rampTexture) {
42667
+ this.rampTexture.destroy();
42668
+ this.rampTexture = null;
42669
+ }
41203
42670
  if (this.groundTexture) {
41204
42671
  this.groundTexture.destroy();
41205
42672
  this.groundTexture = null;
@@ -41293,6 +42760,25 @@ void main() {
41293
42760
  const width = Cartesian3.distance(southWest, southEast);
41294
42761
  return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
41295
42762
  }
42763
+ syncRampTexture(context) {
42764
+ if (this.rampTexture) {
42765
+ return;
42766
+ }
42767
+ this.rampTexture = new Texture({
42768
+ context,
42769
+ pixelFormat: PixelFormat.RGBA,
42770
+ source: { width: this.rampPixels.length / 4, height: 1,
42771
+ arrayBufferView: this.rampPixels },
42772
+ flipY: false,
42773
+ sampler: new Sampler({
42774
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
42775
+ wrapT: TextureWrap.CLAMP_TO_EDGE,
42776
+ // Linear across the ramp, so a gradient between stops stays smooth.
42777
+ minificationFilter: TextureMinificationFilter.LINEAR,
42778
+ magnificationFilter: TextureMagnificationFilter.LINEAR
42779
+ })
42780
+ });
42781
+ }
41296
42782
  syncTexture(context) {
41297
42783
  const sizeChanged = this.texture
41298
42784
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -41300,6 +42786,14 @@ void main() {
41300
42786
  if (this.texture) {
41301
42787
  this.texture.destroy();
41302
42788
  }
42789
+ // Nearest for a packed value, because filtering would blend the two bytes of the
42790
+ // packing rather than the values they encode, the same trap sampleGround avoids.
42791
+ const minification = this.packedValue
42792
+ ? TextureMinificationFilter.NEAREST
42793
+ : TextureMinificationFilter.LINEAR;
42794
+ const magnification = this.packedValue
42795
+ ? TextureMagnificationFilter.NEAREST
42796
+ : TextureMagnificationFilter.LINEAR;
41303
42797
  this.texture = new Texture({
41304
42798
  context,
41305
42799
  source: this.source,
@@ -41309,8 +42803,8 @@ void main() {
41309
42803
  sampler: new Sampler({
41310
42804
  wrapS: TextureWrap.CLAMP_TO_EDGE,
41311
42805
  wrapT: TextureWrap.CLAMP_TO_EDGE,
41312
- minificationFilter: TextureMinificationFilter.LINEAR,
41313
- magnificationFilter: TextureMagnificationFilter.LINEAR
42806
+ minificationFilter: minification,
42807
+ magnificationFilter: magnification
41314
42808
  })
41315
42809
  });
41316
42810
  this.textureDirty = false;
@@ -41411,6 +42905,7 @@ void main() {
41411
42905
  uniformMap: {
41412
42906
  u_valueTexture: () => self.texture,
41413
42907
  u_groundTexture: () => self.groundTexture,
42908
+ u_valuePacked: () => (self.packedValue ? 1 : 0),
41414
42909
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
41415
42910
  u_groundRange: () => new Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
41416
42911
  u_groundSize: () => new Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
@@ -41423,8 +42918,7 @@ void main() {
41423
42918
  ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
41424
42919
  : 0,
41425
42920
  u_coverageCutoff: () => COVERAGE_CUTOFF,
41426
- u_lowColor: () => self.lowColor,
41427
- u_highColor: () => self.highColor,
42921
+ u_rampTexture: () => self.rampTexture,
41428
42922
  u_texelSize: () => new Cartesian2(1 / self.source.width, 1 / self.source.height)
41429
42923
  }
41430
42924
  });
@@ -41464,33 +42958,6 @@ void main() {
41464
42958
  }
41465
42959
  return tiles;
41466
42960
  }
41467
- /**
41468
- * Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
41469
- *
41470
- * Derived from the whole extent rather than per tile: scaling each tile to its own size would
41471
- * make neighbours disagree along their shared edge and tear the surface apart.
41472
- * @param extent
41473
- * @param valueMin
41474
- * @param valueMax
41475
- */
41476
- function autoExaggeration(extent, valueMin, valueMax) {
41477
- const range = Math.abs(valueMax - valueMin);
41478
- if (!(range > 0)) {
41479
- return 1;
41480
- }
41481
- const southWest = Cartesian3.fromDegrees(extent.West, extent.South, 0);
41482
- const southEast = Cartesian3.fromDegrees(extent.East, extent.South, 0);
41483
- const northWest = Cartesian3.fromDegrees(extent.West, extent.North, 0);
41484
- const shorterSide = Math.min(Cartesian3.distance(southWest, southEast), Cartesian3.distance(southWest, northWest));
41485
- if (!(shorterSide > 0)) {
41486
- return 1;
41487
- }
41488
- return (shorterSide * AUTO_RELIEF_FRACTION) / range;
41489
- }
41490
- DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
41491
- function toCesiumColor(color) {
41492
- return new Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
41493
- }
41494
42961
  function now() {
41495
42962
  return typeof performance !== "undefined" ? performance.now() : Date.now();
41496
42963
  }
@@ -41498,11 +42965,12 @@ void main() {
41498
42965
 
41499
42966
  const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
41500
42967
  const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
42968
+ const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
41501
42969
  const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
41502
42970
  var EntityRenderEnginePolygon;
41503
42971
  (function (EntityRenderEnginePolygon) {
41504
42972
  async function Render(params) {
41505
- var _a, _b, _c, _d;
42973
+ var _a, _b, _c, _d, _e;
41506
42974
  const entity = params.entity;
41507
42975
  const style = params.style;
41508
42976
  const pRings = Entity$1.GetValue({
@@ -41553,18 +43021,22 @@ var EntityRenderEnginePolygon;
41553
43021
  console.error(`Polygon.Render: failed to resolve extrusion texture for entity ${((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.ID) || "<NONE>"}:`, e);
41554
43022
  }
41555
43023
  }
43024
+ const retained = params.offline
43025
+ ? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
43026
+ : null;
43027
+ const keepsAnimator = Boolean(retained && !retained.IsDisposed());
41556
43028
  const drawsDisplacedSurface = Boolean(extrusionArchive);
41557
43029
  // The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
41558
43030
  // Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
41559
43031
  const hasFill = (drawsDisplacedSurface ? true
41560
43032
  : fillType === Style.EPolygonFillType.Texture
41561
- ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
43033
+ ? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
41562
43034
  : cFillColor.alpha > 0);
41563
43035
  const fillMaterial = drawsDisplacedSurface
41564
43036
  ? Color.WHITE.withAlpha(0)
41565
43037
  : textureDataUri
41566
43038
  ? new ImageMaterialProperty({ image: textureDataUri, transparent: true })
41567
- : frameArchive
43039
+ : (frameArchive || keepsAnimator)
41568
43040
  ? Color.WHITE.withAlpha(0)
41569
43041
  : cFillColor;
41570
43042
  const lineColorTrace = style.lineColor ? Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
@@ -41734,7 +43206,7 @@ var EntityRenderEnginePolygon;
41734
43206
  }
41735
43207
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
41736
43208
  // has to know whether it is baking over a live animation or over a disposed one's restored material.
41737
- const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
43209
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
41738
43210
  syncDisplacedSurface({
41739
43211
  cEntity,
41740
43212
  extrusionArchive,
@@ -41742,7 +43214,30 @@ var EntityRenderEnginePolygon;
41742
43214
  style,
41743
43215
  entity,
41744
43216
  heightRef,
41745
- outerRingPosses: posses
43217
+ outerRingPosses: posses,
43218
+ offline: params.offline
43219
+ });
43220
+ const drapeExtent = polygonBbox(posses);
43221
+ const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
43222
+ syncTextureValueLabels({
43223
+ cEntity,
43224
+ viewer: params.viewer,
43225
+ settings: overlaySettings(style, entity, params.tags),
43226
+ extent: labelArchive
43227
+ ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
43228
+ : null,
43229
+ animator: drawsDisplacedSurface
43230
+ ? cEntity[EXTRUSION_ANIMATOR_KEY]
43231
+ : animator,
43232
+ metadata: labelArchive ? labelArchive.metadata : undefined,
43233
+ offline: params.offline,
43234
+ clipRing: posses.map((pos) => {
43235
+ const carto = Cartographic.fromCartesian(pos);
43236
+ return {
43237
+ lon: Math$1.toDegrees(carto.longitude),
43238
+ lat: Math$1.toDegrees(carto.latitude)
43239
+ };
43240
+ })
41746
43241
  });
41747
43242
  if (animator) {
41748
43243
  CesiumEntityStyler.SetDefaultTextureImage({
@@ -41803,7 +43298,7 @@ var EntityRenderEnginePolygon;
41803
43298
  }
41804
43299
  }
41805
43300
  let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
41806
- let cEntityBorder = (_d = (_c = params.rendered) === null || _c === void 0 ? void 0 : _c._siblingGraphics) === null || _d === void 0 ? void 0 : _d[0];
43301
+ let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
41807
43302
  cEntity._siblingGraphics = [];
41808
43303
  if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
41809
43304
  (!cEntityBorder.corridor && units == "m"))) {
@@ -42054,6 +43549,11 @@ var EntityRenderEnginePolygon;
42054
43549
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
42055
43550
  */
42056
43551
  function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
43552
+ const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
43553
+ if (labelOverlay) {
43554
+ labelOverlay.remove();
43555
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43556
+ }
42057
43557
  const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
42058
43558
  if (existing && !existing.IsDisposed()) {
42059
43559
  existing.Dispose();
@@ -42179,10 +43679,13 @@ const _textureCache = new LRUCache(50);
42179
43679
  * @param params
42180
43680
  */
42181
43681
  function syncDisplacedSurface(params) {
42182
- var _a;
42183
43682
  const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
42184
43683
  const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
42185
43684
  const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
43685
+ // Same reason as the fill animator: an unresolvable archive is not a removed one.
43686
+ if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
43687
+ return;
43688
+ }
42186
43689
  // Nothing to draw and nothing left over, which is every polygon that does not use this feature.
42187
43690
  if (!extrusionArchive && !existingAnimator && !existingSurface) {
42188
43691
  return;
@@ -42193,12 +43696,12 @@ function syncDisplacedSurface(params) {
42193
43696
  && existingAnimator.GetArchiveUrl() === extrusionArchive.url
42194
43697
  && existingSurface
42195
43698
  && !existingSurface.isDestroyed();
42196
- const placement = surfacePlacement(entity, heightRef);
43699
+ const placement = surfacePlacement(entity, heightRef, extrusionArchive && TextureFrameSeriesAnimator.IsDatumHeight(extrusionArchive.metadata));
42197
43700
  // Whether the surface follows terrain is baked into its ground sampling, so a change there has
42198
43701
  // to rebuild rather than update in place.
42199
43702
  if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
42200
43703
  existingSurface.SetBaseHeight(placement.baseHeight);
42201
- existingSurface.SetExaggeration((_a = style.extrusionExaggeration) !== null && _a !== void 0 ? _a : DisplacedSurfacePrimitive.autoExaggeration(existingSurface.GetExtent(), extrusionArchive.metadata.ValueMin, extrusionArchive.metadata.ValueMax));
43704
+ existingSurface.SetExaggeration(exaggerationFor(style));
42202
43705
  return;
42203
43706
  }
42204
43707
  disposeDisplacedSurface(cEntity, viewer);
@@ -42220,8 +43723,8 @@ function syncDisplacedSurface(params) {
42220
43723
  textureColorMask: style.textureColorMask,
42221
43724
  driveMaterial: false,
42222
43725
  produceValueCanvas: true,
42223
- baselineMask: extrusionArchive.metadata.BaselineMask,
42224
- // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
43726
+ metadata: extrusionArchive.metadata,
43727
+ cellTexels: 1,
42225
43728
  maskBaseline: style.maskTextureBaseline
42226
43729
  });
42227
43730
  cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
@@ -42236,9 +43739,16 @@ function syncDisplacedSurface(params) {
42236
43739
  terrainProvider: viewer && viewer.terrainProvider,
42237
43740
  valueMin: extrusionArchive.metadata.ValueMin,
42238
43741
  valueMax: extrusionArchive.metadata.ValueMax,
42239
- exaggeration: style.extrusionExaggeration,
43742
+ packedValue: TextureFrameSeriesAnimator.IsPackedValue(extrusionArchive.metadata),
43743
+ exaggeration: exaggerationFor(style),
42240
43744
  lowColor: mask ? Color$1.ColorFromStr(mask.minColor) : null,
42241
- highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null
43745
+ highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null,
43746
+ // Normalised the same way the animator normalises them, so the surface and the flat drape
43747
+ // agree about which values the style hides.
43748
+ rampStops: ((mask && mask.points) || []).map((stop) => ({
43749
+ position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
43750
+ color: Color$1.ColorFromStr(stop.color)
43751
+ })).filter((stop) => Boolean(stop.color))
42242
43752
  });
42243
43753
  surface.BindAnimator(animator);
42244
43754
  viewer.scene.primitives.add(surface);
@@ -42251,7 +43761,7 @@ function syncDisplacedSurface(params) {
42251
43761
  * @param entity
42252
43762
  * @param heightRef
42253
43763
  */
42254
- function surfacePlacement(entity, heightRef) {
43764
+ function surfacePlacement(entity, heightRef, datumHeight) {
42255
43765
  const rawAltitude = Entity$1.GetValue({
42256
43766
  entity,
42257
43767
  path: ["Bruce", "Location", "altitude"]
@@ -42263,14 +43773,34 @@ function surfacePlacement(entity, heightRef) {
42263
43773
  if (heightRef === HeightReference.RELATIVE_TO_GROUND) {
42264
43774
  return { baseHeight: altitude, followGround: true };
42265
43775
  }
42266
- return { baseHeight: altitude, followGround: false };
43776
+ // Values that ARE heights against a datum already say where they belong, so adding the entity's
43777
+ // altitude on top would move a measured surface off the datum it was measured against.
43778
+ return { baseHeight: datumHeight ? 0 : altitude, followGround: false };
42267
43779
  }
42268
- /**
42269
- * The archive's own extent when it recorded one, otherwise the polygon's own bounds so archives
42270
- * generated before the extent was written still render somewhere sensible.
42271
- * @param metadata
42272
- * @param posses
43780
+ /*
43781
+ * The factor a displaced surface should use, which is the style's or nothing.
42273
43782
  */
43783
+ function exaggerationFor(style) {
43784
+ if (style.extrusionExaggeration != null) {
43785
+ return style.extrusionExaggeration;
43786
+ }
43787
+ return 1;
43788
+ }
43789
+ /*
43790
+ * The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
43791
+ */
43792
+ function polygonBbox(posses) {
43793
+ if (!posses || posses.length === 0) {
43794
+ return undefined;
43795
+ }
43796
+ const rectangle = Rectangle.fromCartesianArray(posses);
43797
+ return {
43798
+ West: Math$1.toDegrees(rectangle.west),
43799
+ East: Math$1.toDegrees(rectangle.east),
43800
+ South: Math$1.toDegrees(rectangle.south),
43801
+ North: Math$1.toDegrees(rectangle.north)
43802
+ };
43803
+ }
42274
43804
  function archiveExtent(metadata, posses) {
42275
43805
  if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
42276
43806
  return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
@@ -42321,9 +43851,22 @@ function disposeDisplacedSurface(cEntity, viewer) {
42321
43851
  * @param viewer
42322
43852
  * @param textureColorMask
42323
43853
  */
42324
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
43854
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
42325
43855
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
42326
- if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
43856
+ // Offline says nothing about whether the archive changed, only that it could not be asked.
43857
+ if (offline && existing && !existing.IsDisposed()) {
43858
+ return existing;
43859
+ }
43860
+ // Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
43861
+ // polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
43862
+ // style edit appeared to do nothing at all.
43863
+ const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
43864
+ textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
43865
+ valueCanvas: produceValueCanvas, drape: drapeExtent
43866
+ });
43867
+ if (frameArchive && existing && !existing.IsDisposed()
43868
+ && existing.GetArchiveUrl() === frameArchive.url
43869
+ && existing.GetAppearanceSignature() === appearance) {
42327
43870
  return existing;
42328
43871
  }
42329
43872
  if (existing && !existing.IsDisposed()) {
@@ -42339,12 +43882,115 @@ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask
42339
43882
  archiveUrl: frameArchive.url,
42340
43883
  frames: frameArchive.metadata.Frames,
42341
43884
  textureColorMask,
42342
- baselineMask: frameArchive.metadata.BaselineMask,
42343
- maskBaseline: maskTextureBaseline
43885
+ metadata: frameArchive.metadata,
43886
+ cellBorder,
43887
+ cellTexels,
43888
+ maskBaseline: maskTextureBaseline,
43889
+ produceValueCanvas,
43890
+ drapeExtent
42344
43891
  });
42345
43892
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
42346
43893
  return animator;
42347
43894
  }
43895
+ /**
43896
+ * Folds the cell-border and value-label style into the single overlay that draws both.
43897
+ *
43898
+ * Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
43899
+ */
43900
+ function wantsLabels(style) {
43901
+ const texture = style.texture;
43902
+ return Boolean(texture && !Array.isArray(texture) && texture.label);
43903
+ }
43904
+ /*
43905
+ * How the cell grid and its values are drawn, which the style does not get a say in.
43906
+ *
43907
+ * A white hairline reads over every ramp colour without competing with it, and the spacing and cap
43908
+ * are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
43909
+ * bought nothing except thresholds an author could get wrong.
43910
+ */
43911
+ const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
43912
+ const LABEL_GRID_WIDTH_PIXELS = 1;
43913
+ const LABEL_MIN_SPACING_PIXELS = 78;
43914
+ const LABEL_MAX_LABELS = 240;
43915
+ function overlaySettings(style, entity, tags) {
43916
+ if (!wantsLabels(style)) {
43917
+ return undefined;
43918
+ }
43919
+ return {
43920
+ gridColor: LABEL_GRID_COLOR,
43921
+ gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
43922
+ minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
43923
+ maxLabels: LABEL_MAX_LABELS
43924
+ };
43925
+ }
43926
+ /**
43927
+ * Keeps a value-label overlay in step with whichever animator is driving the polygon.
43928
+ *
43929
+ * Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
43930
+ * move them even when the data has not advanced at all.
43931
+ */
43932
+ function syncTextureValueLabels(params) {
43933
+ const { cEntity, viewer, settings, extent, animator, metadata } = params;
43934
+ const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
43935
+ // Offline has no extent to rebuild the overlay from, so an existing one is left running.
43936
+ if (params.offline && existing) {
43937
+ return;
43938
+ }
43939
+ if (existing) {
43940
+ existing.remove();
43941
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43942
+ }
43943
+ if (!settings || !extent || !animator) {
43944
+ return;
43945
+ }
43946
+ const labels = new TextureValueLabels.Labels({
43947
+ viewer, extent, settings, metadata,
43948
+ tiles: TextureFrameSeriesAnimator.TilesOf(metadata).map((tile) => ({
43949
+ West: tile.West, East: tile.East, South: tile.South, North: tile.North,
43950
+ ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
43951
+ })),
43952
+ clipRing: params.clipRing
43953
+ });
43954
+ let lastVersion = -1;
43955
+ let appliedFloor = null;
43956
+ let appliedCeiling = null;
43957
+ // The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
43958
+ // Range GETs here even though nothing else in the render path wants them.
43959
+ animator.EnsureExtremes();
43960
+ const remove = viewer.scene.preRender.addEventListener(() => {
43961
+ if (animator.IsDisposed()) {
43962
+ return;
43963
+ }
43964
+ const canvas = animator.GetValueCanvas();
43965
+ if (!canvas) {
43966
+ return;
43967
+ }
43968
+ const version = animator.GetPresentVersion();
43969
+ if (version !== lastVersion) {
43970
+ lastVersion = version;
43971
+ const ctx = canvas.getContext("2d");
43972
+ labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
43973
+ labels.SetPainted(animator.GetDisplayedPixels());
43974
+ }
43975
+ // Tracked by reference rather than latched on the first one to arrive: a tiled archive
43976
+ // composites the floor and the ceiling separately, so latching once would leave every label
43977
+ // showing a minimum and no maximum.
43978
+ const extremes = animator.GetExtremes();
43979
+ if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
43980
+ appliedFloor = extremes.floor;
43981
+ appliedCeiling = extremes.ceiling;
43982
+ labels.SetExtremes(appliedFloor, appliedCeiling);
43983
+ }
43984
+ labels.Render();
43985
+ });
43986
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = {
43987
+ labels,
43988
+ remove: () => {
43989
+ remove();
43990
+ labels.Dispose();
43991
+ }
43992
+ };
43993
+ }
42348
43994
  /**
42349
43995
  * Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
42350
43996
  * for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
@@ -42436,10 +44082,14 @@ async function resolveTextureValue(params) {
42436
44082
  url = clientFile.URL;
42437
44083
  const clientFileData = clientFile.Data;
42438
44084
  if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
44085
+ const adapted = TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
44086
+ if (!adapted) {
44087
+ return { dataUri: null, effective: textureTrace.effective };
44088
+ }
42439
44089
  return {
42440
44090
  dataUri: null,
42441
44091
  effective: textureTrace.effective,
42442
- frameArchive: { url, metadata: clientFileData.generation }
44092
+ frameArchive: { url, metadata: adapted }
42443
44093
  };
42444
44094
  }
42445
44095
  }
@@ -42468,7 +44118,11 @@ async function resolveTextureCondition(params) {
42468
44118
  const url = clientFile.URL;
42469
44119
  const clientFileData = clientFile.Data;
42470
44120
  if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
42471
- return { dataUri: null, effective: null, frameArchive: { url, metadata: clientFileData.generation } };
44121
+ const adapted = TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
44122
+ if (adapted) {
44123
+ return { dataUri: null, effective: null, frameArchive: { url, metadata: adapted } };
44124
+ }
44125
+ return { dataUri: null, effective: null };
42472
44126
  }
42473
44127
  const cacheKey = "texture-" + url + "-none";
42474
44128
  let prom = _textureCache.Get(cacheKey);
@@ -43952,7 +45606,7 @@ var StyleUtils;
43952
45606
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
43953
45607
  })(StyleUtils || (StyleUtils = {}));
43954
45608
 
43955
- const VERSION = "7.2.1";
45609
+ const VERSION = "7.2.3";
43956
45610
  /**
43957
45611
  * Updates the environment instance used by bruce-cesium to one specified.
43958
45612
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -43969,5 +45623,5 @@ const getENVIRONMENT = () => {
43969
45623
  return ENVIRONMENT;
43970
45624
  };
43971
45625
 
43972
- export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
45626
+ export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
43973
45627
  //# sourceMappingURL=bruce-cesium.es5.js.map