WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

2024-08-18 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

بۇ قوليازمىنى بىۋاسىتە قاچىلاشقا بولمايدۇ. بۇ باشقا قوليازمىلارنىڭ ئىشلىتىشى ئۈچۈن تەمىنلەنگەن ئامبار بولۇپ، ئىشلىتىش ئۈچۈن مېتا كۆرسەتمىسىگە قىستۇرىدىغان كود: // @require https://update.greatest.deepsurf.us/scripts/39208/1430167/WME%20Utils%20-%20Google%20Link%20Enhancer.js

  1. // ==UserScript==
  2. // @name WME Utils - Google Link Enhancer
  3. // @namespace WazeDev
  4. // @version 2024.08.18.000
  5. // @description Adds some extra WME functionality related to Google place links.
  6. // @author MapOMatic, WazeDev group
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @license GNU GPLv3
  9. // ==/UserScript==
  10.  
  11. /* global OpenLayers */
  12. /* global W */
  13. /* global google */
  14.  
  15. // /* eslint-disable */
  16. /* eslint-disable no-unused-vars */
  17. class GoogleLinkEnhancer {
  18. #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic.
  19. #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider';
  20. #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit';
  21. #EXT_PROV_ELEM_CONTENT_QUERY = 'div.external-provider-content';
  22. #LINK_CACHE_NAME = 'gle_link_cache';
  23. #LINK_CACHE_CLEAN_INTERVAL_MIN = 1; // Interval to remove old links and save new ones.
  24. #LINK_CACHE_LIFESPAN_HR = 6; // Remove old links when they exceed this time limit.
  25.  
  26. #linkCache;
  27. #enabled = false;
  28. #disableApiUntil = null; // When a serious API error occurs (OVER_QUERY_LIMIT, REQUEST_DENIED), set this to a time in the future.
  29. #mapLayer = null;
  30. #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place.
  31. // Area place is calculated as #distanceLimit + <distance between centroid and furthest node>
  32. #showTempClosedPOIs = true;
  33. #placesService;
  34. #linkObserver;
  35. #modeObserver;
  36. #searchResultsObserver;
  37. #lzString;
  38. #cacheCleanIntervalID;
  39. #originalHeadAppendChildMethod;
  40. #ptFeature;
  41. #lineFeature;
  42. #timeoutID;
  43. strings = {
  44. permClosedPlace: 'Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.',
  45. tempClosedPlace: 'Google indicates this place is temporarily closed.',
  46. multiLinked: 'Linked more than once already. Please find and remove multiple links.',
  47. linkedToThisPlace: 'Already linked to this place',
  48. linkedNearby: 'Already linked to a nearby place',
  49. linkedToXPlaces: 'This is linked to {0} places',
  50. badLink: 'Invalid Google link. Please remove it.',
  51. tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.'
  52. };
  53.  
  54. /* eslint-enable no-unused-vars */
  55. constructor() {
  56. const attributionElem = document.createElement('div');
  57. this.#placesService = new google.maps.places.PlacesService(attributionElem);
  58. this.#initLZString();
  59. const STORED_CACHE = localStorage.getItem(this.#LINK_CACHE_NAME);
  60. try {
  61. this.#linkCache = STORED_CACHE ? $.parseJSON(this.#lzString.decompressFromUTF16(STORED_CACHE)) : {};
  62. } catch (ex) {
  63. if (ex.name === 'SyntaxError') {
  64. // In case the cache is corrupted and can't be read.
  65. this.#linkCache = {};
  66. console.warn('GoogleLinkEnhancer:', 'An error occurred while loading the stored cache. A new cache was created.');
  67. } else {
  68. throw ex;
  69. }
  70. }
  71. if (this.#linkCache === null || this.#linkCache.length === 0) this.#linkCache = {};
  72.  
  73. this.#initLayer();
  74.  
  75. // NOTE: Arrow functions are necessary for calling methods on object instances.
  76. // This could be made more efficient by only processing the relevant places.
  77. W.model.events.register('mergeend', null, () => { this.#processPlaces(); });
  78. W.model.venues.on('objectschanged', () => { this.#processPlaces(); });
  79. W.model.venues.on('objectsremoved', () => { this.#processPlaces(); });
  80. W.model.venues.on('objectsadded', () => { this.#processPlaces(); });
  81.  
  82. // Watch for ext provider elements being added to the DOM, and add hover events.
  83. this.#linkObserver = new MutationObserver(mutations => {
  84. mutations.forEach(mutation => {
  85. for (let idx = 0; idx < mutation.addedNodes.length; idx++) {
  86. const nd = mutation.addedNodes[idx];
  87. if (nd.nodeType === Node.ELEMENT_NODE) {
  88. const $el = $(nd);
  89. const $subel = $el.find(this.#EXT_PROV_ELEM_QUERY);
  90. if ($el.is(this.#EXT_PROV_ELEM_QUERY)) {
  91. this.#addHoverEvent($el);
  92. this.#formatLinkElements();
  93. } else if ($subel.length) {
  94. for (let i = 0; i < $subel.length; i++) {
  95. this.#addHoverEvent($($subel[i]));
  96. }
  97. this.#formatLinkElements();
  98. }
  99. if ($el.is(this.#EXT_PROV_ELEM_EDIT_QUERY)) {
  100. this.#searchResultsObserver.observe($el.find('wz-autocomplete[placeholder="Search for a place"]')[0].shadowRoot, { childList: true, subtree: true });
  101. }
  102. }
  103. }
  104. for (let idx = 0; idx < mutation.removedNodes.length; idx++) {
  105. const nd = mutation.removedNodes[idx];
  106. if (nd.nodeType === Node.ELEMENT_NODE) {
  107. const $el = $(nd);
  108. if ($el.is(this.#EXT_PROV_ELEM_EDIT_QUERY)) {
  109. this.#searchResultsObserver.disconnect();
  110. }
  111. }
  112. }
  113. });
  114. });
  115.  
  116. // Watch for Google place search result list items being added to the DOM
  117. const that = this;
  118. this.#searchResultsObserver = new MutationObserver(mutations => {
  119. mutations.forEach(mutation => {
  120. for (let idx = 0; idx < mutation.addedNodes.length; idx++) {
  121. const nd = mutation.addedNodes[idx];
  122. if (nd.nodeType === Node.ELEMENT_NODE && $(nd).is('wz-menu-item.simple-item')) {
  123. $(nd).mouseenter(() => {
  124. // When mousing over a list item, find the Google place ID from the list that was stored previously.
  125. // Then add the point/line to the map.
  126. that.#addPoint($(nd).attr('item-id'));
  127. }).mouseleave(() => {
  128. // When leaving the list item, remove the point.
  129. that.#destroyPoint();
  130. });
  131. }
  132. }
  133. });
  134. });
  135.  
  136. // Watch the side panel for addition of the sidebar-layout div, which indicates a mode change.
  137. this.#modeObserver = new MutationObserver(mutations => {
  138. mutations.forEach(mutation => {
  139. for (let idx = 0; idx < mutation.addedNodes.length; idx++) {
  140. const nd = mutation.addedNodes[idx];
  141. if (nd.nodeType === Node.ELEMENT_NODE && $(nd).is('.sidebar-layout')) {
  142. this.#observeLinks();
  143. break;
  144. }
  145. }
  146. });
  147. });
  148.  
  149. // This is a special event that will be triggered when DOM elements are destroyed.
  150. /* eslint-disable wrap-iife, func-names, object-shorthand */
  151. (function($) {
  152. $.event.special.destroyed = {
  153. remove: function(o) {
  154. if (o.handler && o.type !== 'destroyed') {
  155. o.handler();
  156. }
  157. }
  158. };
  159. })(jQuery);
  160. /* eslint-enable wrap-iife, func-names, object-shorthand */
  161.  
  162. // In case a place is already selected on load.
  163. const selObjects = W.selectionManager.getSelectedDataModelObjects();
  164. if (selObjects.length && selObjects[0].type === 'venue') {
  165. this.#formatLinkElements();
  166. }
  167. }
  168.  
  169. #initLayer() {
  170. this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', {
  171. uniqueName: '___GoogleLinkEnhancements',
  172. displayInLayerSwitcher: true,
  173. styleMap: new OpenLayers.StyleMap({
  174. default: {
  175. strokeColor: '${strokeColor}',
  176. strokeWidth: '${strokeWidth}',
  177. strokeDashstyle: '${strokeDashstyle}',
  178. pointRadius: '15',
  179. fillOpacity: '0'
  180. }
  181. })
  182. });
  183.  
  184. this.#mapLayer.setOpacity(0.8);
  185. W.map.addLayer(this.#mapLayer);
  186. }
  187.  
  188. enable() {
  189. if (!this.#enabled) {
  190. this.#modeObserver.observe($('.edit-area #sidebarContent')[0], { childList: true, subtree: false });
  191. this.#observeLinks();
  192. // Watch for JSONP callbacks. JSONP is used for the autocomplete results when searching for Google links.
  193. this.#addJsonpInterceptor();
  194. // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function.
  195. $('#map').on('mouseenter', null, this, GoogleLinkEnhancer.#onMapMouseenter);
  196. $(window).on('unload', null, this, GoogleLinkEnhancer.#onWindowUnload);
  197. W.model.venues.on('objectschanged', this.#formatLinkElements, this);
  198. this.#processPlaces();
  199. this.#cleanAndSaveLinkCache();
  200. this.#cacheCleanIntervalID = setInterval(() => this.#cleanAndSaveLinkCache(), 1000 * 60 * this.#LINK_CACHE_CLEAN_INTERVAL_MIN);
  201. this.#enabled = true;
  202. }
  203. }
  204.  
  205. disable() {
  206. if (this.#enabled) {
  207. this.#modeObserver.disconnect();
  208. this.#linkObserver.disconnect();
  209. this.#searchResultsObserver.disconnect();
  210. this.#removeJsonpInterceptor();
  211. $('#map').off('mouseenter', GoogleLinkEnhancer.#onMapMouseenter);
  212. $(window).off('unload', null, this, GoogleLinkEnhancer.#onWindowUnload);
  213. W.model.venues.off('objectschanged', this.#formatLinkElements, this);
  214. if (this.#cacheCleanIntervalID) clearInterval(this.#cacheCleanIntervalID);
  215. this.#cleanAndSaveLinkCache();
  216. this.#enabled = false;
  217. }
  218. }
  219.  
  220. // The distance (in meters) before flagging a Waze place that is too far from the linked Google place.
  221. // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node.
  222. get distanceLimit() {
  223. return this.#distanceLimit;
  224. }
  225.  
  226. set distanceLimit(value) {
  227. this.#distanceLimit = value;
  228. this.#processPlaces();
  229. }
  230.  
  231. get showTempClosedPOIs() {
  232. return this.#showTempClosedPOIs;
  233. }
  234.  
  235. set showTempClosedPOIs(value) {
  236. this.#showTempClosedPOIs = value;
  237. this.#processPlaces();
  238. }
  239.  
  240. static #onWindowUnload(evt) {
  241. evt.data.#cleanAndSaveLinkCache();
  242. }
  243.  
  244. #cleanAndSaveLinkCache() {
  245. if (!this.#linkCache) return;
  246. const now = new Date();
  247. Object.keys(this.#linkCache).forEach(id => {
  248. const link = this.#linkCache[id];
  249. // Bug fix:
  250. if (link.location) {
  251. link.loc = link.location;
  252. delete link.location;
  253. }
  254. // Delete link if older than X hours.
  255. if (!link.ts || (now - new Date(link.ts)) > this.#LINK_CACHE_LIFESPAN_HR * 3600 * 1000) {
  256. delete this.#linkCache[id];
  257. }
  258. });
  259. localStorage.setItem(this.#LINK_CACHE_NAME, this.#lzString.compressToUTF16(JSON.stringify(this.#linkCache)));
  260. // console.log('link cache count: ' + Object.keys(this.#linkCache).length, this.#linkCache);
  261. }
  262.  
  263. // Borrowed from WazeWrap
  264. static #distanceBetweenPoints(point1, point2) {
  265. const line = new OpenLayers.Geometry.LineString([point1, point2]);
  266. const length = line.getGeodesicLength(W.map.getProjectionObject());
  267. return length; // multiply by 3.28084 to convert to feet
  268. }
  269.  
  270. #isLinkTooFar(link, venue) {
  271. if (link.loc) {
  272. const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat);
  273. linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject());
  274. let venuePt;
  275. let distanceLim = this.distanceLimit;
  276. if (venue.isPoint()) {
  277. venuePt = venue.geometry.getCentroid();
  278. } else {
  279. const bounds = venue.geometry.getBounds();
  280. const center = bounds.getCenterLonLat();
  281. venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat);
  282. const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top);
  283. distanceLim += GoogleLinkEnhancer.#distanceBetweenPoints(venuePt, topRightPt);
  284. }
  285. const distance = GoogleLinkEnhancer.#distanceBetweenPoints(linkPt, venuePt);
  286. return distance > distanceLim;
  287. }
  288. return false;
  289. }
  290.  
  291. // eslint-disable-next-line class-methods-use-this
  292. #processPlaces() {
  293. // Disabled until we can find a fix.
  294.  
  295. // try {
  296. // if (this.#enabled) {
  297. // const that = this;
  298. // // Get a list of already-linked id's
  299. // const existingLinks = GoogleLinkEnhancer.#getExistingLinks();
  300. // this.#mapLayer.removeAllFeatures();
  301. // const drawnLinks = [];
  302. // W.model.venues.getObjectArray().forEach(venue => {
  303. // const promises = [];
  304. // venue.attributes.externalProviderIDs.forEach(provID => {
  305. // const id = provID.attributes.uuid;
  306.  
  307. // // Check for duplicate links
  308. // const linkInfo = existingLinks[id];
  309. // if (linkInfo.count > 1) {
  310. // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone();
  311. // const width = venue.isPoint() ? '4' : '12';
  312. // const color = '#fb8d00';
  313. // const features = [new OpenLayers.Feature.Vector(geometry, {
  314. // strokeWidth: width, strokeColor: color
  315. // })];
  316. // const lineStart = geometry.getCentroid();
  317. // linkInfo.venues.forEach(linkVenue => {
  318. // if (linkVenue !== venue
  319. // && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) {
  320. // features.push(
  321. // new OpenLayers.Feature.Vector(
  322. // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]),
  323. // {
  324. // strokeWidth: 4,
  325. // strokeColor: color,
  326. // strokeDashstyle: '12 12'
  327. // }
  328. // )
  329. // );
  330. // drawnLinks.push([venue, linkVenue]);
  331. // }
  332. // });
  333. // that.#mapLayer.addFeatures(features);
  334. // }
  335.  
  336. // // Get Google link info, and store results for processing.
  337. // promises.push(that.#getLinkInfoAsync(id));
  338. // });
  339.  
  340. // // Process all results of link lookups and add a highlight feature if needed.
  341. // Promise.all(promises).then(results => {
  342. // let strokeColor = null;
  343. // let strokeDashStyle = 'solid';
  344. // if (!that.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) {
  345. // if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.attributes.name)
  346. // || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.attributes.name)) {
  347. // strokeDashStyle = venue.isPoint() ? '2 6' : '2 16';
  348. // }
  349. // strokeColor = '#F00';
  350. // } else if (results.some(res => that.#isLinkTooFar(res, venue))) {
  351. // strokeColor = '#0FF';
  352. // } else if (!that.#DISABLE_CLOSED_PLACES && that.#showTempClosedPOIs && results.some(res => res.tempclosed)) {
  353. // if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.attributes.name)
  354. // || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.attributes.name)) {
  355. // strokeDashStyle = venue.isPoint() ? '2 6' : '2 16';
  356. // }
  357. // strokeColor = '#FD3';
  358. // } else if (results.some(res => res.notFound)) {
  359. // strokeColor = '#F0F';
  360. // }
  361. // if (strokeColor) {
  362. // const style = {
  363. // strokeWidth: venue.isPoint() ? '4' : '12',
  364. // strokeColor,
  365. // strokeDashStyle
  366. // };
  367. // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone();
  368. // that.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]);
  369. // }
  370. // });
  371. // });
  372. // }
  373. // } catch (ex) {
  374. // console.error('PIE (Google Link Enhancer) error:', ex);
  375. // }
  376. }
  377.  
  378. #cacheLink(id, link) {
  379. link.ts = new Date();
  380. this.#linkCache[id] = link;
  381. // console.log('link cache count: ' + Object.keys(this.#linkCache).length, this.#linkCache);
  382. }
  383.  
  384. #getLinkInfoAsync(placeId) {
  385. return new Promise(resolve => {
  386. let link = this.#linkCache[placeId];
  387. if (!link) {
  388. const request = {
  389. placeId,
  390. fields: ['geometry', 'business_status']
  391. };
  392. this.#placesService.getDetails(request, (place, requestStatus) => {
  393. link = {};
  394. if (requestStatus === google.maps.places.PlacesServiceStatus.OK) {
  395. const loc = place.geometry.location;
  396. link.loc = { lng: loc.lng(), lat: loc.lat() };
  397. if (place.business_status === 'CLOSED_PERMANENTLY') {
  398. link.permclosed = true;
  399. } else if (place.business_status === 'CLOSED_TEMPORARILY') {
  400. link.tempclosed = true;
  401. }
  402. this.#cacheLink(placeId, link);
  403. } else if (requestStatus === google.maps.places.PlacesServiceStatus.NOT_FOUND) {
  404. link.notfound = true;
  405. this.#cacheLink(placeId, link);
  406. } else if (this.#disableApiUntil) {
  407. link.apiDisabled = true;
  408. } else {
  409. link.error = requestStatus;
  410. // res.errorMessage = json.error_message;
  411. this.#disableApiUntil = Date.now() + 10 * 1000; // Disable api calls for 10 seconds.
  412. console.error(`${GM_info.script.name}, Google Link Enhancer disabled for 10 seconds due to API error.`, link);
  413. }
  414. resolve(link);
  415. });
  416. } else {
  417. resolve(link);
  418. }
  419. });
  420. }
  421.  
  422. // _getLinkInfoAsync(id) {
  423. // const link = this.#linkCache[id];
  424. // if (link) {
  425. // return Promise.resolve(link);
  426. // }
  427. // if (this.#disableApiUntil) {
  428. // if (Date.now() < this.#disableApiUntil) {
  429. // return Promise.resolve({ apiDisabled: true });
  430. // }
  431. // this.#disableApiUntil = null;
  432. // }
  433. // return new Promise(resolve => {
  434.  
  435. // let fullUrl = this._urlBase;
  436. // if (id.startsWith('q=') || id.startsWith('cid=')) {
  437. // fullUrl += id;
  438. // } else {
  439. // fullUrl += `place_id=${id}`;
  440. // }
  441. // $.getJSON(fullUrl).then(json => {
  442. // const res = {};
  443. // if (json.status === 'OK') {
  444. // res.loc = json.result.geometry.location;
  445. // if (json.result.business_status === 'CLOSED_PERMANENTLY') {
  446. // res.permclosed = true;
  447. // } else if (json.result.business_status === 'CLOSED_TEMPORARILY') {
  448. // res.tempclosed = true;
  449. // }
  450. // this._cacheLink(id, res);
  451. // } else if (json.status === 'NOT_FOUND') {
  452. // res.notfound = true;
  453. // this._cacheLink(id, res);
  454. // } else if (this.#disableApiUntil) {
  455. // res.apiDisabled = true;
  456. // } else {
  457. // res.error = json.status;
  458. // res.errorMessage = json.error_message;
  459. // this.#disableApiUntil = Date.now() + 10 * 1000; // Disable api calls for 10 seconds.
  460. // console.error(`${GM_info.script.name}, Google Link Enhancer disabled for 10 seconds due to API error.`, res);
  461. // }
  462. // resolve(res);
  463. // });
  464. // });
  465. // }
  466.  
  467. static #onMapMouseenter(event) {
  468. // If the point isn't destroyed yet, destroy it when mousing over the map.
  469. event.data.#destroyPoint();
  470. }
  471.  
  472. async #formatLinkElements(callCount = 0) {
  473. const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY);
  474. const selObjects = W.selectionManager.getSelectedDataModelObjects();
  475. if (!$links.length) {
  476. // If links aren't available, continue calling this function for up to 3 seconds unless place has been deselected.
  477. if (callCount < 30 && selObjects.length && selObjects[0].type === 'venue') {
  478. setTimeout(() => this.#formatLinkElements(++callCount), 100);
  479. }
  480. } else {
  481. const existingLinks = GoogleLinkEnhancer.#getExistingLinks();
  482.  
  483. // fetch all links first
  484. const promises = [];
  485. const extProvElements = [];
  486. $links.each((ix, linkEl) => {
  487. const $linkEl = $(linkEl);
  488. extProvElements.push($linkEl);
  489.  
  490. const id = GoogleLinkEnhancer.#getIdFromElement($linkEl);
  491. if (!id) return;
  492.  
  493. promises.push(this.#getLinkInfoAsync(id));
  494. });
  495. await Promise.all(promises);
  496.  
  497. extProvElements.forEach($extProvElem => {
  498. const id = GoogleLinkEnhancer.#getIdFromElement($extProvElem);
  499.  
  500. if (!id) return;
  501.  
  502. const link = this.#linkCache[id];
  503. if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) {
  504. setTimeout(() => {
  505. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA500' }).attr({
  506. title: this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count)
  507. });
  508. }, 50);
  509. }
  510. this.#addHoverEvent($extProvElem);
  511. if (link) {
  512. if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) {
  513. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace);
  514. } else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) {
  515. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace);
  516. } else if (link.notFound) {
  517. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink);
  518. } else {
  519. const venue = W.selectionManager.getSelectedDataModelObjects()[0];
  520. if (this.#isLinkTooFar(link, venue)) {
  521. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit));
  522. } else { // reset in case we just deleted another provider
  523. $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', '');
  524. }
  525. }
  526. }
  527. });
  528. }
  529. }
  530.  
  531. static #getExistingLinks() {
  532. const existingLinks = {};
  533. const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0];
  534. W.model.venues.getObjectArray().forEach(venue => {
  535. const isThisVenue = venue === thisVenue;
  536. const thisPlaceIDs = [];
  537. venue.attributes.externalProviderIDs.forEach(provID => {
  538. const id = provID.attributes.uuid;
  539. if (thisPlaceIDs.indexOf(id) === -1) {
  540. thisPlaceIDs.push(id);
  541. let link = existingLinks[id];
  542. if (link) {
  543. link.count++;
  544. link.venues.push(venue);
  545. } else {
  546. link = { count: 1, venues: [venue] };
  547. existingLinks[id] = link;
  548. if (provID.attributes.url != null) {
  549. const u = provID.attributes.url.replace('https://maps.google.com/?', '');
  550. link.url = u;
  551. }
  552. }
  553. link.isThisVenue = link.isThisVenue || isThisVenue;
  554. }
  555. });
  556. });
  557. return existingLinks;
  558. }
  559.  
  560. // Remove the POI point from the map.
  561. #destroyPoint() {
  562. if (this.#ptFeature) {
  563. this.#ptFeature.destroy();
  564. this.#ptFeature = null;
  565. this.#lineFeature.destroy();
  566. this.#lineFeature = null;
  567. }
  568. }
  569.  
  570. static #getOLMapExtent() {
  571. let extent = W.map.getExtent();
  572. if (Array.isArray(extent)) {
  573. extent = new OpenLayers.Bounds(extent);
  574. extent.transform('EPSG:4326', 'EPSG:3857');
  575. }
  576. return extent;
  577. }
  578.  
  579. // Add the POI point to the map.
  580. #addPoint(id) {
  581. if (!id) return;
  582. const link = this.#linkCache[id];
  583. if (link) {
  584. if (!link.notFound) {
  585. const coord = link.loc;
  586. const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat);
  587. poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode);
  588. const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid();
  589. const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y);
  590. const ext = GoogleLinkEnhancer.#getOLMapExtent();
  591. const lsBounds = new OpenLayers.Geometry.LineString([
  592. new OpenLayers.Geometry.Point(ext.left, ext.bottom),
  593. new OpenLayers.Geometry.Point(ext.left, ext.top),
  594. new OpenLayers.Geometry.Point(ext.right, ext.top),
  595. new OpenLayers.Geometry.Point(ext.right, ext.bottom),
  596. new OpenLayers.Geometry.Point(ext.left, ext.bottom)]);
  597. let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]);
  598.  
  599. // If the line extends outside the bounds, split it so we don't draw a line across the world.
  600. const splits = lsLine.splitWith(lsBounds);
  601. let label = '';
  602. if (splits) {
  603. let splitPoints;
  604. splits.forEach(split => {
  605. split.components.forEach(component => {
  606. if (component.x === placePt.x && component.y === placePt.y) splitPoints = split;
  607. });
  608. });
  609. lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]);
  610. let distance = GoogleLinkEnhancer.#distanceBetweenPoints(poiPt, placePt);
  611. let unitConversion;
  612. let unit1;
  613. let unit2;
  614. if (W.model.isImperial) {
  615. distance *= 3.28084;
  616. unitConversion = 5280;
  617. unit1 = ' ft';
  618. unit2 = ' mi';
  619. } else {
  620. unitConversion = 1000;
  621. unit1 = ' m';
  622. unit2 = ' km';
  623. }
  624. if (distance > unitConversion * 10) {
  625. label = Math.round(distance / unitConversion) + unit2;
  626. } else if (distance > 1000) {
  627. label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2;
  628. } else {
  629. label = Math.round(distance) + unit1;
  630. }
  631. }
  632.  
  633. this.#destroyPoint(); // Just in case it still exists.
  634. this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, {
  635. pointRadius: 6,
  636. strokeWidth: 30,
  637. strokeColor: '#FF0',
  638. fillColor: '#FF0',
  639. strokeOpacity: 0.5
  640. });
  641. this.#lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, {
  642. strokeWidth: 3,
  643. strokeDashstyle: '12 8',
  644. strokeColor: '#FF0',
  645. label,
  646. labelYOffset: 45,
  647. fontColor: '#FF0',
  648. fontWeight: 'bold',
  649. labelOutlineColor: '#000',
  650. labelOutlineWidth: 4,
  651. fontSize: '18'
  652. });
  653. W.map.getLayerByUniqueName('venues').addFeatures([this.#ptFeature, this.#lineFeature]);
  654. this.#timeoutDestroyPoint();
  655. }
  656. } else {
  657. this.#getLinkInfoAsync(id).then(res => {
  658. if (res.error || res.apiDisabled) {
  659. // API was temporarily disabled. Ignore for now.
  660. } else {
  661. this.#addPoint(id);
  662. }
  663. });
  664. }
  665. }
  666.  
  667. // Destroy the point after some time, if it hasn't been destroyed already.
  668. #timeoutDestroyPoint() {
  669. if (this.#timeoutID) clearTimeout(this.#timeoutID);
  670. this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000);
  671. }
  672.  
  673. static #getIdFromElement($el) {
  674. const providerIndex = $el.parent().children().toArray().indexOf($el[0]);
  675. return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex]?.attributes.uuid;
  676. }
  677.  
  678. #addHoverEvent($el) {
  679. $el.hover(() => this.#addPoint(GoogleLinkEnhancer.#getIdFromElement($el)), () => this.#destroyPoint());
  680. }
  681.  
  682. #observeLinks() {
  683. this.elem = document.querySelector('#edit-panel');
  684. this.#linkObserver.observe(document.querySelector('#edit-panel'), { childList: true, subtree: true });
  685. }
  686.  
  687. // The JSONP interceptor is used to watch the head element for the addition of JSONP functions
  688. // that process Google link search results. Those functions are overridden by our own so we can
  689. // process the results before sending them on to the original function.
  690. #addJsonpInterceptor() {
  691. // The idea for this function was hatched here:
  692. // https://stackoverflow.com/questions/6803521/can-google-maps-places-autocomplete-api-be-used-via-ajax/9856786
  693.  
  694. // The head element, where the Google Autocomplete code will insert a tag
  695. // for a javascript file.
  696. const head = $('head')[0];
  697. // The name of the method the Autocomplete code uses to insert the tag.
  698. const method = 'appendChild';
  699. // The method we will be overriding.
  700. const originalMethod = head[method];
  701. this.#originalHeadAppendChildMethod = originalMethod;
  702. const that = this;
  703. /* eslint-disable func-names, prefer-rest-params */ // Doesn't work as an arrow function (at least not without some modifications)
  704. head[method] = function() {
  705. // Check that the element is a javascript tag being inserted by Google.
  706. if (arguments[0] && arguments[0].src && arguments[0].src.match(/GetPredictions/)) {
  707. // Regex to extract the name of the callback method that the JSONP will call.
  708. const callbackMatchObject = (/callback=([^&]+)&|$/).exec(arguments[0].src);
  709.  
  710. // Regex to extract the search term that was entered by the user.
  711. const searchTermMatchObject = (/\?1s([^&]+)&/).exec(arguments[0].src);
  712.  
  713. // const searchTerm = unescape(searchTermMatchObject[1]);
  714. if (callbackMatchObject && searchTermMatchObject) {
  715. // The JSONP callback method is in the form "abc.def" and each time has a different random name.
  716. const names = callbackMatchObject[1].split('.');
  717. // Store the original callback method.
  718. const originalCallback = names[0] && names[1] && window[names[0]] && window[names[0]][names[1]];
  719.  
  720. if (originalCallback) {
  721. const newCallback = function() { // Define your own JSONP callback
  722. if (arguments[0] && arguments[0].predictions) {
  723. // SUCCESS!
  724.  
  725. // The autocomplete results
  726. const data = arguments[0];
  727.  
  728. // console.log('GLE: ' + JSON.stringify(data));
  729. that._lastSearchResultPlaceIds = data.predictions.map(pred => pred.place_id);
  730.  
  731. // Call the original callback so the WME dropdown can do its thing.
  732. originalCallback(data);
  733. }
  734. };
  735.  
  736. // Add copy of all the attributes of the old callback function to the new callback function.
  737. // This prevents the autocomplete functionality from throwing an error.
  738. Object.keys(originalCallback).forEach(key => {
  739. newCallback[key] = originalCallback[key];
  740. });
  741. window[names[0]][names[1]] = newCallback; // Override the JSONP callback
  742. }
  743. }
  744. }
  745. // Insert the element into the dom, regardless of whether it was being inserted by Google.
  746. return originalMethod.apply(this, arguments);
  747. };
  748. /* eslint-enable func-names, prefer-rest-params */
  749. }
  750.  
  751. #removeJsonpInterceptor() {
  752. $('head')[0].appendChild = this.#originalHeadAppendChildMethod;
  753. }
  754.  
  755. /* eslint-disable */ // Disabling eslint since this is copied code.
  756. #initLZString() {
  757. // LZ Compressor
  758. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  759. // This work is free. You can redistribute it and/or modify it
  760. // under the terms of the WTFPL, Version 2
  761. // LZ-based compression algorithm, version 1.4.4
  762. this.#lzString = (function () {
  763. // private property
  764. const f = String.fromCharCode;
  765. const keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  766. const keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  767. const baseReverseDic = {};
  768.  
  769. function getBaseValue(alphabet, character) {
  770. if (!baseReverseDic[alphabet]) {
  771. baseReverseDic[alphabet] = {};
  772. for (let i = 0; i < alphabet.length; i++) {
  773. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  774. }
  775. }
  776. return baseReverseDic[alphabet][character];
  777. }
  778. var LZString = {
  779. compressToBase64: function (input) {
  780. if (input === null) return "";
  781. const res = LZString._compress(input, 6, function (a) {
  782. return keyStrBase64.charAt(a);
  783. });
  784. switch (res.length % 4) { // To produce valid Base64
  785. default: // When could this happen ?
  786. case 0:
  787. return res;
  788. case 1:
  789. return res + "===";
  790. case 2:
  791. return res + "==";
  792. case 3:
  793. return res + "=";
  794. }
  795. },
  796. decompressFromBase64: function (input) {
  797. if (input === null) return "";
  798. if (input === "") return null;
  799. return LZString._decompress(input.length, 32, function (index) {
  800. return getBaseValue(keyStrBase64, input.charAt(index));
  801. });
  802. },
  803. compressToUTF16: function (input) {
  804. if (input === null) return "";
  805. return LZString._compress(input, 15, function (a) {
  806. return f(a + 32);
  807. }) + " ";
  808. },
  809. decompressFromUTF16: function (compressed) {
  810. if (compressed === null) return "";
  811. if (compressed === "") return null;
  812. return LZString._decompress(compressed.length, 16384, function (index) {
  813. return compressed.charCodeAt(index) - 32;
  814. });
  815. },
  816.  
  817. compress: function (uncompressed) {
  818. return LZString._compress(uncompressed, 16, function (a) {
  819. return f(a);
  820. });
  821. },
  822. _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
  823. if (uncompressed === null) return "";
  824. let i, value,
  825. context_dictionary = {},
  826. context_dictionaryToCreate = {},
  827. context_c = "",
  828. context_wc = "",
  829. context_w = "",
  830. context_enlargeIn = 2, // Compensate for the first entry which should not count
  831. context_dictSize = 3,
  832. context_numBits = 2,
  833. context_data = [],
  834. context_data_val = 0,
  835. context_data_position = 0,
  836. ii;
  837. for (ii = 0; ii < uncompressed.length; ii += 1) {
  838. context_c = uncompressed.charAt(ii);
  839. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  840. context_dictionary[context_c] = context_dictSize++;
  841. context_dictionaryToCreate[context_c] = true;
  842. }
  843. context_wc = context_w + context_c;
  844. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  845. context_w = context_wc;
  846. } else {
  847. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  848. if (context_w.charCodeAt(0) < 256) {
  849. for (i = 0; i < context_numBits; i++) {
  850. context_data_val = (context_data_val << 1);
  851. if (context_data_position === bitsPerChar - 1) {
  852. context_data_position = 0;
  853. context_data.push(getCharFromInt(context_data_val));
  854. context_data_val = 0;
  855. } else {
  856. context_data_position++;
  857. }
  858. }
  859. value = context_w.charCodeAt(0);
  860. for (i = 0; i < 8; i++) {
  861. context_data_val = (context_data_val << 1) | (value & 1);
  862. if (context_data_position === bitsPerChar - 1) {
  863. context_data_position = 0;
  864. context_data.push(getCharFromInt(context_data_val));
  865. context_data_val = 0;
  866. } else {
  867. context_data_position++;
  868. }
  869. value = value >> 1;
  870. }
  871. } else {
  872. value = 1;
  873. for (i = 0; i < context_numBits; i++) {
  874. context_data_val = (context_data_val << 1) | value;
  875. if (context_data_position === bitsPerChar - 1) {
  876. context_data_position = 0;
  877. context_data.push(getCharFromInt(context_data_val));
  878. context_data_val = 0;
  879. } else {
  880. context_data_position++;
  881. }
  882. value = 0;
  883. }
  884. value = context_w.charCodeAt(0);
  885. for (i = 0; i < 16; i++) {
  886. context_data_val = (context_data_val << 1) | (value & 1);
  887. if (context_data_position === bitsPerChar - 1) {
  888. context_data_position = 0;
  889. context_data.push(getCharFromInt(context_data_val));
  890. context_data_val = 0;
  891. } else {
  892. context_data_position++;
  893. }
  894. value = value >> 1;
  895. }
  896. }
  897. context_enlargeIn--;
  898. if (context_enlargeIn === 0) {
  899. context_enlargeIn = Math.pow(2, context_numBits);
  900. context_numBits++;
  901. }
  902. delete context_dictionaryToCreate[context_w];
  903. } else {
  904. value = context_dictionary[context_w];
  905. for (i = 0; i < context_numBits; i++) {
  906. context_data_val = (context_data_val << 1) | (value & 1);
  907. if (context_data_position === bitsPerChar - 1) {
  908. context_data_position = 0;
  909. context_data.push(getCharFromInt(context_data_val));
  910. context_data_val = 0;
  911. } else {
  912. context_data_position++;
  913. }
  914. value = value >> 1;
  915. }
  916. }
  917. context_enlargeIn--;
  918. if (context_enlargeIn === 0) {
  919. context_enlargeIn = Math.pow(2, context_numBits);
  920. context_numBits++;
  921. }
  922. // Add wc to the dictionary.
  923. context_dictionary[context_wc] = context_dictSize++;
  924. context_w = String(context_c);
  925. }
  926. }
  927. // Output the code for w.
  928. if (context_w !== "") {
  929. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  930. if (context_w.charCodeAt(0) < 256) {
  931. for (i = 0; i < context_numBits; i++) {
  932. context_data_val = (context_data_val << 1);
  933. if (context_data_position === bitsPerChar - 1) {
  934. context_data_position = 0;
  935. context_data.push(getCharFromInt(context_data_val));
  936. context_data_val = 0;
  937. } else {
  938. context_data_position++;
  939. }
  940. }
  941. value = context_w.charCodeAt(0);
  942. for (i = 0; i < 8; i++) {
  943. context_data_val = (context_data_val << 1) | (value & 1);
  944. if (context_data_position === bitsPerChar - 1) {
  945. context_data_position = 0;
  946. context_data.push(getCharFromInt(context_data_val));
  947. context_data_val = 0;
  948. } else {
  949. context_data_position++;
  950. }
  951. value = value >> 1;
  952. }
  953. } else {
  954. value = 1;
  955. for (i = 0; i < context_numBits; i++) {
  956. context_data_val = (context_data_val << 1) | value;
  957. if (context_data_position === bitsPerChar - 1) {
  958. context_data_position = 0;
  959. context_data.push(getCharFromInt(context_data_val));
  960. context_data_val = 0;
  961. } else {
  962. context_data_position++;
  963. }
  964. value = 0;
  965. }
  966. value = context_w.charCodeAt(0);
  967. for (i = 0; i < 16; i++) {
  968. context_data_val = (context_data_val << 1) | (value & 1);
  969. if (context_data_position === bitsPerChar - 1) {
  970. context_data_position = 0;
  971. context_data.push(getCharFromInt(context_data_val));
  972. context_data_val = 0;
  973. } else {
  974. context_data_position++;
  975. }
  976. value = value >> 1;
  977. }
  978. }
  979. context_enlargeIn--;
  980. if (context_enlargeIn === 0) {
  981. context_enlargeIn = Math.pow(2, context_numBits);
  982. context_numBits++;
  983. }
  984. delete context_dictionaryToCreate[context_w];
  985. } else {
  986. value = context_dictionary[context_w];
  987. for (i = 0; i < context_numBits; i++) {
  988. context_data_val = (context_data_val << 1) | (value & 1);
  989. if (context_data_position === bitsPerChar - 1) {
  990. context_data_position = 0;
  991. context_data.push(getCharFromInt(context_data_val));
  992. context_data_val = 0;
  993. } else {
  994. context_data_position++;
  995. }
  996. value = value >> 1;
  997. }
  998. }
  999. context_enlargeIn--;
  1000. if (context_enlargeIn === 0) {
  1001. context_enlargeIn = Math.pow(2, context_numBits);
  1002. context_numBits++;
  1003. }
  1004. }
  1005. // Mark the end of the stream
  1006. value = 2;
  1007. for (i = 0; i < context_numBits; i++) {
  1008. context_data_val = (context_data_val << 1) | (value & 1);
  1009. if (context_data_position === bitsPerChar - 1) {
  1010. context_data_position = 0;
  1011. context_data.push(getCharFromInt(context_data_val));
  1012. context_data_val = 0;
  1013. } else {
  1014. context_data_position++;
  1015. }
  1016. value = value >> 1;
  1017. }
  1018. // Flush the last char
  1019. while (true) {
  1020. context_data_val = (context_data_val << 1);
  1021. if (context_data_position === bitsPerChar - 1) {
  1022. context_data.push(getCharFromInt(context_data_val));
  1023. break;
  1024. } else context_data_position++;
  1025. }
  1026. return context_data.join('');
  1027. },
  1028. decompress: function (compressed) {
  1029. if (compressed === null) return "";
  1030. if (compressed === "") return null;
  1031. return LZString._decompress(compressed.length, 32768, function (index) {
  1032. return compressed.charCodeAt(index);
  1033. });
  1034. },
  1035. _decompress: function (length, resetValue, getNextValue) {
  1036. let dictionary = [],
  1037. next,
  1038. enlargeIn = 4,
  1039. dictSize = 4,
  1040. numBits = 3,
  1041. entry = "",
  1042. result = [],
  1043. i,
  1044. w,
  1045. bits, resb, maxpower, power,
  1046. c,
  1047. data = {
  1048. val: getNextValue(0),
  1049. position: resetValue,
  1050. index: 1
  1051. };
  1052. for (i = 0; i < 3; i += 1) {
  1053. dictionary[i] = i;
  1054. }
  1055. bits = 0;
  1056. maxpower = Math.pow(2, 2);
  1057. power = 1;
  1058. while (power !== maxpower) {
  1059. resb = data.val & data.position;
  1060. data.position >>= 1;
  1061. if (data.position === 0) {
  1062. data.position = resetValue;
  1063. data.val = getNextValue(data.index++);
  1064. }
  1065. bits |= (resb > 0 ? 1 : 0) * power;
  1066. power <<= 1;
  1067. }
  1068. switch (next = bits) {
  1069. case 0:
  1070. bits = 0;
  1071. maxpower = Math.pow(2, 8);
  1072. power = 1;
  1073. while (power !== maxpower) {
  1074. resb = data.val & data.position;
  1075. data.position >>= 1;
  1076. if (data.position === 0) {
  1077. data.position = resetValue;
  1078. data.val = getNextValue(data.index++);
  1079. }
  1080. bits |= (resb > 0 ? 1 : 0) * power;
  1081. power <<= 1;
  1082. }
  1083. c = f(bits);
  1084. break;
  1085. case 1:
  1086. bits = 0;
  1087. maxpower = Math.pow(2, 16);
  1088. power = 1;
  1089. while (power !== maxpower) {
  1090. resb = data.val & data.position;
  1091. data.position >>= 1;
  1092. if (data.position === 0) {
  1093. data.position = resetValue;
  1094. data.val = getNextValue(data.index++);
  1095. }
  1096. bits |= (resb > 0 ? 1 : 0) * power;
  1097. power <<= 1;
  1098. }
  1099. c = f(bits);
  1100. break;
  1101. case 2:
  1102. return "";
  1103. }
  1104. dictionary[3] = c;
  1105. w = c;
  1106. result.push(c);
  1107. while (true) {
  1108. if (data.index > length) {
  1109. return "";
  1110. }
  1111. bits = 0;
  1112. maxpower = Math.pow(2, numBits);
  1113. power = 1;
  1114. while (power !== maxpower) {
  1115. resb = data.val & data.position;
  1116. data.position >>= 1;
  1117. if (data.position === 0) {
  1118. data.position = resetValue;
  1119. data.val = getNextValue(data.index++);
  1120. }
  1121. bits |= (resb > 0 ? 1 : 0) * power;
  1122. power <<= 1;
  1123. }
  1124. switch (c = bits) {
  1125. case 0:
  1126. bits = 0;
  1127. maxpower = Math.pow(2, 8);
  1128. power = 1;
  1129. while (power !== maxpower) {
  1130. resb = data.val & data.position;
  1131. data.position >>= 1;
  1132. if (data.position === 0) {
  1133. data.position = resetValue;
  1134. data.val = getNextValue(data.index++);
  1135. }
  1136. bits |= (resb > 0 ? 1 : 0) * power;
  1137. power <<= 1;
  1138. }
  1139. dictionary[dictSize++] = f(bits);
  1140. c = dictSize - 1;
  1141. enlargeIn--;
  1142. break;
  1143. case 1:
  1144. bits = 0;
  1145. maxpower = Math.pow(2, 16);
  1146. power = 1;
  1147. while (power !== maxpower) {
  1148. resb = data.val & data.position;
  1149. data.position >>= 1;
  1150. if (data.position === 0) {
  1151. data.position = resetValue;
  1152. data.val = getNextValue(data.index++);
  1153. }
  1154. bits |= (resb > 0 ? 1 : 0) * power;
  1155. power <<= 1;
  1156. }
  1157. dictionary[dictSize++] = f(bits);
  1158. c = dictSize - 1;
  1159. enlargeIn--;
  1160. break;
  1161. case 2:
  1162. return result.join('');
  1163. }
  1164. if (enlargeIn === 0) {
  1165. enlargeIn = Math.pow(2, numBits);
  1166. numBits++;
  1167. }
  1168. if (dictionary[c]) {
  1169. entry = dictionary[c];
  1170. } else {
  1171. if (c === dictSize) {
  1172. entry = w + w.charAt(0);
  1173. } else {
  1174. return null;
  1175. }
  1176. }
  1177. result.push(entry);
  1178. // Add w+entry[0] to the dictionary.
  1179. dictionary[dictSize++] = w + entry.charAt(0);
  1180. enlargeIn--;
  1181. w = entry;
  1182. if (enlargeIn === 0) {
  1183. enlargeIn = Math.pow(2, numBits);
  1184. numBits++;
  1185. }
  1186. }
  1187. }
  1188. };
  1189. return LZString;
  1190. })();
  1191. }
  1192. /* eslint-enable */
  1193. }