bruce-cesium 2.1.7 → 2.1.9

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.
@@ -0,0 +1,809 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.PointClustering = void 0;
40
+ var bruce_models_1 = require("bruce-models");
41
+ var Cesium = require("cesium");
42
+ function GetValue(viewer, obj) {
43
+ if (obj === null || obj === void 0 ? void 0 : obj.getValue) {
44
+ return obj.getValue(viewer.scene.lastRenderTime);
45
+ }
46
+ return obj;
47
+ }
48
+ var Point = /** @class */ (function () {
49
+ function Point(lon, lat, id) {
50
+ this.lon = lon;
51
+ this.lat = lat;
52
+ this.id = id;
53
+ }
54
+ return Point;
55
+ }());
56
+ var Circle = /** @class */ (function () {
57
+ function Circle(x, y, radius) {
58
+ this.x = x;
59
+ this.y = y;
60
+ this.radius = radius;
61
+ this.width = 2 * radius;
62
+ this.height = 2 * radius;
63
+ }
64
+ Circle.prototype.Intersects = function (range) {
65
+ var xDist = Math.abs(range.x - this.x);
66
+ var yDist = Math.abs(range.y - this.y);
67
+ var r = this.radius;
68
+ var w = range.width;
69
+ var h = range.height;
70
+ if (xDist > (w + r) || yDist > (h + r)) {
71
+ return false;
72
+ }
73
+ if (xDist <= w || yDist <= h) {
74
+ return true;
75
+ }
76
+ var dx = xDist - w;
77
+ var dy = yDist - h;
78
+ return (dx * dx + dy * dy <= this.radius * this.radius);
79
+ };
80
+ Circle.prototype.Contains = function (point) {
81
+ var dx = this.x - point.lon;
82
+ var dy = this.y - point.lat;
83
+ var distance = Math.sqrt(dx * dx + dy * dy);
84
+ return distance <= this.radius;
85
+ };
86
+ return Circle;
87
+ }());
88
+ var Rectangle = /** @class */ (function () {
89
+ function Rectangle(x, y, w, h) {
90
+ this.x = x;
91
+ this.y = y;
92
+ this.w = w;
93
+ this.h = h;
94
+ this.width = 2 * w;
95
+ this.height = 2 * h;
96
+ }
97
+ Rectangle.prototype.Contains = function (point) {
98
+ return (point.lon >= this.x - this.w &&
99
+ point.lon < this.x + this.w &&
100
+ point.lat >= this.y - this.h &&
101
+ point.lat < this.y + this.h);
102
+ };
103
+ Rectangle.prototype.Intersects = function (range) {
104
+ return !(range.x - range.w > this.x + this.w ||
105
+ range.x + range.w < this.x - this.w ||
106
+ range.y - range.h > this.y + this.h ||
107
+ range.y + range.h < this.y - this.h);
108
+ };
109
+ return Rectangle;
110
+ }());
111
+ var Quad = /** @class */ (function () {
112
+ function Quad(boundary, capacity) {
113
+ this.boundary = boundary;
114
+ this.capacity = capacity;
115
+ this.points = [];
116
+ this.divided = false;
117
+ }
118
+ Quad.prototype.Subdivide = function () {
119
+ var x = this.boundary.x;
120
+ var y = this.boundary.y;
121
+ var w = this.boundary.w / 2;
122
+ var h = this.boundary.h / 2;
123
+ var ne = new Rectangle(x + w / 2, y - h / 2, w, h);
124
+ this.northeast = new Quad(ne, this.capacity);
125
+ var nw = new Rectangle(x - w / 2, y - h / 2, w, h);
126
+ this.northwest = new Quad(nw, this.capacity);
127
+ var se = new Rectangle(x + w / 2, y + h / 2, w, h);
128
+ this.southeast = new Quad(se, this.capacity);
129
+ var sw = new Rectangle(x - w / 2, y + h / 2, w, h);
130
+ this.southwest = new Quad(sw, this.capacity);
131
+ this.divided = true;
132
+ };
133
+ Quad.prototype.Insert = function (point) {
134
+ if (!this.boundary.Contains(point)) {
135
+ return false;
136
+ }
137
+ if (this.points.length < this.capacity) {
138
+ this.points.push(point);
139
+ return true;
140
+ }
141
+ if (!this.divided) {
142
+ this.Subdivide();
143
+ }
144
+ return (this.northeast.Insert(point) || this.northwest.Insert(point) ||
145
+ this.southeast.Insert(point) || this.southwest.Insert(point));
146
+ };
147
+ Quad.prototype.Query = function (range, found) {
148
+ if (!found) {
149
+ found = [];
150
+ }
151
+ if (!range.Intersects(this.boundary)) {
152
+ return found;
153
+ }
154
+ for (var _i = 0, _a = this.points; _i < _a.length; _i++) {
155
+ var p = _a[_i];
156
+ if (range.Contains(p)) {
157
+ found.push(p);
158
+ }
159
+ }
160
+ if (this.divided) {
161
+ this.northwest.Query(range, found);
162
+ this.northeast.Query(range, found);
163
+ this.southwest.Query(range, found);
164
+ this.southeast.Query(range, found);
165
+ }
166
+ return found;
167
+ };
168
+ Quad.prototype.Find = function (id) {
169
+ for (var _i = 0, _a = this.points; _i < _a.length; _i++) {
170
+ var point = _a[_i];
171
+ if (point.id === id) {
172
+ return point;
173
+ }
174
+ }
175
+ if (this.divided) {
176
+ return this.northwest.Find(id) || this.northeast.Find(id) ||
177
+ this.southwest.Find(id) || this.southeast.Find(id);
178
+ }
179
+ return null;
180
+ };
181
+ Quad.prototype.Remove = function (point) {
182
+ var index = this.points.indexOf(point);
183
+ if (index !== -1) {
184
+ this.points.splice(index, 1);
185
+ }
186
+ if (this.divided) {
187
+ this.northwest.Remove(point);
188
+ this.northeast.Remove(point);
189
+ this.southwest.Remove(point);
190
+ this.southeast.Remove(point);
191
+ }
192
+ };
193
+ return Quad;
194
+ }());
195
+ var _clusterImageCache = new bruce_models_1.LRUCache(500);
196
+ var _clusterImageLoadedCache = new bruce_models_1.LRUCache(500);
197
+ function _loadClusterImage(params) {
198
+ return __awaiter(this, void 0, void 0, function () {
199
+ var size, txtColor, bgColor, text, iconUrl, key, cacheData, prom;
200
+ return __generator(this, function (_a) {
201
+ switch (_a.label) {
202
+ case 0:
203
+ size = params.size, txtColor = params.txtColor, bgColor = params.bgColor, text = params.text, iconUrl = params.iconUrl;
204
+ key = "".concat(size, "-").concat(txtColor, "-").concat(bgColor, "-").concat(text, "-").concat(iconUrl);
205
+ cacheData = _clusterImageCache.Get(key);
206
+ if (!cacheData) return [3 /*break*/, 2];
207
+ return [4 /*yield*/, cacheData];
208
+ case 1: return [2 /*return*/, _a.sent()];
209
+ case 2:
210
+ prom = new Promise(function (res, rej) {
211
+ var canvas = document.createElement("canvas");
212
+ canvas.width = size;
213
+ canvas.height = size;
214
+ var ctx = canvas.getContext("2d");
215
+ var drawWithoutImage = function (img) {
216
+ ctx.beginPath();
217
+ ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
218
+ var fill = null;
219
+ var txt = null;
220
+ if (img) {
221
+ var brightness = calculateImageBrightness(img);
222
+ // brightness < 128 = light background.
223
+ fill = brightness < 128 ? "white" : "#114d78";
224
+ txt = brightness < 128 ? "black" : "white";
225
+ }
226
+ else {
227
+ fill = bgColor ? bgColor : "#114d78";
228
+ txt = txtColor ? txtColor : "white";
229
+ }
230
+ ctx.fillStyle = fill;
231
+ ctx.fill();
232
+ var maxTextWidth = size * 0.8;
233
+ var maxTextHeight = size * 0.8;
234
+ var minTextSize = Math.floor(size / 10);
235
+ var textSize = findOptimalFontSize(text, maxTextWidth, maxTextHeight, minTextSize);
236
+ ctx.font = "bold ".concat(textSize, "px Arial");
237
+ ctx.fillStyle = txt;
238
+ ctx.textAlign = "center";
239
+ ctx.textBaseline = "middle";
240
+ ctx.fillText(text, size / 2, size / 2);
241
+ };
242
+ var drawWithImage = function (img) {
243
+ var aspectRatio = img.width / img.height;
244
+ var imageSize = Math.min(size / 2, img.width, img.height);
245
+ if (imageSize / aspectRatio > size / 2) {
246
+ imageSize = (size / 2) * aspectRatio;
247
+ }
248
+ var imageX = (size - imageSize) / 2;
249
+ var imageY = (size - imageSize) / 2 - imageSize / 3;
250
+ ctx.beginPath();
251
+ ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI, false);
252
+ var brightness = calculateImageBrightness(img);
253
+ var bgColor = brightness < 128 ? "white" : "#114d78";
254
+ var txtColor = brightness < 128 ? "black" : "white";
255
+ ctx.fillStyle = bgColor;
256
+ ctx.fill();
257
+ ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
258
+ ctx.shadowOffsetX = 3;
259
+ ctx.shadowOffsetY = 3;
260
+ ctx.shadowBlur = 5;
261
+ ctx.drawImage(img, imageX, imageY, imageSize, imageSize);
262
+ var padding = imageSize / 6;
263
+ var maxTextWidth = imageSize;
264
+ var maxTextHeight = imageSize - padding;
265
+ var minTextSize = Math.floor(imageSize / 5);
266
+ var textSize = findOptimalFontSize(text, maxTextWidth, maxTextHeight, minTextSize);
267
+ ctx.font = "bold ".concat(textSize, "px Arial");
268
+ ctx.fillStyle = txtColor;
269
+ ctx.textAlign = "center";
270
+ ctx.textBaseline = "top";
271
+ ctx.shadowColor = "rgba(0, 0, 0, 0)";
272
+ ctx.shadowOffsetX = 0;
273
+ ctx.shadowOffsetY = 0;
274
+ ctx.shadowBlur = 0;
275
+ ctx.fillText(text, size / 2, imageY + imageSize + padding);
276
+ };
277
+ var findOptimalFontSize = function (text, maxWidth, maxHeight, minSize) {
278
+ var fontSize = maxHeight;
279
+ var tempCanvas = document.createElement("canvas");
280
+ var tempCtx = tempCanvas.getContext("2d");
281
+ while (fontSize > minSize) {
282
+ tempCtx.font = "bold ".concat(fontSize, "px Arial");
283
+ var measuredWidth = tempCtx.measureText(text).width;
284
+ var measuredHeight = fontSize * 1.2; // Allowing some padding
285
+ if (measuredWidth <= maxWidth && measuredHeight <= maxHeight) {
286
+ break;
287
+ }
288
+ fontSize--;
289
+ }
290
+ return fontSize;
291
+ };
292
+ var calculateImageBrightness = function (img) {
293
+ var brightness = 0;
294
+ var tempCanvas = document.createElement("canvas");
295
+ tempCanvas.width = img.width;
296
+ tempCanvas.height = img.height;
297
+ var tempCtx = tempCanvas.getContext("2d");
298
+ tempCtx.drawImage(img, 0, 0);
299
+ var imageData = tempCtx.getImageData(0, 0, img.width, img.height).data;
300
+ for (var i = 0; i < imageData.length; i += 4) {
301
+ var r = imageData[i];
302
+ var g = imageData[i + 1];
303
+ var b = imageData[i + 2];
304
+ brightness += (r + g + b) / 3;
305
+ }
306
+ return Math.round(brightness / (img.width * img.height));
307
+ };
308
+ if (iconUrl) {
309
+ var img_1 = new Image();
310
+ img_1.crossOrigin = "anonymous";
311
+ img_1.src = iconUrl;
312
+ img_1.onload = function () {
313
+ if (size > 50) {
314
+ drawWithImage(img_1);
315
+ }
316
+ else {
317
+ drawWithoutImage(img_1);
318
+ }
319
+ res(canvas);
320
+ };
321
+ img_1.onerror = function () {
322
+ drawWithoutImage();
323
+ res(canvas);
324
+ };
325
+ }
326
+ else {
327
+ drawWithoutImage();
328
+ res(canvas);
329
+ }
330
+ });
331
+ _clusterImageCache.Set(key, prom);
332
+ prom.then(function (canvas) {
333
+ _clusterImageLoadedCache.Set(key, canvas);
334
+ _clusterImageCache.Set(key, null);
335
+ });
336
+ return [4 /*yield*/, prom];
337
+ case 3: return [2 /*return*/, _a.sent()];
338
+ }
339
+ });
340
+ });
341
+ }
342
+ function getClusterImage(params) {
343
+ var size = params.size, txtColor = params.txtColor, bgColor = params.bgColor, text = params.text, iconUrl = params.iconUrl;
344
+ var key = "".concat(size, "-").concat(txtColor, "-").concat(bgColor, "-").concat(text, "-").concat(iconUrl);
345
+ var cacheData = _clusterImageLoadedCache.Get(key);
346
+ // If available then return.
347
+ if (cacheData) {
348
+ return cacheData;
349
+ }
350
+ // If not available then queue for it to cook.
351
+ else {
352
+ _loadClusterImage(params);
353
+ }
354
+ return null;
355
+ }
356
+ var PointClustering = /** @class */ (function () {
357
+ function PointClustering(register, menuItemId) {
358
+ var _this = this;
359
+ this.disposed = false;
360
+ this.registeredEntityIds = new Set();
361
+ this.register = register;
362
+ this.viewer = register.Viewer;
363
+ this.menuItemId = menuItemId;
364
+ this.updateClusterSpacing(0);
365
+ var boundary = new Rectangle(0, 0, 360, 180);
366
+ this.quadTree = new Quad(boundary, 4);
367
+ this.prevClusteredEntities = new Set();
368
+ this.currClusteredEntities = new Set();
369
+ this.clusterEntities = new Map();
370
+ this.updateQueue = new bruce_models_1.DelayQueue(function () {
371
+ _this.doUpdate();
372
+ }, 1000);
373
+ this.listenCamera();
374
+ }
375
+ /**
376
+ * Starts listening to camera changes.
377
+ * This will trigger the clustering update whenever camera is moved.
378
+ */
379
+ PointClustering.prototype.listenCamera = function () {
380
+ var _this = this;
381
+ var viewer = this.viewer;
382
+ var lastPos3d;
383
+ var hasMoved = function () {
384
+ var _a;
385
+ var cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.positionCartographic;
386
+ if (!cameraPos) {
387
+ return false;
388
+ }
389
+ var pos3d = Cesium.Cartographic.toCartesian(cameraPos);
390
+ if (!lastPos3d) {
391
+ lastPos3d = pos3d;
392
+ return false;
393
+ }
394
+ var distance = Cesium.Cartesian3.distance(pos3d, lastPos3d);
395
+ lastPos3d = pos3d;
396
+ // 50 meter movement at least.
397
+ return distance > 50;
398
+ };
399
+ this.listenCameraRemoval = viewer.scene.camera.changed.addEventListener(function () {
400
+ if (hasMoved()) {
401
+ _this.updateQueue.Call();
402
+ }
403
+ });
404
+ };
405
+ /**
406
+ * Stops listening to camera changes.
407
+ */
408
+ PointClustering.prototype.unlistenCamera = function () {
409
+ if (this.listenCameraRemoval) {
410
+ this.listenCameraRemoval();
411
+ this.listenCameraRemoval = null;
412
+ }
413
+ };
414
+ /**
415
+ * Removes all clusters and updates entities to no longer be suppressed.
416
+ * @returns
417
+ */
418
+ PointClustering.prototype.Dispose = function () {
419
+ if (this.disposed) {
420
+ return;
421
+ }
422
+ for (var _i = 0, _a = Array.from(this.clusterEntities); _i < _a.length; _i++) {
423
+ var _b = _a[_i], entity = _b[1];
424
+ this.viewer.entities.remove(entity);
425
+ }
426
+ this.clusterEntities.clear();
427
+ this.unlistenCamera();
428
+ this.disposed = true;
429
+ // Restore entities.
430
+ var toUpdateIds = [];
431
+ for (var _c = 0, _d = Array.from(this.registeredEntityIds); _c < _d.length; _c++) {
432
+ var id = _d[_c];
433
+ var rego = this.register.GetRego({
434
+ entityId: id,
435
+ menuItemId: this.menuItemId
436
+ });
437
+ if (rego && rego.suppressShow) {
438
+ rego.suppressShow = false;
439
+ toUpdateIds.push(id);
440
+ }
441
+ }
442
+ if (toUpdateIds.length) {
443
+ this.register.ForceUpdate({
444
+ entityIds: toUpdateIds,
445
+ });
446
+ }
447
+ };
448
+ /**
449
+ * Calculates center of given points.
450
+ * @param points
451
+ * @returns
452
+ */
453
+ PointClustering.prototype.calculateCentroid = function (points) {
454
+ var lonSum = 0;
455
+ var latSum = 0;
456
+ for (var _i = 0, points_1 = points; _i < points_1.length; _i++) {
457
+ var point = points_1[_i];
458
+ lonSum += point.lon;
459
+ latSum += point.lat;
460
+ }
461
+ var lonAvg = lonSum / points.length;
462
+ var latAvg = latSum / points.length;
463
+ return new Point(lonAvg, latAvg, "");
464
+ };
465
+ /**
466
+ * Gathers current clusters and renders them.
467
+ * @returns
468
+ */
469
+ PointClustering.prototype.doUpdate = function () {
470
+ var _this = this;
471
+ if (this.disposed) {
472
+ return;
473
+ }
474
+ // 1: Update precision.
475
+ // This defines how far apart these clusters can be.
476
+ var cameraHeight = this.viewer.camera.positionCartographic.height;
477
+ this.updateClusterSpacing(cameraHeight);
478
+ // 2: Get clusters.
479
+ var _a = this.getClusters(), clusters = _a.clusters, noLongerClustered = _a.noLongerClustered;
480
+ // 3: Remove all cesium cluster entities that are no longer clustered.
481
+ for (var _i = 0, _b = Array.from(noLongerClustered); _i < _b.length; _i++) {
482
+ var id = _b[_i];
483
+ this.removeClusterEntity(id);
484
+ }
485
+ // 4: Stop hiding entities that are no longer clustered.
486
+ var previousEntityIds = new Set(this.registeredEntityIds);
487
+ for (var _c = 0, _d = Array.from(previousEntityIds); _c < _d.length; _c++) {
488
+ var id = _d[_c];
489
+ if (!this.currClusteredEntities.has(id)) {
490
+ var rego = this.register.GetRego({
491
+ entityId: id,
492
+ menuItemId: this.menuItemId
493
+ });
494
+ if (rego && rego.suppressShow) {
495
+ rego.suppressShow = false;
496
+ this.register.ForceUpdate({
497
+ entityIds: [id],
498
+ });
499
+ }
500
+ }
501
+ }
502
+ var getScale = function (count) {
503
+ var baseSize = 20;
504
+ var scalingFactor = 2;
505
+ var scale = baseSize + scalingFactor * (count - 1);
506
+ scale = Math.min(scale, 120);
507
+ return scale;
508
+ };
509
+ /**
510
+ * Generate circle with label in center.
511
+ * @param count
512
+ * @returns
513
+ */
514
+ var getCanvas = function (count) {
515
+ var params = {
516
+ size: getScale(count),
517
+ txtColor: _this.pointColorTxt,
518
+ bgColor: _this.pointColorBg,
519
+ text: String(count),
520
+ iconUrl: _this.iconUrl
521
+ };
522
+ return getClusterImage(params);
523
+ };
524
+ var _loop_1 = function (cluster) {
525
+ var clusterId = cluster.center.id;
526
+ var clusterEntity = this_1.clusterEntities.get(clusterId);
527
+ var centroid = this_1.calculateCentroid(cluster.points);
528
+ var count = cluster.points.length;
529
+ if (clusterEntity) {
530
+ clusterEntity.position = Cesium.Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150);
531
+ clusterEntity.billboard.image = new Cesium.CallbackProperty(function () {
532
+ return getCanvas(count);
533
+ }, false),
534
+ clusterEntity.billboard.width = getScale(cluster.points.length);
535
+ clusterEntity.billboard.height = getScale(cluster.points.length);
536
+ }
537
+ else {
538
+ clusterEntity = this_1.viewer.entities.add({
539
+ position: Cesium.Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150),
540
+ billboard: {
541
+ heightReference: Cesium.HeightReference.NONE,
542
+ image: new Cesium.CallbackProperty(function () {
543
+ return getCanvas(count);
544
+ }, false),
545
+ width: getScale(count),
546
+ height: getScale(count),
547
+ verticalOrigin: Cesium.VerticalOrigin.CENTER,
548
+ horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
549
+ disableDepthTestDistance: Number.POSITIVE_INFINITY
550
+ }
551
+ });
552
+ this_1.clusterEntities.set(clusterId, clusterEntity);
553
+ }
554
+ for (var i = 0; i < cluster.points.length; i++) {
555
+ var entityId = cluster.points[i].id;
556
+ var rego = this_1.register.GetRego({
557
+ entityId: entityId,
558
+ menuItemId: this_1.menuItemId
559
+ });
560
+ if (rego && !rego.suppressShow) {
561
+ rego.suppressShow = true;
562
+ this_1.register.ForceUpdate({
563
+ entityIds: [entityId],
564
+ });
565
+ }
566
+ }
567
+ };
568
+ var this_1 = this;
569
+ // 5: iterate over clusters and add/update them as Cesium entities.
570
+ for (var _e = 0, clusters_1 = clusters; _e < clusters_1.length; _e++) {
571
+ var cluster = clusters_1[_e];
572
+ _loop_1(cluster);
573
+ }
574
+ var _loop_2 = function (clusterId, clusterEntity) {
575
+ if (!clusters.find(function (x) { return x.center.id == clusterId; })) {
576
+ this_2.viewer.entities.remove(clusterEntity);
577
+ this_2.clusterEntities.delete(clusterId);
578
+ }
579
+ };
580
+ var this_2 = this;
581
+ // 6: Iterate over existing cluster entities and remove those that are no longer clustered.
582
+ for (var _f = 0, _g = Array.from(this.clusterEntities); _f < _g.length; _f++) {
583
+ var _h = _g[_f], clusterId = _h[0], clusterEntity = _h[1];
584
+ _loop_2(clusterId, clusterEntity);
585
+ }
586
+ };
587
+ /**
588
+ * Updates how apart clusters can be based on camera distance.
589
+ * @param cameraHeight
590
+ */
591
+ PointClustering.prototype.updateClusterSpacing = function (cameraHeight) {
592
+ // Camera height thresholds in meters.
593
+ var cameraHeightThresholds = [4000, 5000, 8000, 13000, 25000, 40000];
594
+ // Distance increments in degrees.
595
+ var distanceIncrements = [0.005, 0.01, 0.02, 0.04, 0.1, 0.5];
596
+ // Find the appropriate spacing based on the camera height.
597
+ var spacing = 0;
598
+ for (var i = 0; i < cameraHeightThresholds.length; i++) {
599
+ if (cameraHeight && cameraHeight < cameraHeightThresholds[i]) {
600
+ spacing += distanceIncrements[i];
601
+ break;
602
+ }
603
+ spacing += distanceIncrements[i];
604
+ }
605
+ this.distanceBetweenClusters = spacing;
606
+ };
607
+ /**
608
+ * Gathers clusters.
609
+ * @returns
610
+ */
611
+ PointClustering.prototype.getClusters = function () {
612
+ var _this = this;
613
+ this.currClusteredEntities.clear();
614
+ var clusters = [];
615
+ var processedPoints = new Set();
616
+ var cameraPosition = this.viewer.camera.position;
617
+ for (var _i = 0, _a = this.quadTree.points; _i < _a.length; _i++) {
618
+ var point = _a[_i];
619
+ // Skip points already processed in previous clusters.
620
+ if (processedPoints.has(point.id)) {
621
+ continue;
622
+ }
623
+ // Skip points closer than 4000 meters to the camera.
624
+ var cartesian3 = Cesium.Cartesian3.fromDegrees(point.lon, point.lat);
625
+ if (Cesium.Cartesian3.distance(cartesian3, cameraPosition) <= 4000) {
626
+ continue;
627
+ }
628
+ var found = [];
629
+ var nearbyPoints = this.quadTree.Query(new Circle(point.lon, point.lat, this.distanceBetweenClusters), found);
630
+ if (nearbyPoints.length > 1) {
631
+ var cluster = { center: point, points: [] };
632
+ for (var _b = 0, nearbyPoints_1 = nearbyPoints; _b < nearbyPoints_1.length; _b++) {
633
+ var nearby = nearbyPoints_1[_b];
634
+ if (!cluster.points.includes(nearby)) {
635
+ cluster.points.push(nearby);
636
+ processedPoints.add(nearby.id);
637
+ this.currClusteredEntities.add(nearby.id);
638
+ }
639
+ }
640
+ clusters.push(cluster);
641
+ }
642
+ }
643
+ // Filter out clusters with only one point.
644
+ var validClusters = clusters.filter(function (cluster) { return cluster.points.length > 1; });
645
+ // Merge adjacent clusters.
646
+ var mergedClusters = this.mergeClusters(validClusters);
647
+ // Get the entity IDs that are no longer clustered
648
+ var noLongerClustered = new Set(Array.from(this.prevClusteredEntities).filter(function (id) { return !_this.currClusteredEntities.has(id); }));
649
+ // Update the previous clustered entities ref.
650
+ this.prevClusteredEntities = new Set(this.currClusteredEntities);
651
+ return { clusters: mergedClusters, noLongerClustered: noLongerClustered };
652
+ };
653
+ /**
654
+ * Merges clusters that are nearby based on the distanceBetweenClusters value.
655
+ * @param clusters
656
+ * @returns
657
+ */
658
+ PointClustering.prototype.mergeClusters = function (clusters) {
659
+ var _a;
660
+ var mergedClusters = [].concat(clusters);
661
+ // Keep looping while merges keep happening.
662
+ var mergeOccurred = true;
663
+ while (mergeOccurred) {
664
+ mergeOccurred = false;
665
+ for (var i = 0; i < mergedClusters.length - 1; i++) {
666
+ for (var j = i + 1; j < mergedClusters.length; j++) {
667
+ var cluster1 = mergedClusters[i];
668
+ var cluster2 = mergedClusters[j];
669
+ var centerDistance = this.calculateDistance(cluster1.center.lon, cluster1.center.lat, cluster2.center.lon, cluster2.center.lat);
670
+ var distanceThreshold = this.distanceBetweenClusters;
671
+ if (centerDistance <= distanceThreshold) {
672
+ // Merge clusters.
673
+ (_a = cluster1.points).push.apply(_a, cluster2.points);
674
+ mergedClusters.splice(j, 1);
675
+ this.removeClusterEntity(cluster2.center.id);
676
+ mergeOccurred = true;
677
+ break;
678
+ }
679
+ }
680
+ if (mergeOccurred) {
681
+ break;
682
+ }
683
+ }
684
+ }
685
+ return mergedClusters;
686
+ };
687
+ /**
688
+ * Removes Cesium cluster entity.
689
+ * @param clusterId
690
+ */
691
+ PointClustering.prototype.removeClusterEntity = function (clusterId) {
692
+ var clusterEntity = this.clusterEntities.get(clusterId);
693
+ if (clusterEntity) {
694
+ this.viewer.entities.remove(clusterEntity);
695
+ this.clusterEntities.delete(clusterId);
696
+ }
697
+ };
698
+ PointClustering.prototype.convertCartesianToCartographic = function (cartesian3) {
699
+ var carto = Cesium.Cartographic.fromCartesian(cartesian3);
700
+ var lon = Cesium.Math.toDegrees(carto.longitude);
701
+ var lat = Cesium.Math.toDegrees(carto.latitude);
702
+ var id = lon + ',' + lat;
703
+ return new Point(lon, lat, id);
704
+ };
705
+ /**
706
+ * Adds new entity to the clustering logic.
707
+ * @param id Bruce entity ID.
708
+ * @param cartesian3 entity's position.
709
+ */
710
+ PointClustering.prototype.addPoint = function (id, cartesian3) {
711
+ var point = this.convertCartesianToCartographic(cartesian3);
712
+ point.id = id;
713
+ this.quadTree.Insert(point);
714
+ };
715
+ /**
716
+ * Calculates rough distance across earth between two points.
717
+ * @param lon1
718
+ * @param lat1
719
+ * @param lon2
720
+ * @param lat2
721
+ * @returns
722
+ */
723
+ PointClustering.prototype.calculateDistance = function (lon1, lat1, lon2, lat2) {
724
+ var lonDelta = Math.abs(lon1 - lon2);
725
+ var latDelta = Math.abs(lat1 - lat2);
726
+ // Approximate radius of the Earth in kilometers
727
+ var earthRadius = 6371;
728
+ var distance = 2 * Math.asin(Math.sqrt(Math.sin(latDelta / 2) * Math.sin(latDelta / 2) +
729
+ Math.cos(lat1) * Math.cos(lat2) * Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2))) * earthRadius;
730
+ return distance;
731
+ };
732
+ /**
733
+ * Adds entity to clustering logic.
734
+ * Will return false if entity could not be clustered and therefor should not be hidden.
735
+ * @param id
736
+ * @param entity
737
+ * @returns
738
+ */
739
+ PointClustering.prototype.AddEntity = function (id, entity) {
740
+ if (this.disposed) {
741
+ return false;
742
+ }
743
+ if (!entity) {
744
+ return false;
745
+ }
746
+ if (entity.polygon || entity.polyline) {
747
+ return false;
748
+ }
749
+ var pos3d = GetValue(this.viewer, entity.position);
750
+ if (!(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
751
+ return false;
752
+ }
753
+ if (!this.pointColorBg && entity.point) {
754
+ var pointColorBg = GetValue(this.viewer, entity.point.color);
755
+ if (pointColorBg) {
756
+ var cColor = null;
757
+ if (pointColorBg instanceof Object) {
758
+ cColor = new Cesium.Color(pointColorBg.red, pointColorBg.green, pointColorBg.blue, pointColorBg.alpha);
759
+ }
760
+ else if (typeof pointColorBg === "string") {
761
+ cColor = Cesium.Color.fromCssColorString(pointColorBg);
762
+ }
763
+ // Determine if text color should instead be black based on background.
764
+ // cColor contains r,g,b,a values where r,g,b are in the range [0,1].
765
+ if (cColor) {
766
+ var brightness = (cColor.red * 299 + cColor.green * 587 + cColor.blue * 114) / 1000;
767
+ if (brightness > 0.5) {
768
+ this.pointColorTxt = "black";
769
+ }
770
+ else {
771
+ this.pointColorTxt = "white";
772
+ }
773
+ this.pointColorBg = cColor.toCssColorString();
774
+ }
775
+ }
776
+ }
777
+ if (!this.iconUrl && entity.billboard) {
778
+ var iconUrl = GetValue(this.viewer, entity.billboard.image);
779
+ if (typeof iconUrl == "string") {
780
+ this.iconUrl = iconUrl;
781
+ }
782
+ }
783
+ this.registeredEntityIds.add(id);
784
+ this.addPoint(id, pos3d);
785
+ this.updateQueue.Call();
786
+ return true;
787
+ };
788
+ /**
789
+ * Removes entity from clustering logic.
790
+ * Warning: This will not reveal the entity, suppressShow will remain true.
791
+ * This is made with the assumption that the entity is being removed from viewer.
792
+ * @param id
793
+ * @returns
794
+ */
795
+ PointClustering.prototype.RemoveEntity = function (id) {
796
+ if (this.disposed) {
797
+ return;
798
+ }
799
+ var pointToRemove = this.quadTree.Find(id);
800
+ if (pointToRemove) {
801
+ this.quadTree.Remove(pointToRemove);
802
+ this.updateQueue.Call();
803
+ }
804
+ this.registeredEntityIds.delete(id);
805
+ };
806
+ return PointClustering;
807
+ }());
808
+ exports.PointClustering = PointClustering;
809
+ //# sourceMappingURL=point-clustering.js.map