bruce-cesium 7.5.8 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TextureValueLabels = void 0;
4
4
  const Cesium = require("cesium");
5
5
  const texture_frame_series_animator_1 = require("./texture-frame-series-animator");
6
+ const entity_label_1 = require("./render-managers/common/entity-label");
6
7
  /**
7
8
  * Prints a value texture's own numbers over the cells they belong to.
8
9
  */
@@ -14,13 +15,15 @@ var TextureValueLabels;
14
15
  const MIN_VISIBLE_PIXELS = 60;
15
16
  const MAX_STRIDE = 512;
16
17
  const STRIDE_HYSTERESIS = 1.4;
17
- const DEFAULT_MIN_SPACING_PIXELS = 78;
18
- const DEFAULT_MAX_LABELS = 240;
19
- const LABEL_RADIUS = 4;
20
- const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
21
- // Clear space either side of a label box, so neighbours read as separate.
22
- const LABEL_GAP_PIXELS = 16;
23
- const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
18
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
19
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
20
+ const DEFAULT_MAX_LABELS = 60;
21
+ // How far past the edge of the view an anchor may sit before its label is dropped.
22
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
23
+ // Distance discount for a label already up when the cap forces a choice.
24
+ const LABEL_KEEP_BIAS = 0.6;
25
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
26
+ const LABEL_LINGER_MS = 300;
24
27
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
25
28
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
26
29
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -34,7 +37,8 @@ var TextureValueLabels;
34
37
  const TILE_OUTLINE_ALPHA = 0.45;
35
38
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
36
39
  const MAX_GRID_STRIDE = 256;
37
- const LINE_HEIGHT = 13;
40
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
41
+ const HOVER_REFINE_STEPS = 3;
38
42
  /**
39
43
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
40
44
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -128,11 +132,25 @@ var TextureValueLabels;
128
132
  this.sourceMap = null;
129
133
  this.source = null;
130
134
  this.painted = null;
135
+ this.paintedRamp = null;
131
136
  this.floor = null;
132
137
  this.ceiling = null;
133
138
  this.disposed = false;
134
- this.drawn = 0;
135
139
  this.stride = 0;
140
+ this.cellLabels = new Map();
141
+ this.lingerTimer = null;
142
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
143
+ this.drawnView = null;
144
+ this.drawnInputs = [];
145
+ this.scratchWorld = new Cesium.Cartesian3();
146
+ this.scratchWindow = new Cesium.Cartesian2();
147
+ this.scratchClip = new Cesium.Cartesian4();
148
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
149
+ this.viewProjection = null;
150
+ this.viewProjectionScratch = new Cesium.Matrix4();
151
+ this.hovering = false;
152
+ this.hoverFrame = 0;
153
+ this.hoverAt = null;
136
154
  this.viewer = options.viewer;
137
155
  this.extent = options.extent;
138
156
  this.settings = options.settings || {};
@@ -142,6 +160,9 @@ var TextureValueLabels;
142
160
  this.sourceMap = texture_frame_series_animator_1.TextureFrameSeriesAnimator.SourceMap(options.metadata);
143
161
  this.tiles = options.tiles;
144
162
  this.clipRing = options.clipRing;
163
+ this.heightAt = options.heightAt;
164
+ this.heightKey = options.heightKey;
165
+ this.hover = options.hover;
145
166
  this.canvas = document.createElement("canvas");
146
167
  this.canvas.style.position = "absolute";
147
168
  this.canvas.style.inset = "0";
@@ -151,12 +172,31 @@ var TextureValueLabels;
151
172
  parent.appendChild(this.canvas);
152
173
  }
153
174
  this.ctx = this.canvas.getContext("2d");
175
+ this.onPointerMove = (event) => {
176
+ const bounds = this.viewer.canvas.getBoundingClientRect();
177
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
178
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
179
+ if (!this.hoverFrame) {
180
+ this.hoverFrame = requestAnimationFrame(() => {
181
+ this.hoverFrame = 0;
182
+ this.updateHover();
183
+ });
184
+ }
185
+ };
186
+ this.onPointerLeave = () => {
187
+ this.hoverAt = null;
188
+ this.releaseHover();
189
+ };
190
+ if (this.hover && this.hover.register) {
191
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
192
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
193
+ }
154
194
  }
155
195
  SetExtent(extent) {
156
196
  this.extent = extent;
157
197
  }
158
198
  /**
159
- * The current frame's decoded values, as the animator's value canvas already provides them.
199
+ * The current frame's decoded values.
160
200
  * @param source value pixels, one texel per cell
161
201
  */
