WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

目前为 2021-07-27 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name WME Utils - Google Link Enhancer
  3. // @namespace WazeDev
  4. // @version 2021.07.27.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. 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 OpenLayers.Layer.Vector('Google Link Enhancements.', {
  133. uniqueName: '___GoogleLinkEnhancements',
  134. displayInLayerSwitcher: true,
  135. styleMap: new OpenLayers.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) { this._processPlaces(); }, this);
  163. W.model.venues.on('objectsremoved', function (e) { this._processPlaces(); }, this);
  164. W.model.venues.on('objectsadded', function (e) { this._processPlaces(); }, 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. get showTempClosedPOIs(){
  210. return this._showTempClosedPOIs;
  211. }
  212. set showTempClosedPOIs(value){
  213. this._showTempClosedPOIs = value;
  214. this._processPlaces();
  215. }
  216.  
  217. _onWindowUnload(evt) {
  218. evt.data._cleanAndSaveLinkCache();
  219. }
  220.  
  221. _cleanAndSaveLinkCache() {
  222. if (!this._linkCache) return;
  223. let now = new Date();
  224. Object.keys(this._linkCache).forEach(id => {
  225. let link = this._linkCache[id];
  226. // Bug fix:
  227. if (link.location) {
  228. link.loc = link.location;
  229. delete link.location;
  230. }
  231. // Delete link if older than X hours.
  232. if (!link.ts || (now - new Date(link.ts)) > this.LINK_CACHE_LIFESPAN_HR * 3600 * 1000) {
  233. delete this._linkCache[id];
  234. }
  235. });
  236. localStorage.setItem(this.LINK_CACHE_NAME, this._LZString.compressToUTF16(JSON.stringify(this._linkCache)));
  237. //console.log('link cache count: ' + Object.keys(this._linkCache).length, this._linkCache);
  238. }
  239.  
  240. _distanceBetweenPoints(x1, y1, x2, y2) {
  241. return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
  242. }
  243.  
  244. _isLinkTooFar(link, venue) {
  245. if (link.loc) {
  246. let linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat);
  247. linkPt.transform(W.map.getOLMap().displayProjection, W.map.getProjectionObject());
  248. let venuePt;
  249. let distanceLimit;
  250. if (venue.isPoint()) {
  251. venuePt = venue.geometry.getCentroid();
  252. distanceLimit = this.distanceLimit;
  253. } else {
  254. let bounds = venue.geometry.getBounds();
  255. let center = bounds.getCenterLonLat();
  256. venuePt = { x: center.lon, y: center.lat };
  257. distanceLimit = this._distanceBetweenPoints(center.lon, center.lat, bounds.right, bounds.top) + this.distanceLimit;
  258. }
  259. let distance = this._distanceBetweenPoints(linkPt.x, linkPt.y, venuePt.x, venuePt.y);
  260.  
  261. return distance > distanceLimit;
  262. } else {
  263. return false;
  264. }
  265. }
  266.  
  267. _processPlaces() {
  268. try {
  269. if (this._enabled) {
  270. let that = this;
  271. let projFrom = W.map.getOLMap().displayProjection;
  272. let projTo = W.map.getProjectionObject();
  273. let mapExtent = W.map.getExtent();
  274. // Get a list of already-linked id's
  275. let existingLinks = this._getExistingLinks();
  276. this._mapLayer.removeAllFeatures();
  277. let drawnLinks = [];
  278. W.model.venues.getObjectArray().forEach(function (venue) {
  279. const promises = [];
  280. venue.attributes.externalProviderIDs.forEach(provID => {
  281. let id = provID.attributes.uuid;
  282.  
  283. // Check for duplicate links
  284. let linkInfo = existingLinks[id];
  285. if (linkInfo.count > 1) {
  286. let geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone();
  287. let width = venue.isPoint() ? '4' : '12';
  288. let color = '#fb8d00';
  289. let features = [new OpenLayers.Feature.Vector(geometry, {
  290. strokeWidth: width, strokeColor: color
  291. })];
  292. let lineStart = geometry.getCentroid();
  293. linkInfo.venues.forEach(linkVenue => {
  294. if (linkVenue !== venue
  295. && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) {
  296. features.push(
  297. new OpenLayers.Feature.Vector(
  298. new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]),
  299. {
  300. strokeWidth: 4,
  301. strokeColor: color,
  302. strokeDashstyle: '12 12',
  303. })
  304. )
  305. drawnLinks.push([venue, linkVenue]);
  306. }
  307. })
  308. that._mapLayer.addFeatures(features);
  309. }
  310.  
  311. // Get Google link info, and store results for processing.
  312. promises.push(that._getLinkInfoAsync(id));
  313. });
  314.  
  315. // Process all results of link lookups and add a highlight feature if needed.
  316. Promise.all(promises).then(results => {
  317. let strokeColor = null;
  318. let strokeDashStyle = 'solid';
  319. if (results.some(res => that._isLinkTooFar(res, venue))) {
  320. strokeColor = '#0FF';
  321. } else if (!that.DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) {
  322. if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.attributes.name)
  323. || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.attributes.name)) {
  324. strokeDashStyle = venue.isPoint() ? '2 6' : '2 16';
  325. }
  326. strokeColor = '#F00';
  327. } else if (!that.DISABLE_CLOSED_PLACES && that._showTempClosedPOIs && results.some(res => res.tempclosed)) {
  328. if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.attributes.name)
  329. || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.attributes.name)) {
  330. strokeDashStyle = venue.isPoint() ? '2 6' : '2 16';
  331. }
  332. strokeColor = '#FD3';
  333. } else if (results.some(res => res.notFound)) {
  334. strokeColor = '#F0F';
  335. }
  336. if (strokeColor) {
  337. const style = {
  338. strokeWidth: venue.isPoint() ? '4' : '12',
  339. strokeColor,
  340. strokeDashStyle
  341. }
  342. const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone();
  343. that._mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]);
  344. }
  345. });
  346. });
  347. }
  348. } catch (ex) {
  349. console.error('PIE (Google Link Enhancer) error:', ex);
  350. }
  351. }
  352.  
  353. _cacheLink(id, link) {
  354. link.ts = new Date();
  355. this._linkCache[id] = link;
  356. //console.log('link cache count: ' + Object.keys(this._linkCache).length, this._linkCache);
  357. }
  358.  
  359. _getLinkInfoAsync(id) {
  360. var link = this._linkCache[id];
  361. if (link) {
  362. return Promise.resolve(link);
  363. } else {
  364. if (this._disableApiUntil) {
  365. if (Date.now() < this._disableApiUntil) {
  366. return Promise.resolve({ apiDisabled: true });
  367. }
  368. this._disableApiUntil = null;
  369. }
  370. return new Promise((resolve, reject) => {
  371. $.getJSON(`${this._urlBase}${id}`).then(json => {
  372. let res = {};
  373. if (json.status === "OK") {
  374. res.loc = json.result.geometry.location;
  375. if (json.result.business_status == "CLOSED_PERMANENTLY") {
  376. res.permclosed = true;
  377. } else if (json.result.business_status == "CLOSED_TEMPORARILY") {
  378. res.tempclosed = true;
  379. }
  380. this._cacheLink(id, res);
  381. } else if (json.status === "NOT_FOUND") {
  382. res.notfound = true;
  383. this._cacheLink(id, res);
  384. } else {
  385. if (this._disableApiUntil) {
  386. res.apiDisabled = true;
  387. } else {
  388. res.error = json.status;
  389. res.errorMessage = json.error_message;
  390. this._disableApiUntil = Date.now() + 10 * 1000 // Disable api calls for 10 seconds.
  391. console.error(GM_info.script.name + ', Google Link Enhancer disabled for 10 seconds due to API error.', res);
  392. }
  393. }
  394. resolve(res);
  395. });
  396. });
  397. }
  398. }
  399.  
  400. _onMapMouseenter(event) {
  401. // If the point isn't destroyed yet, destroy it when mousing over the map.
  402. event.data._destroyPoint();
  403. }
  404.  
  405. _getSelectedFeatures() {
  406. if (!W.selectionManager.getSelectedFeatures)
  407. return W.selectionManager.selectedItems;
  408. return W.selectionManager.getSelectedFeatures();
  409. }
  410.  
  411. _formatLinkElements(a, b, c) {
  412. let existingLinks = this._getExistingLinks();
  413. $('#edit-panel').find(this.EXT_PROV_ELEM_QUERY).each((ix, childEl) => {
  414. console.log(childEl);
  415. let $childEl = $(childEl);
  416. let id = this._getIdFromElement($childEl);
  417. if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) {
  418. setTimeout(() => {
  419. $childEl.find('div.uuid').css({ backgroundColor: '#FFA500' }).attr({ 'title': this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count) });
  420. }, 50);
  421. }
  422. this._addHoverEvent($(childEl));
  423.  
  424. let link = this._linkCache[id];
  425. if (link) {
  426. if (link.permclosed && !this.DISABLE_CLOSED_PLACES) {
  427. // A delay is needed to allow the UI to do its formatting so it doesn't overwrite ours.
  428. // EDIT 2019.03.14 - Tested without the timeouts and it appears to be working now.
  429.  
  430. //setTimeout(() => {
  431. $childEl.find('div.uuid').css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace);
  432. //}, 50);
  433. } else if (link.tempclosed && !this.DISABLE_CLOSED_PLACES) {
  434. //setTimeout(() => {
  435. $childEl.find('div.uuid').css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace);
  436. //}, 50);
  437. } else if (link.notFound) {
  438. //setTimeout(() => {
  439. $childEl.find('div.uuid').css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink);
  440. //}, 50);
  441. } else {
  442. let venue = this._getSelectedFeatures()[0].model;
  443. if (this._isLinkTooFar(link, venue)) {
  444. //setTimeout(() => {
  445. $childEl.find('div.uuid').css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit));
  446. //}, 50);
  447. }
  448. }
  449. }
  450. });
  451. }
  452.  
  453. _getExistingLinks() {
  454. let existingLinks = {};
  455. let thisVenue;
  456. if (this._getSelectedFeatures().length) {
  457. thisVenue = this._getSelectedFeatures()[0].model;
  458. }
  459. W.model.venues.getObjectArray().forEach(venue => {
  460. let isThisVenue = venue === thisVenue;
  461. let thisPlaceIDs = [];
  462. venue.attributes.externalProviderIDs.forEach(provID => {
  463. let id = provID.attributes.uuid;
  464. if (thisPlaceIDs.indexOf(id) === -1) {
  465. thisPlaceIDs.push(id);
  466. let link = existingLinks[id];
  467. if (link) {
  468. link.count++;
  469. link.venues.push(venue);
  470. } else {
  471. link = { count: 1, venues: [venue] };
  472. existingLinks[id] = link;
  473. }
  474. link.isThisVenue = link.isThisVenue || isThisVenue;
  475. }
  476. });
  477. });
  478. return existingLinks;
  479. }
  480.  
  481. // Remove the POI point from the map.
  482. _destroyPoint() {
  483. if (this._ptFeature) {
  484. this._ptFeature.destroy();
  485. this._ptFeature = null;
  486. this._lineFeature.destroy();
  487. this._lineFeature = null;
  488. }
  489. }
  490.  
  491. // Add the POI point to the map.
  492. _addPoint(id) {
  493. if (!id) return;
  494. let link = this._linkCache[id];
  495. if (link) {
  496. if (!link.notFound) {
  497. let coord = link.loc;
  498. let poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat);
  499. poiPt.transform(W.map.getOLMap().displayProjection, W.map.getProjectionObject());
  500. let placeGeom = this._getSelectedFeatures()[0].geometry.getCentroid();
  501. let placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y);
  502. let ext = W.map.getExtent();
  503. var lsBounds = new OpenLayers.Geometry.LineString([
  504. new OpenLayers.Geometry.Point(ext.left, ext.bottom),
  505. new OpenLayers.Geometry.Point(ext.left, ext.top),
  506. new OpenLayers.Geometry.Point(ext.right, ext.top),
  507. new OpenLayers.Geometry.Point(ext.right, ext.bottom),
  508. new OpenLayers.Geometry.Point(ext.left, ext.bottom)]);
  509. let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]);
  510.  
  511. // If the line extends outside the bounds, split it so we don't draw a line across the world.
  512. let splits = lsLine.splitWith(lsBounds);
  513. let label = '';
  514. if (splits) {
  515. let splitPoints;
  516. splits.forEach(split => {
  517. split.components.forEach(component => {
  518. if (component.x === placePt.x && component.y === placePt.y) splitPoints = split;
  519. });
  520. });
  521. lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]);
  522. let distance = poiPt.distanceTo(placePt);
  523. let unitConversion, unit1, unit2;
  524. if (W.model.isImperial) {
  525. distance *= 3.28084;
  526. unitConversion = 5280;
  527. unit1 = ' ft';
  528. unit2 = ' mi';
  529. } else {
  530. unitConversion = 1000;
  531. unit1 = ' m';
  532. unit2 = ' km';
  533. }
  534. if (distance > unitConversion * 10) {
  535. label = Math.round(distance / unitConversion) + unit2;
  536. } else if (distance > 1000) {
  537. label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2;
  538. } else {
  539. label = Math.round(distance) + unit1;
  540. }
  541. }
  542.  
  543. this._destroyPoint(); // Just in case it still exists.
  544. this._ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, {
  545. pointRadius: 6,
  546. strokeWidth: 30,
  547. strokeColor: '#FF0',
  548. fillColor: '#FF0',
  549. strokeOpacity: 0.5
  550. });
  551. this._lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, {
  552. strokeWidth: 3,
  553. strokeDashstyle: '12 8',
  554. strokeColor: '#FF0',
  555. label: label,
  556. labelYOffset: 45,
  557. fontColor: '#FF0',
  558. fontWeight: 'bold',
  559. labelOutlineColor: "#000",
  560. labelOutlineWidth: 4,
  561. fontSize: '18'
  562. });
  563. W.map.getLayerByUniqueName('venues').addFeatures([this._ptFeature, this._lineFeature]);
  564. this._timeoutDestroyPoint();
  565. }
  566. } else {
  567. this._getLinkInfoAsync(id).then(res => {
  568. if (res.error || res.apiDisabled) {
  569. // API was temporarily disabled. Ignore for now.
  570. } else {
  571. this._addPoint(id);
  572. }
  573. })
  574. }
  575. }
  576.  
  577. // Destroy the point after some time, if it hasn't been destroyed already.
  578. _timeoutDestroyPoint() {
  579. if (this._timeoutID) clearTimeout(this._timeoutID);
  580. this._timeoutID = setTimeout(() => this._destroyPoint(), 4000);
  581. }
  582.  
  583. _getIdFromElement($el) {
  584. return $el.find('input.uuid').attr('value');
  585. }
  586.  
  587. _addHoverEvent($el) {
  588. $el.hover(() => this._addPoint(this._getIdFromElement($el)), () => this._destroyPoint());
  589. }
  590.  
  591. _observeLinks() {
  592. this._linkObserver.observe($('#edit-panel')[0], { childList: true, subtree: true });
  593. }
  594.  
  595. // The JSONP interceptor is used to watch the head element for the addition of JSONP functions
  596. // that process Google link search results. Those functions are overridden by our own so we can
  597. // process the results before sending them on to the original function.
  598. _addJsonpInterceptor() {
  599. // The idea for this function was hatched here:
  600. // https://stackoverflow.com/questions/6803521/can-google-maps-places-autocomplete-api-be-used-via-ajax/9856786
  601.  
  602. // The head element, where the Google Autocomplete code will insert a tag
  603. // for a javascript file.
  604. var head = $('head')[0];
  605. // The name of the method the Autocomplete code uses to insert the tag.
  606. var method = 'appendChild';
  607. // The method we will be overriding.
  608. var originalMethod = head[method];
  609. this._originalHeadAppendChildMethod = originalMethod;
  610. let that = this;
  611. head[method] = function () {
  612. // Check that the element is a javascript tag being inserted by Google.
  613. if (arguments[0] && arguments[0].src && arguments[0].src.match(/GetPredictions/)) {
  614. // Regex to extract the name of the callback method that the JSONP will call.
  615. var callbackMatchObject = (/callback=([^&]+)&|$/).exec(arguments[0].src);
  616.  
  617. // Regex to extract the search term that was entered by the user.
  618. var searchTermMatchObject = (/\?1s([^&]+)&/).exec(arguments[0].src);
  619.  
  620. var searchTerm = unescape(searchTermMatchObject[1]);
  621. if (callbackMatchObject && searchTermMatchObject) {
  622. // The JSONP callback method is in the form "abc.def" and each time has a different random name.
  623. var names = callbackMatchObject[1].split('.');
  624. // Store the original callback method.
  625. var originalCallback = names[0] && names[1] && window[names[0]] && window[names[0]][names[1]];
  626.  
  627. if (originalCallback) {
  628. var newCallback = function () { // Define your own JSONP callback
  629. if (arguments[0] && arguments[0].predictions) {
  630. // SUCCESS!
  631.  
  632. // The autocomplete results
  633. var data = arguments[0];
  634.  
  635. console.log(data);
  636. that._lastSearchResultPlaceIds = data.predictions.map(pred => pred.place_id);
  637.  
  638. // Call the original callback so the WME dropdown can do its thing.
  639. originalCallback(data);
  640. }
  641. }
  642.  
  643. // Add copy all the attributes of the old callback function to the new callback function.
  644. // This prevents the autocomplete functionality from throwing an error.
  645. for (name in originalCallback) {
  646. newCallback[name] = originalCallback[name];
  647. }
  648. window[names[0]][names[1]] = newCallback; // Override the JSONP callback
  649. }
  650. }
  651. }
  652. // Insert the element into the dom, regardless of whether it was being inserted by Google.
  653. return originalMethod.apply(this, arguments);
  654. };
  655. }
  656.  
  657. _removeJsonpInterceptor() {
  658. $('head')[0].appendChild = this._originalHeadAppendChildMethod;
  659. }
  660.  
  661. _initLZString() {
  662. // LZ Compressor
  663. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  664. // This work is free. You can redistribute it and/or modify it
  665. // under the terms of the WTFPL, Version 2
  666. // LZ-based compression algorithm, version 1.4.4
  667. this._LZString = (function () {
  668. // private property
  669. var f = String.fromCharCode;
  670. var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  671. var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  672. var baseReverseDic = {};
  673.  
  674. function getBaseValue(alphabet, character) {
  675. if (!baseReverseDic[alphabet]) {
  676. baseReverseDic[alphabet] = {};
  677. for (var i = 0; i < alphabet.length; i++) {
  678. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  679. }
  680. }
  681. return baseReverseDic[alphabet][character];
  682. }
  683. var LZString = {
  684. compressToBase64: function (input) {
  685. if (input === null) return "";
  686. var res = LZString._compress(input, 6, function (a) {
  687. return keyStrBase64.charAt(a);
  688. });
  689. switch (res.length % 4) { // To produce valid Base64
  690. default: // When could this happen ?
  691. case 0:
  692. return res;
  693. case 1:
  694. return res + "===";
  695. case 2:
  696. return res + "==";
  697. case 3:
  698. return res + "=";
  699. }
  700. },
  701. decompressFromBase64: function (input) {
  702. if (input === null) return "";
  703. if (input === "") return null;
  704. return LZString._decompress(input.length, 32, function (index) {
  705. return getBaseValue(keyStrBase64, input.charAt(index));
  706. });
  707. },
  708. compressToUTF16: function (input) {
  709. if (input === null) return "";
  710. return LZString._compress(input, 15, function (a) {
  711. return f(a + 32);
  712. }) + " ";
  713. },
  714. decompressFromUTF16: function (compressed) {
  715. if (compressed === null) return "";
  716. if (compressed === "") return null;
  717. return LZString._decompress(compressed.length, 16384, function (index) {
  718. return compressed.charCodeAt(index) - 32;
  719. });
  720. },
  721.  
  722. compress: function (uncompressed) {
  723. return LZString._compress(uncompressed, 16, function (a) {
  724. return f(a);
  725. });
  726. },
  727. _compress: function (uncompressed, bitsPerChar, getCharFromInt) {
  728. if (uncompressed === null) return "";
  729. var i, value,
  730. context_dictionary = {},
  731. context_dictionaryToCreate = {},
  732. context_c = "",
  733. context_wc = "",
  734. context_w = "",
  735. context_enlargeIn = 2, // Compensate for the first entry which should not count
  736. context_dictSize = 3,
  737. context_numBits = 2,
  738. context_data = [],
  739. context_data_val = 0,
  740. context_data_position = 0,
  741. ii;
  742. for (ii = 0; ii < uncompressed.length; ii += 1) {
  743. context_c = uncompressed.charAt(ii);
  744. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  745. context_dictionary[context_c] = context_dictSize++;
  746. context_dictionaryToCreate[context_c] = true;
  747. }
  748. context_wc = context_w + context_c;
  749. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  750. context_w = context_wc;
  751. } else {
  752. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  753. if (context_w.charCodeAt(0) < 256) {
  754. for (i = 0; i < context_numBits; i++) {
  755. context_data_val = (context_data_val << 1);
  756. if (context_data_position === bitsPerChar - 1) {
  757. context_data_position = 0;
  758. context_data.push(getCharFromInt(context_data_val));
  759. context_data_val = 0;
  760. } else {
  761. context_data_position++;
  762. }
  763. }
  764. value = context_w.charCodeAt(0);
  765. for (i = 0; i < 8; i++) {
  766. context_data_val = (context_data_val << 1) | (value & 1);
  767. if (context_data_position === bitsPerChar - 1) {
  768. context_data_position = 0;
  769. context_data.push(getCharFromInt(context_data_val));
  770. context_data_val = 0;
  771. } else {
  772. context_data_position++;
  773. }
  774. value = value >> 1;
  775. }
  776. } else {
  777. value = 1;
  778. for (i = 0; i < context_numBits; i++) {
  779. context_data_val = (context_data_val << 1) | value;
  780. if (context_data_position === bitsPerChar - 1) {
  781. context_data_position = 0;
  782. context_data.push(getCharFromInt(context_data_val));
  783. context_data_val = 0;
  784. } else {
  785. context_data_position++;
  786. }
  787. value = 0;
  788. }
  789. value = context_w.charCodeAt(0);
  790. for (i = 0; i < 16; i++) {
  791. context_data_val = (context_data_val << 1) | (value & 1);
  792. if (context_data_position === bitsPerChar - 1) {
  793. context_data_position = 0;
  794. context_data.push(getCharFromInt(context_data_val));
  795. context_data_val = 0;
  796. } else {
  797. context_data_position++;
  798. }
  799. value = value >> 1;
  800. }
  801. }
  802. context_enlargeIn--;
  803. if (context_enlargeIn === 0) {
  804. context_enlargeIn = Math.pow(2, context_numBits);
  805. context_numBits++;
  806. }
  807. delete context_dictionaryToCreate[context_w];
  808. } else {
  809. value = context_dictionary[context_w];
  810. for (i = 0; i < context_numBits; i++) {
  811. context_data_val = (context_data_val << 1) | (value & 1);
  812. if (context_data_position === bitsPerChar - 1) {
  813. context_data_position = 0;
  814. context_data.push(getCharFromInt(context_data_val));
  815. context_data_val = 0;
  816. } else {
  817. context_data_position++;
  818. }
  819. value = value >> 1;
  820. }
  821. }
  822. context_enlargeIn--;
  823. if (context_enlargeIn === 0) {
  824. context_enlargeIn = Math.pow(2, context_numBits);
  825. context_numBits++;
  826. }
  827. // Add wc to the dictionary.
  828. context_dictionary[context_wc] = context_dictSize++;
  829. context_w = String(context_c);
  830. }
  831. }
  832. // Output the code for w.
  833. if (context_w !== "") {
  834. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  835. if (context_w.charCodeAt(0) < 256) {
  836. for (i = 0; i < context_numBits; i++) {
  837. context_data_val = (context_data_val << 1);
  838. if (context_data_position === bitsPerChar - 1) {
  839. context_data_position = 0;
  840. context_data.push(getCharFromInt(context_data_val));
  841. context_data_val = 0;
  842. } else {
  843. context_data_position++;
  844. }
  845. }
  846. value = context_w.charCodeAt(0);
  847. for (i = 0; i < 8; i++) {
  848. context_data_val = (context_data_val << 1) | (value & 1);
  849. if (context_data_position === bitsPerChar - 1) {
  850. context_data_position = 0;
  851. context_data.push(getCharFromInt(context_data_val));
  852. context_data_val = 0;
  853. } else {
  854. context_data_position++;
  855. }
  856. value = value >> 1;
  857. }
  858. } else {
  859. value = 1;
  860. for (i = 0; i < context_numBits; i++) {
  861. context_data_val = (context_data_val << 1) | value;
  862. if (context_data_position === bitsPerChar - 1) {
  863. context_data_position = 0;
  864. context_data.push(getCharFromInt(context_data_val));
  865. context_data_val = 0;
  866. } else {
  867. context_data_position++;
  868. }
  869. value = 0;
  870. }
  871. value = context_w.charCodeAt(0);
  872. for (i = 0; i < 16; i++) {
  873. context_data_val = (context_data_val << 1) | (value & 1);
  874. if (context_data_position === bitsPerChar - 1) {
  875. context_data_position = 0;
  876. context_data.push(getCharFromInt(context_data_val));
  877. context_data_val = 0;
  878. } else {
  879. context_data_position++;
  880. }
  881. value = value >> 1;
  882. }
  883. }
  884. context_enlargeIn--;
  885. if (context_enlargeIn === 0) {
  886. context_enlargeIn = Math.pow(2, context_numBits);
  887. context_numBits++;
  888. }
  889. delete context_dictionaryToCreate[context_w];
  890. } else {
  891. value = context_dictionary[context_w];
  892. for (i = 0; i < context_numBits; i++) {
  893. context_data_val = (context_data_val << 1) | (value & 1);
  894. if (context_data_position === bitsPerChar - 1) {
  895. context_data_position = 0;
  896. context_data.push(getCharFromInt(context_data_val));
  897. context_data_val = 0;
  898. } else {
  899. context_data_position++;
  900. }
  901. value = value >> 1;
  902. }
  903. }
  904. context_enlargeIn--;
  905. if (context_enlargeIn === 0) {
  906. context_enlargeIn = Math.pow(2, context_numBits);
  907. context_numBits++;
  908. }
  909. }
  910. // Mark the end of the stream
  911. value = 2;
  912. for (i = 0; i < context_numBits; i++) {
  913. context_data_val = (context_data_val << 1) | (value & 1);
  914. if (context_data_position === bitsPerChar - 1) {
  915. context_data_position = 0;
  916. context_data.push(getCharFromInt(context_data_val));
  917. context_data_val = 0;
  918. } else {
  919. context_data_position++;
  920. }
  921. value = value >> 1;
  922. }
  923. // Flush the last char
  924. while (true) {
  925. context_data_val = (context_data_val << 1);
  926. if (context_data_position === bitsPerChar - 1) {
  927. context_data.push(getCharFromInt(context_data_val));
  928. break;
  929. } else context_data_position++;
  930. }
  931. return context_data.join('');
  932. },
  933. decompress: function (compressed) {
  934. if (compressed === null) return "";
  935. if (compressed === "") return null;
  936. return LZString._decompress(compressed.length, 32768, function (index) {
  937. return compressed.charCodeAt(index);
  938. });
  939. },
  940. _decompress: function (length, resetValue, getNextValue) {
  941. var dictionary = [],
  942. next,
  943. enlargeIn = 4,
  944. dictSize = 4,
  945. numBits = 3,
  946. entry = "",
  947. result = [],
  948. i,
  949. w,
  950. bits, resb, maxpower, power,
  951. c,
  952. data = {
  953. val: getNextValue(0),
  954. position: resetValue,
  955. index: 1
  956. };
  957. for (i = 0; i < 3; i += 1) {
  958. dictionary[i] = i;
  959. }
  960. bits = 0;
  961. maxpower = Math.pow(2, 2);
  962. power = 1;
  963. while (power !== maxpower) {
  964. resb = data.val & data.position;
  965. data.position >>= 1;
  966. if (data.position === 0) {
  967. data.position = resetValue;
  968. data.val = getNextValue(data.index++);
  969. }
  970. bits |= (resb > 0 ? 1 : 0) * power;
  971. power <<= 1;
  972. }
  973. switch (next = bits) {
  974. case 0:
  975. bits = 0;
  976. maxpower = Math.pow(2, 8);
  977. power = 1;
  978. while (power !== maxpower) {
  979. resb = data.val & data.position;
  980. data.position >>= 1;
  981. if (data.position === 0) {
  982. data.position = resetValue;
  983. data.val = getNextValue(data.index++);
  984. }
  985. bits |= (resb > 0 ? 1 : 0) * power;
  986. power <<= 1;
  987. }
  988. c = f(bits);
  989. break;
  990. case 1:
  991. bits = 0;
  992. maxpower = Math.pow(2, 16);
  993. power = 1;
  994. while (power !== maxpower) {
  995. resb = data.val & data.position;
  996. data.position >>= 1;
  997. if (data.position === 0) {
  998. data.position = resetValue;
  999. data.val = getNextValue(data.index++);
  1000. }
  1001. bits |= (resb > 0 ? 1 : 0) * power;
  1002. power <<= 1;
  1003. }
  1004. c = f(bits);
  1005. break;
  1006. case 2:
  1007. return "";
  1008. }
  1009. dictionary[3] = c;
  1010. w = c;
  1011. result.push(c);
  1012. while (true) {
  1013. if (data.index > length) {
  1014. return "";
  1015. }
  1016. bits = 0;
  1017. maxpower = Math.pow(2, numBits);
  1018. power = 1;
  1019. while (power !== maxpower) {
  1020. resb = data.val & data.position;
  1021. data.position >>= 1;
  1022. if (data.position === 0) {
  1023. data.position = resetValue;
  1024. data.val = getNextValue(data.index++);
  1025. }
  1026. bits |= (resb > 0 ? 1 : 0) * power;
  1027. power <<= 1;
  1028. }
  1029. switch (c = bits) {
  1030. case 0:
  1031. bits = 0;
  1032. maxpower = Math.pow(2, 8);
  1033. power = 1;
  1034. while (power !== maxpower) {
  1035. resb = data.val & data.position;
  1036. data.position >>= 1;
  1037. if (data.position === 0) {
  1038. data.position = resetValue;
  1039. data.val = getNextValue(data.index++);
  1040. }
  1041. bits |= (resb > 0 ? 1 : 0) * power;
  1042. power <<= 1;
  1043. }
  1044. dictionary[dictSize++] = f(bits);
  1045. c = dictSize - 1;
  1046. enlargeIn--;
  1047. break;
  1048. case 1:
  1049. bits = 0;
  1050. maxpower = Math.pow(2, 16);
  1051. power = 1;
  1052. while (power !== maxpower) {
  1053. resb = data.val & data.position;
  1054. data.position >>= 1;
  1055. if (data.position === 0) {
  1056. data.position = resetValue;
  1057. data.val = getNextValue(data.index++);
  1058. }
  1059. bits |= (resb > 0 ? 1 : 0) * power;
  1060. power <<= 1;
  1061. }
  1062. dictionary[dictSize++] = f(bits);
  1063. c = dictSize - 1;
  1064. enlargeIn--;
  1065. break;
  1066. case 2:
  1067. return result.join('');
  1068. }
  1069. if (enlargeIn === 0) {
  1070. enlargeIn = Math.pow(2, numBits);
  1071. numBits++;
  1072. }
  1073. if (dictionary[c]) {
  1074. entry = dictionary[c];
  1075. } else {
  1076. if (c === dictSize) {
  1077. entry = w + w.charAt(0);
  1078. } else {
  1079. return null;
  1080. }
  1081. }
  1082. result.push(entry);
  1083. // Add w+entry[0] to the dictionary.
  1084. dictionary[dictSize++] = w + entry.charAt(0);
  1085. enlargeIn--;
  1086. w = entry;
  1087. if (enlargeIn === 0) {
  1088. enlargeIn = Math.pow(2, numBits);
  1089. numBits++;
  1090. }
  1091. }
  1092. }
  1093. };
  1094. return LZString;
  1095. })();
  1096. }
  1097. }