WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

当前为 2022-08-10 提交的版本,查看 最新版本

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

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