162
202
  SetSource(source) {
@@ -164,11 +204,17 @@ var TextureValueLabels;
164
204
  }
165
205
  /**
166
206
  * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
167
- * @param painted RGBA of the presented frame, or null to fall back to coverage alone
207
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
168
208
  */
169
209
  SetPainted(painted) {
170
210
  this.painted = painted;
171
211
  }
212
+ /**
213
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
214
+ */
215
+ SetPaintedRamp(ramp) {
216
+ this.paintedRamp = ramp;
217
+ }
172
218
  /**
173
219
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
174
220
  * print the current reading, just not the range behind it.
@@ -178,21 +224,28 @@ var TextureValueLabels;
178
224
  this.ceiling = ceiling;
179
225
  }
180
226
  GetDrawnCount() {
181
- return this.drawn;
227
+ return this.cellLabels.size;
182
228
  }
183
229
  Clear() {
184
230
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
185
- this.drawn = 0;
231
+ this.drawnView = null;
186
232
  }
187
233
  Dispose() {
188
234
  this.disposed = true;
235
+ this.clearCellLabels();
236
+ this.releaseHover();
237
+ if (this.hoverFrame) {
238
+ cancelAnimationFrame(this.hoverFrame);
239
+ this.hoverFrame = 0;
240
+ }
241
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
242
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
189
243
  if (this.canvas.parentElement) {
190
244
  this.canvas.parentElement.removeChild(this.canvas);
191
245
  }
192
246
  }
193
247
  /**
194
- * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
195
- * count it considers is bounded by the view rather than by the texture.
248
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
196
249
  */
197
250
  Render() {
198
251
  var _a, _b, _c;
@@ -200,67 +253,296 @@ var TextureValueLabels;
200
253
  return;
201
254
  }
202
255
  const view = this.viewer.canvas;
256
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
257
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
258
+ const camera = this.viewer.camera.viewMatrix;
259
+ if (this.drawnView &&
260
+ Cesium.Matrix4.equals(this.drawnView, camera) &&
261
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
262
+ return;
263
+ }
203
264
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
204
265
  this.canvas.width = view.clientWidth;
205
266
  this.canvas.height = view.clientHeight;
206
267
  }
207
268
  this.Clear();
269
+ this.drawnView = Cesium.Matrix4.clone(camera, this.drawnView || undefined);
270
+ this.drawnInputs = inputs;
271
+ const projection = this.viewer.camera.frustum.projectionMatrix;
272
+ this.viewProjection = this.viewer.scene.mode === Cesium.SceneMode.SCENE3D
273
+ ? Cesium.Matrix4.multiply(projection, camera, this.viewProjectionScratch)
274
+ : null;
208
275
  const cols = this.source.width;
209
276
  const rows = this.source.height;
210
277
  const window = this.visibleTexels(cols, rows);
211
278
  if (!window) {
279
+ this.syncCellLabels([]);
212
280
  return;
213
281
  }
214
- const midCol = (window.col0 + window.col1) / 2;
215
- const midRow = (window.row0 + window.row1) / 2;
216
- const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
217
- const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
282
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
283
+ // texels sits out toward the horizon, where cells are a fraction of the size.
284
+ const focus = this.groundAtScreenCentre();
285
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
286
+ const focused = (Boolean(at) &&
287
+ at.col >= window.col0 &&
288
+ at.col <= window.col1 &&
289
+ at.row >= window.row0 &&
290
+ at.row <= window.row1);
291
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
292
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
293
+ const a = this.projectCell(midCol, midRow, cols, rows);
294
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
218
295
  if (!a || !b) {
296
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
219
297
  return;
220
298
  }
221
299
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
222
300
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
223
301
  // number no longer fits.
224
302
  this.drawGrid(window, cols, rows, cellPixels);
225
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
303
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
304
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
305
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
306
+ const cellSize = across && down
307
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
308
+ : cellPixels;
309
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
310
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
311
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
312
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
313
+ this.syncCellLabels([]);
226
314
  return;
227
315
  }
