WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

当前为 2018-03-16 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name WME Utils - Google Link Enhancer
  3. // @namespace WazeDev
  4. // @version 2018.03.16.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. class GoogleLinkEnhancer {
  12.  
  13. constructor() {
  14. this.EXT_PROV_ELEM_QUERY = 'li.external-provider-item';
  15. this.LINK_CACHE_NAME = 'gle_link_cache';
  16. this.LINK_CACHE_CLEAN_INTERVAL_MIN = 1; // Interval to remove old links and save new ones.
  17. this.LINK_CACHE_LIFESPAN_HR = 6; // Remove old links when they exceed this time limit.
  18. this._enabled = false;
  19. this._mapLayer = null;
  20. this._urlOrigin = window.location.origin;
  21. this._distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place.
  22. // Area place is calculated as _distanceLimit + <distance between centroid and furthest node>
  23.  
  24. this.strings = {};
  25. this.strings.closedPlace = 'Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.';
  26. this.strings.multiLinked = 'Linked more than once already. Please find and remove multiple links.';
  27. this.strings.linkedToThisPlace = 'Already linked to this place';
  28. this.strings.linkedNearby = 'Already linked to a nearby place';
  29. this.strings.linkedToXPlaces = 'This is linked to {0} places';
  30. this.strings.badLink = 'Invalid Google link. Please remove it.';
  31. this.strings.tooFar = 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.';
  32.  
  33. this._initLZString();
  34.  
  35. let storedCache = localStorage.getItem(this.LINK_CACHE_NAME);
  36. this._linkCache = storedCache ? $.parseJSON(this._LZString.decompress(storedCache)) : {};
  37. if (this._linkCache === null) this._linkCache = {};
  38.  
  39. this._initLayer();
  40.  
  41. // Watch for ext provider elements being added to the DOM, and add hover events.
  42. this._linkObserver = new MutationObserver(mutations => {
  43. mutations.forEach(mutation => {
  44. for (let idx=0; idx<mutation.addedNodes.length; idx++) {
  45. let nd = mutation.addedNodes[idx];
  46. if (nd.nodeType === Node.ELEMENT_NODE) {
  47. let $el = $(nd);
  48. if ($el.is(this.EXT_PROV_ELEM_QUERY)) {
  49. this._addHoverEvent($el);
  50. } else {
  51. if ($el.find('div.uuid').length) {
  52. this._formatLinkElements();
  53. }
  54. }
  55. }
  56. }
  57. });
  58. });
  59.  
  60. // Watch the side panel for addition of the sidebar-layout div, which indicates a mode change.
  61. this._modeObserver = new MutationObserver(mutations => {
  62. mutations.forEach(mutation => {
  63. for (let idx = 0; idx<mutation.addedNodes.length; idx++) {
  64. let nd = mutation.addedNodes[idx];
  65. if (nd.nodeType === Node.ELEMENT_NODE && $(nd).is('.sidebar-layout')) {
  66. this._observeLinks();
  67. break;
  68. }
  69. }
  70. });
  71. });
  72.  
  73. // This is a special event that will be triggered when DOM elements are destroyed.
  74. (function($){
  75. $.event.special.destroyed = {
  76. remove: function(o) {
  77. if (o.handler && o.type !== 'destroyed') {
  78. o.handler();
  79. }
  80. }
  81. };
  82. })(jQuery);
  83. }
  84.  
  85.  
  86. _initLayer(){
  87. this._mapLayer = new OL.Layer.Vector('Google Link Enhancements.', {
  88. uniqueName: '___GoogleLinkEnhancements',
  89. displayInLayerSwitcher: true,
  90. styleMap: new OL.StyleMap({
  91. default: {
  92. strokeColor: '${strokeColor}',
  93. strokeWidth: '${strokeWidth}',
  94. strokeDashstyle: '${strokeDashstyle}',
  95. pointRadius: '15',
  96. fillOpacity: '0'
  97. }
  98. })
  99. });
  100.  
  101. this._mapLayer.setOpacity(0.8);
  102.  
  103. W.map.addLayer(this._mapLayer);
  104.  
  105. W.model.events.register('mergeend',this,function(e){
  106. this._processPlaces();
  107. },true);
  108. W.map.events.register('moveend',this,function(e){
  109. this._processPlaces();
  110. },true);
  111. W.model.venues.on('objectschanged', function(e) {
  112. this._processPlaces();
  113. }, this);
  114. }
  115.  
  116. enable() {
  117. this._enabled = true;
  118. this._modeObserver.observe($('.edit-area #sidebarContent')[0], {childList: true, subtree:false});
  119. this._observeLinks();
  120. // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function.
  121. $(document).on('ajaxSuccess', null, this, this._onAjaxSuccess);
  122. $('#map').on('mouseenter', null, this, this._onMapMouseenter);
  123. $(window).on('unload', null, this, this._onWindowUnload);
  124. W.model.venues.on('objectschanged', this._formatLinkElements, this);
  125. this._processPlaces();
  126. this._cleanAndSaveLinkCache();
  127. this._cacheCleanIntervalID = setInterval(() => this._cleanAndSaveLinkCache(), 1000 * 60 * this.LINK_CACHE_CLEAN_INTERVAL_MIN);
  128. }
  129.  
  130. disable() {
  131. this._enabled = false;
  132. this._modeObserver.disconnect();
  133. this._linkObserver.disconnect();
  134. $(document).off('ajaxSuccess', this._onAjaxSuccess);
  135. $('#map').off('mouseenter', this._onMapMouseenter);
  136. $(window).off('unload', null, this, this._onWindowUnload);
  137. W.model.venues.off('objectschanged', this._formatLinkElements, this);
  138. if (this._cacheCleanIntervalID) clearInterval(this._cacheCleanIntervalID);
  139. this._cleanAndSaveLinkCache();
  140. this._mapLayer.removeAllFeatures();
  141. }
  142.  
  143. // The distance (in meters) before flagging a Waze place that is too far from the linked Google place.
  144. // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node.
  145. get distanceLimit() {
  146. return this._distanceLimit;
  147. }
  148. set distanceLimit(value) {
  149. this._distanceLimit = value;
  150. this._processPlaces();
  151. }
  152.  
  153. _onWindowUnload(evt) {
  154. evt.data._cleanAndSaveLinkCache();
  155. }
  156.  
  157. _cleanAndSaveLinkCache() {
  158. if (!this._linkCache) return;
  159. let now = new Date();
  160. Object.keys(this._linkCache).forEach(id => {
  161. let link = this._linkCache[id];
  162. // Bug fix:
  163. if (link.location) {
  164. link.loc = link.location;
  165. delete link.location;
  166. }
  167. // Delete link if older than X hours.
  168. if (!link.ts || (now - new Date(link.ts)) > this.LINK_CACHE_LIFESPAN_HR * 3600 * 1000) {
  169. delete this._linkCache[id];
  170. }
  171. });
  172. localStorage.setItem(this.LINK_CACHE_NAME, this._LZString.compress(JSON.stringify(this._linkCache)));
  173. //console.log('link cache count: ' + Object.keys(this._linkCache).length, this._linkCache);
  174. }
  175.  
  176. _distanceBetweenPoints(x1, y1, x2, y2) {
  177. return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
  178. }
  179.  
  180. _isLinkTooFar(link, venue) {
  181. if (link.loc) {
  182. let linkPt = new OL.Geometry.Point(link.loc.lng, link.loc.lat);
  183. linkPt.transform(W.map.displayProjection, W.map.projection);
  184. let venuePt;
  185. let distanceLimit;
  186. if (venue.isPoint()) {
  187. venuePt = venue.geometry.getCentroid();
  188. distanceLimit = this.distanceLimit;
  189. } else {
  190. let bounds = venue.geometry.getBounds();
  191. let center = bounds.getCenterLonLat();
  192. venuePt = {x: center.lon, y: center.lat};
  193. distanceLimit = this._distanceBetweenPoints(center.lon, center.lat, bounds.right, bounds.top) + this.distanceLimit;
  194. }
  195. let distance = this._distanceBetweenPoints(linkPt.x, linkPt.y, venuePt.x, venuePt.y);
  196.  
  197. return distance > distanceLimit;
  198. } else {
  199. return false;
  200. }
  201. }
  202.  
  203. _processPlaces() {
  204. try {
  205. if (this._enabled) {
  206. let that = this;
  207. let projFrom = W.map.displayProjection;
  208. let projTo = W.map.projection;
  209. let mapExtent = W.map.getExtent();
  210. this._mapLayer.removeAllFeatures();
  211. W.model.venues.getObjectArray().forEach(function(venue) {
  212. let isTooFar = false;
  213. venue.attributes.externalProviderIDs.forEach(provID => {
  214. let id = provID.attributes.uuid;
  215. that._getLinkInfoAsync(id).then(link => {
  216. // Check for distance from Google POI.
  217. if (that._isLinkTooFar(link, venue) && !isTooFar) {
  218. isTooFar = true;
  219. let venuePt = venue.geometry.getCentroid();
  220. let dashStyle = 'solid'; //venue.isPoint() ? '2 6' : '2 16';
  221. let geometry = venue.isPoint() ? venuePt : venue.geometry.clone();
  222. let width = venue.isPoint() ? '4' : '12';
  223. that._mapLayer.addFeatures([new OL.Feature.Vector(geometry, {strokeWidth:width, strokeColor:'#0FF', strokeDashstyle:dashStyle})]);
  224. }
  225.  
  226. // Check for closed places or invalid Google links.
  227. if (link.closed || link.notFound) {
  228. 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';
  229. let geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone();
  230. let width = venue.isPoint() ? '4' : '12';
  231. let color = link.notFound ? '#F0F' : '#F00';
  232. that._mapLayer.addFeatures([new OL.Feature.Vector(geometry, {strokeWidth:width, strokeColor:color, strokeDashstyle:dashStyle})]);
  233. }
  234. }).catch(res => {
  235. console.log(res);
  236. });
  237. });
  238. });
  239. }
  240. } catch (ex) {
  241. console.log(ex);
  242. }
  243. }
  244.  
  245. _cacheLink(id, link) {
  246. link.ts = new Date();
  247. this._linkCache[id] = link;
  248. //console.log('link cache count: ' + Object.keys(this._linkCache).length, this._linkCache);
  249. }
  250.  
  251. _getLinkInfoAsync(id) {
  252. var link = this._linkCache[id];
  253. if (link) {
  254. return Promise.resolve(link);
  255. } else {
  256. return new Promise((resolve, reject) => {
  257. $.getJSON(this._urlOrigin + '/maps/api/place/details/json?&key=AIzaSyDf-q2MCay0AE7RF6oIMrDPrjBwxVtsUuI&placeid=' + id).then(json => {
  258. if (json.status==='NOT_FOUND') {
  259. link = {notFound: true};
  260. } else {
  261. link = {loc:json.result.geometry.location,closed:json.result.permanently_closed};
  262. }
  263. this._cacheLink(id, link);
  264. resolve(link);
  265. }).fail(res => {
  266. reject(res);
  267. });
  268. });
  269. }
  270. }
  271.  
  272. _onMapMouseenter(event) {
  273. // If the point isn't destroyed yet, destroy it when mousing over the map.
  274. event.data._destroyPoint();
  275. }
  276.  
  277. _formatLinkElements(a,b,c) {
  278. let existingLinks = this._getExistingLinks();
  279. $('#edit-panel').find(this.EXT_PROV_ELEM_QUERY).each((ix, childEl) => {
  280. let $childEl = $(childEl);
  281. let id = this._getIdFromElement($childEl);
  282. if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) {
  283. setTimeout(() => {
  284. $childEl.find('div.uuid').css({backgroundColor:'#FFA500'}).attr({'title':this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count)});
  285. }, 50);
  286. }
  287. this._addHoverEvent($(childEl));
  288.  
  289. let link = this._linkCache[id];
  290. if (link) {
  291. if (link.closed) {
  292. // A delay is needed to allow the UI to do its formatting so it doesn't overwrite ours.
  293. setTimeout(() => {
  294. $childEl.find('div.uuid').css({backgroundColor:'#FAA'}).attr('title',this.strings.closedPlace);
  295. }, 50);
  296. } else if (link.notFound) {
  297. setTimeout(() => {
  298. $childEl.find('div.uuid').css({backgroundColor:'#F0F'}).attr('title',this.strings.badLink);
  299. }, 50);
  300. } else {
  301. let venue = W.selectionManager.selectedItems[0].model;
  302. if (this._isLinkTooFar(link, venue)) {
  303. setTimeout(() => {
  304. $childEl.find('div.uuid').css({backgroundColor:'#0FF'}).attr('title',this.strings.tooFar.replace('{0}',this.distanceLimit));
  305. }, 50);
  306. }
  307. }
  308. }
  309. });
  310. }
  311.  
  312. _getExistingLinks() {
  313. let existingLinks = {};
  314. let thisVenue;
  315. if (W.selectionManager.selectedItems.length) {
  316. thisVenue = W.selectionManager.selectedItems[0].model;
  317. }
  318. W.model.venues.getObjectArray().forEach(venue => {
  319. let isThisVenue = venue === thisVenue;
  320. let thisPlaceIDs = [];
  321. venue.attributes.externalProviderIDs.forEach(provID => {
  322. let id = provID.attributes.uuid;
  323. if (thisPlaceIDs.indexOf(id) === -1) {
  324. thisPlaceIDs.push(id);
  325. let link = existingLinks[id];
  326. if (link) {
  327. link.count++;
  328. } else {
  329. link = {count: 1};
  330. existingLinks[id] = link;
  331. }
  332. link.isThisVenue = link.isThisVenue || isThisVenue;
  333. }
  334. });
  335. });
  336. return existingLinks;
  337. }
  338.  
  339. _onAjaxSuccess(event, jqXHR, ajaxOptions, data) {
  340. let url = ajaxOptions.url;
  341. let that = event.data;
  342.  
  343. if(/^\/maps\/api\/place\/autocomplete\/json?/i.test(url)) {
  344. // After an "autocomplete" api call...
  345.  
  346. // Get a list of already-linked id's
  347. let existingLinks = that._getExistingLinks();
  348.  
  349. // Examine the suggestions and format any that are already linked.
  350. $('#select2-drop ul li').each((idx, el) => {
  351. let $el = $(el);
  352. let linkData = $el.data('select2Data');
  353. if (linkData) {
  354. let link = existingLinks[linkData.id];
  355. if (link) {
  356. let title, bgColor, textColor, fontWeight;
  357. if (link.count > 1) {
  358. title = that.strings.multiLinked;
  359. textColor = '#000';
  360. bgColor = '#FFA500';
  361. } else {
  362. bgColor = '#ddd';
  363. if (link.isThisVenue) {
  364. title = that.strings.linkedToThisPlace;
  365. textColor = '#444';
  366. fontWeight = 600;
  367. } else {
  368. title = that.strings.linkedNearby;
  369. textColor = '#888';
  370. }
  371. }
  372. if (bgColor) $el.css({backgroundColor: bgColor});
  373. if (textColor) $el.css({color: textColor});
  374. if (fontWeight) $el.css({fontWeight: fontWeight});
  375. $el.attr('title',title);
  376. }
  377. $el.mouseover(function() {
  378. that._addPoint(linkData.id);
  379. }).mouseleave(() => that._destroyPoint()).bind('destroyed', () => that._destroyPoint()).mousedown(() => that._destroyPoint());
  380. }
  381. });
  382. } else if (/^\/maps\/api\/place\/details\/json?/i.test(url)) {
  383. //After a "details" api call...
  384.  
  385. // Cache location results. Note this isn't absolutely necessary because they're
  386. // cached when calling for them on mouseover. However, WME calls this api for each link
  387. // when the place is first loaded, so might as well take advantage of it.
  388.  
  389. let link = {};
  390. if (data.status === 'NOT_FOUND') {
  391. link.notFound = true;
  392. } else {
  393. link.loc = data.result.geometry.location;
  394. link.closed = data.result.permanently_closed;
  395. }
  396. var id = url.match(/placeid=(.*)&?/)[0];
  397. that._cacheLink(id, link);
  398. that._formatLinkElements();
  399. }
  400. }
  401.  
  402. // Remove the POI point from the map.
  403. _destroyPoint() {
  404. if (this._ptFeature) {
  405. this._ptFeature.destroy();
  406. this._ptFeature = null;
  407. this._lineFeature.destroy();
  408. this._lineFeature = null;
  409. }
  410. }
  411.  
  412. // Add the POI point to the map.
  413. _addPoint(id) {
  414. if (!id) return;
  415. let link = this._linkCache[id];
  416. if (link) {
  417. if (!link.notFound) {
  418. let coord = link.loc;
  419. let poiPt = new OL.Geometry.Point(coord.lng, coord.lat);
  420. poiPt.transform(W.map.displayProjection, W.map.projection);
  421. let placeGeom = W.selectionManager.selectedItems[0].geometry.getCentroid();
  422. let placePt = new OL.Geometry.Point(placeGeom.x, placeGeom.y);
  423. //if ( poiPt.intersects(W.map.getExtent().toGeometry()) ) {
  424. this._destroyPoint(); // Just in case it still exists.
  425. this._ptFeature = new OL.Feature.Vector(poiPt,{poiCoord:true},{
  426. pointRadius: 6,
  427. strokeWidth: 30,
  428. strokeColor: '#FF0',
  429. fillColor: '#FF0',
  430. strokeOpacity: 0.5
  431. });
  432. this._lineFeature = new OL.Feature.Vector(new OL.Geometry.LineString([placePt, poiPt]), {}, {
  433. strokeWidth: 3,
  434. strokeDashstyle: '12 8',
  435. strokeColor: '#FF0',
  436. //strokeOpacity: 0.9
  437. });
  438. W.map.getLayerByUniqueName('landmarks').addFeatures([this._ptFeature, this._lineFeature]);
  439. this._timeoutDestroyPoint();
  440. //}
  441. }
  442. } else {
  443. $.getJSON(this._urlOrigin + '/maps/api/place/details/json?&key=AIzaSyDf-q2MCay0AE7RF6oIMrDPrjBwxVtsUuI&placeid=' + id).then(json => {
  444. this._cacheLink(id, {loc:json.result.geometry.location,closed:json.result.permanently_closed});
  445. this._addPoint(id);
  446. });
  447. }
  448. }
  449.  
  450. // Destroy the point after some time, if it hasn't been destroyed already.
  451. _timeoutDestroyPoint() {
  452. if (this._timeoutID) clearTimeout(this._timeoutID);
  453. this._timeoutID = setTimeout(() => this._destroyPoint(), 4000);
  454. }
  455.  
  456. _getIdFromElement($el) {
  457. return $el.find('input.uuid').attr('value');
  458. }
  459.  
  460. _addHoverEvent($el) {
  461. $el.hover(() => this._addPoint(this._getIdFromElement($el)) , () => this._destroyPoint());
  462. }
  463.  
  464. _observeLinks() {
  465. this._linkObserver.observe($('#edit-panel')[0],{ childList: true, subtree: true });
  466. }
  467.  
  468. _initLZString() {
  469. // LZ Compressor
  470. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  471. // This work is free. You can redistribute it and/or modify it
  472. // under the terms of the WTFPL, Version 2
  473. // LZ-based compression algorithm, version 1.4.4
  474. this._LZString = (function() {
  475. // private property
  476. var f = String.fromCharCode;
  477. var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  478. var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  479. var baseReverseDic = {};
  480.  
  481. function getBaseValue(alphabet, character) {
  482. if (!baseReverseDic[alphabet]) {
  483. baseReverseDic[alphabet] = {};
  484. for (var i = 0; i < alphabet.length; i++) {
  485. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  486. }
  487. }
  488. return baseReverseDic[alphabet][character];
  489. }
  490. var LZString = {
  491. compressToBase64: function(input) {
  492. if (input === null) return "";
  493. var res = LZString._compress(input, 6, function(a) {
  494. return keyStrBase64.charAt(a);
  495. });
  496. switch (res.length % 4) { // To produce valid Base64
  497. default: // When could this happen ?
  498. case 0:
  499. return res;
  500. case 1:
  501. return res + "===";
  502. case 2:
  503. return res + "==";
  504. case 3:
  505. return res + "=";
  506. }
  507. },
  508. decompressFromBase64: function(input) {
  509. if (input === null) return "";
  510. if (input === "") return null;
  511. return LZString._decompress(input.length, 32, function(index) {
  512. return getBaseValue(keyStrBase64, input.charAt(index));
  513. });
  514. },
  515. compressToUTF16: function(input) {
  516. if (input === null) return "";
  517. return LZString._compress(input, 15, function(a) {
  518. return f(a + 32);
  519. }) + " ";
  520. },
  521. decompressFromUTF16: function(compressed) {
  522. if (compressed === null) return "";
  523. if (compressed === "") return null;
  524. return LZString._decompress(compressed.length, 16384, function(index) {
  525. return compressed.charCodeAt(index) - 32;
  526. });
  527. },
  528.  
  529. compress: function(uncompressed) {
  530. return LZString._compress(uncompressed, 16, function(a) {
  531. return f(a);
  532. });
  533. },
  534. _compress: function(uncompressed, bitsPerChar, getCharFromInt) {
  535. if (uncompressed === null) return "";
  536. var i, value,
  537. context_dictionary = {},
  538. context_dictionaryToCreate = {},
  539. context_c = "",
  540. context_wc = "",
  541. context_w = "",
  542. context_enlargeIn = 2, // Compensate for the first entry which should not count
  543. context_dictSize = 3,
  544. context_numBits = 2,
  545. context_data = [],
  546. context_data_val = 0,
  547. context_data_position = 0,
  548. ii;
  549. for (ii = 0; ii < uncompressed.length; ii += 1) {
  550. context_c = uncompressed.charAt(ii);
  551. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  552. context_dictionary[context_c] = context_dictSize++;
  553. context_dictionaryToCreate[context_c] = true;
  554. }
  555. context_wc = context_w + context_c;
  556. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  557. context_w = context_wc;
  558. } else {
  559. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  560. if (context_w.charCodeAt(0) < 256) {
  561. for (i = 0; i < context_numBits; i++) {
  562. context_data_val = (context_data_val << 1);
  563. if (context_data_position === bitsPerChar - 1) {
  564. context_data_position = 0;
  565. context_data.push(getCharFromInt(context_data_val));
  566. context_data_val = 0;
  567. } else {
  568. context_data_position++;
  569. }
  570. }
  571. value = context_w.charCodeAt(0);
  572. for (i = 0; i < 8; i++) {
  573. context_data_val = (context_data_val << 1) | (value & 1);
  574. if (context_data_position === bitsPerChar - 1) {
  575. context_data_position = 0;
  576. context_data.push(getCharFromInt(context_data_val));
  577. context_data_val = 0;
  578. } else {
  579. context_data_position++;
  580. }
  581. value = value >> 1;
  582. }
  583. } else {
  584. value = 1;
  585. for (i = 0; i < context_numBits; i++) {
  586. context_data_val = (context_data_val << 1) | value;
  587. if (context_data_position === bitsPerChar - 1) {
  588. context_data_position = 0;
  589. context_data.push(getCharFromInt(context_data_val));
  590. context_data_val = 0;
  591. } else {
  592. context_data_position++;
  593. }
  594. value = 0;
  595. }
  596. value = context_w.charCodeAt(0);
  597. for (i = 0; i < 16; i++) {
  598. context_data_val = (context_data_val << 1) | (value & 1);
  599. if (context_data_position === bitsPerChar - 1) {
  600. context_data_position = 0;
  601. context_data.push(getCharFromInt(context_data_val));
  602. context_data_val = 0;
  603. } else {
  604. context_data_position++;
  605. }
  606. value = value >> 1;
  607. }
  608. }
  609. context_enlargeIn--;
  610. if (context_enlargeIn === 0) {
  611. context_enlargeIn = Math.pow(2, context_numBits);
  612. context_numBits++;
  613. }
  614. delete context_dictionaryToCreate[context_w];
  615. } else {
  616. value = context_dictionary[context_w];
  617. for (i = 0; i < context_numBits; i++) {
  618. context_data_val = (context_data_val << 1) | (value & 1);
  619. if (context_data_position === bitsPerChar - 1) {
  620. context_data_position = 0;
  621. context_data.push(getCharFromInt(context_data_val));
  622. context_data_val = 0;
  623. } else {
  624. context_data_position++;
  625. }
  626. value = value >> 1;
  627. }
  628. }
  629. context_enlargeIn--;
  630. if (context_enlargeIn === 0) {
  631. context_enlargeIn = Math.pow(2, context_numBits);
  632. context_numBits++;
  633. }
  634. // Add wc to the dictionary.
  635. context_dictionary[context_wc] = context_dictSize++;
  636. context_w = String(context_c);
  637. }
  638. }
  639. // Output the code for w.
  640. if (context_w !== "") {
  641. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  642. if (context_w.charCodeAt(0) < 256) {
  643. for (i = 0; i < context_numBits; i++) {
  644. context_data_val = (context_data_val << 1);
  645. if (context_data_position === bitsPerChar - 1) {
  646. context_data_position = 0;
  647. context_data.push(getCharFromInt(context_data_val));
  648. context_data_val = 0;
  649. } else {
  650. context_data_position++;
  651. }
  652. }
  653. value = context_w.charCodeAt(0);
  654. for (i = 0; i < 8; i++) {
  655. context_data_val = (context_data_val << 1) | (value & 1);
  656. if (context_data_position === bitsPerChar - 1) {
  657. context_data_position = 0;
  658. context_data.push(getCharFromInt(context_data_val));
  659. context_data_val = 0;
  660. } else {
  661. context_data_position++;
  662. }
  663. value = value >> 1;
  664. }
  665. } else {
  666. value = 1;
  667. for (i = 0; i < context_numBits; i++) {
  668. context_data_val = (context_data_val << 1) | value;
  669. if (context_data_position === bitsPerChar - 1) {
  670. context_data_position = 0;
  671. context_data.push(getCharFromInt(context_data_val));
  672. context_data_val = 0;
  673. } else {
  674. context_data_position++;
  675. }
  676. value = 0;
  677. }
  678. value = context_w.charCodeAt(0);
  679. for (i = 0; i < 16; i++) {
  680. context_data_val = (context_data_val << 1) | (value & 1);
  681. if (context_data_position === bitsPerChar - 1) {
  682. context_data_position = 0;
  683. context_data.push(getCharFromInt(context_data_val));
  684. context_data_val = 0;
  685. } else {
  686. context_data_position++;
  687. }
  688. value = value >> 1;
  689. }
  690. }
  691. context_enlargeIn--;
  692. if (context_enlargeIn === 0) {
  693. context_enlargeIn = Math.pow(2, context_numBits);
  694. context_numBits++;
  695. }
  696. delete context_dictionaryToCreate[context_w];
  697. } else {
  698. value = context_dictionary[context_w];
  699. for (i = 0; i < context_numBits; i++) {
  700. context_data_val = (context_data_val << 1) | (value & 1);
  701. if (context_data_position === bitsPerChar - 1) {
  702. context_data_position = 0;
  703. context_data.push(getCharFromInt(context_data_val));
  704. context_data_val = 0;
  705. } else {
  706. context_data_position++;
  707. }
  708. value = value >> 1;
  709. }
  710. }
  711. context_enlargeIn--;
  712. if (context_enlargeIn === 0) {
  713. context_enlargeIn = Math.pow(2, context_numBits);
  714. context_numBits++;
  715. }
  716. }
  717. // Mark the end of the stream
  718. value = 2;
  719. for (i = 0; i < context_numBits; i++) {
  720. context_data_val = (context_data_val << 1) | (value & 1);
  721. if (context_data_position === bitsPerChar - 1) {
  722. context_data_position = 0;
  723. context_data.push(getCharFromInt(context_data_val));
  724. context_data_val = 0;
  725. } else {
  726. context_data_position++;
  727. }
  728. value = value >> 1;
  729. }
  730. // Flush the last char
  731. while (true) {
  732. context_data_val = (context_data_val << 1);
  733. if (context_data_position === bitsPerChar - 1) {
  734. context_data.push(getCharFromInt(context_data_val));
  735. break;
  736. } else context_data_position++;
  737. }
  738. return context_data.join('');
  739. },
  740. decompress: function(compressed) {
  741. if (compressed === null) return "";
  742. if (compressed === "") return null;
  743. return LZString._decompress(compressed.length, 32768, function(index) {
  744. return compressed.charCodeAt(index);
  745. });
  746. },
  747. _decompress: function(length, resetValue, getNextValue) {
  748. var dictionary = [],
  749. next,
  750. enlargeIn = 4,
  751. dictSize = 4,
  752. numBits = 3,
  753. entry = "",
  754. result = [],
  755. i,
  756. w,
  757. bits, resb, maxpower, power,
  758. c,
  759. data = {
  760. val: getNextValue(0),
  761. position: resetValue,
  762. index: 1
  763. };
  764. for (i = 0; i < 3; i += 1) {
  765. dictionary[i] = i;
  766. }
  767. bits = 0;
  768. maxpower = Math.pow(2, 2);
  769. power = 1;
  770. while (power !== maxpower) {
  771. resb = data.val & data.position;
  772. data.position >>= 1;
  773. if (data.position === 0) {
  774. data.position = resetValue;
  775. data.val = getNextValue(data.index++);
  776. }
  777. bits |= (resb > 0 ? 1 : 0) * power;
  778. power <<= 1;
  779. }
  780. switch (next = bits) {
  781. case 0:
  782. bits = 0;
  783. maxpower = Math.pow(2, 8);
  784. power = 1;
  785. while (power !== maxpower) {
  786. resb = data.val & data.position;
  787. data.position >>= 1;
  788. if (data.position === 0) {
  789. data.position = resetValue;
  790. data.val = getNextValue(data.index++);
  791. }
  792. bits |= (resb > 0 ? 1 : 0) * power;
  793. power <<= 1;
  794. }
  795. c = f(bits);
  796. break;
  797. case 1:
  798. bits = 0;
  799. maxpower = Math.pow(2, 16);
  800. power = 1;
  801. while (power !== maxpower) {
  802. resb = data.val & data.position;
  803. data.position >>= 1;
  804. if (data.position === 0) {
  805. data.position = resetValue;
  806. data.val = getNextValue(data.index++);
  807. }
  808. bits |= (resb > 0 ? 1 : 0) * power;
  809. power <<= 1;
  810. }
  811. c = f(bits);
  812. break;
  813. case 2:
  814. return "";
  815. }
  816. dictionary[3] = c;
  817. w = c;
  818. result.push(c);
  819. while (true) {
  820. if (data.index > length) {
  821. return "";
  822. }
  823. bits = 0;
  824. maxpower = Math.pow(2, numBits);
  825. power = 1;
  826. while (power !== maxpower) {
  827. resb = data.val & data.position;
  828. data.position >>= 1;
  829. if (data.position === 0) {
  830. data.position = resetValue;
  831. data.val = getNextValue(data.index++);
  832. }
  833. bits |= (resb > 0 ? 1 : 0) * power;
  834. power <<= 1;
  835. }
  836. switch (c = bits) {
  837. case 0:
  838. bits = 0;
  839. maxpower = Math.pow(2, 8);
  840. power = 1;
  841. while (power !== maxpower) {
  842. resb = data.val & data.position;
  843. data.position >>= 1;
  844. if (data.position === 0) {
  845. data.position = resetValue;
  846. data.val = getNextValue(data.index++);
  847. }
  848. bits |= (resb > 0 ? 1 : 0) * power;
  849. power <<= 1;
  850. }
  851. dictionary[dictSize++] = f(bits);
  852. c = dictSize - 1;
  853. enlargeIn--;
  854. break;
  855. case 1:
  856. bits = 0;
  857. maxpower = Math.pow(2, 16);
  858. power = 1;
  859. while (power !== maxpower) {
  860. resb = data.val & data.position;
  861. data.position >>= 1;
  862. if (data.position === 0) {
  863. data.position = resetValue;
  864. data.val = getNextValue(data.index++);
  865. }
  866. bits |= (resb > 0 ? 1 : 0) * power;
  867. power <<= 1;
  868. }
  869. dictionary[dictSize++] = f(bits);
  870. c = dictSize - 1;
  871. enlargeIn--;
  872. break;
  873. case 2:
  874. return result.join('');
  875. }
  876. if (enlargeIn === 0) {
  877. enlargeIn = Math.pow(2, numBits);
  878. numBits++;
  879. }
  880. if (dictionary[c]) {
  881. entry = dictionary[c];
  882. } else {
  883. if (c === dictSize) {
  884. entry = w + w.charAt(0);
  885. } else {
  886. return null;
  887. }
  888. }
  889. result.push(entry);
  890. // Add w+entry[0] to the dictionary.
  891. dictionary[dictSize++] = w + entry.charAt(0);
  892. enlargeIn--;
  893. w = entry;
  894. if (enlargeIn === 0) {
  895. enlargeIn = Math.pow(2, numBits);
  896. numBits++;
  897. }
  898. }
  899. }
  900. };
  901. return LZString;
  902. })();
  903. }
  904. }