bruce-cesium 7.2.1 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bruce-cesium.es5.js +1666 -59
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +1659 -56
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +2 -1
- package/dist/lib/bruce-cesium.js.map +1 -1
- package/dist/lib/internal/image-utils.js +106 -1
- package/dist/lib/internal/image-utils.js.map +1 -1
- package/dist/lib/rendering/displaced-surface-primitive.js +41 -8
- package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-polygon.js +211 -13
- package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
- package/dist/lib/rendering/texture-frame-series-animator.js +621 -28
- package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
- package/dist/lib/rendering/texture-value-labels.js +687 -0
- package/dist/lib/rendering/texture-value-labels.js.map +1 -0
- package/dist/lib/rendering/visuals-register.js +8 -3
- package/dist/lib/rendering/visuals-register.js.map +1 -1
- package/dist/types/bruce-cesium.d.ts +2 -1
- package/dist/types/internal/image-utils.d.ts +25 -0
- package/dist/types/rendering/displaced-surface-primitive.d.ts +5 -2
- package/dist/types/rendering/texture-frame-series-animator.d.ts +172 -1
- package/dist/types/rendering/texture-value-labels.d.ts +139 -0
- package/dist/types/rendering/visuals-register.d.ts +1 -0
- package/package.json +2 -2
package/dist/bruce-cesium.es5.js
CHANGED
|
@@ -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,
|
|
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,
|
|
2
|
+
import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, 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, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, SceneTransforms, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, NearFarScalar, Matrix3, GeoJsonDataSource, SceneMode, KmlDataSource, Ion, IonResource, Cesium3DTileStyle, HeadingPitchRange, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Cesium3DTileColorBlendMode, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, CustomDataSource, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, 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, Bounds, Color as Color$1, BruceEvent, EntityCoords, DataLab, DelayQueue, 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
|
-
|
|
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
|
-
|
|
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,6 +40400,87 @@ function loadImage(src) {
|
|
|
40294
40400
|
|
|
40295
40401
|
var TextureFrameSeriesAnimator;
|
|
40296
40402
|
(function (TextureFrameSeriesAnimator) {
|
|
40403
|
+
// Values are measured up from the ground beneath them.
|
|
40404
|
+
TextureFrameSeriesAnimator.QUANTITY_THICKNESS = "thickness";
|
|
40405
|
+
// Values are heights against a vertical datum, so they need that datum to be placed.
|
|
40406
|
+
TextureFrameSeriesAnimator.QUANTITY_ELEVATION = "elevation";
|
|
40407
|
+
TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
|
|
40408
|
+
/**
|
|
40409
|
+
* The generator that produced an archive, for telling a stale one from a current one.
|
|
40410
|
+
*
|
|
40411
|
+
* Read the version rather than sniffing for fields: an archive can legitimately omit Tiles or
|
|
40412
|
+
* GeoidSeparation and still be current, so their absence says nothing about its age.
|
|
40413
|
+
* @param metadata the archive's Data.generation
|
|
40414
|
+
*/
|
|
40415
|
+
function GeneratorVersion(metadata) {
|
|
40416
|
+
if (!metadata || typeof metadata.GeneratorVersion !== "number") {
|
|
40417
|
+
return 1;
|
|
40418
|
+
}
|
|
40419
|
+
return metadata.GeneratorVersion;
|
|
40420
|
+
}
|
|
40421
|
+
TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
|
|
40422
|
+
/**
|
|
40423
|
+
* Whether an archive is an adaptive pyramid rather than one raster.
|
|
40424
|
+
* @param metadata the archive's Data.generation
|
|
40425
|
+
*/
|
|
40426
|
+
function IsTiledLayout(metadata) {
|
|
40427
|
+
return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
|
|
40428
|
+
metadata.Tiles && metadata.Tiles.length > 0));
|
|
40429
|
+
}
|
|
40430
|
+
TextureFrameSeriesAnimator.IsTiledLayout = IsTiledLayout;
|
|
40431
|
+
// Frames carry the value as the source reported it.
|
|
40432
|
+
TextureFrameSeriesAnimator.MEASURE_ABSOLUTE = "absolute";
|
|
40433
|
+
// Frames carry the value minus each cell's own minimum over the series.
|
|
40434
|
+
TextureFrameSeriesAnimator.MEASURE_ANOMALY = "anomaly";
|
|
40435
|
+
/**
|
|
40436
|
+
* Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
|
|
40437
|
+
*
|
|
40438
|
+
* Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
|
|
40439
|
+
* actually has, and the archive's own published range is what makes that expressible.
|
|
40440
|
+
* @param metadata the archive's Data.generation
|
|
40441
|
+
* @param value in the attribute's units
|
|
40442
|
+
*/
|
|
40443
|
+
function NormalisePosition(metadata, value) {
|
|
40444
|
+
const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
|
|
40445
|
+
const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
|
|
40446
|
+
const span = hi - lo;
|
|
40447
|
+
if (!(span > 0)) {
|
|
40448
|
+
return 0;
|
|
40449
|
+
}
|
|
40450
|
+
return Math.min(1, Math.max(0, (value - lo) / span));
|
|
40451
|
+
}
|
|
40452
|
+
TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
|
|
40453
|
+
/**
|
|
40454
|
+
* Whether the frames are a departure from each cell's own normal rather than a raw reading.
|
|
40455
|
+
*
|
|
40456
|
+
* Worth asking before labelling anything: the two measures need different words for the same
|
|
40457
|
+
* number, and calling an anomaly "water depth" is a worse error than the coarse ramp it replaced.
|
|
40458
|
+
* @param metadata the archive's Data.generation
|
|
40459
|
+
*/
|
|
40460
|
+
function IsAnomalyMeasure(metadata) {
|
|
40461
|
+
return Boolean(metadata && metadata.Measure === TextureFrameSeriesAnimator.MEASURE_ANOMALY);
|
|
40462
|
+
}
|
|
40463
|
+
TextureFrameSeriesAnimator.IsAnomalyMeasure = IsAnomalyMeasure;
|
|
40464
|
+
/**
|
|
40465
|
+
* How much the value moves anywhere in the series, in source units.
|
|
40466
|
+
*
|
|
40467
|
+
* Falls back to the published range, which is the right answer for an anomaly archive (its range
|
|
40468
|
+
* IS the movement) and the only available one for an archive predating AnomalyMax.
|
|
40469
|
+
* @param metadata the archive's Data.generation
|
|
40470
|
+
*/
|
|
40471
|
+
function MovingRange(metadata) {
|
|
40472
|
+
if (!metadata) {
|
|
40473
|
+
return undefined;
|
|
40474
|
+
}
|
|
40475
|
+
if (typeof metadata.AnomalyMax === "number" && metadata.AnomalyMax > 0) {
|
|
40476
|
+
return metadata.AnomalyMax;
|
|
40477
|
+
}
|
|
40478
|
+
if (typeof metadata.ValueMin === "number" && typeof metadata.ValueMax === "number") {
|
|
40479
|
+
return Math.abs(metadata.ValueMax - metadata.ValueMin);
|
|
40480
|
+
}
|
|
40481
|
+
return undefined;
|
|
40482
|
+
}
|
|
40483
|
+
TextureFrameSeriesAnimator.MovingRange = MovingRange;
|
|
40297
40484
|
/**
|
|
40298
40485
|
* Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
|
|
40299
40486
|
* so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
|
|
@@ -40303,15 +40490,51 @@ var TextureFrameSeriesAnimator;
|
|
|
40303
40490
|
return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
|
|
40304
40491
|
}
|
|
40305
40492
|
TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
|
|
40306
|
-
//
|
|
40493
|
+
// First resolvable static number out of a calculator field list, since that is all a border needs.
|
|
40494
|
+
function resolveNumber(fields) {
|
|
40495
|
+
if (!fields) {
|
|
40496
|
+
return 0;
|
|
40497
|
+
}
|
|
40498
|
+
for (const field of fields) {
|
|
40499
|
+
const value = Number(field && field.value);
|
|
40500
|
+
if (Number.isFinite(value)) {
|
|
40501
|
+
return value;
|
|
40502
|
+
}
|
|
40503
|
+
}
|
|
40504
|
+
return 0;
|
|
40505
|
+
}
|
|
40506
|
+
// First resolvable static colour out of a calculator field list.
|
|
40507
|
+
function resolveColor(fields) {
|
|
40508
|
+
if (!fields) {
|
|
40509
|
+
return null;
|
|
40510
|
+
}
|
|
40511
|
+
for (const field of fields) {
|
|
40512
|
+
const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
|
|
40513
|
+
? Color$1.ColorFromStr(field.value)
|
|
40514
|
+
: null;
|
|
40515
|
+
if (parsed) {
|
|
40516
|
+
return parsed;
|
|
40517
|
+
}
|
|
40518
|
+
}
|
|
40519
|
+
return null;
|
|
40520
|
+
}
|
|
40521
|
+
/*
|
|
40522
|
+
* Everything about an Animator that a style can change, as a comparable string.
|
|
40523
|
+
*/
|
|
40524
|
+
function AppearanceSignature(options) {
|
|
40525
|
+
var _a, _b, _c;
|
|
40526
|
+
return JSON.stringify([
|
|
40527
|
+
(_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
|
|
40528
|
+
(_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
|
|
40529
|
+
(_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
|
|
40530
|
+
Boolean(options.maskBaseline)
|
|
40531
|
+
]);
|
|
40532
|
+
}
|
|
40533
|
+
TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
|
|
40534
|
+
const MAX_COMPOSITE_TEXELS = 2048;
|
|
40307
40535
|
const VALUE_DILATE_PASSES = 2;
|
|
40308
40536
|
/*
|
|
40309
40537
|
* Bleeds covered values outward into uncovered texels, leaving alpha untouched.
|
|
40310
|
-
*
|
|
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.
|
|
40315
40538
|
*/
|
|
40316
40539
|
function dilateValues(value, width, height, passes) {
|
|
40317
40540
|
const texels = width * height;
|
|
@@ -40357,7 +40580,7 @@ var TextureFrameSeriesAnimator;
|
|
|
40357
40580
|
const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
|
|
40358
40581
|
class Animator {
|
|
40359
40582
|
constructor(options) {
|
|
40360
|
-
var _a, _b;
|
|
40583
|
+
var _a, _b, _c;
|
|
40361
40584
|
this.removeOnTick = null;
|
|
40362
40585
|
this.removeCrossfadeTick = null;
|
|
40363
40586
|
this.disposed = false;
|
|
@@ -40383,8 +40606,12 @@ var TextureFrameSeriesAnimator;
|
|
|
40383
40606
|
// forcing Cesium to re-upload the texture only when something changed.
|
|
40384
40607
|
this.pool = [document.createElement("canvas"), document.createElement("canvas")];
|
|
40385
40608
|
this.poolIdx = 0;
|
|
40609
|
+
this.scratch = null;
|
|
40386
40610
|
this.valueDims = null;
|
|
40387
40611
|
this.presentVersion = 0;
|
|
40612
|
+
this.extremesLoad = null;
|
|
40613
|
+
this.floorPixels = null;
|
|
40614
|
+
this.ceilingPixels = null;
|
|
40388
40615
|
if (!options.entity.polygon) {
|
|
40389
40616
|
throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
|
|
40390
40617
|
}
|
|
@@ -40396,16 +40623,41 @@ var TextureFrameSeriesAnimator;
|
|
|
40396
40623
|
this.archiveUrl = options.archiveUrl;
|
|
40397
40624
|
this.frames = options.frames;
|
|
40398
40625
|
this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
|
|
40626
|
+
this.appearance = AppearanceSignature(options);
|
|
40399
40627
|
const mask = options.textureColorMask;
|
|
40400
40628
|
this.lowColor = (mask && Color$1.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
|
|
40401
40629
|
this.highColor = (mask && Color$1.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
|
|
40630
|
+
// Positions are authored in the attribute's units and normalised once here, so the
|
|
40631
|
+
// per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
|
|
40632
|
+
const points = mask === null || mask === void 0 ? void 0 : mask.points;
|
|
40633
|
+
this.rampStops = (points && points.length > 0)
|
|
40634
|
+
? points.map((p) => ({
|
|
40635
|
+
position: NormalisePosition(options.metadata, p.position),
|
|
40636
|
+
color: Color$1.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
|
|
40637
|
+
}))
|
|
40638
|
+
: null;
|
|
40639
|
+
const border = options.cellBorder;
|
|
40640
|
+
const borderWidth = border ? resolveNumber(border.width) : 0;
|
|
40641
|
+
const borderColor = border ? resolveColor(border.color) : null;
|
|
40642
|
+
// Zero width or a fully transparent colour means no grid, matching how the polygon's own
|
|
40643
|
+
// outline behaves, so an existing style that wants no border keeps getting none.
|
|
40644
|
+
this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
|
|
40645
|
+
? {
|
|
40646
|
+
cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
|
|
40647
|
+
widthTexels: borderWidth,
|
|
40648
|
+
color: borderColor
|
|
40649
|
+
}
|
|
40650
|
+
: null;
|
|
40402
40651
|
this.frameDates = this.frames.map((f) => JulianDate.fromIso8601(f.Timestamp));
|
|
40403
40652
|
this.frameCache = new Array(this.frames.length).fill(null);
|
|
40404
40653
|
this.frameDims = new Array(this.frames.length).fill(null);
|
|
40405
40654
|
this.valueCache = new Array(this.frames.length).fill(null);
|
|
40406
40655
|
this.produceValueCanvas = Boolean(options.produceValueCanvas);
|
|
40656
|
+
this.metadata = options.metadata;
|
|
40657
|
+
this.drapeExtent = options.drapeExtent;
|
|
40658
|
+
this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
|
|
40407
40659
|
this.baselineMaskEntry = options.baselineMask || null;
|
|
40408
|
-
this.maskBaseline = (
|
|
40660
|
+
this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
|
|
40409
40661
|
this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
|
|
40410
40662
|
this.driveMaterial = options.driveMaterial !== false;
|
|
40411
40663
|
this.originalMaterial = options.entity.polygon.material;
|
|
@@ -40441,6 +40693,10 @@ var TextureFrameSeriesAnimator;
|
|
|
40441
40693
|
IsDisposed() {
|
|
40442
40694
|
return this.disposed;
|
|
40443
40695
|
}
|
|
40696
|
+
// What this Animator was built to look like, for deciding whether it can be reused.
|
|
40697
|
+
GetAppearanceSignature() {
|
|
40698
|
+
return this.appearance;
|
|
40699
|
+
}
|
|
40444
40700
|
/**
|
|
40445
40701
|
* The archive URL this instance was constructed with,
|
|
40446
40702
|
* lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
|
|
@@ -40455,6 +40711,15 @@ var TextureFrameSeriesAnimator;
|
|
|
40455
40711
|
GetImageProperty() {
|
|
40456
40712
|
return this.imageProperty;
|
|
40457
40713
|
}
|
|
40714
|
+
/**
|
|
40715
|
+
* The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
|
|
40716
|
+
*
|
|
40717
|
+
* Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
|
|
40718
|
+
* reading and still be painted nothing, which is exactly what a hidden floor band does.
|
|
40719
|
+
*/
|
|
40720
|
+
GetDisplayedPixels() {
|
|
40721
|
+
return this.displayedPixels;
|
|
40722
|
+
}
|
|
40458
40723
|
/**
|
|
40459
40724
|
* The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
|
|
40460
40725
|
* or null unless the instance was constructed with produceValueCanvas.
|
|
@@ -40567,11 +40832,33 @@ var TextureFrameSeriesAnimator;
|
|
|
40567
40832
|
});
|
|
40568
40833
|
}
|
|
40569
40834
|
async fetchAndTint(idx) {
|
|
40570
|
-
const
|
|
40571
|
-
|
|
40572
|
-
|
|
40573
|
-
|
|
40574
|
-
|
|
40835
|
+
const tiles = this.tiles;
|
|
40836
|
+
let buffer;
|
|
40837
|
+
let span = 0;
|
|
40838
|
+
if (tiles) {
|
|
40839
|
+
// Every tile's frame for one timestep sits contiguously, because the generator writes
|
|
40840
|
+
// frame major. So a tiled frame is still ONE range request, not one per tile.
|
|
40841
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
40842
|
+
let hi = 0;
|
|
40843
|
+
for (const tile of tiles) {
|
|
40844
|
+
const at = tile.Frames[idx];
|
|
40845
|
+
if (!at) {
|
|
40846
|
+
continue;
|
|
40847
|
+
}
|
|
40848
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
40849
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
40850
|
+
}
|
|
40851
|
+
span = lo;
|
|
40852
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
40853
|
+
buffer = await response.arrayBuffer();
|
|
40854
|
+
}
|
|
40855
|
+
else {
|
|
40856
|
+
const entry = this.frames[idx];
|
|
40857
|
+
const start = entry.ByteOffset;
|
|
40858
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
40859
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
40860
|
+
buffer = await response.arrayBuffer();
|
|
40861
|
+
}
|
|
40575
40862
|
if (this.disposed) {
|
|
40576
40863
|
return;
|
|
40577
40864
|
}
|
|
@@ -40581,7 +40868,9 @@ var TextureFrameSeriesAnimator;
|
|
|
40581
40868
|
if (this.disposed) {
|
|
40582
40869
|
return;
|
|
40583
40870
|
}
|
|
40584
|
-
const decoded =
|
|
40871
|
+
const decoded = tiles
|
|
40872
|
+
? await this.composeTiles(tiles, idx, buffer, span)
|
|
40873
|
+
: await this.decodeAndTint(buffer);
|
|
40585
40874
|
if (this.disposed) {
|
|
40586
40875
|
return;
|
|
40587
40876
|
}
|
|
@@ -40610,21 +40899,438 @@ var TextureFrameSeriesAnimator;
|
|
|
40610
40899
|
canvas.height = image.height;
|
|
40611
40900
|
const ctx = canvas.getContext("2d");
|
|
40612
40901
|
ctx.drawImage(image, 0, 0);
|
|
40613
|
-
|
|
40902
|
+
return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
40903
|
+
}
|
|
40904
|
+
/*
|
|
40905
|
+
* Draws every tile of one frame into a single raster covering the drape extent.
|
|
40906
|
+
*/
|
|
40907
|
+
async composeTiles(tiles, idx, buffer, spanStart) {
|
|
40908
|
+
const target = this.compositeExtent();
|
|
40909
|
+
const size = this.compositeSize(tiles, target);
|
|
40910
|
+
const canvas = document.createElement("canvas");
|
|
40911
|
+
canvas.width = size.width;
|
|
40912
|
+
canvas.height = size.height;
|
|
40913
|
+
const ctx = canvas.getContext("2d");
|
|
40914
|
+
ctx.imageSmoothingEnabled = false;
|
|
40915
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
40916
|
+
const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
|
|
40917
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
40918
|
+
const image = images[i];
|
|
40919
|
+
if (!image || this.disposed) {
|
|
40920
|
+
continue;
|
|
40921
|
+
}
|
|
40922
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
40923
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
40924
|
+
}
|
|
40925
|
+
return this.tint(ctx.getImageData(0, 0, size.width, size.height));
|
|
40926
|
+
}
|
|
40927
|
+
/*
|
|
40928
|
+
* Where a composited frame is draped, which is the polygon's rectangle when there is one.
|
|
40929
|
+
*/
|
|
40930
|
+
compositeExtent() {
|
|
40931
|
+
if (this.drapeExtent) {
|
|
40932
|
+
return this.drapeExtent;
|
|
40933
|
+
}
|
|
40934
|
+
const m = this.metadata;
|
|
40935
|
+
return {
|
|
40936
|
+
West: m && m.West != null ? m.West : -180,
|
|
40937
|
+
East: m && m.East != null ? m.East : 180,
|
|
40938
|
+
South: m && m.South != null ? m.South : -90,
|
|
40939
|
+
North: m && m.North != null ? m.North : 90
|
|
40940
|
+
};
|
|
40941
|
+
}
|
|
40942
|
+
/*
|
|
40943
|
+
* Composite resolution, capped by what can be uploaded as a texture every frame.
|
|
40944
|
+
*/
|
|
40945
|
+
compositeSize(tiles, target) {
|
|
40946
|
+
let finest = Number.MAX_VALUE;
|
|
40947
|
+
for (const tile of tiles) {
|
|
40948
|
+
if (tile.TexelMetres > 0) {
|
|
40949
|
+
finest = Math.min(finest, tile.TexelMetres);
|
|
40950
|
+
}
|
|
40951
|
+
}
|
|
40952
|
+
if (!(finest > 0) || finest === Number.MAX_VALUE) {
|
|
40953
|
+
finest = 1;
|
|
40954
|
+
}
|
|
40955
|
+
const mid = (target.South + target.North) / 2 * Math.PI / 180;
|
|
40956
|
+
const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
|
|
40957
|
+
const heightM = Math.max((target.North - target.South) * 110574, 1);
|
|
40958
|
+
let width = Math.ceil(widthM / finest);
|
|
40959
|
+
let height = Math.ceil(heightM / finest);
|
|
40960
|
+
const longest = Math.max(width, height);
|
|
40961
|
+
if (longest > MAX_COMPOSITE_TEXELS) {
|
|
40962
|
+
const shrink = MAX_COMPOSITE_TEXELS / longest;
|
|
40963
|
+
width = Math.max(1, Math.round(width * shrink));
|
|
40964
|
+
height = Math.max(1, Math.round(height * shrink));
|
|
40965
|
+
}
|
|
40966
|
+
return { width: Math.max(1, width), height: Math.max(1, height) };
|
|
40967
|
+
}
|
|
40968
|
+
/*
|
|
40969
|
+
* Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
|
|
40970
|
+
*/
|
|
40971
|
+
tint(imageData) {
|
|
40972
|
+
const canvas = { width: imageData.width, height: imageData.height };
|
|
40614
40973
|
// Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
|
|
40615
40974
|
// recovered from the tinted pixels afterwards.
|
|
40616
40975
|
const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
|
|
40617
|
-
|
|
40976
|
+
// Stops win when supplied: they can express a band and a hidden floor, which a two
|
|
40977
|
+
// colour ramp cannot. The pair stays the fallback so an older style still draws.
|
|
40978
|
+
if (this.rampStops && this.rampStops.length > 0) {
|
|
40979
|
+
ApplyGradientStops(imageData, this.rampStops);
|
|
40980
|
+
}
|
|
40981
|
+
else {
|
|
40982
|
+
ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
|
|
40983
|
+
}
|
|
40984
|
+
if (this.cellBorder) {
|
|
40985
|
+
ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
|
|
40986
|
+
}
|
|
40618
40987
|
this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
|
|
40619
40988
|
if (valuePixels) {
|
|
40620
40989
|
dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
|
|
40621
40990
|
}
|
|
40622
40991
|
return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
|
|
40623
40992
|
}
|
|
40993
|
+
/*
|
|
40994
|
+
* Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
|
|
40995
|
+
*/
|
|
40996
|
+
writeInto(canvas, pixels, dims) {
|
|
40997
|
+
const rect = this.sourceRect(dims);
|
|
40998
|
+
if (!rect) {
|
|
40999
|
+
canvas.width = dims.width;
|
|
41000
|
+
canvas.height = dims.height;
|
|
41001
|
+
const direct = canvas.getContext("2d");
|
|
41002
|
+
const image = direct.createImageData(dims.width, dims.height);
|
|
41003
|
+
image.data.set(pixels);
|
|
41004
|
+
direct.putImageData(image, 0, 0);
|
|
41005
|
+
return;
|
|
41006
|
+
}
|
|
41007
|
+
if (!this.scratch) {
|
|
41008
|
+
this.scratch = document.createElement("canvas");
|
|
41009
|
+
}
|
|
41010
|
+
this.scratch.width = dims.width;
|
|
41011
|
+
this.scratch.height = dims.height;
|
|
41012
|
+
const scratchCtx = this.scratch.getContext("2d");
|
|
41013
|
+
const image = scratchCtx.createImageData(dims.width, dims.height);
|
|
41014
|
+
image.data.set(pixels);
|
|
41015
|
+
scratchCtx.putImageData(image, 0, 0);
|
|
41016
|
+
canvas.width = dims.width;
|
|
41017
|
+
canvas.height = dims.height;
|
|
41018
|
+
const ctx = canvas.getContext("2d");
|
|
41019
|
+
// Nearest neighbour: a value texel has to survive the resample exactly, or a label reads
|
|
41020
|
+
// a blend of two cells rather than the cell it points at.
|
|
41021
|
+
ctx.imageSmoothingEnabled = false;
|
|
41022
|
+
ctx.clearRect(0, 0, dims.width, dims.height);
|
|
41023
|
+
ctx.drawImage(this.scratch, rect.x, rect.y, rect.width, rect.height, 0, 0, dims.width, dims.height);
|
|
41024
|
+
}
|
|
41025
|
+
/*
|
|
41026
|
+
* The part of the archive raster that the drape extent covers, in source pixels.
|
|
41027
|
+
*/
|
|
41028
|
+
sourceRect(dims) {
|
|
41029
|
+
const drape = this.drapeExtent;
|
|
41030
|
+
const m = this.metadata;
|
|
41031
|
+
// A composited frame was drawn into the drape extent already, so resampling it again
|
|
41032
|
+
// would apply the same correction twice.
|
|
41033
|
+
if (this.tiles) {
|
|
41034
|
+
return null;
|
|
41035
|
+
}
|
|
41036
|
+
if (!drape || !m || m.West == null || m.East == null || m.South == null || m.North == null) {
|
|
41037
|
+
return null;
|
|
41038
|
+
}
|
|
41039
|
+
const spanLon = m.East - m.West;
|
|
41040
|
+
const spanLat = m.North - m.South;
|
|
41041
|
+
if (!(spanLon > 0) || !(spanLat > 0)) {
|
|
41042
|
+
return null;
|
|
41043
|
+
}
|
|
41044
|
+
const x = (drape.West - m.West) / spanLon * dims.width;
|
|
41045
|
+
const width = (drape.East - drape.West) / spanLon * dims.width;
|
|
41046
|
+
const y = (m.North - drape.North) / spanLat * dims.height;
|
|
41047
|
+
const height = (drape.North - drape.South) / spanLat * dims.height;
|
|
41048
|
+
// A drape that already matches the archive is the common case and must not pay for a
|
|
41049
|
+
// resample, nor lose a half pixel to rounding.
|
|
41050
|
+
if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01
|
|
41051
|
+
&& Math.abs(width - dims.width) < 0.01 && Math.abs(height - dims.height) < 0.01) {
|
|
41052
|
+
return null;
|
|
41053
|
+
}
|
|
41054
|
+
return { x, y, width, height };
|
|
41055
|
+
}
|
|
41056
|
+
/**
|
|
41057
|
+
* The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
|
|
41058
|
+
*/
|
|
41059
|
+
GetExtremes() {
|
|
41060
|
+
return { floor: this.floorPixels, ceiling: this.ceilingPixels };
|
|
41061
|
+
}
|
|
41062
|
+
/**
|
|
41063
|
+
* Fetches and decodes the floor and ceiling rasters, at most once.
|
|
41064
|
+
*/
|
|
41065
|
+
EnsureExtremes() {
|
|
41066
|
+
if (this.extremesLoad) {
|
|
41067
|
+
return this.extremesLoad;
|
|
41068
|
+
}
|
|
41069
|
+
if (this.tiles) {
|
|
41070
|
+
this.extremesLoad = this.loadTiledExtremes();
|
|
41071
|
+
return this.extremesLoad;
|
|
41072
|
+
}
|
|
41073
|
+
const floor = this.metadata && this.metadata.Floor;
|
|
41074
|
+
const ceiling = this.metadata && this.metadata.Ceiling;
|
|
41075
|
+
if (!floor && !ceiling) {
|
|
41076
|
+
this.extremesLoad = Promise.resolve();
|
|
41077
|
+
return this.extremesLoad;
|
|
41078
|
+
}
|
|
41079
|
+
this.extremesLoad = Promise.all([
|
|
41080
|
+
floor ? this.loadImageDataAt(floor) : Promise.resolve(null),
|
|
41081
|
+
ceiling ? this.loadImageDataAt(ceiling) : Promise.resolve(null)
|
|
41082
|
+
]).then(([f, c]) => {
|
|
41083
|
+
if (this.disposed) {
|
|
41084
|
+
return;
|
|
41085
|
+
}
|
|
41086
|
+
this.floorPixels = f;
|
|
41087
|
+
this.ceilingPixels = c;
|
|
41088
|
+
}).catch((e) => {
|
|
41089
|
+
// Missing extremes cost a label its range, and must not take the animation with them.
|
|
41090
|
+
console.warn("TextureFrameSeriesAnimator: could not load the extremes rasters.", e);
|
|
41091
|
+
});
|
|
41092
|
+
return this.extremesLoad;
|
|
41093
|
+
}
|
|
41094
|
+
/*
|
|
41095
|
+
* Composites each tile's baseline mask into one covering the composited frames.
|
|
41096
|
+
*
|
|
41097
|
+
* A tiled archive publishes the mask per tile, so reading the archive level BaselineMask
|
|
41098
|
+
* finds nothing and hiding dry cells silently does nothing at all.
|
|
41099
|
+
*/
|
|
41100
|
+
async loadTiledBaselineMask() {
|
|
41101
|
+
const tiles = this.tiles;
|
|
41102
|
+
if (!tiles) {
|
|
41103
|
+
return;
|
|
41104
|
+
}
|
|
41105
|
+
const entries = tiles.map((tile) => tile.BaselineMask);
|
|
41106
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
41107
|
+
let hi = 0;
|
|
41108
|
+
for (const at of entries) {
|
|
41109
|
+
if (!at) {
|
|
41110
|
+
continue;
|
|
41111
|
+
}
|
|
41112
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41113
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41114
|
+
}
|
|
41115
|
+
if (!(hi > lo)) {
|
|
41116
|
+
return;
|
|
41117
|
+
}
|
|
41118
|
+
try {
|
|
41119
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41120
|
+
const block = await response.arrayBuffer();
|
|
41121
|
+
if (this.disposed) {
|
|
41122
|
+
return;
|
|
41123
|
+
}
|
|
41124
|
+
const images = await this.decodeSlices(block, lo, entries);
|
|
41125
|
+
if (this.disposed) {
|
|
41126
|
+
return;
|
|
41127
|
+
}
|
|
41128
|
+
const target = this.compositeExtent();
|
|
41129
|
+
const size = this.compositeSize(tiles, target);
|
|
41130
|
+
const canvas = document.createElement("canvas");
|
|
41131
|
+
canvas.width = size.width;
|
|
41132
|
+
canvas.height = size.height;
|
|
41133
|
+
const ctx = canvas.getContext("2d");
|
|
41134
|
+
ctx.imageSmoothingEnabled = false;
|
|
41135
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41136
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41137
|
+
if (!images[i]) {
|
|
41138
|
+
continue;
|
|
41139
|
+
}
|
|
41140
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41141
|
+
ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
|
|
41142
|
+
}
|
|
41143
|
+
const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
|
|
41144
|
+
const flags = new Uint8Array(size.width * size.height);
|
|
41145
|
+
for (let p = 0; p < flags.length; p++) {
|
|
41146
|
+
flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
|
|
41147
|
+
}
|
|
41148
|
+
this.baselineFlags = flags;
|
|
41149
|
+
this.baselineDims = { width: size.width, height: size.height };
|
|
41150
|
+
}
|
|
41151
|
+
catch (e) {
|
|
41152
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
|
|
41153
|
+
}
|
|
41154
|
+
}
|
|
41155
|
+
/*
|
|
41156
|
+
* Composites each tile's floor and ceiling into rasters matching the composited frames.
|
|
41157
|
+
*
|
|
41158
|
+
* A tiled archive has no whole-extent extremes to fetch, only per-tile ones, and a label
|
|
41159
|
+
* indexes them by the same texel as the value canvas, so they have to be laid out the same way.
|
|
41160
|
+
*/
|
|
41161
|
+
async loadTiledExtremes() {
|
|
41162
|
+
const tiles = this.tiles;
|
|
41163
|
+
if (!tiles) {
|
|
41164
|
+
return;
|
|
41165
|
+
}
|
|
41166
|
+
const target = this.compositeExtent();
|
|
41167
|
+
const size = this.compositeSize(tiles, target);
|
|
41168
|
+
// One request for the whole extremes block. They sit contiguously after the frames, so
|
|
41169
|
+
// fetching per tile would be 164 round trips for an 82 tile pyramid.
|
|
41170
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
41171
|
+
let hi = 0;
|
|
41172
|
+
for (const tile of tiles) {
|
|
41173
|
+
for (const at of [tile.Floor, tile.Ceiling]) {
|
|
41174
|
+
if (!at) {
|
|
41175
|
+
continue;
|
|
41176
|
+
}
|
|
41177
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41178
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41179
|
+
}
|
|
41180
|
+
}
|
|
41181
|
+
if (!(hi > lo)) {
|
|
41182
|
+
return;
|
|
41183
|
+
}
|
|
41184
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41185
|
+
const block = await response.arrayBuffer();
|
|
41186
|
+
if (this.disposed) {
|
|
41187
|
+
return;
|
|
41188
|
+
}
|
|
41189
|
+
const draw = async (pick) => {
|
|
41190
|
+
const images = await this.decodeSlices(block, lo, tiles.map(pick));
|
|
41191
|
+
if (this.disposed) {
|
|
41192
|
+
return null;
|
|
41193
|
+
}
|
|
41194
|
+
const canvas = document.createElement("canvas");
|
|
41195
|
+
canvas.width = size.width;
|
|
41196
|
+
canvas.height = size.height;
|
|
41197
|
+
const ctx = canvas.getContext("2d");
|
|
41198
|
+
ctx.imageSmoothingEnabled = false;
|
|
41199
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41200
|
+
let drew = false;
|
|
41201
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41202
|
+
const image = images[i];
|
|
41203
|
+
if (!image) {
|
|
41204
|
+
continue;
|
|
41205
|
+
}
|
|
41206
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41207
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
41208
|
+
drew = true;
|
|
41209
|
+
}
|
|
41210
|
+
return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
|
|
41211
|
+
};
|
|
41212
|
+
try {
|
|
41213
|
+
this.floorPixels = await draw((tile) => tile.Floor);
|
|
41214
|
+
this.ceilingPixels = await draw((tile) => tile.Ceiling);
|
|
41215
|
+
}
|
|
41216
|
+
catch (e) {
|
|
41217
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
|
|
41218
|
+
}
|
|
41219
|
+
}
|
|
41220
|
+
/*
|
|
41221
|
+
* Where one tile lands on the composite, snapped to whole pixels.
|
|
41222
|
+
*
|
|
41223
|
+
* Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
|
|
41224
|
+
* a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
|
|
41225
|
+
* background shows between them.
|
|
41226
|
+
*/
|
|
41227
|
+
tileRect(tile, target, size) {
|
|
41228
|
+
const spanLon = target.East - target.West;
|
|
41229
|
+
const spanLat = target.North - target.South;
|
|
41230
|
+
const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
|
|
41231
|
+
const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
|
|
41232
|
+
const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
|
|
41233
|
+
const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
|
|
41234
|
+
return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
|
|
41235
|
+
}
|
|
41236
|
+
/*
|
|
41237
|
+
* Decodes many tile images at once.
|
|
41238
|
+
*
|
|
41239
|
+
* Serially awaiting each decode makes a frame cost as many round trips through the image
|
|
41240
|
+
* decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
|
|
41241
|
+
*/
|
|
41242
|
+
decodeSlices(block, base, entries) {
|
|
41243
|
+
return Promise.all(entries.map(async (at) => {
|
|
41244
|
+
if (!at) {
|
|
41245
|
+
return null;
|
|
41246
|
+
}
|
|
41247
|
+
const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
|
|
41248
|
+
const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
|
|
41249
|
+
try {
|
|
41250
|
+
return await loadImage(objectUrl);
|
|
41251
|
+
}
|
|
41252
|
+
catch {
|
|
41253
|
+
return null;
|
|
41254
|
+
}
|
|
41255
|
+
finally {
|
|
41256
|
+
URL.revokeObjectURL(objectUrl);
|
|
41257
|
+
}
|
|
41258
|
+
}));
|
|
41259
|
+
}
|
|
41260
|
+
/*
|
|
41261
|
+
* Range GETs one entry and decodes it to an image.
|
|
41262
|
+
*/
|
|
41263
|
+
async loadImageAt(entry) {
|
|
41264
|
+
const start = entry.ByteOffset;
|
|
41265
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
41266
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
41267
|
+
const buffer = await response.arrayBuffer();
|
|
41268
|
+
const objectUrl = URL.createObjectURL(new Blob([buffer], { type: "image/png" }));
|
|
41269
|
+
try {
|
|
41270
|
+
return await loadImage(objectUrl);
|
|
41271
|
+
}
|
|
41272
|
+
finally {
|
|
41273
|
+
URL.revokeObjectURL(objectUrl);
|
|
41274
|
+
}
|
|
41275
|
+
}
|
|
41276
|
+
/*
|
|
41277
|
+
* Puts a static raster through the same resample the frames get.
|
|
41278
|
+
*/
|
|
41279
|
+
remap(image) {
|
|
41280
|
+
if (!image) {
|
|
41281
|
+
return null;
|
|
41282
|
+
}
|
|
41283
|
+
const rect = this.sourceRect({ width: image.width, height: image.height });
|
|
41284
|
+
if (!rect) {
|
|
41285
|
+
return image;
|
|
41286
|
+
}
|
|
41287
|
+
const from = document.createElement("canvas");
|
|
41288
|
+
from.width = image.width;
|
|
41289
|
+
from.height = image.height;
|
|
41290
|
+
from.getContext("2d").putImageData(image, 0, 0);
|
|
41291
|
+
const to = document.createElement("canvas");
|
|
41292
|
+
to.width = image.width;
|
|
41293
|
+
to.height = image.height;
|
|
41294
|
+
const ctx = to.getContext("2d");
|
|
41295
|
+
ctx.imageSmoothingEnabled = false;
|
|
41296
|
+
ctx.clearRect(0, 0, image.width, image.height);
|
|
41297
|
+
ctx.drawImage(from, rect.x, rect.y, rect.width, rect.height, 0, 0, image.width, image.height);
|
|
41298
|
+
return ctx.getImageData(0, 0, image.width, image.height);
|
|
41299
|
+
}
|
|
41300
|
+
/*
|
|
41301
|
+
* Range GETs one entry out of the blob and decodes it to pixels.
|
|
41302
|
+
*/
|
|
41303
|
+
async loadImageDataAt(entry) {
|
|
41304
|
+
const start = entry.ByteOffset;
|
|
41305
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
41306
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
41307
|
+
const buffer = await response.arrayBuffer();
|
|
41308
|
+
const blob = new Blob([buffer], { type: "image/png" });
|
|
41309
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
41310
|
+
let image;
|
|
41311
|
+
try {
|
|
41312
|
+
image = await loadImage(objectUrl);
|
|
41313
|
+
}
|
|
41314
|
+
finally {
|
|
41315
|
+
URL.revokeObjectURL(objectUrl);
|
|
41316
|
+
}
|
|
41317
|
+
const canvas = document.createElement("canvas");
|
|
41318
|
+
canvas.width = image.width;
|
|
41319
|
+
canvas.height = image.height;
|
|
41320
|
+
const ctx = canvas.getContext("2d");
|
|
41321
|
+
ctx.drawImage(image, 0, 0);
|
|
41322
|
+
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
41323
|
+
}
|
|
40624
41324
|
/**
|
|
40625
41325
|
* Fetches and decodes the archive's static baseline mask, at most once.
|
|
40626
41326
|
*/
|
|
40627
41327
|
ensureBaselineMask() {
|
|
41328
|
+
if (this.maskBaseline && this.tiles) {
|
|
41329
|
+
if (!this.baselineLoad) {
|
|
41330
|
+
this.baselineLoad = this.loadTiledBaselineMask();
|
|
41331
|
+
}
|
|
41332
|
+
return this.baselineLoad;
|
|
41333
|
+
}
|
|
40628
41334
|
if (!this.maskBaseline || !this.baselineMaskEntry) {
|
|
40629
41335
|
return Promise.resolve();
|
|
40630
41336
|
}
|
|
@@ -40737,24 +41443,17 @@ var TextureFrameSeriesAnimator;
|
|
|
40737
41443
|
presentPixels(pixels, dims, valuePixels) {
|
|
40738
41444
|
this.poolIdx = 1 - this.poolIdx;
|
|
40739
41445
|
const target = this.pool[this.poolIdx];
|
|
40740
|
-
target
|
|
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);
|
|
41446
|
+
this.writeInto(target, pixels, dims);
|
|
40746
41447
|
this.displayedPixels = pixels;
|
|
40747
41448
|
if (this.valueCanvas && valuePixels) {
|
|
40748
|
-
this.valueCanvas
|
|
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);
|
|
41449
|
+
this.writeInto(this.valueCanvas, valuePixels, dims);
|
|
40754
41450
|
this.displayedValuePixels = valuePixels;
|
|
40755
41451
|
this.valueDims = dims;
|
|
40756
41452
|
}
|
|
40757
41453
|
this.presentVersion++;
|
|
41454
|
+
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
|
|
41455
|
+
this.viewer.scene.requestRender();
|
|
41456
|
+
}
|
|
40758
41457
|
}
|
|
40759
41458
|
}
|
|
40760
41459
|
TextureFrameSeriesAnimator.Animator = Animator;
|
|
@@ -40766,6 +41465,688 @@ var TextureFrameSeriesAnimator;
|
|
|
40766
41465
|
}
|
|
40767
41466
|
})(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
|
|
40768
41467
|
|
|
41468
|
+
/**
|
|
41469
|
+
* Prints a value texture's own numbers over the cells they belong to.
|
|
41470
|
+
*/
|
|
41471
|
+
var TextureValueLabels;
|
|
41472
|
+
(function (TextureValueLabels) {
|
|
41473
|
+
// Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
|
|
41474
|
+
// distant view readable.
|
|
41475
|
+
const DEFAULT_MIN_CELL_PIXELS = 0;
|
|
41476
|
+
const MIN_VISIBLE_PIXELS = 60;
|
|
41477
|
+
const MAX_STRIDE = 512;
|
|
41478
|
+
const STRIDE_HYSTERESIS = 1.4;
|
|
41479
|
+
const DEFAULT_MIN_SPACING_PIXELS = 78;
|
|
41480
|
+
const DEFAULT_MAX_LABELS = 240;
|
|
41481
|
+
const LABEL_RADIUS = 4;
|
|
41482
|
+
const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
41483
|
+
// Clear space either side of a label box, so neighbours read as separate.
|
|
41484
|
+
const LABEL_GAP_PIXELS = 16;
|
|
41485
|
+
const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
|
|
41486
|
+
// 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
|
|
41487
|
+
// bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
|
|
41488
|
+
const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
|
|
41489
|
+
const DEFAULT_GRID_WIDTH_PIXELS = 1;
|
|
41490
|
+
// Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
|
|
41491
|
+
// whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
|
|
41492
|
+
// too tightly made the grid depend on which tile happened to be walked first.
|
|
41493
|
+
const MAX_GRID_CELLS = 40000;
|
|
41494
|
+
// Probes per side when hunting a labellable cell inside one lattice block.
|
|
41495
|
+
const MAX_LABEL_PROBES_PER_SIDE = 8;
|
|
41496
|
+
const TILE_OUTLINE_ALPHA = 0.45;
|
|
41497
|
+
// Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
|
|
41498
|
+
const MAX_GRID_STRIDE = 256;
|
|
41499
|
+
const LINE_HEIGHT = 13;
|
|
41500
|
+
/**
|
|
41501
|
+
* A clip outline from a projected ring, or null when the ring cannot be drawn.
|
|
41502
|
+
* @param projected one entry per ring vertex, null where the vertex has no window coordinate
|
|
41503
|
+
*/
|
|
41504
|
+
function ClipOutline(projected) {
|
|
41505
|
+
if (projected.length < 3) {
|
|
41506
|
+
return null;
|
|
41507
|
+
}
|
|
41508
|
+
const out = [];
|
|
41509
|
+
for (const at of projected) {
|
|
41510
|
+
if (!at) {
|
|
41511
|
+
return null;
|
|
41512
|
+
}
|
|
41513
|
+
out.push(at);
|
|
41514
|
+
}
|
|
41515
|
+
return out;
|
|
41516
|
+
}
|
|
41517
|
+
TextureValueLabels.ClipOutline = ClipOutline;
|
|
41518
|
+
/**
|
|
41519
|
+
* How many cells apart labels should sit, as a power of two.
|
|
41520
|
+
* @param options cellPixels is one cell's size on screen, held is the stride already in use
|
|
41521
|
+
*/
|
|
41522
|
+
function ChooseLabelStride(options) {
|
|
41523
|
+
const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
|
|
41524
|
+
const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
|
|
41525
|
+
&& Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
|
|
41526
|
+
let stride = 1;
|
|
41527
|
+
while (stride < MAX_STRIDE && !fits(stride, 1)) {
|
|
41528
|
+
stride *= 2;
|
|
41529
|
+
}
|
|
41530
|
+
const held = options.held;
|
|
41531
|
+
if (held && fits(held, STRIDE_HYSTERESIS)) {
|
|
41532
|
+
if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
|
|
41533
|
+
return held;
|
|
41534
|
+
}
|
|
41535
|
+
if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
|
|
41536
|
+
return held;
|
|
41537
|
+
}
|
|
41538
|
+
}
|
|
41539
|
+
return stride;
|
|
41540
|
+
}
|
|
41541
|
+
TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
|
|
41542
|
+
class Labels {
|
|
41543
|
+
constructor(options) {
|
|
41544
|
+
this.source = null;
|
|
41545
|
+
this.painted = null;
|
|
41546
|
+
this.floor = null;
|
|
41547
|
+
this.ceiling = null;
|
|
41548
|
+
this.disposed = false;
|
|
41549
|
+
this.drawn = 0;
|
|
41550
|
+
this.stride = 0;
|
|
41551
|
+
this.viewer = options.viewer;
|
|
41552
|
+
this.extent = options.extent;
|
|
41553
|
+
this.settings = options.settings || {};
|
|
41554
|
+
this.metadata = options.metadata;
|
|
41555
|
+
this.tiles = options.tiles;
|
|
41556
|
+
this.clipRing = options.clipRing;
|
|
41557
|
+
this.canvas = document.createElement("canvas");
|
|
41558
|
+
this.canvas.style.position = "absolute";
|
|
41559
|
+
this.canvas.style.inset = "0";
|
|
41560
|
+
this.canvas.style.pointerEvents = "none";
|
|
41561
|
+
const parent = this.viewer.canvas.parentElement;
|
|
41562
|
+
if (parent) {
|
|
41563
|
+
parent.appendChild(this.canvas);
|
|
41564
|
+
}
|
|
41565
|
+
this.ctx = this.canvas.getContext("2d");
|
|
41566
|
+
}
|
|
41567
|
+
SetExtent(extent) {
|
|
41568
|
+
this.extent = extent;
|
|
41569
|
+
}
|
|
41570
|
+
/**
|
|
41571
|
+
* The current frame's decoded values, as the animator's value canvas already provides them.
|
|
41572
|
+
* @param source value pixels, one texel per cell
|
|
41573
|
+
*/
|
|
41574
|
+
SetSource(source) {
|
|
41575
|
+
this.source = source;
|
|
41576
|
+
}
|
|
41577
|
+
/**
|
|
41578
|
+
* The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
|
|
41579
|
+
* @param painted RGBA of the presented frame, or null to fall back to coverage alone
|
|
41580
|
+
*/
|
|
41581
|
+
SetPainted(painted) {
|
|
41582
|
+
this.painted = painted;
|
|
41583
|
+
}
|
|
41584
|
+
/**
|
|
41585
|
+
* The archive's per-cell extremes, when it publishes them. Without these a label can still
|
|
41586
|
+
* print the current reading, just not the range behind it.
|
|
41587
|
+
*/
|
|
41588
|
+
SetExtremes(floor, ceiling) {
|
|
41589
|
+
this.floor = floor;
|
|
41590
|
+
this.ceiling = ceiling;
|
|
41591
|
+
}
|
|
41592
|
+
GetDrawnCount() {
|
|
41593
|
+
return this.drawn;
|
|
41594
|
+
}
|
|
41595
|
+
Clear() {
|
|
41596
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
41597
|
+
this.drawn = 0;
|
|
41598
|
+
}
|
|
41599
|
+
Dispose() {
|
|
41600
|
+
this.disposed = true;
|
|
41601
|
+
if (this.canvas.parentElement) {
|
|
41602
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
41603
|
+
}
|
|
41604
|
+
}
|
|
41605
|
+
/**
|
|
41606
|
+
* Redraws every visible label. Cheap enough to call per rendered frame, because the cell
|
|
41607
|
+
* count it considers is bounded by the view rather than by the texture.
|
|
41608
|
+
*/
|
|
41609
|
+
Render() {
|
|
41610
|
+
var _a, _b, _c;
|
|
41611
|
+
if (this.disposed || !this.source) {
|
|
41612
|
+
return;
|
|
41613
|
+
}
|
|
41614
|
+
const view = this.viewer.canvas;
|
|
41615
|
+
if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
|
|
41616
|
+
this.canvas.width = view.clientWidth;
|
|
41617
|
+
this.canvas.height = view.clientHeight;
|
|
41618
|
+
}
|
|
41619
|
+
this.Clear();
|
|
41620
|
+
const cols = this.source.width;
|
|
41621
|
+
const rows = this.source.height;
|
|
41622
|
+
const window = this.visibleTexels(cols, rows);
|
|
41623
|
+
if (!window) {
|
|
41624
|
+
return;
|
|
41625
|
+
}
|
|
41626
|
+
const midCol = (window.col0 + window.col1) / 2;
|
|
41627
|
+
const midRow = (window.row0 + window.row1) / 2;
|
|
41628
|
+
const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
|
|
41629
|
+
const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
|
|
41630
|
+
if (!a || !b) {
|
|
41631
|
+
return;
|
|
41632
|
+
}
|
|
41633
|
+
const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
|
|
41634
|
+
// Ahead of the label threshold on purpose: a grid still reads at a cell size where a
|
|
41635
|
+
// number no longer fits.
|
|
41636
|
+
this.drawGrid(window, cols, rows, cellPixels);
|
|
41637
|
+
if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
|
|
41638
|
+
return;
|
|
41639
|
+
}
|
|
41640
|
+
const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
|
|
41641
|
+
const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
|
|
41642
|
+
if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
|
|
41643
|
+
return;
|
|
41644
|
+
}
|
|
41645
|
+
const parts = this.settings.parts || ["value", "floor", "ceiling"];
|
|
41646
|
+
this.ctx.font = LABEL_FONT;
|
|
41647
|
+
this.ctx.textAlign = "center";
|
|
41648
|
+
const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
|
|
41649
|
+
this.stride = ChooseLabelStride({
|
|
41650
|
+
cellPixels,
|
|
41651
|
+
visibleCols: window.col1 - window.col0 + 1,
|
|
41652
|
+
visibleRows: window.row1 - window.row0 + 1,
|
|
41653
|
+
maxLabels,
|
|
41654
|
+
// Measured, not assumed: the spacing that keeps labels apart is whatever the widest
|
|
41655
|
+
// one is, so changing what a label says cannot quietly start overlapping them.
|
|
41656
|
+
minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
|
|
41657
|
+
held: this.stride
|
|
41658
|
+
});
|
|
41659
|
+
const stride = this.stride;
|
|
41660
|
+
const firstRow = Math.floor(window.row0 / stride) * stride;
|
|
41661
|
+
const firstCol = Math.floor(window.col0 / stride) * stride;
|
|
41662
|
+
for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
|
|
41663
|
+
for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
|
|
41664
|
+
const found = this.readNear(col, row, cols, rows, stride);
|
|
41665
|
+
if (!found) {
|
|
41666
|
+
continue;
|
|
41667
|
+
}
|
|
41668
|
+
const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
|
|
41669
|
+
if (!point || point.x < 0 || point.y < 0
|
|
41670
|
+
|| point.x > this.canvas.width || point.y > this.canvas.height) {
|
|
41671
|
+
continue;
|
|
41672
|
+
}
|
|
41673
|
+
this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
|
|
41674
|
+
}
|
|
41675
|
+
}
|
|
41676
|
+
}
|
|
41677
|
+
/*
|
|
41678
|
+
* Outlines each covered cell, from a projected lattice of its corners.
|
|
41679
|
+
*/
|
|
41680
|
+
drawGrid(window, cols, rows, cellPixels) {
|
|
41681
|
+
var _a, _b;
|
|
41682
|
+
const color = this.settings.gridColor;
|
|
41683
|
+
if (!color) {
|
|
41684
|
+
return;
|
|
41685
|
+
}
|
|
41686
|
+
if (this.tiles && this.tiles.length) {
|
|
41687
|
+
this.ctx.save();
|
|
41688
|
+
this.applyRingClip();
|
|
41689
|
+
this.drawTileGrid(color);
|
|
41690
|
+
this.ctx.restore();
|
|
41691
|
+
return;
|
|
41692
|
+
}
|
|
41693
|
+
if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
|
|
41694
|
+
return;
|
|
41695
|
+
}
|
|
41696
|
+
// Corners are shared by four cells, so projecting the lattice once rather than per cell
|
|
41697
|
+
// cuts the work by about four and keeps neighbouring outlines exactly coincident.
|
|
41698
|
+
const wide = window.col1 - window.col0 + 1;
|
|
41699
|
+
const high = window.row1 - window.row0 + 1;
|
|
41700
|
+
if (wide * high > MAX_GRID_CELLS) {
|
|
41701
|
+
return;
|
|
41702
|
+
}
|
|
41703
|
+
const lattice = new Array((wide + 1) * (high + 1));
|
|
41704
|
+
for (let r = 0; r <= high; r++) {
|
|
41705
|
+
for (let c = 0; c <= wide; c++) {
|
|
41706
|
+
// Corners sit half a texel out from the centres lonAt/latAt return.
|
|
41707
|
+
const lon = this.lonAt(window.col0 + c - 0.5, cols);
|
|
41708
|
+
const lat = this.latAt(window.row0 + r - 0.5, rows);
|
|
41709
|
+
lattice[r * (wide + 1) + c] = this.project(lon, lat);
|
|
41710
|
+
}
|
|
41711
|
+
}
|
|
41712
|
+
const path = new Path2D();
|
|
41713
|
+
let drew = false;
|
|
41714
|
+
for (let r = 0; r < high; r++) {
|
|
41715
|
+
for (let c = 0; c < wide; c++) {
|
|
41716
|
+
if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
|
|
41717
|
+
continue;
|
|
41718
|
+
}
|
|
41719
|
+
const tl = lattice[r * (wide + 1) + c];
|
|
41720
|
+
const tr = lattice[r * (wide + 1) + c + 1];
|
|
41721
|
+
const bl = lattice[(r + 1) * (wide + 1) + c];
|
|
41722
|
+
const br = lattice[(r + 1) * (wide + 1) + c + 1];
|
|
41723
|
+
if (!tl || !tr || !bl || !br) {
|
|
41724
|
+
continue;
|
|
41725
|
+
}
|
|
41726
|
+
path.moveTo(tl.x, tl.y);
|
|
41727
|
+
path.lineTo(tr.x, tr.y);
|
|
41728
|
+
path.lineTo(br.x, br.y);
|
|
41729
|
+
path.lineTo(bl.x, bl.y);
|
|
41730
|
+
path.closePath();
|
|
41731
|
+
drew = true;
|
|
41732
|
+
}
|
|
41733
|
+
}
|
|
41734
|
+
if (!drew) {
|
|
41735
|
+
return;
|
|
41736
|
+
}
|
|
41737
|
+
this.ctx.strokeStyle = color;
|
|
41738
|
+
this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41739
|
+
this.ctx.stroke(path);
|
|
41740
|
+
+" alpha" + this.ctx.globalAlpha;
|
|
41741
|
+
}
|
|
41742
|
+
/*
|
|
41743
|
+
* Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
|
|
41744
|
+
*/
|
|
41745
|
+
applyRingClip() {
|
|
41746
|
+
const ring = this.clipRing;
|
|
41747
|
+
if (!ring || ring.length < 3) {
|
|
41748
|
+
return;
|
|
41749
|
+
}
|
|
41750
|
+
const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
|
|
41751
|
+
if (!outline) {
|
|
41752
|
+
return;
|
|
41753
|
+
}
|
|
41754
|
+
const path = new Path2D();
|
|
41755
|
+
path.moveTo(outline[0].x, outline[0].y);
|
|
41756
|
+
for (let i = 1; i < outline.length; i++) {
|
|
41757
|
+
path.lineTo(outline[i].x, outline[i].y);
|
|
41758
|
+
}
|
|
41759
|
+
path.closePath();
|
|
41760
|
+
this.ctx.clip(path);
|
|
41761
|
+
}
|
|
41762
|
+
/*
|
|
41763
|
+
* The cell of whichever tile covers a point, as screen corners.
|
|
41764
|
+
*/
|
|
41765
|
+
tileCellOutline(lon, lat) {
|
|
41766
|
+
for (const tile of this.tiles) {
|
|
41767
|
+
if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
|
|
41768
|
+
continue;
|
|
41769
|
+
}
|
|
41770
|
+
const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
|
|
41771
|
+
const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
|
|
41772
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41773
|
+
const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41774
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41775
|
+
const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41776
|
+
const corners = [
|
|
41777
|
+
this.project(w0, n0), this.project(w1, n0),
|
|
41778
|
+
this.project(w1, n1), this.project(w0, n1)
|
|
41779
|
+
];
|
|
41780
|
+
return corners.every((q) => q) ? corners : null;
|
|
41781
|
+
}
|
|
41782
|
+
return null;
|
|
41783
|
+
}
|
|
41784
|
+
/*
|
|
41785
|
+
* Outlines each tile's own cells, so cell size still shows where the mesh is refined.
|
|
41786
|
+
*/
|
|
41787
|
+
drawTileGrid(color) {
|
|
41788
|
+
var _a, _b, _c;
|
|
41789
|
+
const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
|
|
41790
|
+
if (!view) {
|
|
41791
|
+
return;
|
|
41792
|
+
}
|
|
41793
|
+
const west = Math$1.toDegrees(view.west);
|
|
41794
|
+
const east = Math$1.toDegrees(view.east);
|
|
41795
|
+
const south = Math$1.toDegrees(view.south);
|
|
41796
|
+
const north = Math$1.toDegrees(view.north);
|
|
41797
|
+
const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
|
|
41798
|
+
const focus = this.groundAtScreenCentre();
|
|
41799
|
+
const path = new Path2D();
|
|
41800
|
+
// A tile too fine to rule, or one the budget cannot afford, contributes its own outline
|
|
41801
|
+
// instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
|
|
41802
|
+
const coarse = new Path2D();
|
|
41803
|
+
let drew = false;
|
|
41804
|
+
let drewCoarse = false;
|
|
41805
|
+
const outlineTile = (tile) => {
|
|
41806
|
+
const tl = this.project(tile.West, tile.North);
|
|
41807
|
+
const tr = this.project(tile.East, tile.North);
|
|
41808
|
+
const br = this.project(tile.East, tile.South);
|
|
41809
|
+
const bl = this.project(tile.West, tile.South);
|
|
41810
|
+
if (!tl || !tr || !br || !bl) {
|
|
41811
|
+
return;
|
|
41812
|
+
}
|
|
41813
|
+
coarse.moveTo(tl.x, tl.y);
|
|
41814
|
+
coarse.lineTo(tr.x, tr.y);
|
|
41815
|
+
coarse.lineTo(br.x, br.y);
|
|
41816
|
+
coarse.lineTo(bl.x, bl.y);
|
|
41817
|
+
coarse.closePath();
|
|
41818
|
+
drewCoarse = true;
|
|
41819
|
+
};
|
|
41820
|
+
// Planned for every visible tile BEFORE any is drawn.
|
|
41821
|
+
const planned = [];
|
|
41822
|
+
for (const tile of this.tiles) {
|
|
41823
|
+
if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
|
|
41824
|
+
continue;
|
|
41825
|
+
}
|
|
41826
|
+
const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
|
|
41827
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41828
|
+
const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
|
|
41829
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41830
|
+
const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
|
|
41831
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41832
|
+
const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
|
|
41833
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41834
|
+
if (c1 <= c0 || r1 <= r0) {
|
|
41835
|
+
continue;
|
|
41836
|
+
}
|
|
41837
|
+
const focusC = focus
|
|
41838
|
+
? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
|
|
41839
|
+
: Math.floor((c0 + c1) / 2);
|
|
41840
|
+
const focusR = focus
|
|
41841
|
+
? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
|
|
41842
|
+
: Math.floor((r0 + r1) / 2);
|
|
41843
|
+
const midC = Math.min(c1 - 1, Math.max(c0, focusC));
|
|
41844
|
+
const midR = Math.min(r1 - 1, Math.max(r0, focusR));
|
|
41845
|
+
const cellLon = (tile.East - tile.West) / tile.ResolutionX;
|
|
41846
|
+
const cellLat = (tile.North - tile.South) / tile.ResolutionY;
|
|
41847
|
+
const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
|
|
41848
|
+
const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
|
|
41849
|
+
if (!at || !next) {
|
|
41850
|
+
outlineTile(tile);
|
|
41851
|
+
continue;
|
|
41852
|
+
}
|
|
41853
|
+
const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
|
|
41854
|
+
let step = 1;
|
|
41855
|
+
while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
|
|
41856
|
+
step *= 2;
|
|
41857
|
+
}
|
|
41858
|
+
if (onScreen * step < minCell) {
|
|
41859
|
+
outlineTile(tile);
|
|
41860
|
+
continue;
|
|
41861
|
+
}
|
|
41862
|
+
planned.push({ tile, c0, c1, r0, r1, step });
|
|
41863
|
+
}
|
|
41864
|
+
// Coarsened together rather than dropped one by one, so a busy view thins out evenly.
|
|
41865
|
+
const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
|
|
41866
|
+
let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41867
|
+
while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
|
|
41868
|
+
for (const p of planned) {
|
|
41869
|
+
p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
|
|
41870
|
+
}
|
|
41871
|
+
total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41872
|
+
}
|
|
41873
|
+
for (const { tile, c0, c1, r0, r1, step } of planned) {
|
|
41874
|
+
// Anchored to the tile's own origin so lines stay on the same boundaries as the camera
|
|
41875
|
+
// moves, and so each step up is a subset of the one below it.
|
|
41876
|
+
const firstR = Math.floor(r0 / step) * step;
|
|
41877
|
+
const firstC = Math.floor(c0 / step) * step;
|
|
41878
|
+
for (let r = firstR; r < r1; r += step) {
|
|
41879
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41880
|
+
const n1 = tile.North
|
|
41881
|
+
- (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41882
|
+
for (let c = firstC; c < c1; c += step) {
|
|
41883
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41884
|
+
const w1 = tile.West
|
|
41885
|
+
+ (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41886
|
+
if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
|
|
41887
|
+
continue;
|
|
41888
|
+
}
|
|
41889
|
+
const tl = this.project(w0, n0);
|
|
41890
|
+
const tr = this.project(w1, n0);
|
|
41891
|
+
const br = this.project(w1, n1);
|
|
41892
|
+
const bl = this.project(w0, n1);
|
|
41893
|
+
if (!tl || !tr || !br || !bl) {
|
|
41894
|
+
continue;
|
|
41895
|
+
}
|
|
41896
|
+
path.moveTo(tl.x, tl.y);
|
|
41897
|
+
path.lineTo(tr.x, tr.y);
|
|
41898
|
+
path.lineTo(br.x, br.y);
|
|
41899
|
+
path.lineTo(bl.x, bl.y);
|
|
41900
|
+
path.closePath();
|
|
41901
|
+
drew = true;
|
|
41902
|
+
}
|
|
41903
|
+
}
|
|
41904
|
+
}
|
|
41905
|
+
const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41906
|
+
if (drewCoarse) {
|
|
41907
|
+
this.ctx.save();
|
|
41908
|
+
// Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
|
|
41909
|
+
// rather than as a cell in its own right.
|
|
41910
|
+
this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
|
|
41911
|
+
this.ctx.strokeStyle = color;
|
|
41912
|
+
this.ctx.lineWidth = gridWidth;
|
|
41913
|
+
this.ctx.stroke(coarse);
|
|
41914
|
+
this.ctx.restore();
|
|
41915
|
+
}
|
|
41916
|
+
if (!drew) {
|
|
41917
|
+
return;
|
|
41918
|
+
}
|
|
41919
|
+
this.ctx.strokeStyle = color;
|
|
41920
|
+
this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41921
|
+
this.ctx.stroke(path);
|
|
41922
|
+
}
|
|
41923
|
+
/*
|
|
41924
|
+
* Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
|
|
41925
|
+
*/
|
|
41926
|
+
groundAtScreenCentre() {
|
|
41927
|
+
const scene = this.viewer.scene;
|
|
41928
|
+
const centre = new Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
|
|
41929
|
+
const hit = scene.camera.pickEllipsoid(centre, Ellipsoid.WGS84);
|
|
41930
|
+
if (!hit) {
|
|
41931
|
+
return null;
|
|
41932
|
+
}
|
|
41933
|
+
const carto = Cartographic.fromCartesian(hit);
|
|
41934
|
+
return {
|
|
41935
|
+
lon: Math$1.toDegrees(carto.longitude),
|
|
41936
|
+
lat: Math$1.toDegrees(carto.latitude)
|
|
41937
|
+
};
|
|
41938
|
+
}
|
|
41939
|
+
/*
|
|
41940
|
+
* Whether the composited raster holds data at a lon/lat.
|
|
41941
|
+
*/
|
|
41942
|
+
coveredAt(lon, lat) {
|
|
41943
|
+
if (!this.source) {
|
|
41944
|
+
return false;
|
|
41945
|
+
}
|
|
41946
|
+
const cols = this.source.width;
|
|
41947
|
+
const rows = this.source.height;
|
|
41948
|
+
const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
|
|
41949
|
+
const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
|
|
41950
|
+
if (col < 0 || row < 0 || col >= cols || row >= rows) {
|
|
41951
|
+
return false;
|
|
41952
|
+
}
|
|
41953
|
+
const at = (row * cols + col) * 4 + 3;
|
|
41954
|
+
// Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
|
|
41955
|
+
// ruling a grid over it draws detail where the picture shows none.
|
|
41956
|
+
if (this.painted && this.painted.length === cols * rows * 4) {
|
|
41957
|
+
return this.painted[at] > 0;
|
|
41958
|
+
}
|
|
41959
|
+
return this.source.data[at] >= 128;
|
|
41960
|
+
}
|
|
41961
|
+
isCovered(col, row, cols) {
|
|
41962
|
+
return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
|
|
41963
|
+
}
|
|
41964
|
+
/*
|
|
41965
|
+
* Width of the widest label this overlay could draw, in pixels.
|
|
41966
|
+
*/
|
|
41967
|
+
widestLabel(parts) {
|
|
41968
|
+
const sample = [];
|
|
41969
|
+
if (parts.indexOf("value") >= 0) {
|
|
41970
|
+
sample.push("-000.00");
|
|
41971
|
+
}
|
|
41972
|
+
const range = [];
|
|
41973
|
+
if (parts.indexOf("floor") >= 0) {
|
|
41974
|
+
range.push("min: -000.00");
|
|
41975
|
+
}
|
|
41976
|
+
if (parts.indexOf("ceiling") >= 0) {
|
|
41977
|
+
range.push("max: -000.00");
|
|
41978
|
+
}
|
|
41979
|
+
if (range.length) {
|
|
41980
|
+
sample.push(range.join(" - "));
|
|
41981
|
+
}
|
|
41982
|
+
if (!sample.length) {
|
|
41983
|
+
return 0;
|
|
41984
|
+
}
|
|
41985
|
+
const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
|
|
41986
|
+
return widest + LABEL_GAP_PIXELS;
|
|
41987
|
+
}
|
|
41988
|
+
/*
|
|
41989
|
+
* The four screen corners of one cell, or null if any of them will not project.
|
|
41990
|
+
*/
|
|
41991
|
+
cellOutline(col, row, cols, rows) {
|
|
41992
|
+
// On a pyramid the composited texel is not a cell. Outlining it would draw a second,
|
|
41993
|
+
// uniform lattice over the tile grid, misaligned with it everywhere.
|
|
41994
|
+
if (this.tiles && this.tiles.length) {
|
|
41995
|
+
return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
|
|
41996
|
+
}
|
|
41997
|
+
const corners = [
|
|
41998
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
41999
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
42000
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
|
|
42001
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
|
|
42002
|
+
];
|
|
42003
|
+
return corners.every((c) => c) ? corners : null;
|
|
42004
|
+
}
|
|
42005
|
+
drawLabel(point, reading, parts, outline) {
|
|
42006
|
+
var _a;
|
|
42007
|
+
const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
|
|
42008
|
+
const lines = [];
|
|
42009
|
+
if (parts.indexOf("value") >= 0) {
|
|
42010
|
+
lines.push(reading.value.toFixed(dp));
|
|
42011
|
+
}
|
|
42012
|
+
// Named rather than a bare pair: two numbers under a third say nothing about which is
|
|
42013
|
+
// the series low and which is its high.
|
|
42014
|
+
const range = [];
|
|
42015
|
+
if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
|
|
42016
|
+
range.push(`min: ${reading.floor.toFixed(dp)}`);
|
|
42017
|
+
}
|
|
42018
|
+
if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
|
|
42019
|
+
range.push(`max: ${reading.ceiling.toFixed(dp)}`);
|
|
42020
|
+
}
|
|
42021
|
+
if (range.length) {
|
|
42022
|
+
lines.push(range.join(" - "));
|
|
42023
|
+
}
|
|
42024
|
+
if (!lines.length) {
|
|
42025
|
+
return;
|
|
42026
|
+
}
|
|
42027
|
+
if (outline) {
|
|
42028
|
+
this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
|
|
42029
|
+
this.ctx.lineWidth = 1.5;
|
|
42030
|
+
this.ctx.beginPath();
|
|
42031
|
+
this.ctx.moveTo(outline[0].x, outline[0].y);
|
|
42032
|
+
for (let i = 1; i < outline.length; i++) {
|
|
42033
|
+
this.ctx.lineTo(outline[i].x, outline[i].y);
|
|
42034
|
+
}
|
|
42035
|
+
this.ctx.closePath();
|
|
42036
|
+
this.ctx.stroke();
|
|
42037
|
+
}
|
|
42038
|
+
const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
|
|
42039
|
+
const height = lines.length * LINE_HEIGHT + 6;
|
|
42040
|
+
const top = point.y - height / 2;
|
|
42041
|
+
this.ctx.fillStyle = "rgba(16,16,14,0.72)";
|
|
42042
|
+
this.ctx.beginPath();
|
|
42043
|
+
this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
|
|
42044
|
+
this.ctx.fill();
|
|
42045
|
+
lines.forEach((line, i) => {
|
|
42046
|
+
this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
|
|
42047
|
+
this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
|
|
42048
|
+
});
|
|
42049
|
+
this.drawn++;
|
|
42050
|
+
}
|
|
42051
|
+
/*
|
|
42052
|
+
* The first cell with a reading at or near a lattice point, searched within its own block.
|
|
42053
|
+
*/
|
|
42054
|
+
readNear(col, row, cols, rows, stride) {
|
|
42055
|
+
const direct = this.readAt(col, row, cols);
|
|
42056
|
+
if (direct && this.paintedAt(col, row, cols)) {
|
|
42057
|
+
return { col, row, reading: direct };
|
|
42058
|
+
}
|
|
42059
|
+
if (stride <= 1) {
|
|
42060
|
+
return null;
|
|
42061
|
+
}
|
|
42062
|
+
// Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
|
|
42063
|
+
// expensive part of a frame.
|
|
42064
|
+
const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
|
|
42065
|
+
for (let r = row; r < Math.min(rows, row + stride); r += step) {
|
|
42066
|
+
for (let c = col; c < Math.min(cols, col + stride); c += step) {
|
|
42067
|
+
const reading = this.readAt(c, r, cols);
|
|
42068
|
+
if (reading && this.paintedAt(c, r, cols)) {
|
|
42069
|
+
return { col: c, row: r, reading };
|
|
42070
|
+
}
|
|
42071
|
+
}
|
|
42072
|
+
}
|
|
42073
|
+
return null;
|
|
42074
|
+
}
|
|
42075
|
+
paintedAt(col, row, cols) {
|
|
42076
|
+
if (!this.painted) {
|
|
42077
|
+
return true;
|
|
42078
|
+
}
|
|
42079
|
+
const at = (row * cols + col) * 4 + 3;
|
|
42080
|
+
return this.painted[at] > 0;
|
|
42081
|
+
}
|
|
42082
|
+
// Decodes one cell, skipping anything the texture marks as having no data.
|
|
42083
|
+
readAt(col, row, cols) {
|
|
42084
|
+
var _a, _b;
|
|
42085
|
+
const i = (row * cols + col) * 4;
|
|
42086
|
+
if (!this.source || this.source.data[i + 3] < 128) {
|
|
42087
|
+
return null;
|
|
42088
|
+
}
|
|
42089
|
+
const lo = this.metadata && typeof this.metadata.ValueMin === "number" ? this.metadata.ValueMin : 0;
|
|
42090
|
+
const hi = this.metadata && typeof this.metadata.ValueMax === "number" ? this.metadata.ValueMax : 1;
|
|
42091
|
+
const value = lo + (this.source.data[i] / 255) * (hi - lo);
|
|
42092
|
+
const reading = { value };
|
|
42093
|
+
const eLo = (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.ExtremesValueMin;
|
|
42094
|
+
const eHi = (_b = this.metadata) === null || _b === void 0 ? void 0 : _b.ExtremesValueMax;
|
|
42095
|
+
if (this.floor && typeof eLo === "number" && typeof eHi === "number") {
|
|
42096
|
+
reading.floor = eLo + decode16(this.floor.data, i) * (eHi - eLo);
|
|
42097
|
+
// An anomaly archive's frames are already floor-relative, so the reading a user
|
|
42098
|
+
// wants is the absolute one. This is the whole reason the floor is published.
|
|
42099
|
+
if (TextureFrameSeriesAnimator.IsAnomalyMeasure(this.metadata)) {
|
|
42100
|
+
reading.value += reading.floor;
|
|
42101
|
+
}
|
|
42102
|
+
}
|
|
42103
|
+
if (this.ceiling && typeof eLo === "number" && typeof eHi === "number") {
|
|
42104
|
+
reading.ceiling = eLo + decode16(this.ceiling.data, i) * (eHi - eLo);
|
|
42105
|
+
}
|
|
42106
|
+
return reading;
|
|
42107
|
+
}
|
|
42108
|
+
lonAt(col, cols) {
|
|
42109
|
+
return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
|
|
42110
|
+
}
|
|
42111
|
+
latAt(row, rows) {
|
|
42112
|
+
return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
|
|
42113
|
+
}
|
|
42114
|
+
// The texel range overlapping the camera's view, so an off-screen texture costs nothing.
|
|
42115
|
+
visibleTexels(cols, rows) {
|
|
42116
|
+
const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
|
|
42117
|
+
if (!view) {
|
|
42118
|
+
return null;
|
|
42119
|
+
}
|
|
42120
|
+
const west = Math.max(this.extent.West, Math$1.toDegrees(view.west));
|
|
42121
|
+
const east = Math.min(this.extent.East, Math$1.toDegrees(view.east));
|
|
42122
|
+
const south = Math.max(this.extent.South, Math$1.toDegrees(view.south));
|
|
42123
|
+
const north = Math.min(this.extent.North, Math$1.toDegrees(view.north));
|
|
42124
|
+
if (west >= east || south >= north) {
|
|
42125
|
+
return null;
|
|
42126
|
+
}
|
|
42127
|
+
const spanLon = this.extent.East - this.extent.West;
|
|
42128
|
+
const spanLat = this.extent.North - this.extent.South;
|
|
42129
|
+
return {
|
|
42130
|
+
col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
|
|
42131
|
+
col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
|
|
42132
|
+
row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
|
|
42133
|
+
row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
|
|
42134
|
+
};
|
|
42135
|
+
}
|
|
42136
|
+
project(lon, lat) {
|
|
42137
|
+
const world = Cartesian3.fromDegrees(lon, lat, 0);
|
|
42138
|
+
const transforms = SceneTransforms;
|
|
42139
|
+
// Renamed in newer Cesium, and this library runs on whatever the host app supplies.
|
|
42140
|
+
const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
|
|
42141
|
+
return fn ? fn.call(transforms, this.viewer.scene, world) : null;
|
|
42142
|
+
}
|
|
42143
|
+
}
|
|
42144
|
+
TextureValueLabels.Labels = Labels;
|
|
42145
|
+
function decode16(data, i) {
|
|
42146
|
+
return (data[i] * 256 + data[i + 1]) / 65535;
|
|
42147
|
+
}
|
|
42148
|
+
})(TextureValueLabels || (TextureValueLabels = {}));
|
|
42149
|
+
|
|
40769
42150
|
/**
|
|
40770
42151
|
* Renders a value texture as a displaced, coloured surface.
|
|
40771
42152
|
* The source canvas is expected as RGBA: value in R, coverage in A.
|
|
@@ -40788,6 +42169,9 @@ var DisplacedSurfacePrimitive;
|
|
|
40788
42169
|
const COVERAGE_CUTOFF = 0.35;
|
|
40789
42170
|
// With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
|
|
40790
42171
|
const AUTO_RELIEF_FRACTION = 0.02;
|
|
42172
|
+
// Ceiling on the derived relief. Sizing purely off the extent is fine for a parcel and absurd for
|
|
42173
|
+
// a whole estuary: 4 m of water over an 82 km extent came out 1.6 km tall.
|
|
42174
|
+
const AUTO_MAX_RELIEF_METRES = 250.0;
|
|
40791
42175
|
// Terrain is sampled on this grid and interpolated between samples.
|
|
40792
42176
|
// Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
|
|
40793
42177
|
const GROUND_SAMPLES_PER_SIDE = 64;
|
|
@@ -40866,8 +42250,7 @@ void main() {
|
|
|
40866
42250
|
`;
|
|
40867
42251
|
const FRAGMENT_SHADER_GLSL300 = `
|
|
40868
42252
|
uniform sampler2D u_valueTexture;
|
|
40869
|
-
uniform
|
|
40870
|
-
uniform vec4 u_highColor;
|
|
42253
|
+
uniform sampler2D u_rampTexture;
|
|
40871
42254
|
uniform float u_coverageCutoff;
|
|
40872
42255
|
uniform vec2 u_texelSize;
|
|
40873
42256
|
uniform float u_valueRange;
|
|
@@ -40882,7 +42265,13 @@ void main() {
|
|
|
40882
42265
|
if (v_coverage < u_coverageCutoff) {
|
|
40883
42266
|
discard;
|
|
40884
42267
|
}
|
|
40885
|
-
|
|
42268
|
+
// The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
|
|
42269
|
+
// hidden floor reads the same here as it does on the flat drape.
|
|
42270
|
+
// Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
|
|
42271
|
+
vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
|
|
42272
|
+
if (color.a <= 0.0) {
|
|
42273
|
+
discard;
|
|
42274
|
+
}
|
|
40886
42275
|
|
|
40887
42276
|
// Relief shading from the value gradient, so the displacement reads at a distance without
|
|
40888
42277
|
// recomputing vertex normals every time the texture changes.
|
|
@@ -41081,6 +42470,7 @@ void main() {
|
|
|
41081
42470
|
this.show = true;
|
|
41082
42471
|
this.valueSource = null;
|
|
41083
42472
|
this.lastPresentVersion = -1;
|
|
42473
|
+
this.rampTexture = null;
|
|
41084
42474
|
this.texture = null;
|
|
41085
42475
|
this.textureDirty = true;
|
|
41086
42476
|
this.shaderProgram = null;
|
|
@@ -41102,8 +42492,7 @@ void main() {
|
|
|
41102
42492
|
this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
|
|
41103
42493
|
this.pixelPlacement = options.pixelPlacement;
|
|
41104
42494
|
this.tileSkirts = Boolean(options.tileSkirts);
|
|
41105
|
-
this.
|
|
41106
|
-
this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
|
|
42495
|
+
this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
|
|
41107
42496
|
this.tiles = buildTiles(hasCoverage);
|
|
41108
42497
|
}
|
|
41109
42498
|
GetFollowsGround() {
|
|
@@ -41162,6 +42551,7 @@ void main() {
|
|
|
41162
42551
|
return;
|
|
41163
42552
|
}
|
|
41164
42553
|
this.syncTexture(context);
|
|
42554
|
+
this.syncRampTexture(context);
|
|
41165
42555
|
this.requestGround();
|
|
41166
42556
|
this.syncGroundTexture(context);
|
|
41167
42557
|
// A tile needs one mesh before it has a bounding sphere to measure against.
|
|
@@ -41200,6 +42590,10 @@ void main() {
|
|
|
41200
42590
|
this.texture.destroy();
|
|
41201
42591
|
this.texture = null;
|
|
41202
42592
|
}
|
|
42593
|
+
if (this.rampTexture) {
|
|
42594
|
+
this.rampTexture.destroy();
|
|
42595
|
+
this.rampTexture = null;
|
|
42596
|
+
}
|
|
41203
42597
|
if (this.groundTexture) {
|
|
41204
42598
|
this.groundTexture.destroy();
|
|
41205
42599
|
this.groundTexture = null;
|
|
@@ -41293,6 +42687,25 @@ void main() {
|
|
|
41293
42687
|
const width = Cartesian3.distance(southWest, southEast);
|
|
41294
42688
|
return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
|
|
41295
42689
|
}
|
|
42690
|
+
syncRampTexture(context) {
|
|
42691
|
+
if (this.rampTexture) {
|
|
42692
|
+
return;
|
|
42693
|
+
}
|
|
42694
|
+
this.rampTexture = new Texture({
|
|
42695
|
+
context,
|
|
42696
|
+
pixelFormat: PixelFormat.RGBA,
|
|
42697
|
+
source: { width: this.rampPixels.length / 4, height: 1,
|
|
42698
|
+
arrayBufferView: this.rampPixels },
|
|
42699
|
+
flipY: false,
|
|
42700
|
+
sampler: new Sampler({
|
|
42701
|
+
wrapS: TextureWrap.CLAMP_TO_EDGE,
|
|
42702
|
+
wrapT: TextureWrap.CLAMP_TO_EDGE,
|
|
42703
|
+
// Linear across the ramp, so a gradient between stops stays smooth.
|
|
42704
|
+
minificationFilter: TextureMinificationFilter.LINEAR,
|
|
42705
|
+
magnificationFilter: TextureMagnificationFilter.LINEAR
|
|
42706
|
+
})
|
|
42707
|
+
});
|
|
42708
|
+
}
|
|
41296
42709
|
syncTexture(context) {
|
|
41297
42710
|
const sizeChanged = this.texture
|
|
41298
42711
|
&& (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
|
|
@@ -41423,8 +42836,7 @@ void main() {
|
|
|
41423
42836
|
? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
|
|
41424
42837
|
: 0,
|
|
41425
42838
|
u_coverageCutoff: () => COVERAGE_CUTOFF,
|
|
41426
|
-
|
|
41427
|
-
u_highColor: () => self.highColor,
|
|
42839
|
+
u_rampTexture: () => self.rampTexture,
|
|
41428
42840
|
u_texelSize: () => new Cartesian2(1 / self.source.width, 1 / self.source.height)
|
|
41429
42841
|
}
|
|
41430
42842
|
});
|
|
@@ -41485,12 +42897,10 @@ void main() {
|
|
|
41485
42897
|
if (!(shorterSide > 0)) {
|
|
41486
42898
|
return 1;
|
|
41487
42899
|
}
|
|
41488
|
-
|
|
42900
|
+
const relief = Math.min(shorterSide * AUTO_RELIEF_FRACTION, AUTO_MAX_RELIEF_METRES);
|
|
42901
|
+
return relief / range;
|
|
41489
42902
|
}
|
|
41490
42903
|
DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
|
|
41491
|
-
function toCesiumColor(color) {
|
|
41492
|
-
return new Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
|
|
41493
|
-
}
|
|
41494
42904
|
function now() {
|
|
41495
42905
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
41496
42906
|
}
|
|
@@ -41498,11 +42908,12 @@ void main() {
|
|
|
41498
42908
|
|
|
41499
42909
|
const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
|
|
41500
42910
|
const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
|
|
42911
|
+
const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
|
|
41501
42912
|
const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
|
|
41502
42913
|
var EntityRenderEnginePolygon;
|
|
41503
42914
|
(function (EntityRenderEnginePolygon) {
|
|
41504
42915
|
async function Render(params) {
|
|
41505
|
-
var _a, _b, _c, _d;
|
|
42916
|
+
var _a, _b, _c, _d, _e;
|
|
41506
42917
|
const entity = params.entity;
|
|
41507
42918
|
const style = params.style;
|
|
41508
42919
|
const pRings = Entity$1.GetValue({
|
|
@@ -41553,18 +42964,22 @@ var EntityRenderEnginePolygon;
|
|
|
41553
42964
|
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
42965
|
}
|
|
41555
42966
|
}
|
|
42967
|
+
const retained = params.offline
|
|
42968
|
+
? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
|
|
42969
|
+
: null;
|
|
42970
|
+
const keepsAnimator = Boolean(retained && !retained.IsDisposed());
|
|
41556
42971
|
const drawsDisplacedSurface = Boolean(extrusionArchive);
|
|
41557
42972
|
// The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
|
|
41558
42973
|
// Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
|
|
41559
42974
|
const hasFill = (drawsDisplacedSurface ? true
|
|
41560
42975
|
: fillType === Style.EPolygonFillType.Texture
|
|
41561
|
-
? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
|
|
42976
|
+
? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
|
|
41562
42977
|
: cFillColor.alpha > 0);
|
|
41563
42978
|
const fillMaterial = drawsDisplacedSurface
|
|
41564
42979
|
? Color.WHITE.withAlpha(0)
|
|
41565
42980
|
: textureDataUri
|
|
41566
42981
|
? new ImageMaterialProperty({ image: textureDataUri, transparent: true })
|
|
41567
|
-
: frameArchive
|
|
42982
|
+
: (frameArchive || keepsAnimator)
|
|
41568
42983
|
? Color.WHITE.withAlpha(0)
|
|
41569
42984
|
: cFillColor;
|
|
41570
42985
|
const lineColorTrace = style.lineColor ? Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
|
|
@@ -41734,7 +43149,7 @@ var EntityRenderEnginePolygon;
|
|
|
41734
43149
|
}
|
|
41735
43150
|
// Must run before any material is baked below: the Animator takes over polygon.material, so a bake
|
|
41736
43151
|
// 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);
|
|
43152
|
+
const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
|
|
41738
43153
|
syncDisplacedSurface({
|
|
41739
43154
|
cEntity,
|
|
41740
43155
|
extrusionArchive,
|
|
@@ -41742,7 +43157,30 @@ var EntityRenderEnginePolygon;
|
|
|
41742
43157
|
style,
|
|
41743
43158
|
entity,
|
|
41744
43159
|
heightRef,
|
|
41745
|
-
outerRingPosses: posses
|
|
43160
|
+
outerRingPosses: posses,
|
|
43161
|
+
offline: params.offline
|
|
43162
|
+
});
|
|
43163
|
+
const drapeExtent = polygonBbox(posses);
|
|
43164
|
+
const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
|
|
43165
|
+
syncTextureValueLabels({
|
|
43166
|
+
cEntity,
|
|
43167
|
+
viewer: params.viewer,
|
|
43168
|
+
settings: overlaySettings(style, entity, params.tags),
|
|
43169
|
+
extent: labelArchive
|
|
43170
|
+
? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
|
|
43171
|
+
: null,
|
|
43172
|
+
animator: drawsDisplacedSurface
|
|
43173
|
+
? cEntity[EXTRUSION_ANIMATOR_KEY]
|
|
43174
|
+
: animator,
|
|
43175
|
+
metadata: labelArchive ? labelArchive.metadata : undefined,
|
|
43176
|
+
offline: params.offline,
|
|
43177
|
+
clipRing: posses.map((pos) => {
|
|
43178
|
+
const carto = Cartographic.fromCartesian(pos);
|
|
43179
|
+
return {
|
|
43180
|
+
lon: Math$1.toDegrees(carto.longitude),
|
|
43181
|
+
lat: Math$1.toDegrees(carto.latitude)
|
|
43182
|
+
};
|
|
43183
|
+
})
|
|
41746
43184
|
});
|
|
41747
43185
|
if (animator) {
|
|
41748
43186
|
CesiumEntityStyler.SetDefaultTextureImage({
|
|
@@ -41803,7 +43241,7 @@ var EntityRenderEnginePolygon;
|
|
|
41803
43241
|
}
|
|
41804
43242
|
}
|
|
41805
43243
|
let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
|
|
41806
|
-
let cEntityBorder = (
|
|
43244
|
+
let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
|
|
41807
43245
|
cEntity._siblingGraphics = [];
|
|
41808
43246
|
if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
|
|
41809
43247
|
(!cEntityBorder.corridor && units == "m"))) {
|
|
@@ -42054,6 +43492,11 @@ var EntityRenderEnginePolygon;
|
|
|
42054
43492
|
* Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
|
|
42055
43493
|
*/
|
|
42056
43494
|
function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
|
|
43495
|
+
const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43496
|
+
if (labelOverlay) {
|
|
43497
|
+
labelOverlay.remove();
|
|
43498
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43499
|
+
}
|
|
42057
43500
|
const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
42058
43501
|
if (existing && !existing.IsDisposed()) {
|
|
42059
43502
|
existing.Dispose();
|
|
@@ -42179,10 +43622,13 @@ const _textureCache = new LRUCache(50);
|
|
|
42179
43622
|
* @param params
|
|
42180
43623
|
*/
|
|
42181
43624
|
function syncDisplacedSurface(params) {
|
|
42182
|
-
var _a;
|
|
42183
43625
|
const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
|
|
42184
43626
|
const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
|
|
42185
43627
|
const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
|
|
43628
|
+
// Same reason as the fill animator: an unresolvable archive is not a removed one.
|
|
43629
|
+
if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
|
|
43630
|
+
return;
|
|
43631
|
+
}
|
|
42186
43632
|
// Nothing to draw and nothing left over, which is every polygon that does not use this feature.
|
|
42187
43633
|
if (!extrusionArchive && !existingAnimator && !existingSurface) {
|
|
42188
43634
|
return;
|
|
@@ -42198,7 +43644,7 @@ function syncDisplacedSurface(params) {
|
|
|
42198
43644
|
// to rebuild rather than update in place.
|
|
42199
43645
|
if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
|
|
42200
43646
|
existingSurface.SetBaseHeight(placement.baseHeight);
|
|
42201
|
-
existingSurface.SetExaggeration((
|
|
43647
|
+
existingSurface.SetExaggeration(exaggerationFor(style, extrusionArchive.metadata, existingSurface.GetExtent()));
|
|
42202
43648
|
return;
|
|
42203
43649
|
}
|
|
42204
43650
|
disposeDisplacedSurface(cEntity, viewer);
|
|
@@ -42220,6 +43666,8 @@ function syncDisplacedSurface(params) {
|
|
|
42220
43666
|
textureColorMask: style.textureColorMask,
|
|
42221
43667
|
driveMaterial: false,
|
|
42222
43668
|
produceValueCanvas: true,
|
|
43669
|
+
metadata: extrusionArchive.metadata,
|
|
43670
|
+
cellTexels: 1,
|
|
42223
43671
|
baselineMask: extrusionArchive.metadata.BaselineMask,
|
|
42224
43672
|
// Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
|
|
42225
43673
|
maskBaseline: style.maskTextureBaseline
|
|
@@ -42236,9 +43684,15 @@ function syncDisplacedSurface(params) {
|
|
|
42236
43684
|
terrainProvider: viewer && viewer.terrainProvider,
|
|
42237
43685
|
valueMin: extrusionArchive.metadata.ValueMin,
|
|
42238
43686
|
valueMax: extrusionArchive.metadata.ValueMax,
|
|
42239
|
-
exaggeration: style.
|
|
43687
|
+
exaggeration: exaggerationFor(style, extrusionArchive.metadata, extent),
|
|
42240
43688
|
lowColor: mask ? Color$1.ColorFromStr(mask.minColor) : null,
|
|
42241
|
-
highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null
|
|
43689
|
+
highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null,
|
|
43690
|
+
// Normalised the same way the animator normalises them, so the surface and the flat drape
|
|
43691
|
+
// agree about which values the style hides.
|
|
43692
|
+
rampStops: (mask && mask.points || []).map((stop) => ({
|
|
43693
|
+
position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
|
|
43694
|
+
color: Color$1.ColorFromStr(stop.color)
|
|
43695
|
+
})).filter((stop) => Boolean(stop.color))
|
|
42242
43696
|
});
|
|
42243
43697
|
surface.BindAnimator(animator);
|
|
42244
43698
|
viewer.scene.primitives.add(surface);
|
|
@@ -42271,6 +43725,40 @@ function surfacePlacement(entity, heightRef) {
|
|
|
42271
43725
|
* @param metadata
|
|
42272
43726
|
* @param posses
|
|
42273
43727
|
*/
|
|
43728
|
+
/*
|
|
43729
|
+
* The factor a displaced surface should use, honouring an explicit style setting first.
|
|
43730
|
+
*
|
|
43731
|
+
* An elevation is a real height against a datum, so scaling it is not a display choice: it lifts the
|
|
43732
|
+
* surface off the datum it belongs to. Only a thickness is safe to exaggerate, which is why an
|
|
43733
|
+
* elevation archive defaults to 1 rather than to a derived factor.
|
|
43734
|
+
*/
|
|
43735
|
+
function exaggerationFor(style, metadata, extent) {
|
|
43736
|
+
if (style.extrusionExaggeration != null) {
|
|
43737
|
+
return style.extrusionExaggeration;
|
|
43738
|
+
}
|
|
43739
|
+
if (metadata.Quantity === TextureFrameSeriesAnimator.QUANTITY_ELEVATION) {
|
|
43740
|
+
return 1;
|
|
43741
|
+
}
|
|
43742
|
+
if (!extent) {
|
|
43743
|
+
return undefined;
|
|
43744
|
+
}
|
|
43745
|
+
return DisplacedSurfacePrimitive.autoExaggeration(extent, metadata.ValueMin, metadata.ValueMax);
|
|
43746
|
+
}
|
|
43747
|
+
/*
|
|
43748
|
+
* The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
|
|
43749
|
+
*/
|
|
43750
|
+
function polygonBbox(posses) {
|
|
43751
|
+
if (!posses || posses.length === 0) {
|
|
43752
|
+
return undefined;
|
|
43753
|
+
}
|
|
43754
|
+
const rectangle = Rectangle.fromCartesianArray(posses);
|
|
43755
|
+
return {
|
|
43756
|
+
West: Math$1.toDegrees(rectangle.west),
|
|
43757
|
+
East: Math$1.toDegrees(rectangle.east),
|
|
43758
|
+
South: Math$1.toDegrees(rectangle.south),
|
|
43759
|
+
North: Math$1.toDegrees(rectangle.north)
|
|
43760
|
+
};
|
|
43761
|
+
}
|
|
42274
43762
|
function archiveExtent(metadata, posses) {
|
|
42275
43763
|
if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
|
|
42276
43764
|
return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
|
|
@@ -42321,9 +43809,22 @@ function disposeDisplacedSurface(cEntity, viewer) {
|
|
|
42321
43809
|
* @param viewer
|
|
42322
43810
|
* @param textureColorMask
|
|
42323
43811
|
*/
|
|
42324
|
-
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
|
|
43812
|
+
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
|
|
42325
43813
|
const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
42326
|
-
|
|
43814
|
+
// Offline says nothing about whether the archive changed, only that it could not be asked.
|
|
43815
|
+
if (offline && existing && !existing.IsDisposed()) {
|
|
43816
|
+
return existing;
|
|
43817
|
+
}
|
|
43818
|
+
// Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
|
|
43819
|
+
// polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
|
|
43820
|
+
// style edit appeared to do nothing at all.
|
|
43821
|
+
const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
|
|
43822
|
+
textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
|
|
43823
|
+
valueCanvas: produceValueCanvas, drape: drapeExtent
|
|
43824
|
+
});
|
|
43825
|
+
if (frameArchive && existing && !existing.IsDisposed()
|
|
43826
|
+
&& existing.GetArchiveUrl() === frameArchive.url
|
|
43827
|
+
&& existing.GetAppearanceSignature() === appearance) {
|
|
42327
43828
|
return existing;
|
|
42328
43829
|
}
|
|
42329
43830
|
if (existing && !existing.IsDisposed()) {
|
|
@@ -42339,12 +43840,118 @@ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask
|
|
|
42339
43840
|
archiveUrl: frameArchive.url,
|
|
42340
43841
|
frames: frameArchive.metadata.Frames,
|
|
42341
43842
|
textureColorMask,
|
|
43843
|
+
metadata: frameArchive.metadata,
|
|
43844
|
+
cellBorder,
|
|
43845
|
+
cellTexels,
|
|
42342
43846
|
baselineMask: frameArchive.metadata.BaselineMask,
|
|
42343
|
-
maskBaseline: maskTextureBaseline
|
|
43847
|
+
maskBaseline: maskTextureBaseline,
|
|
43848
|
+
produceValueCanvas,
|
|
43849
|
+
drapeExtent
|
|
42344
43850
|
});
|
|
42345
43851
|
cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
|
|
42346
43852
|
return animator;
|
|
42347
43853
|
}
|
|
43854
|
+
/**
|
|
43855
|
+
* Folds the cell-border and value-label style into the single overlay that draws both.
|
|
43856
|
+
*
|
|
43857
|
+
* Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
|
|
43858
|
+
*/
|
|
43859
|
+
function wantsLabels(style) {
|
|
43860
|
+
const texture = style.texture;
|
|
43861
|
+
return Boolean(texture && !Array.isArray(texture) && texture.label);
|
|
43862
|
+
}
|
|
43863
|
+
/*
|
|
43864
|
+
* How the cell grid and its values are drawn, which the style does not get a say in.
|
|
43865
|
+
*
|
|
43866
|
+
* A white hairline reads over every ramp colour without competing with it, and the spacing and cap
|
|
43867
|
+
* are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
|
|
43868
|
+
* bought nothing except thresholds an author could get wrong.
|
|
43869
|
+
*/
|
|
43870
|
+
const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
|
|
43871
|
+
const LABEL_GRID_WIDTH_PIXELS = 1;
|
|
43872
|
+
const LABEL_MIN_SPACING_PIXELS = 78;
|
|
43873
|
+
const LABEL_MAX_LABELS = 240;
|
|
43874
|
+
function overlaySettings(style, entity, tags) {
|
|
43875
|
+
if (!wantsLabels(style)) {
|
|
43876
|
+
return undefined;
|
|
43877
|
+
}
|
|
43878
|
+
return {
|
|
43879
|
+
gridColor: LABEL_GRID_COLOR,
|
|
43880
|
+
gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
|
|
43881
|
+
minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
|
|
43882
|
+
maxLabels: LABEL_MAX_LABELS
|
|
43883
|
+
};
|
|
43884
|
+
}
|
|
43885
|
+
/**
|
|
43886
|
+
* Keeps a value-label overlay in step with whichever animator is driving the polygon.
|
|
43887
|
+
*
|
|
43888
|
+
* Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
|
|
43889
|
+
* move them even when the data has not advanced at all.
|
|
43890
|
+
*/
|
|
43891
|
+
function syncTextureValueLabels(params) {
|
|
43892
|
+
const { cEntity, viewer, settings, extent, animator, metadata } = params;
|
|
43893
|
+
const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43894
|
+
// Offline has no extent to rebuild the overlay from, so an existing one is left running.
|
|
43895
|
+
if (params.offline && existing) {
|
|
43896
|
+
return;
|
|
43897
|
+
}
|
|
43898
|
+
if (existing) {
|
|
43899
|
+
existing.remove();
|
|
43900
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43901
|
+
}
|
|
43902
|
+
if (!settings || !extent || !animator) {
|
|
43903
|
+
return;
|
|
43904
|
+
}
|
|
43905
|
+
const labels = new TextureValueLabels.Labels({
|
|
43906
|
+
viewer, extent, settings, metadata,
|
|
43907
|
+
tiles: TextureFrameSeriesAnimator.IsTiledLayout(metadata)
|
|
43908
|
+
? metadata.Tiles.map((tile) => ({
|
|
43909
|
+
West: tile.West, East: tile.East, South: tile.South, North: tile.North,
|
|
43910
|
+
ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
|
|
43911
|
+
}))
|
|
43912
|
+
: undefined,
|
|
43913
|
+
clipRing: params.clipRing
|
|
43914
|
+
});
|
|
43915
|
+
let lastVersion = -1;
|
|
43916
|
+
let appliedFloor = null;
|
|
43917
|
+
let appliedCeiling = null;
|
|
43918
|
+
// The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
|
|
43919
|
+
// Range GETs here even though nothing else in the render path wants them.
|
|
43920
|
+
animator.EnsureExtremes();
|
|
43921
|
+
const remove = viewer.scene.preRender.addEventListener(() => {
|
|
43922
|
+
if (animator.IsDisposed()) {
|
|
43923
|
+
return;
|
|
43924
|
+
}
|
|
43925
|
+
const canvas = animator.GetValueCanvas();
|
|
43926
|
+
if (!canvas) {
|
|
43927
|
+
return;
|
|
43928
|
+
}
|
|
43929
|
+
const version = animator.GetPresentVersion();
|
|
43930
|
+
if (version !== lastVersion) {
|
|
43931
|
+
lastVersion = version;
|
|
43932
|
+
const ctx = canvas.getContext("2d");
|
|
43933
|
+
labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
43934
|
+
labels.SetPainted(animator.GetDisplayedPixels());
|
|
43935
|
+
}
|
|
43936
|
+
// Tracked by reference rather than latched on the first one to arrive: a tiled archive
|
|
43937
|
+
// composites the floor and the ceiling separately, so latching once would leave every label
|
|
43938
|
+
// showing a minimum and no maximum.
|
|
43939
|
+
const extremes = animator.GetExtremes();
|
|
43940
|
+
if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
|
|
43941
|
+
appliedFloor = extremes.floor;
|
|
43942
|
+
appliedCeiling = extremes.ceiling;
|
|
43943
|
+
labels.SetExtremes(appliedFloor, appliedCeiling);
|
|
43944
|
+
}
|
|
43945
|
+
labels.Render();
|
|
43946
|
+
});
|
|
43947
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = {
|
|
43948
|
+
labels,
|
|
43949
|
+
remove: () => {
|
|
43950
|
+
remove();
|
|
43951
|
+
labels.Dispose();
|
|
43952
|
+
}
|
|
43953
|
+
};
|
|
43954
|
+
}
|
|
42348
43955
|
/**
|
|
42349
43956
|
* Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
|
|
42350
43957
|
* for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
|
|
@@ -43952,7 +45559,7 @@ var StyleUtils;
|
|
|
43952
45559
|
StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
|
|
43953
45560
|
})(StyleUtils || (StyleUtils = {}));
|
|
43954
45561
|
|
|
43955
|
-
const VERSION = "7.2.
|
|
45562
|
+
const VERSION = "7.2.2";
|
|
43956
45563
|
/**
|
|
43957
45564
|
* Updates the environment instance used by bruce-cesium to one specified.
|
|
43958
45565
|
* 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 +45576,5 @@ const getENVIRONMENT = () => {
|
|
|
43969
45576
|
return ENVIRONMENT;
|
|
43970
45577
|
};
|
|
43971
45578
|
|
|
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 };
|
|
45579
|
+
export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, 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
45580
|
//# sourceMappingURL=bruce-cesium.es5.js.map
|