228
- const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
229
- const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
230
- if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
231
- return;
232
- }
233
- const parts = this.settings.parts || ["value", "floor", "ceiling"];
234
- this.ctx.font = LABEL_FONT;
235
- this.ctx.textAlign = "center";
236
316
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
237
317
  this.stride = ChooseLabelStride({
238
- cellPixels,
318
+ cellPixels: cellSize,
239
319
  visibleCols: window.col1 - window.col0 + 1,
240
320
  visibleRows: window.row1 - window.row0 + 1,
241
321
  maxLabels,
242
- // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
243
- // one is, so changing what a label says cannot quietly start overlapping them.
244
- minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
322
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
245
323
  held: this.stride
246
324
  });
247
- const stride = this.stride;
325
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
326
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
327
+ }
328
+ /*
329
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
330
+ */
331
+ labelCells(window, cols, rows, stride, max, parts) {
332
+ const width = this.canvas.width;
333
+ const height = this.canvas.height;
248
334
  const firstRow = Math.floor(window.row0 / stride) * stride;
249
335
  const firstCol = Math.floor(window.col0 / stride) * stride;
250
- for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
251
- for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
336
+ const candidates = [];
337
+ for (let row = firstRow; row <= window.row1; row += stride) {
338
+ for (let col = firstCol; col <= window.col1; col += stride) {
252
339
  const found = this.readNear(col, row, cols, rows, stride);
253
340
  if (!found) {
254
341
  continue;
255
342
  }
256
- const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
257
- if (!point || point.x < 0 || point.y < 0
258
- || point.x > this.canvas.width || point.y > this.canvas.height) {
343
+ const point = this.projectCell(found.col, found.row, cols, rows);
344
+ // A margin past the edge, so a label does not drop the moment its anchor does.
345
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
346
+ if (!point ||
347
+ point.x < -margin ||
348
+ point.y < -margin ||
349
+ point.x > width + margin ||
350
+ point.y > height + margin) {
351
+ continue;
352
+ }
353
+ const text = this.labelLines(found.reading, parts).join("\n");
354
+ if (!text) {
259
355
  continue;
260
356
  }
261
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
357
+ const key = col + "," + row;
358
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
359
+ candidates.push({
360
+ key,
361
+ lon: this.lonAt(found.col, cols),
362
+ lat: this.latAt(found.row, rows),
363
+ text,
364
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
365
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
366
+ });
367
+ }
368
+ }
369
+ if (candidates.length > max) {
370
+ candidates.sort((x, y) => x.rank - y.rank);
371
+ candidates.length = max;
372
+ }
373
+ return candidates;
374
+ }
375
+ /*
376
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
377
+ */
378
+ syncCellLabels(wanted) {
379
+ var _a;
380
+ const keep = new Set(wanted.map((w) => w.key));
381
+ const now = Date.now();
382
+ let lingering = false;
383
+ for (const [key, entry] of this.cellLabels) {
384
+ if (keep.has(key)) {
385
+ entry.missingSince = undefined;
386
+ continue;
387
+ }
388
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
389
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
390
+ entry.label.Dispose();
391
+ this.cellLabels.delete(key);
392
+ }
393
+ else {
394
+ lingering = true;
395
+ }
396
+ }
397
+ // A still camera draws nothing more, so a lingering label is given its own later look.
398
+ if (lingering && !this.lingerTimer) {
399
+ this.lingerTimer = setTimeout(() => {
400
+ this.lingerTimer = null;
401
+ if (!this.disposed) {
402
+ this.drawnView = null;
403
+ this.viewer.scene.requestRender();
404
+ }
405
+ }, LABEL_LINGER_MS);
406
+ }
407
+ for (const want of wanted) {
408
+ const pos3d = Cesium.Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
409
+ const options = {
410
+ presentation: "text",
411
+ text: want.text,
412
+ leader: true,
413
+ selectOnClick: false
414
+ };
415
+ const existing = this.cellLabels.get(want.key);
416
+ if (existing) {
417
+ if (!Cesium.Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
418
+ existing.label.pos3d = pos3d;
419
+ }
420
+ if (existing.text !== want.text) {
421
+ existing.label.Configure(options);
422
+ existing.text = want.text;
423
+ }
424
+ continue;
425
+ }
426
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
427
+ const label = new entity_label_1.EntityLabel.Label({
428
+ rego: {
429
+ entityId: id,
430
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
431
+ name: want.text,
432
+ priority: 0,
433
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
434
+ visual: new Cesium.Entity({ position: pos3d })
435
+ },
436
+ viewer: this.viewer,
437
+ pos3d,
438
+ options,
439
+ persistent: true,
440
+ visible: () => !this.disposed
441
+ });
442
+ this.cellLabels.set(want.key, { label, text: want.text });
443
+ }
444
+ }
445
+ clearCellLabels() {
446
+ if (this.lingerTimer) {
447
+ clearTimeout(this.lingerTimer);
448
+ this.lingerTimer = null;
449
+ }
450
+ for (const entry of this.cellLabels.values()) {
451
+ entry.label.Dispose();
452
+ }
453
+ this.cellLabels.clear();
454
+ }
455
+ /*
456
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
457
+ */
458
+ updateHover() {
459
+ const hover = this.hover;
460
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
461
+ return;
462
+ }
463
+ const register = hover.register;
464
+ const target = register.GetLabelHoverTarget();
465
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
466
+ if (target && !ours) {
467
+ this.hovering = false;
468
+ return;
469
+ }
470
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
471
+ if (!under) {
472
+ this.releaseHover();
473
+ return;
474
+ }
475
+ register.SetLabelHover({
476
+ entityId: hover.entityId,
477
+ menuItemId: hover.menuItemId,
478
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
479
+ text: under.text,
480
+ pos3d: under.pos3d
481
+ });
482
+ this.hovering = true;
483
+ }
484
+ releaseHover() {
485
+ const hover = this.hover;
486
+ if (!this.hovering || !hover || !hover.register) {
487
+ return;
488
+ }
489
+ this.hovering = false;
490
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
491
+ hover.register.ClearLabelHover();
492
+ }
493
+ }
494
+ /*
495
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
496
+ */
497
+ readingUnder(x, y) {
498
+ const at = this.cellUnder(x, y);
499
+ if (!at) {
500
+ return null;
501
+ }
502
+ const cols = this.source.width;
503
+ const found = this.readAt(at.col, at.row, cols);
504
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
505
+ return null;
506
+ }
507
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
508
+ if (!text) {
509
+ return null;
510
+ }
511
+ const lon = this.lonAt(at.col, cols);
512
+ const lat = this.latAt(at.row, this.source.height);
513
+ return { text, pos3d: Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
514
+ }
515
+ /*
516
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
517
+ */
518
+ cellUnder(x, y) {
519
+ const scene = this.viewer.scene;
520
+ const camera = this.viewer.camera;
521
+ const screen = new Cesium.Cartesian2(x, y);
522
+ const ray = camera.getPickRay(screen);
523
+ if (!ray) {
524
+ return null;
525
+ }
526
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
527
+ if (!hit) {
528
+ return null;
529
+ }
530
+ const radii = Cesium.Ellipsoid.WGS84.radii;
531
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
532
+ const carto = Cesium.Cartographic.fromCartesian(hit);
533
+ const height = this.heightAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
534
+ if (height == null) {
535
+ break;
262
536
  }
537
+ const inflated = new Cesium.Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
538
+ const interval = Cesium.IntersectionTests.rayEllipsoid(ray, inflated);
539
+ if (!interval) {
540
+ break;
541
+ }
542
+ hit = Cesium.Ray.getPoint(ray, interval.start);
263
543
  }
