bruce-cesium 7.5.5 → 7.5.6
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 +841 -346
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +839 -344
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +1 -1
- package/dist/lib/internal/image-utils.js +24 -1
- package/dist/lib/internal/image-utils.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-point.js +16 -25
- package/dist/lib/rendering/entity-render-engine-point.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-polygon.js +16 -14
- package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
- package/dist/lib/rendering/render-managers/common/entity-label-group.js +178 -64
- package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -1
- package/dist/lib/rendering/render-managers/common/entity-label.js +437 -71
- package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
- package/dist/lib/rendering/render-managers/other/google-search-render-manager.js +32 -37
- package/dist/lib/rendering/render-managers/other/google-search-render-manager.js.map +1 -1
- package/dist/lib/rendering/visuals-register.js +7 -0
- package/dist/lib/rendering/visuals-register.js.map +1 -1
- package/dist/types/bruce-cesium.d.ts +1 -1
- package/dist/types/internal/image-utils.d.ts +1 -0
- package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +9 -1
- package/dist/types/rendering/render-managers/common/entity-label.d.ts +13 -2
- package/dist/types/rendering/visuals-register.d.ts +1 -0
- package/package.json +1 -1
|
@@ -47,11 +47,196 @@ var EntityLabel;
|
|
|
47
47
|
const ICON_RING_PADDING = 4;
|
|
48
48
|
// Small points still get a ring as large as the default near ring.
|
|
49
49
|
const ICON_RING_MIN = 10;
|
|
50
|
+
// A photo grows by this much while its entity is hovered, since it is content worth a closer look.
|
|
51
|
+
const PHOTO_HOVER_SCALE = 2.5;
|
|
52
|
+
// How long a label must stay on show before its photo is fetched, so labels that only flash past while
|
|
53
|
+
// clusters settle or the camera sweeps by cost nothing.
|
|
54
|
+
const PHOTO_SHOW_DELAY = 250;
|
|
55
|
+
// Photos rest this much larger than an icon on its chip, since they are content rather than a marker.
|
|
56
|
+
const PHOTO_SIZE_SCALE = 1.5;
|
|
57
|
+
// Corner radius of a photo too oblong for a circle, relative to its longest side.
|
|
58
|
+
const PHOTO_CORNER = 0.15;
|
|
59
|
+
// How long a photo takes to grow into its label, and to grow on hover.
|
|
60
|
+
const PHOTO_TRANSITION = 250;
|
|
61
|
+
// Downsized photos kept per viewer before the least recently used is released.
|
|
62
|
+
const PHOTO_CACHE_LIMIT = 500;
|
|
63
|
+
// Key where the viewer keeps its downsized photos.
|
|
64
|
+
const PHOTO_CACHE_KEY = "_nextspaceLabelPhotos";
|
|
50
65
|
// One per tinted icon, since an SVG mask is addressed by id across the whole document.
|
|
51
66
|
let tintCounter = 0;
|
|
52
67
|
// Key where the label instance is stored within the cesium entity.
|
|
53
68
|
// Eg: rego[ATTACH_KEY] = new EntityLabel(...);
|
|
54
69
|
EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
|
|
70
|
+
// Image types a label draws, read from the file extension. Anything else stays text.
|
|
71
|
+
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"];
|
|
72
|
+
// Shortest side a thumbnail needs before it is preferred over the full file.
|
|
73
|
+
const IMAGE_THUMBNAIL_MIN = 64;
|
|
74
|
+
// Size a found image is drawn at when the group names none, so a photo never lands at its natural size.
|
|
75
|
+
const IMAGE_SIZE = 24;
|
|
76
|
+
function isImageFile(name) {
|
|
77
|
+
return typeof name === "string" && IMAGE_EXTENSIONS.includes(name.split(".").pop().toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
// Whether a label was configured with an icon of its own, which always outranks a found photo.
|
|
80
|
+
function HasIcon(options) {
|
|
81
|
+
return Boolean(options.iconUrl || options.iconClass || options.icon);
|
|
82
|
+
}
|
|
83
|
+
EntityLabel.HasIcon = HasIcon;
|
|
84
|
+
/*
|
|
85
|
+
* Whether a label line is an http(s) link to an image, judged by its path so a query string cannot pass for one.
|
|
86
|
+
*/
|
|
87
|
+
function IsImageUrl(value) {
|
|
88
|
+
const text = value === null || value === void 0 ? void 0 : value.trim();
|
|
89
|
+
if (!text || /\s/.test(text)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
let url;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(text);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return (url.protocol === "http:" || url.protocol === "https:") && isImageFile(url.pathname);
|
|
100
|
+
}
|
|
101
|
+
EntityLabel.IsImageUrl = IsImageUrl;
|
|
102
|
+
/*
|
|
103
|
+
* The entity's default image as InfoView picks it: its first photo attachment by display order that is an image file.
|
|
104
|
+
* Reads attachments expanded onto the entity, and prefers the smallest thumbnail that still covers an icon.
|
|
105
|
+
*/
|
|
106
|
+
function GetDefaultImageUrl(entity, api) {
|
|
107
|
+
var _a, _b, _c;
|
|
108
|
+
const attachments = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Attachment) !== null && _b !== void 0 ? _b : [];
|
|
109
|
+
const photo = attachments
|
|
110
|
+
.filter(x => (x === null || x === void 0 ? void 0 : x["EntityAttachmentType.ID"]) == bruce_models_1.EntityAttachmentType.EType.Photo && x.ClientFile)
|
|
111
|
+
.filter(x => isImageFile(x.ClientFile.FileExt || x.ClientFile.OriginalFileName))
|
|
112
|
+
.sort((a, b) => { var _a, _b; return ((_a = a.DisplayOrder) !== null && _a !== void 0 ? _a : 0) - ((_b = b.DisplayOrder) !== null && _b !== void 0 ? _b : 0); })[0];
|
|
113
|
+
if (!photo) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const thumbnail = ((_c = photo.ClientFile.Thumbnails) !== null && _c !== void 0 ? _c : [])
|
|
117
|
+
.filter(x => Math.min(x.Width, x.Height) >= IMAGE_THUMBNAIL_MIN && (x.URL || x["ClientFile.ID"]))
|
|
118
|
+
.sort((a, b) => a.Width * a.Height - b.Width * b.Height)[0];
|
|
119
|
+
if (thumbnail) {
|
|
120
|
+
return thumbnail.URL || bruce_models_1.ClientFile.GetUrl({ fileId: thumbnail["ClientFile.ID"], api, viaCdn: true });
|
|
121
|
+
}
|
|
122
|
+
return photo.ClientFile.URL || photo.URL || bruce_models_1.ClientFile.GetUrl({ fileId: photo["ClientFile.ID"], api, viaCdn: true });
|
|
123
|
+
}
|
|
124
|
+
EntityLabel.GetDefaultImageUrl = GetDefaultImageUrl;
|
|
125
|
+
/*
|
|
126
|
+
* Gives a label an image it found rather than one it was configured with.
|
|
127
|
+
* A text label gains it beside the text, or alone when the image was all the text there was.
|
|
128
|
+
*/
|
|
129
|
+
function WithPhoto(options, url) {
|
|
130
|
+
var _a, _b, _c, _d;
|
|
131
|
+
const presentation = (_a = options.presentation) !== null && _a !== void 0 ? _a : "text";
|
|
132
|
+
options.iconUrl = url;
|
|
133
|
+
options.iconPhoto = true;
|
|
134
|
+
// A tint would flatten a photo into a silhouette of its own outline.
|
|
135
|
+
options.iconColor = undefined;
|
|
136
|
+
(_b = options.iconSize) !== null && _b !== void 0 ? _b : (options.iconSize = IMAGE_SIZE);
|
|
137
|
+
if (!((_c = options.text) === null || _c === void 0 ? void 0 : _c.trim())) {
|
|
138
|
+
options.presentation = "icon";
|
|
139
|
+
}
|
|
140
|
+
else if (presentation === "text") {
|
|
141
|
+
options.presentation = "icon-text-side";
|
|
142
|
+
}
|
|
143
|
+
if (presentation === "text") {
|
|
144
|
+
// Keeps the leader a text label draws by default.
|
|
145
|
+
(_d = options.leader) !== null && _d !== void 0 ? _d : (options.leader = true);
|
|
146
|
+
}
|
|
147
|
+
return options;
|
|
148
|
+
}
|
|
149
|
+
EntityLabel.WithPhoto = WithPhoto;
|
|
150
|
+
/*
|
|
151
|
+
* Near-square photos are cropped to a circle the size of an icon chip; the rest keep their shape with rounded corners.
|
|
152
|
+
* An unknown size reads as square, since the circle crops whatever arrives.
|
|
153
|
+
*/
|
|
154
|
+
function PhotoShape(width, height) {
|
|
155
|
+
if (!(width > 0 && height > 0)) {
|
|
156
|
+
return "circle";
|
|
157
|
+
}
|
|
158
|
+
const longest = Math.max(width, height);
|
|
159
|
+
return (longest - Math.min(width, height)) / longest <= ICON_BACKGROUND_UNIFORM_TOLERANCE ? "circle" : "rect";
|
|
160
|
+
}
|
|
161
|
+
EntityLabel.PhotoShape = PhotoShape;
|
|
162
|
+
/*
|
|
163
|
+
* Loads a photo through a plain image element, so a host without CORS still works, and redraws it once onto a canvas
|
|
164
|
+
* no larger than it is shown. Labels draw from that canvas, so none of them holds a camera original.
|
|
165
|
+
*/
|
|
166
|
+
async function shrinkPhoto(url, pixels) {
|
|
167
|
+
const none = { source: null, width: 0, height: 0 };
|
|
168
|
+
const image = new Image();
|
|
169
|
+
image.decoding = "async";
|
|
170
|
+
image.src = url;
|
|
171
|
+
try {
|
|
172
|
+
await image.decode();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return none;
|
|
176
|
+
}
|
|
177
|
+
const width = image.naturalWidth;
|
|
178
|
+
const height = image.naturalHeight;
|
|
179
|
+
if (!width || !height) {
|
|
180
|
+
return none;
|
|
181
|
+
}
|
|
182
|
+
// A circle only ever shows the centre square, so that is all worth keeping.
|
|
183
|
+
const circle = PhotoShape(width, height) === "circle";
|
|
184
|
+
const sourceWidth = circle ? Math.min(width, height) : width;
|
|
185
|
+
const sourceHeight = circle ? Math.min(width, height) : height;
|
|
186
|
+
const scale = Math.min(1, pixels / Math.max(sourceWidth, sourceHeight));
|
|
187
|
+
const canvas = document.createElement("canvas");
|
|
188
|
+
canvas.width = Math.max(1, Math.round(sourceWidth * scale));
|
|
189
|
+
canvas.height = Math.max(1, Math.round(sourceHeight * scale));
|
|
190
|
+
const context = canvas.getContext("2d");
|
|
191
|
+
if (!context) {
|
|
192
|
+
return none;
|
|
193
|
+
}
|
|
194
|
+
context.imageSmoothingQuality = "high";
|
|
195
|
+
context.drawImage(image, (width - sourceWidth) / 2, (height - sourceHeight) / 2, sourceWidth, sourceHeight, 0, 0, canvas.width, canvas.height);
|
|
196
|
+
return { source: canvas, width, height };
|
|
197
|
+
}
|
|
198
|
+
/*
|
|
199
|
+
* Downsized label photos, kept on the viewer so every label in a scene shares one load and one decode.
|
|
200
|
+
*/
|
|
201
|
+
class Photos {
|
|
202
|
+
constructor() {
|
|
203
|
+
this.entries = new Map();
|
|
204
|
+
this.ready = new Map();
|
|
205
|
+
}
|
|
206
|
+
static Get(viewer) {
|
|
207
|
+
var _a;
|
|
208
|
+
return (_a = viewer[PHOTO_CACHE_KEY]) !== null && _a !== void 0 ? _a : (viewer[PHOTO_CACHE_KEY] = new Photos());
|
|
209
|
+
}
|
|
210
|
+
// The photo when it is already drawn, so a rebuilt label shows it at once instead of growing it again.
|
|
211
|
+
Peek(url, pixels) {
|
|
212
|
+
return this.ready.get(`${pixels} ${url}`);
|
|
213
|
+
}
|
|
214
|
+
Load(url, pixels) {
|
|
215
|
+
const key = `${pixels} ${url}`;
|
|
216
|
+
let entry = this.entries.get(key);
|
|
217
|
+
if (entry) {
|
|
218
|
+
// Reinsertion keeps the map in least recently used order.
|
|
219
|
+
this.entries.delete(key);
|
|
220
|
+
this.entries.set(key, entry);
|
|
221
|
+
return entry;
|
|
222
|
+
}
|
|
223
|
+
entry = shrinkPhoto(url, pixels).then(photo => {
|
|
224
|
+
if (this.entries.get(key) === entry) {
|
|
225
|
+
this.ready.set(key, photo);
|
|
226
|
+
}
|
|
227
|
+
return photo;
|
|
228
|
+
});
|
|
229
|
+
this.entries.set(key, entry);
|
|
230
|
+
for (const oldest of this.entries.keys()) {
|
|
231
|
+
if (this.entries.size <= PHOTO_CACHE_LIMIT) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
this.entries.delete(oldest);
|
|
235
|
+
this.ready.delete(oldest);
|
|
236
|
+
}
|
|
237
|
+
return entry;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
55
240
|
/*
|
|
56
241
|
* Indexes screen rectangles so sparse scenes do not compare every label with every other label.
|
|
57
242
|
*/
|
|
@@ -768,6 +953,10 @@ var EntityLabel;
|
|
|
768
953
|
this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
|
|
769
954
|
// Counter to help differentiate between createLabel calls.
|
|
770
955
|
this.counter = 0;
|
|
956
|
+
// Whether this label has asked its group for a default image, which it does once on screen.
|
|
957
|
+
this.imageAsked = false;
|
|
958
|
+
// The photo a label outside any group found while reading its attributes.
|
|
959
|
+
this.photo = null;
|
|
771
960
|
// The label element itself.
|
|
772
961
|
this.label = null;
|
|
773
962
|
// Callback to kill render listener.
|
|
@@ -802,6 +991,7 @@ var EntityLabel;
|
|
|
802
991
|
this.visible = params.visible;
|
|
803
992
|
if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
|
|
804
993
|
this.additionalTexts = [];
|
|
994
|
+
this.photo = null;
|
|
805
995
|
this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
|
|
806
996
|
this.createLabel(params.pos3d);
|
|
807
997
|
if (!params.api && !bruce_models_1.ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
|
|
@@ -864,6 +1054,7 @@ var EntityLabel;
|
|
|
864
1054
|
this.options = options || {};
|
|
865
1055
|
this.groupId = groupId;
|
|
866
1056
|
this.additionalTexts = this.options.text != null ? [] : null;
|
|
1057
|
+
this.photo = null;
|
|
867
1058
|
this.createLabel();
|
|
868
1059
|
}
|
|
869
1060
|
/**
|
|
@@ -871,8 +1062,8 @@ var EntityLabel;
|
|
|
871
1062
|
* @param starterPos3d optional position to create label at. Ignored for billboard labels.
|
|
872
1063
|
*/
|
|
873
1064
|
async createLabel(starterPos3d) {
|
|
874
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
875
|
-
|
|
1065
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
1066
|
+
// The old label stays until its replacement is built, so a rebuild never blinks while a location loads.
|
|
876
1067
|
let pos3d = starterPos3d ? starterPos3d.clone() : null;
|
|
877
1068
|
const counter = ++this.counter;
|
|
878
1069
|
if (this.disposed) {
|
|
@@ -1030,6 +1221,7 @@ var EntityLabel;
|
|
|
1030
1221
|
}
|
|
1031
1222
|
}
|
|
1032
1223
|
if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
|
|
1224
|
+
this.removeLabel();
|
|
1033
1225
|
return;
|
|
1034
1226
|
}
|
|
1035
1227
|
pos3d = drawing_utils_1.DrawingUtils.EnsurePosHeight({
|
|
@@ -1039,6 +1231,7 @@ var EntityLabel;
|
|
|
1039
1231
|
viewer: this.viewer
|
|
1040
1232
|
});
|
|
1041
1233
|
if (isNaN(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
|
|
1234
|
+
this.removeLabel();
|
|
1042
1235
|
return;
|
|
1043
1236
|
}
|
|
1044
1237
|
this._pos3d = pos3d;
|
|
@@ -1104,35 +1297,39 @@ var EntityLabel;
|
|
|
1104
1297
|
catch (e) {
|
|
1105
1298
|
console.error(e);
|
|
1106
1299
|
}
|
|
1107
|
-
|
|
1108
|
-
//
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
console.error(e);
|
|
1300
|
+
const attributes = (_a = this.options.attributes) !== null && _a !== void 0 ? _a : (_c = (_b = entityType === null || entityType === void 0 ? void 0 : entityType.VisualSettings) === null || _b === void 0 ? void 0 : _b.label) === null || _c === void 0 ? void 0 : _c.attributes;
|
|
1301
|
+
// Read for its attributes, and for a photo when the label has no icon of its own.
|
|
1302
|
+
if (attributes || !HasIcon(this.options)) {
|
|
1303
|
+
let entity = null;
|
|
1304
|
+
try {
|
|
1305
|
+
entity = (await bruce_models_1.Entity.Get({
|
|
1306
|
+
api: this.api,
|
|
1307
|
+
entityId: this.rego.entityId,
|
|
1308
|
+
scenario: ((_d = this.rego.outline) === null || _d === void 0 ? void 0 : _d.length) ? (_e = this.rego.outline.find(x => !!x.Scenario)) === null || _e === void 0 ? void 0 : _e.Scenario : null,
|
|
1309
|
+
schemaId: this.rego.schema,
|
|
1310
|
+
migrated: true,
|
|
1311
|
+
expandAttachments: !HasIcon(this.options)
|
|
1312
|
+
})).entity;
|
|
1313
|
+
}
|
|
1314
|
+
// Entity doesn't exist.
|
|
1315
|
+
// Likely a graphic with an ID that points to no record.
|
|
1316
|
+
catch (e) {
|
|
1317
|
+
console.error(e);
|
|
1318
|
+
}
|
|
1319
|
+
for (let i = 0; entity && attributes && i < attributes.length; i++) {
|
|
1320
|
+
const value = bruce_models_1.Entity.GetValue({
|
|
1321
|
+
entity: entity,
|
|
1322
|
+
path: attributes[i]
|
|
1323
|
+
});
|
|
1324
|
+
if (value != null) {
|
|
1325
|
+
additionalTexts.push(value);
|
|
1134
1326
|
}
|
|
1135
1327
|
}
|
|
1328
|
+
// A linked image outranks the default one, as it does for group labels.
|
|
1329
|
+
if (entity && !HasIcon(this.options)) {
|
|
1330
|
+
const at = additionalTexts.findIndex(value => IsImageUrl(String(value)));
|
|
1331
|
+
this.photo = at >= 0 ? String(additionalTexts.splice(at, 1)[0]).trim() : GetDefaultImageUrl(entity, this.api);
|
|
1332
|
+
}
|
|
1136
1333
|
}
|
|
1137
1334
|
}
|
|
1138
1335
|
this.additionalTexts = additionalTexts;
|
|
@@ -1151,9 +1348,13 @@ var EntityLabel;
|
|
|
1151
1348
|
if (this.disposed || this.counter != counter) {
|
|
1152
1349
|
return;
|
|
1153
1350
|
}
|
|
1154
|
-
|
|
1351
|
+
this.removeLabel();
|
|
1352
|
+
// A found photo draws as if configured, without changing the options clustering compares and measures.
|
|
1353
|
+
const photo = HasIcon(this.options) ? null : (_j = this.options.photo) !== null && _j !== void 0 ? _j : this.photo;
|
|
1354
|
+
const look = photo ? WithPhoto({ ...this.options, text: (_k = this.options.text) !== null && _k !== void 0 ? _k : [text, ...additionalTexts].join("\n") }, photo) : this.options;
|
|
1355
|
+
const presentation = look.presentation || "text";
|
|
1155
1356
|
let clickEntityIds = [];
|
|
1156
|
-
const selectOnClick = (
|
|
1357
|
+
const selectOnClick = (_l = this.options.selectOnClick) !== null && _l !== void 0 ? _l : Boolean(this.groupId);
|
|
1157
1358
|
const select = (event) => {
|
|
1158
1359
|
event.preventDefault();
|
|
1159
1360
|
event.stopPropagation();
|
|
@@ -1182,11 +1383,11 @@ var EntityLabel;
|
|
|
1182
1383
|
target.addEventListener(type, event => event.stopPropagation());
|
|
1183
1384
|
}
|
|
1184
1385
|
};
|
|
1185
|
-
const normalLeader = (
|
|
1386
|
+
const normalLeader = (_m = look.leader) !== null && _m !== void 0 ? _m : presentation === "text";
|
|
1186
1387
|
const angled = this.options.leaderStyle === "angled";
|
|
1187
1388
|
let leader = normalLeader;
|
|
1188
1389
|
const content = document.createElement("span");
|
|
1189
|
-
const originalText = (
|
|
1390
|
+
const originalText = (_o = this.options.text) !== null && _o !== void 0 ? _o : [text, ...additionalTexts].join("\n");
|
|
1190
1391
|
let textElement;
|
|
1191
1392
|
let sourceText;
|
|
1192
1393
|
let sourceNaturalText;
|
|
@@ -1201,10 +1402,33 @@ var EntityLabel;
|
|
|
1201
1402
|
if (presentation === "icon-text-above") {
|
|
1202
1403
|
content.style.flexDirection = "column-reverse";
|
|
1203
1404
|
}
|
|
1405
|
+
// A found photo's shape decides its layout once it loads: a rectangle sits under the name it adds to, a circle beside it.
|
|
1406
|
+
let photoShape = null;
|
|
1407
|
+
let growPhoto = null;
|
|
1408
|
+
// Loads the photo; called once the label is actually showing, so labels out of view or folded into a cluster never fetch one.
|
|
1409
|
+
let startPhoto = null;
|
|
1410
|
+
let photoTimer = null;
|
|
1411
|
+
const placePhoto = (shape, grow) => {
|
|
1412
|
+
photoShape = shape;
|
|
1413
|
+
growPhoto = grow;
|
|
1414
|
+
content.style.flexDirection = shape === "rect" ? "column-reverse" : "";
|
|
1415
|
+
};
|
|
1204
1416
|
if (presentation !== "text") {
|
|
1205
|
-
const built = this.buildIcon(
|
|
1417
|
+
const built = this.buildIcon(look, counter, placePhoto);
|
|
1418
|
+
startPhoto = built.start;
|
|
1206
1419
|
const icon = iconElement = built.icon;
|
|
1207
1420
|
fitIconBackground = built.fit;
|
|
1421
|
+
if (built.arriving) {
|
|
1422
|
+
// The gap opens with the photo, so a text label does not shift before its photo grows in.
|
|
1423
|
+
content.style.gap = "0px";
|
|
1424
|
+
built.arriving.then(() => {
|
|
1425
|
+
if (iconElement.style.display === "none") {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
content.style.transition = `gap ${PHOTO_TRANSITION}ms ease-out`;
|
|
1429
|
+
content.style.gap = "6px";
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1208
1432
|
makeClickable(icon);
|
|
1209
1433
|
// Keep badges outside the glyph so font classes and image masks cannot affect them.
|
|
1210
1434
|
iconContainer = document.createElement("span");
|
|
@@ -1226,7 +1450,7 @@ var EntityLabel;
|
|
|
1226
1450
|
if (presentation === "text")
|
|
1227
1451
|
makeClickable(textElement);
|
|
1228
1452
|
content.appendChild(textElement);
|
|
1229
|
-
if ((
|
|
1453
|
+
if ((_p = this.options.cluster) === null || _p === void 0 ? void 0 : _p.enabled) {
|
|
1230
1454
|
sourceText = document.createElement("span");
|
|
1231
1455
|
sourceText.className = "entity-label-source-text";
|
|
1232
1456
|
sourceText.setAttribute("aria-hidden", "true");
|
|
@@ -1246,7 +1470,9 @@ var EntityLabel;
|
|
|
1246
1470
|
countElement.style.cssText = "flex:none;align-self:center;white-space:nowrap;background:#fff;color:#000;text-shadow:none;border-radius:8px;padding:0 3px;display:none;";
|
|
1247
1471
|
// Text above the icon would have the badge land in it, so the free corner is the lower one.
|
|
1248
1472
|
const badgeBelow = presentation === "icon-text-above";
|
|
1249
|
-
|
|
1473
|
+
// A photo is put away while its label stands for a cluster, so its count sits with the text instead.
|
|
1474
|
+
const badgeOnIcon = Boolean(iconContainer) && !look.iconPhoto;
|
|
1475
|
+
if (badgeOnIcon) {
|
|
1250
1476
|
countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
|
|
1251
1477
|
iconContainer.appendChild(countElement);
|
|
1252
1478
|
}
|
|
@@ -1263,11 +1489,11 @@ var EntityLabel;
|
|
|
1263
1489
|
else {
|
|
1264
1490
|
ele = document.createElement("div");
|
|
1265
1491
|
ele.appendChild(content);
|
|
1266
|
-
if (!
|
|
1492
|
+
if (!badgeOnIcon) {
|
|
1267
1493
|
ele.appendChild(countElement);
|
|
1268
1494
|
}
|
|
1269
1495
|
ele.className = "entity-label";
|
|
1270
|
-
ele.dataset.entityId = ((
|
|
1496
|
+
ele.dataset.entityId = ((_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId) || "";
|
|
1271
1497
|
ele.dataset.presentation = presentation;
|
|
1272
1498
|
if (this.groupId) {
|
|
1273
1499
|
ele.dataset.groupId = this.groupId;
|
|
@@ -1420,7 +1646,7 @@ var EntityLabel;
|
|
|
1420
1646
|
};
|
|
1421
1647
|
let cursorMotion = this.cursorMotion;
|
|
1422
1648
|
const project = () => {
|
|
1423
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
|
|
1649
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
1424
1650
|
if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
|
|
1425
1651
|
(presentation === "icon" && this.options.iconScale === 0)) {
|
|
1426
1652
|
return null;
|
|
@@ -1458,13 +1684,18 @@ var EntityLabel;
|
|
|
1458
1684
|
if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
|
|
1459
1685
|
return null;
|
|
1460
1686
|
}
|
|
1461
|
-
|
|
1687
|
+
const onScreen = anchor.x >= 0 && anchor.y >= 0 && anchor.x < this.viewer.canvas.clientWidth && anchor.y < this.viewer.canvas.clientHeight;
|
|
1688
|
+
if (leader && !onScreen)
|
|
1462
1689
|
return null;
|
|
1690
|
+
if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
|
|
1691
|
+
this.imageAsked = true;
|
|
1692
|
+
(_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.RequestLabelImage((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId);
|
|
1693
|
+
}
|
|
1463
1694
|
const transientPivot = (this.Transient && !this.Persistent ?
|
|
1464
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
1695
|
+
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.GetLabelHoverPosition((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId, (_j = this.rego) === null || _j === void 0 ? void 0 : _j.menuItemId)) : undefined);
|
|
1465
1696
|
const hover = (presentation !== "icon" ?
|
|
1466
1697
|
(this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
|
|
1467
|
-
(
|
|
1698
|
+
(_k = this.visualRegister) === null || _k === void 0 ? void 0 : _k.GetLabelHoverPosition((_l = this.rego) === null || _l === void 0 ? void 0 : _l.entityId, (_m = this.rego) === null || _m === void 0 ? void 0 : _m.menuItemId)) : undefined);
|
|
1468
1699
|
const reused = !this.Transient || this.Persistent;
|
|
1469
1700
|
const now = Date.now();
|
|
1470
1701
|
const animateCursor = () => {
|
|
@@ -1494,8 +1725,8 @@ var EntityLabel;
|
|
|
1494
1725
|
const icon = measureIcon();
|
|
1495
1726
|
const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
|
|
1496
1727
|
const pivot = icon ? undefined : transientPivot;
|
|
1497
|
-
let x = ((
|
|
1498
|
-
let y = ((
|
|
1728
|
+
let x = ((_o = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _o !== void 0 ? _o : anchor.x) + (icon || pixelOffset).x;
|
|
1729
|
+
let y = ((_p = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _p !== void 0 ? _p : anchor.y) + (icon || pixelOffset).y;
|
|
1499
1730
|
if (cursorAmount > 0 && !icon) {
|
|
1500
1731
|
// A leader-less label starts centred on its anchor; preserve that box as it lifts away.
|
|
1501
1732
|
if (!normalLeader) {
|
|
@@ -1548,11 +1779,12 @@ var EntityLabel;
|
|
|
1548
1779
|
const width = ele.offsetWidth / coordinates.scaleX;
|
|
1549
1780
|
const height = ele.offsetHeight / coordinates.scaleY;
|
|
1550
1781
|
let clusterBounds;
|
|
1551
|
-
if ((
|
|
1782
|
+
if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
|
|
1552
1783
|
const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
|
|
1553
1784
|
const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
|
|
1554
|
-
|
|
1555
|
-
const
|
|
1785
|
+
// A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
|
|
1786
|
+
const iconWidth = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
|
|
1787
|
+
const iconHeight = look.iconPhoto ? 0 : (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
|
|
1556
1788
|
const gap = sourceText && iconWidth ? 6 : 0;
|
|
1557
1789
|
const above = presentation === "icon-text-above";
|
|
1558
1790
|
// Match the fixed content gap and label padding, excluding the count badge.
|
|
@@ -1581,7 +1813,7 @@ var EntityLabel;
|
|
|
1581
1813
|
if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
|
|
1582
1814
|
return null;
|
|
1583
1815
|
}
|
|
1584
|
-
const state = (
|
|
1816
|
+
const state = (_r = this.visualRegister) === null || _r === void 0 ? void 0 : _r.GetState({ entityId: (_s = this.rego) === null || _s === void 0 ? void 0 : _s.entityId, menuItemId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.menuItemId });
|
|
1585
1817
|
const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
|
|
1586
1818
|
return {
|
|
1587
1819
|
canFlip: leader && presentation !== "icon", hasLeader: leader,
|
|
@@ -1589,8 +1821,8 @@ var EntityLabel;
|
|
|
1589
1821
|
transient: this.Transient || Boolean(hover) || cursorAmount > 0,
|
|
1590
1822
|
extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
|
|
1591
1823
|
viewportWidth: this.viewer.canvas.clientWidth,
|
|
1592
|
-
id: this.id, entityKey: ((
|
|
1593
|
-
entityId: (
|
|
1824
|
+
id: this.id, entityKey: ((_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
|
|
1825
|
+
entityId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
|
|
1594
1826
|
iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
|
|
1595
1827
|
// Priority shows the hovered member individually without dragging its cluster along.
|
|
1596
1828
|
opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
|
|
@@ -1599,7 +1831,7 @@ var EntityLabel;
|
|
|
1599
1831
|
let displacement = { x: 0, y: 0 };
|
|
1600
1832
|
let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
|
|
1601
1833
|
const apply = (placement) => {
|
|
1602
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
|
|
1834
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4;
|
|
1603
1835
|
const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
|
|
1604
1836
|
clickEntityIds = show ? placement.entityIds : [];
|
|
1605
1837
|
ele.style.visibility = show ? "visible" : "hidden";
|
|
@@ -1613,9 +1845,30 @@ var EntityLabel;
|
|
|
1613
1845
|
if (!this.Transient) {
|
|
1614
1846
|
this.ringAnimation = undefined;
|
|
1615
1847
|
}
|
|
1848
|
+
clearTimeout(photoTimer);
|
|
1849
|
+
photoTimer = null;
|
|
1616
1850
|
return;
|
|
1617
1851
|
}
|
|
1618
|
-
|
|
1852
|
+
// A label standing for a cluster shows its summary only; its photo is put away and never fetched.
|
|
1853
|
+
const solo = !((_a = placement.members) === null || _a === void 0 ? void 0 : _a.length);
|
|
1854
|
+
if (look.iconPhoto && iconElement && iconElement.dataset.failed !== "true") {
|
|
1855
|
+
const display = solo ? "inline-flex" : "none";
|
|
1856
|
+
if (iconElement.style.display !== display) {
|
|
1857
|
+
iconElement.style.display = display;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (!solo) {
|
|
1861
|
+
clearTimeout(photoTimer);
|
|
1862
|
+
photoTimer = null;
|
|
1863
|
+
}
|
|
1864
|
+
else if (startPhoto && photoTimer == null) {
|
|
1865
|
+
photoTimer = setTimeout(() => {
|
|
1866
|
+
if (!this.disposed && this.counter === counter) {
|
|
1867
|
+
startPhoto();
|
|
1868
|
+
}
|
|
1869
|
+
}, PHOTO_SHOW_DELAY);
|
|
1870
|
+
}
|
|
1871
|
+
const displayText = (_b = placement.text) !== null && _b !== void 0 ? _b : originalText;
|
|
1619
1872
|
if (textElement && textElement.textContent !== displayText) {
|
|
1620
1873
|
textElement.textContent = displayText;
|
|
1621
1874
|
// Measure the summary's footprint next frame without replacing each member's source text.
|
|
@@ -1639,10 +1892,12 @@ var EntityLabel;
|
|
|
1639
1892
|
const stack = JSON.stringify(placement.icons || []);
|
|
1640
1893
|
if (iconStack && stack !== displayedStack) {
|
|
1641
1894
|
displayedStack = stack;
|
|
1642
|
-
const looks = ((
|
|
1895
|
+
const looks = ((_c = placement.icons) === null || _c === void 0 ? void 0 : _c.length) ? [iconLook(this.options), ...placement.icons] : [];
|
|
1643
1896
|
// One small size for all, so no icon in the arrangement reads as more important than another.
|
|
1644
|
-
const size = (this.options.iconSize || 24) * ((
|
|
1897
|
+
const size = (this.options.iconSize || 24) * ((_d = this.options.iconScale) !== null && _d !== void 0 ? _d : 1) * ICON_STACK_SCALE;
|
|
1645
1898
|
const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
|
|
1899
|
+
// A stack is on show already, so its photos load straight away.
|
|
1900
|
+
built.forEach(({ start }) => start === null || start === void 0 ? void 0 : start());
|
|
1646
1901
|
// Two sit side by side; three make a triangle with the representative on top.
|
|
1647
1902
|
iconStack.style.gridTemplateColumns = built.length === 3 ? "auto auto" : `repeat(${built.length}, auto)`;
|
|
1648
1903
|
if (built.length === 3) {
|
|
@@ -1659,7 +1914,7 @@ var EntityLabel;
|
|
|
1659
1914
|
}
|
|
1660
1915
|
for (const fit of stackedFits)
|
|
1661
1916
|
fit();
|
|
1662
|
-
if (
|
|
1917
|
+
if (badgeOnIcon) {
|
|
1663
1918
|
const badgeWidth = countElement.offsetWidth;
|
|
1664
1919
|
const iconWidth = iconElement.offsetWidth;
|
|
1665
1920
|
const narrow = iconWidth < iconElement.offsetHeight * 0.6;
|
|
@@ -1674,16 +1929,28 @@ var EntityLabel;
|
|
|
1674
1929
|
// Summary and badge changes must fit on their first visible frame, including after resize.
|
|
1675
1930
|
const ring = this.options.anchorRing;
|
|
1676
1931
|
const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
|
|
1677
|
-
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (
|
|
1932
|
+
(this.hoverFade.to === 0 ? this.retainedHoverPosition : (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.GetLabelHoverPosition((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId, (_g = this.rego) === null || _g === void 0 ? void 0 : _g.menuItemId)) : undefined));
|
|
1678
1933
|
const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
|
|
1679
|
-
const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((
|
|
1680
|
-
|
|
1934
|
+
const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_h = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _h !== void 0 ? _h : 24, iconRing);
|
|
1935
|
+
if (look.iconPhoto && growPhoto) {
|
|
1936
|
+
// A rectangle leans on the leader's side of the label.
|
|
1937
|
+
if (photoShape === "rect") {
|
|
1938
|
+
const align = !leader ? "center" : placement.textSide === "left" ? "flex-end" : "flex-start";
|
|
1939
|
+
if (content.style.alignItems !== align) {
|
|
1940
|
+
content.style.alignItems = align;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
// Grown in layout rather than scaled, so the border keeps its width, and the label, whose bottom
|
|
1944
|
+
// sits on the leader, grows up and sideways without changing the leader's length.
|
|
1945
|
+
growPhoto(Boolean(hovered) && !((_j = placement.members) === null || _j === void 0 ? void 0 : _j.length));
|
|
1946
|
+
}
|
|
1947
|
+
const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_k = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _k !== void 0 ? _k : 100) ? ((_l = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _l !== void 0 ? _l : 10) : 0, hovered ? ((_m = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _m !== void 0 ? _m : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
|
|
1681
1948
|
const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
|
|
1682
1949
|
// Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
|
|
1683
1950
|
const targetOuter = iconRing || targetRadius || 2.5;
|
|
1684
1951
|
const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
|
|
1685
1952
|
const now = Date.now();
|
|
1686
|
-
const duration = (
|
|
1953
|
+
const duration = (_o = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _o !== void 0 ? _o : 200;
|
|
1687
1954
|
const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
|
|
1688
1955
|
let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
|
|
1689
1956
|
if (!ringAnimation) {
|
|
@@ -1726,19 +1993,19 @@ var EntityLabel;
|
|
|
1726
1993
|
if (leader && angled) {
|
|
1727
1994
|
// The declutter owns the side; the fit only says which one had the room to begin with.
|
|
1728
1995
|
const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
|
|
1729
|
-
const side = (
|
|
1996
|
+
const side = (_q = (_p = placement.textSide) !== null && _p !== void 0 ? _p : frame.leaderSide) !== null && _q !== void 0 ? _q : "right";
|
|
1730
1997
|
const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
|
|
1731
1998
|
position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
|
|
1732
1999
|
}
|
|
1733
2000
|
else if (leader && placement.textSide === "left") {
|
|
1734
2001
|
position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
|
|
1735
2002
|
}
|
|
1736
|
-
const gap = (
|
|
2003
|
+
const gap = (_s = (_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.gap) !== null && _s !== void 0 ? _s : 12;
|
|
1737
2004
|
const containsPointer = (x, y, margin) => {
|
|
1738
2005
|
return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
|
|
1739
2006
|
};
|
|
1740
2007
|
const passive = !selectOnClick || !this.visualRegister;
|
|
1741
|
-
if (passive && ((
|
|
2008
|
+
if (passive && ((_t = this.options.pointerAvoidance) === null || _t === void 0 ? void 0 : _t.enabled) !== false && pointer &&
|
|
1742
2009
|
(containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
|
|
1743
2010
|
if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
|
|
1744
2011
|
// Choose against the original box and retain the offset until the pointer leaves both boxes.
|
|
@@ -1756,7 +2023,7 @@ var EntityLabel;
|
|
|
1756
2023
|
else {
|
|
1757
2024
|
displacement = { x: 0, y: 0 };
|
|
1758
2025
|
}
|
|
1759
|
-
const nudgeDuration = (
|
|
2026
|
+
const nudgeDuration = (_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.transitionDuration) !== null && _v !== void 0 ? _v : 200;
|
|
1760
2027
|
const animateNudge = () => {
|
|
1761
2028
|
const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
|
|
1762
2029
|
const eased = progress * progress * (3 - 2 * progress);
|
|
@@ -1772,7 +2039,7 @@ var EntityLabel;
|
|
|
1772
2039
|
fromY: nudge.y,
|
|
1773
2040
|
targetX: displacement.x,
|
|
1774
2041
|
targetY: displacement.y,
|
|
1775
|
-
start: now + ((
|
|
2042
|
+
start: now + ((_x = (_w = this.options.pointerAvoidance) === null || _w === void 0 ? void 0 : _w.delay) !== null && _x !== void 0 ? _x : 180)
|
|
1776
2043
|
};
|
|
1777
2044
|
}
|
|
1778
2045
|
if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
|
|
@@ -1783,7 +2050,7 @@ var EntityLabel;
|
|
|
1783
2050
|
ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
|
|
1784
2051
|
ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
|
|
1785
2052
|
for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
|
|
1786
|
-
clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(
|
|
2053
|
+
clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_z = (_y = this.options.text) !== null && _y !== void 0 ? _y : text) !== null && _z !== void 0 ? _z : (_0 = this.rego) === null || _0 === void 0 ? void 0 : _0.entityId}`);
|
|
1787
2054
|
}
|
|
1788
2055
|
if (leader) {
|
|
1789
2056
|
const nodeRadius = ringAnimation.outer;
|
|
@@ -1823,13 +2090,13 @@ var EntityLabel;
|
|
|
1823
2090
|
// Keep the stem inside the 126-degree arc, away from antialiased endpoints.
|
|
1824
2091
|
const arcFraction = 0.35;
|
|
1825
2092
|
const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
|
|
1826
|
-
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((
|
|
2093
|
+
const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0);
|
|
1827
2094
|
const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
|
|
1828
|
-
const elapsed = Math.min(32, Math.max(0, now - ((
|
|
2095
|
+
const elapsed = Math.min(32, Math.max(0, now - ((_2 = ringAnimation.arcTime) !== null && _2 !== void 0 ? _2 : now)));
|
|
1829
2096
|
const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
|
|
1830
2097
|
const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
|
|
1831
|
-
ringAnimation.arcOffset = approach((
|
|
1832
|
-
ringAnimation.arcOpen = approach((
|
|
2098
|
+
ringAnimation.arcOffset = approach((_3 = ringAnimation.arcOffset) !== null && _3 !== void 0 ? _3 : 0, targetOffset);
|
|
2099
|
+
ringAnimation.arcOpen = approach((_4 = ringAnimation.arcOpen) !== null && _4 !== void 0 ? _4 : 0, targetOpen);
|
|
1833
2100
|
ringAnimation.arcTime = now;
|
|
1834
2101
|
if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
|
|
1835
2102
|
this.viewer.scene.requestRender();
|
|
@@ -1884,14 +2151,16 @@ var EntityLabel;
|
|
|
1884
2151
|
}
|
|
1885
2152
|
/*
|
|
1886
2153
|
* Builds one icon glyph from label options, with its background chip when asked for.
|
|
1887
|
-
*
|
|
1888
2154
|
* The returned fit squares the chip off once the glyph has a measurable size.
|
|
1889
2155
|
*/
|
|
1890
|
-
buildIcon(options, counter) {
|
|
2156
|
+
buildIcon(options, counter, placePhoto) {
|
|
1891
2157
|
var _a, _b, _c;
|
|
1892
2158
|
let fit;
|
|
1893
2159
|
const icon = document.createElement("span");
|
|
1894
2160
|
icon.className = "entity-label-icon";
|
|
2161
|
+
if (options.iconPhoto && options.iconUrl) {
|
|
2162
|
+
return this.buildPhoto(icon, options, counter, placePhoto);
|
|
2163
|
+
}
|
|
1895
2164
|
icon.style.cssText = `font-size:${(options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1)}px;line-height:1;`;
|
|
1896
2165
|
icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
|
|
1897
2166
|
const iconColor = typeof options.iconColor === "string" ? options.iconColor : undefined;
|
|
@@ -1944,6 +2213,8 @@ var EntityLabel;
|
|
|
1944
2213
|
}
|
|
1945
2214
|
this.viewer.scene.requestRender();
|
|
1946
2215
|
};
|
|
2216
|
+
// A photo is rarely square; fit it inside the icon's box rather than stretching it.
|
|
2217
|
+
image.style.objectFit = "contain";
|
|
1947
2218
|
image.onload = resize;
|
|
1948
2219
|
resize();
|
|
1949
2220
|
image.src = options.iconUrl;
|
|
@@ -1999,6 +2270,101 @@ var EntityLabel;
|
|
|
1999
2270
|
}
|
|
2000
2271
|
return { icon, fit };
|
|
2001
2272
|
}
|
|
2273
|
+
/*
|
|
2274
|
+
* Draws an entity's own photo at the size of an icon on its chip: a circle, or a rounded rectangle when too oblong for one.
|
|
2275
|
+
*/
|
|
2276
|
+
buildPhoto(icon, options, counter, placePhoto) {
|
|
2277
|
+
var _a;
|
|
2278
|
+
const size = (options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1);
|
|
2279
|
+
// The glyph plus the chip's padding of a third either side, enlarged for a photo.
|
|
2280
|
+
const diameter = (size + 2 * (size / 3)) * PHOTO_SIZE_SCALE;
|
|
2281
|
+
// Enough pixels to stay sharp at the hover size.
|
|
2282
|
+
const pixels = Math.ceil(diameter * PHOTO_HOVER_SCALE * (window.devicePixelRatio || 1));
|
|
2283
|
+
const canvas = document.createElement("canvas");
|
|
2284
|
+
canvas.className = "entity-label-photo";
|
|
2285
|
+
canvas.style.cssText = "display:block;box-sizing:border-box;border:2px solid rgba(255,255,255,0.9);box-shadow:0 1px 3px rgba(0,0,0,0.35);";
|
|
2286
|
+
canvas.style.transition = ["width", "height", "opacity"].map(property => `${property} ${PHOTO_TRANSITION}ms ease-out`).join(",");
|
|
2287
|
+
icon.classList.add("entity-label-icon-photo");
|
|
2288
|
+
icon.style.display = options.iconScale === 0 ? "none" : "inline-flex";
|
|
2289
|
+
icon.appendChild(canvas);
|
|
2290
|
+
// Resting size, set once the photo's shape is known.
|
|
2291
|
+
let width = 0;
|
|
2292
|
+
let height = 0;
|
|
2293
|
+
let grown = false;
|
|
2294
|
+
const grow = (on) => {
|
|
2295
|
+
if (on === grown || !width) {
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
grown = on;
|
|
2299
|
+
const scale = on ? PHOTO_HOVER_SCALE : 1;
|
|
2300
|
+
canvas.style.width = `${width * scale}px`;
|
|
2301
|
+
canvas.style.height = `${height * scale}px`;
|
|
2302
|
+
this.renderFor(PHOTO_TRANSITION);
|
|
2303
|
+
};
|
|
2304
|
+
// Draws the photo in, or drops the icon when it would not load so the label stays as its text.
|
|
2305
|
+
const show = (photo) => {
|
|
2306
|
+
var _a;
|
|
2307
|
+
if (!photo.source) {
|
|
2308
|
+
icon.dataset.failed = "true";
|
|
2309
|
+
icon.style.display = "none";
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
canvas.width = photo.source.width;
|
|
2313
|
+
canvas.height = photo.source.height;
|
|
2314
|
+
(_a = canvas.getContext("2d")) === null || _a === void 0 ? void 0 : _a.drawImage(photo.source, 0, 0);
|
|
2315
|
+
const longest = Math.max(photo.width, photo.height);
|
|
2316
|
+
const circle = PhotoShape(photo.width, photo.height) === "circle";
|
|
2317
|
+
width = circle ? diameter : diameter * photo.width / longest;
|
|
2318
|
+
height = circle ? diameter : diameter * photo.height / longest;
|
|
2319
|
+
canvas.style.width = `${width}px`;
|
|
2320
|
+
canvas.style.height = `${height}px`;
|
|
2321
|
+
canvas.style.borderRadius = circle ? "50%" : `${diameter * PHOTO_CORNER}px`;
|
|
2322
|
+
canvas.style.opacity = "1";
|
|
2323
|
+
placePhoto === null || placePhoto === void 0 ? void 0 : placePhoto(circle ? "circle" : "rect", grow);
|
|
2324
|
+
return true;
|
|
2325
|
+
};
|
|
2326
|
+
const photos = Photos.Get(this.viewer);
|
|
2327
|
+
const ready = photos.Peek(options.iconUrl, pixels);
|
|
2328
|
+
if (ready) {
|
|
2329
|
+
show(ready);
|
|
2330
|
+
return { icon };
|
|
2331
|
+
}
|
|
2332
|
+
canvas.style.width = "0px";
|
|
2333
|
+
canvas.style.height = "0px";
|
|
2334
|
+
canvas.style.opacity = "0";
|
|
2335
|
+
let arrived;
|
|
2336
|
+
const arriving = new Promise(resolve => arrived = resolve);
|
|
2337
|
+
let started = false;
|
|
2338
|
+
const start = () => {
|
|
2339
|
+
if (started) {
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
started = true;
|
|
2343
|
+
photos.Load(options.iconUrl, pixels).then(photo => {
|
|
2344
|
+
if (!this.disposed && this.counter === counter && show(photo)) {
|
|
2345
|
+
this.renderFor(PHOTO_TRANSITION);
|
|
2346
|
+
}
|
|
2347
|
+
arrived();
|
|
2348
|
+
});
|
|
2349
|
+
};
|
|
2350
|
+
return { icon, arriving, start };
|
|
2351
|
+
}
|
|
2352
|
+
/*
|
|
2353
|
+
* Keeps rendering while a CSS transition changes a label's size, since placement measures it every frame.
|
|
2354
|
+
*/
|
|
2355
|
+
renderFor(duration) {
|
|
2356
|
+
const end = Date.now() + duration;
|
|
2357
|
+
const step = () => {
|
|
2358
|
+
if (this.disposed || !this.viewer || this.viewer.isDestroyed()) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
this.viewer.scene.requestRender();
|
|
2362
|
+
if (Date.now() < end) {
|
|
2363
|
+
requestAnimationFrame(step);
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
step();
|
|
2367
|
+
}
|
|
2002
2368
|
/**
|
|
2003
2369
|
* Removes label from the viewer and unsubscribes from events.
|
|
2004
2370
|
*/
|