WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

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

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

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