544
+ const carto = Cesium.Cartographic.fromCartesian(hit);
545
+ return this.texelAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
264
546
  }
265
547
  /*
266
548
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -288,44 +570,25 @@ var TextureValueLabels;
288
570
  if (wide * high > MAX_GRID_CELLS) {
289
571
  return;
290
572
  }
291
- const lattice = new Array((wide + 1) * (high + 1));
292
- for (let r = 0; r <= high; r++) {
293
- for (let c = 0; c <= wide; c++) {
294
- // Corners sit half a texel out from the centres lonAt/latAt return.
295
- const lon = this.lonAt(window.col0 + c - 0.5, cols);
296
- const lat = this.latAt(window.row0 + r - 0.5, rows);
297
- lattice[r * (wide + 1) + c] = this.project(lon, lat);
298
- }
299
- }
300
- const path = new Path2D();
301
- let drew = false;
573
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
574
+ // Corners sit half a texel out from the centres lonAt/latAt return.
575
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
576
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
577
+ return this.projectInto(lon, lat);
578
+ });
579
+ const covered = new Uint8Array(wide * high);
302
580
  for (let r = 0; r < high; r++) {
303
581
  for (let c = 0; c < wide; c++) {
304
- if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
305
- continue;
306
- }
307
- const tl = lattice[r * (wide + 1) + c];
308
- const tr = lattice[r * (wide + 1) + c + 1];
309
- const bl = lattice[(r + 1) * (wide + 1) + c];
310
- const br = lattice[(r + 1) * (wide + 1) + c + 1];
311
- if (!tl || !tr || !bl || !br) {
312
- continue;
313
- }
314
- path.moveTo(tl.x, tl.y);
315
- path.lineTo(tr.x, tr.y);
316
- path.lineTo(br.x, br.y);
317
- path.lineTo(bl.x, bl.y);
318
- path.closePath();
319
- drew = true;
582
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
320
583
  }
321
584
  }
322
- if (!drew) {
585
+ const path = new Path2D();
586
+ if (!lattice.Trace(path, covered, wide, high)) {
323
587
  return;
324
588
  }
325
589
  this.ctx.strokeStyle = color;
326
590
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
327
591
  this.ctx.stroke(path);
328
- +" alpha" + this.ctx.globalAlpha;
329
592
  }
330
593
  /*
331
594
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -347,33 +610,11 @@ var TextureValueLabels;
347
610
  path.closePath();
348
611
  this.ctx.clip(path);
349
612
  }
350
- /*
351
- * The cell of whichever tile covers a point, as screen corners.
352
- */
353
- tileCellOutline(lon, lat) {
354
- for (const tile of this.tiles) {
355
- if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
356
- continue;
357
- }
358
- const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
359
- const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
360
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
361
- const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
362
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
363
- const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
364
- const corners = [
365
- this.project(w0, n0), this.project(w1, n0),
366
- this.project(w1, n1), this.project(w0, n1)
367
- ];
368
- return corners.every((q) => q) ? corners : null;
369
- }
370
- return null;
371
- }
372
613
  /*
373
614
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
374
615
  */
