CreditDisplay.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. import AssociativeArray from "../Core/AssociativeArray.js";
  2. import buildModuleUrl from "../Core/buildModuleUrl.js";
  3. import Check from "../Core/Check.js";
  4. import Credit from "../Core/Credit.js";
  5. import defaultValue from "../Core/defaultValue.js";
  6. import defined from "../Core/defined.js";
  7. import destroyObject from "../Core/destroyObject.js";
  8. import Uri from "../ThirdParty/Uri.js";
  9. const mobileWidth = 576;
  10. const lightboxHeight = 100;
  11. const textColor = "#ffffff";
  12. const highlightColor = "#48b";
  13. /**
  14. * Used to sort the credits by frequency of appearance
  15. * when they are later displayed.
  16. *
  17. * @alias CreditDisplay.CreditDisplayElement
  18. * @constructor
  19. *
  20. * @private
  21. */
  22. function CreditDisplayElement(credit, count) {
  23. this.credit = credit;
  24. this.count = defaultValue(count, 1);
  25. }
  26. function contains(credits, credit) {
  27. const len = credits.length;
  28. for (let i = 0; i < len; i++) {
  29. const existingCredit = credits[i];
  30. if (Credit.equals(existingCredit, credit)) {
  31. return true;
  32. }
  33. }
  34. return false;
  35. }
  36. function swapCesiumCredit(creditDisplay) {
  37. // We don't want to clutter the screen with the Cesium logo and the Cesium ion
  38. // logo at the same time. Since the ion logo is required, we just replace the
  39. // Cesium logo or add the logo if the Cesium one was removed.
  40. const previousCredit = creditDisplay._previousCesiumCredit;
  41. const currentCredit = creditDisplay._currentCesiumCredit;
  42. if (Credit.equals(currentCredit, previousCredit)) {
  43. return;
  44. }
  45. if (defined(previousCredit)) {
  46. creditDisplay._cesiumCreditContainer.removeChild(previousCredit.element);
  47. }
  48. if (defined(currentCredit)) {
  49. creditDisplay._cesiumCreditContainer.appendChild(currentCredit.element);
  50. }
  51. creditDisplay._previousCesiumCredit = currentCredit;
  52. }
  53. const delimiterClassName = "cesium-credit-delimiter";
  54. function createDelimiterElement(delimiter) {
  55. const delimiterElement = document.createElement("span");
  56. delimiterElement.textContent = delimiter;
  57. delimiterElement.className = delimiterClassName;
  58. return delimiterElement;
  59. }
  60. function createCreditElement(element, elementWrapperTagName) {
  61. // may need to wrap the credit in another element
  62. if (defined(elementWrapperTagName)) {
  63. const wrapper = document.createElement(elementWrapperTagName);
  64. wrapper._creditId = element._creditId;
  65. wrapper.appendChild(element);
  66. element = wrapper;
  67. }
  68. return element;
  69. }
  70. function displayCredits(container, credits, delimiter, elementWrapperTagName) {
  71. const childNodes = container.childNodes;
  72. let domIndex = -1;
  73. // Sort the credits such that more frequent credits appear first
  74. credits.sort(function (credit1, credit2) {
  75. return credit2.count - credit1.count;
  76. });
  77. for (let creditIndex = 0; creditIndex < credits.length; ++creditIndex) {
  78. const credit = credits[creditIndex].credit;
  79. if (defined(credit)) {
  80. domIndex = creditIndex;
  81. if (defined(delimiter)) {
  82. // credits may be separated by delimiters
  83. domIndex *= 2;
  84. if (creditIndex > 0) {
  85. const delimiterDomIndex = domIndex - 1;
  86. if (childNodes.length <= delimiterDomIndex) {
  87. container.appendChild(createDelimiterElement(delimiter));
  88. } else {
  89. const existingDelimiter = childNodes[delimiterDomIndex];
  90. if (existingDelimiter.className !== delimiterClassName) {
  91. container.replaceChild(
  92. createDelimiterElement(delimiter),
  93. existingDelimiter
  94. );
  95. }
  96. }
  97. }
  98. }
  99. const element = credit.element;
  100. // check to see if the correct credit is in the right place
  101. if (childNodes.length <= domIndex) {
  102. container.appendChild(
  103. createCreditElement(element, elementWrapperTagName)
  104. );
  105. } else {
  106. const existingElement = childNodes[domIndex];
  107. if (existingElement._creditId !== credit._id) {
  108. // not the right credit, swap it in
  109. container.replaceChild(
  110. createCreditElement(element, elementWrapperTagName),
  111. existingElement
  112. );
  113. }
  114. }
  115. }
  116. }
  117. // any remaining nodes in the container are unnecessary
  118. ++domIndex;
  119. while (domIndex < childNodes.length) {
  120. container.removeChild(childNodes[domIndex]);
  121. }
  122. }
  123. function styleLightboxContainer(that) {
  124. const lightboxCredits = that._lightboxCredits;
  125. const width = that.viewport.clientWidth;
  126. const height = that.viewport.clientHeight;
  127. if (width !== that._lastViewportWidth) {
  128. if (width < mobileWidth) {
  129. lightboxCredits.className =
  130. "cesium-credit-lightbox cesium-credit-lightbox-mobile";
  131. lightboxCredits.style.marginTop = "0";
  132. } else {
  133. lightboxCredits.className =
  134. "cesium-credit-lightbox cesium-credit-lightbox-expanded";
  135. lightboxCredits.style.marginTop = `${Math.floor(
  136. (height - lightboxCredits.clientHeight) * 0.5
  137. )}px`;
  138. }
  139. that._lastViewportWidth = width;
  140. }
  141. if (width >= mobileWidth && height !== that._lastViewportHeight) {
  142. lightboxCredits.style.marginTop = `${Math.floor(
  143. (height - lightboxCredits.clientHeight) * 0.5
  144. )}px`;
  145. that._lastViewportHeight = height;
  146. }
  147. }
  148. function addStyle(selector, styles) {
  149. let style = `${selector} {`;
  150. for (const attribute in styles) {
  151. if (styles.hasOwnProperty(attribute)) {
  152. style += `${attribute}: ${styles[attribute]}; `;
  153. }
  154. }
  155. style += " }\n";
  156. return style;
  157. }
  158. function appendCss() {
  159. let style = "";
  160. style += addStyle(".cesium-credit-lightbox-overlay", {
  161. display: "none",
  162. "z-index": "1", //must be at least 1 to draw over top other Cesium widgets
  163. position: "absolute",
  164. top: "0",
  165. left: "0",
  166. width: "100%",
  167. height: "100%",
  168. "background-color": "rgba(80, 80, 80, 0.8)",
  169. });
  170. style += addStyle(".cesium-credit-lightbox", {
  171. "background-color": "#303336",
  172. color: textColor,
  173. position: "relative",
  174. "min-height": `${lightboxHeight}px`,
  175. margin: "auto",
  176. });
  177. style += addStyle(
  178. ".cesium-credit-lightbox > ul > li a, .cesium-credit-lightbox > ul > li a:visited",
  179. {
  180. color: textColor,
  181. }
  182. );
  183. style += addStyle(".cesium-credit-lightbox > ul > li a:hover", {
  184. color: highlightColor,
  185. });
  186. style += addStyle(".cesium-credit-lightbox.cesium-credit-lightbox-expanded", {
  187. border: "1px solid #444",
  188. "border-radius": "5px",
  189. "max-width": "370px",
  190. });
  191. style += addStyle(".cesium-credit-lightbox.cesium-credit-lightbox-mobile", {
  192. height: "100%",
  193. width: "100%",
  194. });
  195. style += addStyle(".cesium-credit-lightbox-title", {
  196. padding: "20px 20px 0 20px",
  197. });
  198. style += addStyle(".cesium-credit-lightbox-close", {
  199. "font-size": "18pt",
  200. cursor: "pointer",
  201. position: "absolute",
  202. top: "0",
  203. right: "6px",
  204. color: textColor,
  205. });
  206. style += addStyle(".cesium-credit-lightbox-close:hover", {
  207. color: highlightColor,
  208. });
  209. style += addStyle(".cesium-credit-lightbox > ul", {
  210. margin: "0",
  211. padding: "12px 20px 12px 40px",
  212. "font-size": "13px",
  213. });
  214. style += addStyle(".cesium-credit-lightbox > ul > li", {
  215. "padding-bottom": "6px",
  216. });
  217. style += addStyle(".cesium-credit-lightbox > ul > li *", {
  218. padding: "0",
  219. margin: "0",
  220. });
  221. style += addStyle(".cesium-credit-expand-link", {
  222. "padding-left": "5px",
  223. cursor: "pointer",
  224. "text-decoration": "underline",
  225. color: textColor,
  226. });
  227. style += addStyle(".cesium-credit-expand-link:hover", {
  228. color: highlightColor,
  229. });
  230. style += addStyle(".cesium-credit-text", {
  231. color: textColor,
  232. });
  233. style += addStyle(
  234. ".cesium-credit-textContainer *, .cesium-credit-logoContainer *",
  235. {
  236. display: "inline",
  237. }
  238. );
  239. const head = document.head;
  240. const css = document.createElement("style");
  241. css.innerHTML = style;
  242. head.insertBefore(css, head.firstChild);
  243. }
  244. /**
  245. * The credit display is responsible for displaying credits on screen.
  246. *
  247. * @param {HTMLElement} container The HTML element where credits will be displayed
  248. * @param {String} [delimiter= ' • '] The string to separate text credits
  249. * @param {HTMLElement} [viewport=document.body] The HTML element that will contain the credits popup
  250. *
  251. * @alias CreditDisplay
  252. * @constructor
  253. *
  254. * @example
  255. * const creditDisplay = new Cesium.CreditDisplay(creditContainer);
  256. */
  257. function CreditDisplay(container, delimiter, viewport) {
  258. //>>includeStart('debug', pragmas.debug);
  259. Check.defined("container", container);
  260. //>>includeEnd('debug');
  261. const that = this;
  262. viewport = defaultValue(viewport, document.body);
  263. const lightbox = document.createElement("div");
  264. lightbox.className = "cesium-credit-lightbox-overlay";
  265. viewport.appendChild(lightbox);
  266. const lightboxCredits = document.createElement("div");
  267. lightboxCredits.className = "cesium-credit-lightbox";
  268. lightbox.appendChild(lightboxCredits);
  269. function hideLightbox(event) {
  270. if (lightboxCredits.contains(event.target)) {
  271. return;
  272. }
  273. that.hideLightbox();
  274. }
  275. lightbox.addEventListener("click", hideLightbox, false);
  276. const title = document.createElement("div");
  277. title.className = "cesium-credit-lightbox-title";
  278. title.textContent = "Data provided by:";
  279. lightboxCredits.appendChild(title);
  280. const closeButton = document.createElement("a");
  281. closeButton.onclick = this.hideLightbox.bind(this);
  282. closeButton.innerHTML = "&times;";
  283. closeButton.className = "cesium-credit-lightbox-close";
  284. lightboxCredits.appendChild(closeButton);
  285. const creditList = document.createElement("ul");
  286. lightboxCredits.appendChild(creditList);
  287. const cesiumCreditContainer = document.createElement("div");
  288. cesiumCreditContainer.className = "cesium-credit-logoContainer";
  289. cesiumCreditContainer.style.display = "inline";
  290. container.appendChild(cesiumCreditContainer);
  291. const screenContainer = document.createElement("div");
  292. screenContainer.className = "cesium-credit-textContainer";
  293. screenContainer.style.display = "inline";
  294. container.appendChild(screenContainer);
  295. const expandLink = document.createElement("a");
  296. expandLink.className = "cesium-credit-expand-link";
  297. expandLink.onclick = this.showLightbox.bind(this);
  298. expandLink.textContent = "Data attribution";
  299. container.appendChild(expandLink);
  300. appendCss();
  301. const cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
  302. this._delimiter = defaultValue(delimiter, " • ");
  303. this._screenContainer = screenContainer;
  304. this._cesiumCreditContainer = cesiumCreditContainer;
  305. this._lastViewportHeight = undefined;
  306. this._lastViewportWidth = undefined;
  307. this._lightboxCredits = lightboxCredits;
  308. this._creditList = creditList;
  309. this._lightbox = lightbox;
  310. this._hideLightbox = hideLightbox;
  311. this._expandLink = expandLink;
  312. this._expanded = false;
  313. this._defaultCredits = [];
  314. this._cesiumCredit = cesiumCredit;
  315. this._previousCesiumCredit = undefined;
  316. this._currentCesiumCredit = cesiumCredit;
  317. this._creditDisplayElementPool = [];
  318. this._creditDisplayElementIndex = 0;
  319. this._currentFrameCredits = {
  320. screenCredits: new AssociativeArray(),
  321. lightboxCredits: new AssociativeArray(),
  322. };
  323. this._defaultCredit = undefined;
  324. this.viewport = viewport;
  325. /**
  326. * The HTML element where credits will be displayed.
  327. * @type {HTMLElement}
  328. */
  329. this.container = container;
  330. }
  331. function setCredit(creditDisplay, credits, credit, count) {
  332. count = defaultValue(count, 1);
  333. let creditDisplayElement = credits.get(credit.id);
  334. if (!defined(creditDisplayElement)) {
  335. const pool = creditDisplay._creditDisplayElementPool;
  336. const poolIndex = creditDisplay._creditDisplayElementPoolIndex;
  337. if (poolIndex < pool.length) {
  338. creditDisplayElement = pool[poolIndex];
  339. creditDisplayElement.credit = credit;
  340. creditDisplayElement.count = count;
  341. } else {
  342. creditDisplayElement = new CreditDisplayElement(credit, count);
  343. pool.push(creditDisplayElement);
  344. }
  345. ++creditDisplay._creditDisplayElementPoolIndex;
  346. credits.set(credit.id, creditDisplayElement);
  347. } else if (creditDisplayElement.count < Number.MAX_VALUE) {
  348. creditDisplayElement.count += count;
  349. }
  350. }
  351. /**
  352. * Adds a credit to the list of current credits to be displayed in the credit container
  353. *
  354. * @param {Credit} credit The credit to display
  355. */
  356. CreditDisplay.prototype.addCredit = function (credit) {
  357. //>>includeStart('debug', pragmas.debug);
  358. Check.defined("credit", credit);
  359. //>>includeEnd('debug');
  360. if (credit._isIon) {
  361. // If this is the an ion logo credit from the ion server
  362. // Juse use the default credit (which is identical) to avoid blinking
  363. if (!defined(this._defaultCredit)) {
  364. this._defaultCredit = Credit.clone(getDefaultCredit());
  365. }
  366. this._currentCesiumCredit = this._defaultCredit;
  367. return;
  368. }
  369. let credits;
  370. if (!credit.showOnScreen) {
  371. credits = this._currentFrameCredits.lightboxCredits;
  372. } else {
  373. credits = this._currentFrameCredits.screenCredits;
  374. }
  375. setCredit(this, credits, credit);
  376. };
  377. /**
  378. * Adds credits that will persist until they are removed
  379. *
  380. * @param {Credit} credit The credit to added to defaults
  381. */
  382. CreditDisplay.prototype.addDefaultCredit = function (credit) {
  383. //>>includeStart('debug', pragmas.debug);
  384. Check.defined("credit", credit);
  385. //>>includeEnd('debug');
  386. const defaultCredits = this._defaultCredits;
  387. if (!contains(defaultCredits, credit)) {
  388. defaultCredits.push(credit);
  389. }
  390. };
  391. /**
  392. * Removes a default credit
  393. *
  394. * @param {Credit} credit The credit to be removed from defaults
  395. */
  396. CreditDisplay.prototype.removeDefaultCredit = function (credit) {
  397. //>>includeStart('debug', pragmas.debug);
  398. Check.defined("credit", credit);
  399. //>>includeEnd('debug');
  400. const defaultCredits = this._defaultCredits;
  401. const index = defaultCredits.indexOf(credit);
  402. if (index !== -1) {
  403. defaultCredits.splice(index, 1);
  404. }
  405. };
  406. CreditDisplay.prototype.showLightbox = function () {
  407. this._lightbox.style.display = "block";
  408. this._expanded = true;
  409. };
  410. CreditDisplay.prototype.hideLightbox = function () {
  411. this._lightbox.style.display = "none";
  412. this._expanded = false;
  413. };
  414. /**
  415. * Updates the credit display before a new frame is rendered.
  416. */
  417. CreditDisplay.prototype.update = function () {
  418. if (this._expanded) {
  419. styleLightboxContainer(this);
  420. }
  421. };
  422. /**
  423. * Resets the credit display to a beginning of frame state, clearing out current credits.
  424. */
  425. CreditDisplay.prototype.beginFrame = function () {
  426. const currentFrameCredits = this._currentFrameCredits;
  427. this._creditDisplayElementPoolIndex = 0;
  428. const screenCredits = currentFrameCredits.screenCredits;
  429. screenCredits.removeAll();
  430. const defaultCredits = this._defaultCredits;
  431. for (let i = 0; i < defaultCredits.length; ++i) {
  432. const defaultCredit = defaultCredits[i];
  433. setCredit(this, screenCredits, defaultCredit, Number.MAX_VALUE);
  434. }
  435. currentFrameCredits.lightboxCredits.removeAll();
  436. if (!Credit.equals(CreditDisplay.cesiumCredit, this._cesiumCredit)) {
  437. this._cesiumCredit = Credit.clone(CreditDisplay.cesiumCredit);
  438. }
  439. this._currentCesiumCredit = this._cesiumCredit;
  440. };
  441. /**
  442. * Sets the credit display to the end of frame state, displaying credits from the last frame in the credit container.
  443. */
  444. CreditDisplay.prototype.endFrame = function () {
  445. const screenCredits = this._currentFrameCredits.screenCredits.values;
  446. displayCredits(
  447. this._screenContainer,
  448. screenCredits,
  449. this._delimiter,
  450. undefined
  451. );
  452. const lightboxCredits = this._currentFrameCredits.lightboxCredits.values;
  453. this._expandLink.style.display =
  454. lightboxCredits.length > 0 ? "inline" : "none";
  455. displayCredits(this._creditList, lightboxCredits, undefined, "li");
  456. swapCesiumCredit(this);
  457. };
  458. /**
  459. * Destroys the resources held by this object. Destroying an object allows for deterministic
  460. * release of resources, instead of relying on the garbage collector to destroy this object.
  461. * <br /><br />
  462. * Once an object is destroyed, it should not be used; calling any function other than
  463. * <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
  464. * assign the return value (<code>undefined</code>) to the object as done in the example.
  465. *
  466. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  467. */
  468. CreditDisplay.prototype.destroy = function () {
  469. this._lightbox.removeEventListener("click", this._hideLightbox, false);
  470. this.container.removeChild(this._cesiumCreditContainer);
  471. this.container.removeChild(this._screenContainer);
  472. this.container.removeChild(this._expandLink);
  473. this.viewport.removeChild(this._lightbox);
  474. return destroyObject(this);
  475. };
  476. /**
  477. * Returns true if this object was destroyed; otherwise, false.
  478. * <br /><br />
  479. *
  480. * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
  481. */
  482. CreditDisplay.prototype.isDestroyed = function () {
  483. return false;
  484. };
  485. CreditDisplay._cesiumCredit = undefined;
  486. CreditDisplay._cesiumCreditInitialized = false;
  487. let defaultCredit;
  488. function getDefaultCredit() {
  489. if (!defined(defaultCredit)) {
  490. let logo = buildModuleUrl("Assets/Images/ion-credit.png");
  491. // When hosting in a WebView, the base URL scheme is file:// or ms-appx-web://
  492. // which is stripped out from the Credit's <img> tag; use the full path instead
  493. if (
  494. logo.indexOf("http://") !== 0 &&
  495. logo.indexOf("https://") !== 0 &&
  496. logo.indexOf("data:") !== 0
  497. ) {
  498. const logoUrl = new Uri(logo);
  499. logo = logoUrl.path();
  500. }
  501. defaultCredit = new Credit(
  502. `<a href="https://cesium.com/" target="_blank"><img src="${logo}" title="Cesium ion"/></a>`,
  503. true
  504. );
  505. }
  506. if (!CreditDisplay._cesiumCreditInitialized) {
  507. CreditDisplay._cesiumCredit = defaultCredit;
  508. CreditDisplay._cesiumCreditInitialized = true;
  509. }
  510. return defaultCredit;
  511. }
  512. Object.defineProperties(CreditDisplay, {
  513. /**
  514. * Gets or sets the Cesium logo credit.
  515. * @memberof CreditDisplay
  516. * @type {Credit}
  517. */
  518. cesiumCredit: {
  519. get: function () {
  520. getDefaultCredit();
  521. return CreditDisplay._cesiumCredit;
  522. },
  523. set: function (value) {
  524. CreditDisplay._cesiumCredit = value;
  525. CreditDisplay._cesiumCreditInitialized = true;
  526. },
  527. },
  528. });
  529. CreditDisplay.CreditDisplayElement = CreditDisplayElement;
  530. export default CreditDisplay;