WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

当前为 2019-03-28 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/39208/684281/WME%20Utils%20-%20Google%20Link%20Enhancer.js

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