375
616
  drawTileGrid(color) {
376
- var _a, _b, _c;
617
+ var _a, _b;
377
618
  const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
378
619
  if (!view) {
379
620
  return;
@@ -402,7 +643,7 @@ var TextureValueLabels;
402
643
  coarse.lineTo(tr.x, tr.y);
403
644
  coarse.lineTo(br.x, br.y);
404
645
  coarse.lineTo(bl.x, bl.y);
405
- coarse.closePath();
646
+ coarse.lineTo(tl.x, tl.y);
406
647
  drewCoarse = true;
407
648
  };
408
649
  // Planned for every visible tile BEFORE any is drawn.
@@ -463,32 +704,30 @@ var TextureValueLabels;
463
704
  // moves, and so each step up is a subset of the one below it.
464
705
  const firstR = Math.floor(r0 / step) * step;
465
706
  const firstC = Math.floor(c0 / step) * step;
466
- for (let r = firstR; r < r1; r += step) {
467
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
468
- const n1 = tile.North
469
- - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
470
- for (let c = firstC; c < c1; c += step) {
471
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
472
- const w1 = tile.West
473
- + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
474
- if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
475
- continue;
476
- }
477
- const tl = this.project(w0, n0);
478
- const tr = this.project(w1, n0);
479
- const br = this.project(w1, n1);
480
- const bl = this.project(w0, n1);
481
- if (!tl || !tr || !br || !bl) {
482
- continue;
707
+ const wide = Math.ceil((c1 - firstC) / step);
708
+ const high = Math.ceil((r1 - firstR) / step);
709
+ const lonOf = (c) => tile.West
710
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
711
+ const latOf = (r) => tile.North
712
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
713
+ const covered = new Uint8Array(wide * high);
714
+ let any = false;
715
+ for (let r = 0; r < high; r++) {
716
+ for (let c = 0; c < wide; c++) {
717
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
718
+ covered[r * wide + c] = 1;
719
+ any = true;
483
720
  }
484
- path.moveTo(tl.x, tl.y);
485
- path.lineTo(tr.x, tr.y);
486
- path.lineTo(br.x, br.y);
487
- path.lineTo(bl.x, bl.y);
488
- path.closePath();
489
- drew = true;
490
721
  }
491
722
  }
723
+ if (!any) {
724
+ continue;
725
+ }
726
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
727
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
728
+ if (lattice.Trace(path, covered, wide, high)) {
729
+ drew = true;
730
+ }
492
731
  }
493
732
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
494
733
  if (drewCoarse) {
@@ -505,7 +744,7 @@ var TextureValueLabels;
505
744
  return;
506
745
  }
507
746
  this.ctx.strokeStyle = color;
508
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
747
+ this.ctx.lineWidth = gridWidth;
509
748
  this.ctx.stroke(path);
510
749
  }
