WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

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

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

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