511
750
  /*
@@ -525,7 +764,7 @@ var TextureValueLabels;
525
764
  };
526
765
  }
527
766
  /*
528
- * Whether the composited raster holds data at a lon/lat.
767
+ * Whether the composited raster holds painted data at a lon/lat.
529
768
  */
530
769
  coveredAt(lon, lat) {
531
770
  if (!this.source) {
@@ -538,60 +777,20 @@ var TextureValueLabels;
538
777
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
539
778
  return false;
540
779
  }
541
- const at = (row * cols + col) * 4 + 3;
542
780
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
543
781
  // ruling a grid over it draws detail where the picture shows none.
544
- if (this.painted && this.painted.length === cols * rows * 4) {
545
- return this.painted[at] > 0;
782
+ if (this.painted || this.paintedRamp) {
783
+ return this.paintedAt(col, row, cols);
546
784
  }
547
- return this.source.data[at] >= 128;
785
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
548
786
  }
549
787
  isCovered(col, row, cols) {
550
788
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
551
789
  }
552
790
  /*
553
- * Width of the widest label this overlay could draw, in pixels.
791
+ * What a label says about one reading, a line at a time.
554
792
  */
555
- widestLabel(parts) {
556
- const sample = [];
557
- if (parts.indexOf("value") >= 0) {
558
- sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
559
- }
560
- const range = [];
561
- const showsRange = !texture_frame_series_animator_1.TextureFrameSeriesAnimator.IsTransformed(this.metadata);
562
- if (showsRange && parts.indexOf("floor") >= 0) {
563
- range.push("min: -000.00");
564
- }
565
- if (showsRange && parts.indexOf("ceiling") >= 0) {
566
- range.push("max: -000.00");
567
- }
568
- if (range.length) {
569
- sample.push(range.join(" - "));
570
- }
571
- if (!sample.length) {
572
- return 0;
573
- }
574
- const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
575
- return widest + LABEL_GAP_PIXELS;
576
- }
577
- /*
578
- * The four screen corners of one cell, or null if any of them will not project.
579
- */
580
- cellOutline(col, row, cols, rows) {
581
- // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
582
- // uniform lattice over the tile grid, misaligned with it everywhere.
583
- if (this.tiles && this.tiles.length) {
584
- return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
585
- }
586
- const corners = [
587
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
588
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
589
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
590
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
591
- ];
592
- return corners.every((c) => c) ? corners : null;
593
- }
594
- drawLabel(point, reading, parts, outline) {
793
+ labelLines(reading, parts) {
595
794
  var _a;
596
795
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
597
796
  const lines = [];
@@ -610,32 +809,7 @@ var TextureValueLabels;
610
809
  if (range.length) {
611
810
  lines.push(range.join(" - "));
612
811
  }
613
- if (!lines.length) {
614
- return;
615
- }
616
- if (outline) {
617
- this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
618
- this.ctx.lineWidth = 1.5;
619
- this.ctx.beginPath();
620
- this.ctx.moveTo(outline[0].x, outline[0].y);
621
- for (let i = 1; i < outline.length; i++) {
622
- this.ctx.lineTo(outline[i].x, outline[i].y);
623
- }
624
- this.ctx.closePath();
625
- this.ctx.stroke();
626
- }
627
- const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
628
- const height = lines.length * LINE_HEIGHT + 6;
629
- const top = point.y - height / 2;
630
- this.ctx.fillStyle = "rgba(16,16,14,0.72)";
631
- this.ctx.beginPath();
632
- this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
633
- this.ctx.fill();
634
- lines.forEach((line, i) => {
635
- this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
636
- this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
637
- });
638
- this.drawn++;
812
+ return lines;
639
813
  }
640
814
  /*
641
815
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -662,11 +836,24 @@ var TextureValueLabels;
662
836
  return null;
663
837
  }
664
838
  paintedAt(col, row, cols) {
665
- if (!this.painted) {
666
- return true;
839
+ const at = (row * cols + col) * 4;
840
+ if (this.painted && this.painted.length === this.source.data.length) {
841
+ return this.painted[at + 3] > 0;
842
+ }
843
+ if (this.paintedRamp) {
844
+ const data = this.source.data;
845
+ if (data[at + 3] === 0) {
846
+ return false;
847
+ }
848
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
849
+ const grey = texture_frame_series_animator_1.TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
850
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
851
+ : data[at];
852
+ const steps = this.paintedRamp.length / 4;
853
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
854
+ return this.paintedRamp[index * 4 + 3] > 0;
667
855
  }
668
- const at = (row * cols + col) * 4 + 3;
669
- return this.painted[at] > 0;
856
+ return true;
670
857
  }
671
858
  // Decodes one cell, skipping anything the texture marks as having no data.
672
859
  readAt(col, row, cols) {
@@ -682,6 +869,16 @@ var TextureValueLabels;
682
869
  sourceMap: this.sourceMap
683
870
  });
684
871
  }
872
+ /*
873
+ * The texel holding a lon/lat, or null outside the raster.
874
+ */
875
+ texelAt(lon, lat) {
876
+ const cols = this.source.width;
877
+ const rows = this.source.height;
878
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
879
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
880
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
881
+ }
685
882
  lonAt(col, cols) {
686
883
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
687
884
  }
@@ -710,15 +907,102 @@ var TextureValueLabels;
710
907
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
711
908
  };
712
909
  }
910
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
911
+ surfaceHeight(lon, lat) {
912
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
913
+ return height == null || !isFinite(height) ? 0 : height;
914
+ }
915
+ projectCell(col, row, cols, rows) {
916
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
917
+ }
918
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
713
919
  project(lon, lat) {
714
- const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
920
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
921
+ }
922
+ /*
923
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
924
+ */
925
+ projectInto(lon, lat) {
926
+ const world = Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Cesium.Ellipsoid.WGS84, this.scratchWorld);
927
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
928
+ if (this.viewProjection) {
929
+ const clip = Cesium.Matrix4.multiplyByVector(this.viewProjection, Cesium.Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
930
+ if (!(clip.w > 0)) {
931
+ return null;
932
+ }
933
+ const view = this.viewer.canvas;
934
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
935
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
936
+ return this.scratchWindow;
937
+ }
715
938
  const transforms = Cesium.SceneTransforms;
716
939
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
717
940
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
718
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
941
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
719
942
  }
720
943
  }
721
944
  TextureValueLabels.Labels = Labels;
945
+ /*
946
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
947
+ */
948
+ class Lattice {
949
+ constructor(wide, high, projectAt) {
950
+ this.wide = wide;
951
+ this.projectAt = projectAt;
952
+ this.xs = new Float64Array(wide * high);
953
+ this.ys = new Float64Array(wide * high);
954
+ this.state = new Uint8Array(wide * high);
955
+ }
956
+ at(c, r) {
957
+ const i = r * this.wide + c;
958
+ if (this.state[i] === 0) {
959
+ const p = this.projectAt(c, r);
960
+ if (p) {
961
+ this.xs[i] = p.x;
962
+ this.ys[i] = p.y;
963
+ this.state[i] = 1;
964
+ }
965
+ else {
966
+ this.state[i] = 2;
967
+ }
968
+ }
969
+ return this.state[i] === 1 ? i : -1;
970
+ }
971
+ edge(path, a, b) {
972
+ if (a < 0 || b < 0) {
973
+ return false;
974
+ }
975
+ path.moveTo(this.xs[a], this.ys[a]);
976
+ path.lineTo(this.xs[b], this.ys[b]);
977
+ return true;
978
+ }
979
+ /*
980
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
981
+ */
982
+ Trace(path, covered, cellsWide, cellsHigh) {
983
+ let drew = false;
984
+ for (let r = 0; r < cellsHigh; r++) {
985
+ for (let c = 0; c < cellsWide; c++) {
986
+ if (!covered[r * cellsWide + c]) {
987
+ continue;
988
+ }
989
+ const tl = this.at(c, r);
990
+ const tr = this.at(c + 1, r);
991
+ const bl = this.at(c, r + 1);
992
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
993
+ drew = this.edge(path, tl, tr) || drew;
994
+ }
995
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
996
+ drew = this.edge(path, tl, bl) || drew;
997
+ }
998
+ const br = this.at(c + 1, r + 1);
999
+ drew = this.edge(path, tr, br) || drew;
1000
+ drew = this.edge(path, bl, br) || drew;
1001
+ }
1002
+ }
1003
+ return drew;
1004
+ }
1005
+ }
722
1006
  /*
723
1007
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
724
1008
  * label to say nothing, and the two lines are plainly the same quantity.