WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

目前為 2018-03-14 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/39208/258664/WME%20Utils%20-%20Google%20Link%20Enhancer.js

  1. // ==UserScript==
  2. // @name WME Utils - Google Link Enhancer
  3. // @namespace WazeDev
  4. // @version 2018.03.14.001
  5. // @description Adds some extra WME functionality related to Google place links.
  6. // @author MapOMatic, WazeDev group
  7. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  8. // @license GNU GPLv3
  9. // ==/UserScript==
  10.  
  11. 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 OpenLayers.Layer.Vector('Google Link Enhancements.', {
  88. uniqueName: '___GoogleLinkEnhancements',
  89. displayInLayerSwitcher: true,
  90. styleMap: new OpenLayers.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 OpenLayers.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 OpenLayers.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._feature) this._feature.destroy();
  405. this._feature = null;
  406. }
  407.  
  408. // Add the POI point to the map.
  409. _addPoint(id) {
  410. if (!id) return;
  411. let link = this._linkCache[id];
  412. if (link) {
  413. if (!link.notFound) {
  414. let coord = link.loc;
  415. let pt = new OpenLayers.Geometry.Point(coord.lng, coord.lat);
  416. pt.transform(W.map.displayProjection, W.map.projection);
  417. if ( pt.intersects(W.map.getExtent().toGeometry()) ) {
  418. this._destroyPoint(); // Just in case it still exists.
  419. this._feature = new OL.Feature.Vector(pt,{poiCoord:true},{
  420. pointRadius: 6,
  421. strokeWidth: 30,
  422. strokeColor: '#FF0',
  423. fillColor: '#FF0',
  424. strokeOpacity: 0.5
  425. });
  426. W.map.getLayerByUniqueName('landmarks').addFeatures([this._feature]);
  427. this._timeoutDestroyPoint();
  428. }
  429. }
  430. } else {
  431. $.getJSON(this._urlOrigin + '/maps/api/place/details/json?&key=AIzaSyDf-q2MCay0AE7RF6oIMrDPrjBwxVtsUuI&placeid=' + id).then(json => {
  432. this._cacheLink(id, {loc:json.result.geometry.location,closed:json.result.permanently_closed});
  433. this._addPoint(id);
  434. });
  435. }
  436. }
  437.  
  438. // Destroy the point after some time, if it hasn't been destroyed already.
  439. _timeoutDestroyPoint() {
  440. if (this._timeoutID) clearTimeout(this._timeoutID);
  441. this._timeoutID = setTimeout(() => this._destroyPoint(), 4000);
  442. }
  443.  
  444. _getIdFromElement($el) {
  445. return $el.find('input.uuid').attr('value');
  446. }
  447.  
  448. _addHoverEvent($el) {
  449. $el.hover(() => this._addPoint(this._getIdFromElement($el)) , () => this._destroyPoint());
  450. }
  451.  
  452. _observeLinks() {
  453. this._linkObserver.observe($('#edit-panel')[0],{ childList: true, subtree: true });
  454. }
  455.  
  456. _initLZString() {
  457. // LZ Compressor
  458. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  459. // This work is free. You can redistribute it and/or modify it
  460. // under the terms of the WTFPL, Version 2
  461. // LZ-based compression algorithm, version 1.4.4
  462. this._LZString = (function() {
  463. // private property
  464. var f = String.fromCharCode;
  465. var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  466. var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  467. var baseReverseDic = {};
  468.  
  469. function getBaseValue(alphabet, character) {
  470. if (!baseReverseDic[alphabet]) {
  471. baseReverseDic[alphabet] = {};
  472. for (var i = 0; i < alphabet.length; i++) {
  473. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  474. }
  475. }
  476. return baseReverseDic[alphabet][character];
  477. }
  478. var LZString = {
  479. compressToBase64: function(input) {
  480. if (input === null) return "";
  481. var res = LZString._compress(input, 6, function(a) {
  482. return keyStrBase64.charAt(a);
  483. });
  484. switch (res.length % 4) { // To produce valid Base64
  485. default: // When could this happen ?
  486. case 0:
  487. return res;
  488. case 1:
  489. return res + "===";
  490. case 2:
  491. return res + "==";
  492. case 3:
  493. return res + "=";
  494. }
  495. },
  496. decompressFromBase64: function(input) {
  497. if (input === null) return "";
  498. if (input === "") return null;
  499. return LZString._decompress(input.length, 32, function(index) {
  500. return getBaseValue(keyStrBase64, input.charAt(index));
  501. });
  502. },
  503. compressToUTF16: function(input) {
  504. if (input === null) return "";
  505. return LZString._compress(input, 15, function(a) {
  506. return f(a + 32);
  507. }) + " ";
  508. },
  509. decompressFromUTF16: function(compressed) {
  510. if (compressed === null) return "";
  511. if (compressed === "") return null;
  512. return LZString._decompress(compressed.length, 16384, function(index) {
  513. return compressed.charCodeAt(index) - 32;
  514. });
  515. },
  516.  
  517. compress: function(uncompressed) {
  518. return LZString._compress(uncompressed, 16, function(a) {
  519. return f(a);
  520. });
  521. },
  522. _compress: function(uncompressed, bitsPerChar, getCharFromInt) {
  523. if (uncompressed === null) return "";
  524. var i, value,
  525. context_dictionary = {},
  526. context_dictionaryToCreate = {},
  527. context_c = "",
  528. context_wc = "",
  529. context_w = "",
  530. context_enlargeIn = 2, // Compensate for the first entry which should not count
  531. context_dictSize = 3,
  532. context_numBits = 2,
  533. context_data = [],
  534. context_data_val = 0,
  535. context_data_position = 0,
  536. ii;
  537. for (ii = 0; ii < uncompressed.length; ii += 1) {
  538. context_c = uncompressed.charAt(ii);
  539. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  540. context_dictionary[context_c] = context_dictSize++;
  541. context_dictionaryToCreate[context_c] = true;
  542. }
  543. context_wc = context_w + context_c;
  544. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  545. context_w = context_wc;
  546. } else {
  547. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  548. if (context_w.charCodeAt(0) < 256) {
  549. for (i = 0; i < context_numBits; i++) {
  550. context_data_val = (context_data_val << 1);
  551. if (context_data_position === bitsPerChar - 1) {
  552. context_data_position = 0;
  553. context_data.push(getCharFromInt(context_data_val));
  554. context_data_val = 0;
  555. } else {
  556. context_data_position++;
  557. }
  558. }
  559. value = context_w.charCodeAt(0);
  560. for (i = 0; i < 8; i++) {
  561. context_data_val = (context_data_val << 1) | (value & 1);
  562. if (context_data_position === bitsPerChar - 1) {
  563. context_data_position = 0;
  564. context_data.push(getCharFromInt(context_data_val));
  565. context_data_val = 0;
  566. } else {
  567. context_data_position++;
  568. }
  569. value = value >> 1;
  570. }
  571. } else {
  572. value = 1;
  573. for (i = 0; i < context_numBits; i++) {
  574. context_data_val = (context_data_val << 1) | value;
  575. if (context_data_position === bitsPerChar - 1) {
  576. context_data_position = 0;
  577. context_data.push(getCharFromInt(context_data_val));
  578. context_data_val = 0;
  579. } else {
  580. context_data_position++;
  581. }
  582. value = 0;
  583. }
  584. value = context_w.charCodeAt(0);
  585. for (i = 0; i < 16; i++) {
  586. context_data_val = (context_data_val << 1) | (value & 1);
  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 = value >> 1;
  595. }
  596. }
  597. context_enlargeIn--;
  598. if (context_enlargeIn === 0) {
  599. context_enlargeIn = Math.pow(2, context_numBits);
  600. context_numBits++;
  601. }
  602. delete context_dictionaryToCreate[context_w];
  603. } else {
  604. value = context_dictionary[context_w];
  605. for (i = 0; i < context_numBits; i++) {
  606. context_data_val = (context_data_val << 1) | (value & 1);
  607. if (context_data_position === bitsPerChar - 1) {
  608. context_data_position = 0;
  609. context_data.push(getCharFromInt(context_data_val));
  610. context_data_val = 0;
  611. } else {
  612. context_data_position++;
  613. }
  614. value = value >> 1;
  615. }
  616. }
  617. context_enlargeIn--;
  618. if (context_enlargeIn === 0) {
  619. context_enlargeIn = Math.pow(2, context_numBits);
  620. context_numBits++;
  621. }
  622. // Add wc to the dictionary.
  623. context_dictionary[context_wc] = context_dictSize++;
  624. context_w = String(context_c);
  625. }
  626. }
  627. // Output the code for w.
  628. if (context_w !== "") {
  629. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  630. if (context_w.charCodeAt(0) < 256) {
  631. for (i = 0; i < context_numBits; i++) {
  632. context_data_val = (context_data_val << 1);
  633. if (context_data_position === bitsPerChar - 1) {
  634. context_data_position = 0;
  635. context_data.push(getCharFromInt(context_data_val));
  636. context_data_val = 0;
  637. } else {
  638. context_data_position++;
  639. }
  640. }
  641. value = context_w.charCodeAt(0);
  642. for (i = 0; i < 8; i++) {
  643. context_data_val = (context_data_val << 1) | (value & 1);
  644. if (context_data_position === bitsPerChar - 1) {
  645. context_data_position = 0;
  646. context_data.push(getCharFromInt(context_data_val));
  647. context_data_val = 0;
  648. } else {
  649. context_data_position++;
  650. }
  651. value = value >> 1;
  652. }
  653. } else {
  654. value = 1;
  655. for (i = 0; i < context_numBits; i++) {
  656. context_data_val = (context_data_val << 1) | value;
  657. if (context_data_position === bitsPerChar - 1) {
  658. context_data_position = 0;
  659. context_data.push(getCharFromInt(context_data_val));
  660. context_data_val = 0;
  661. } else {
  662. context_data_position++;
  663. }
  664. value = 0;
  665. }
  666. value = context_w.charCodeAt(0);
  667. for (i = 0; i < 16; i++) {
  668. context_data_val = (context_data_val << 1) | (value & 1);
  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 = value >> 1;
  677. }
  678. }
  679. context_enlargeIn--;
  680. if (context_enlargeIn === 0) {
  681. context_enlargeIn = Math.pow(2, context_numBits);
  682. context_numBits++;
  683. }
  684. delete context_dictionaryToCreate[context_w];
  685. } else {
  686. value = context_dictionary[context_w];
  687. for (i = 0; i < context_numBits; i++) {
  688. context_data_val = (context_data_val << 1) | (value & 1);
  689. if (context_data_position === bitsPerChar - 1) {
  690. context_data_position = 0;
  691. context_data.push(getCharFromInt(context_data_val));
  692. context_data_val = 0;
  693. } else {
  694. context_data_position++;
  695. }
  696. value = value >> 1;
  697. }
  698. }
  699. context_enlargeIn--;
  700. if (context_enlargeIn === 0) {
  701. context_enlargeIn = Math.pow(2, context_numBits);
  702. context_numBits++;
  703. }
  704. }
  705. // Mark the end of the stream
  706. value = 2;
  707. for (i = 0; i < context_numBits; i++) {
  708. context_data_val = (context_data_val << 1) | (value & 1);
  709. if (context_data_position === bitsPerChar - 1) {
  710. context_data_position = 0;
  711. context_data.push(getCharFromInt(context_data_val));
  712. context_data_val = 0;
  713. } else {
  714. context_data_position++;
  715. }
  716. value = value >> 1;
  717. }
  718. // Flush the last char
  719. while (true) {
  720. context_data_val = (context_data_val << 1);
  721. if (context_data_position === bitsPerChar - 1) {
  722. context_data.push(getCharFromInt(context_data_val));
  723. break;
  724. } else context_data_position++;
  725. }
  726. return context_data.join('');
  727. },
  728. decompress: function(compressed) {
  729. if (compressed === null) return "";
  730. if (compressed === "") return null;
  731. return LZString._decompress(compressed.length, 32768, function(index) {
  732. return compressed.charCodeAt(index);
  733. });
  734. },
  735. _decompress: function(length, resetValue, getNextValue) {
  736. var dictionary = [],
  737. next,
  738. enlargeIn = 4,
  739. dictSize = 4,
  740. numBits = 3,
  741. entry = "",
  742. result = [],
  743. i,
  744. w,
  745. bits, resb, maxpower, power,
  746. c,
  747. data = {
  748. val: getNextValue(0),
  749. position: resetValue,
  750. index: 1
  751. };
  752. for (i = 0; i < 3; i += 1) {
  753. dictionary[i] = i;
  754. }
  755. bits = 0;
  756. maxpower = Math.pow(2, 2);
  757. power = 1;
  758. while (power !== maxpower) {
  759. resb = data.val & data.position;
  760. data.position >>= 1;
  761. if (data.position === 0) {
  762. data.position = resetValue;
  763. data.val = getNextValue(data.index++);
  764. }
  765. bits |= (resb > 0 ? 1 : 0) * power;
  766. power <<= 1;
  767. }
  768. switch (next = bits) {
  769. case 0:
  770. bits = 0;
  771. maxpower = Math.pow(2, 8);
  772. power = 1;
  773. while (power !== maxpower) {
  774. resb = data.val & data.position;
  775. data.position >>= 1;
  776. if (data.position === 0) {
  777. data.position = resetValue;
  778. data.val = getNextValue(data.index++);
  779. }
  780. bits |= (resb > 0 ? 1 : 0) * power;
  781. power <<= 1;
  782. }
  783. c = f(bits);
  784. break;
  785. case 1:
  786. bits = 0;
  787. maxpower = Math.pow(2, 16);
  788. power = 1;
  789. while (power !== maxpower) {
  790. resb = data.val & data.position;
  791. data.position >>= 1;
  792. if (data.position === 0) {
  793. data.position = resetValue;
  794. data.val = getNextValue(data.index++);
  795. }
  796. bits |= (resb > 0 ? 1 : 0) * power;
  797. power <<= 1;
  798. }
  799. c = f(bits);
  800. break;
  801. case 2:
  802. return "";
  803. }
  804. dictionary[3] = c;
  805. w = c;
  806. result.push(c);
  807. while (true) {
  808. if (data.index > length) {
  809. return "";
  810. }
  811. bits = 0;
  812. maxpower = Math.pow(2, numBits);
  813. power = 1;
  814. while (power !== maxpower) {
  815. resb = data.val & data.position;
  816. data.position >>= 1;
  817. if (data.position === 0) {
  818. data.position = resetValue;
  819. data.val = getNextValue(data.index++);
  820. }
  821. bits |= (resb > 0 ? 1 : 0) * power;
  822. power <<= 1;
  823. }
  824. switch (c = bits) {
  825. case 0:
  826. bits = 0;
  827. maxpower = Math.pow(2, 8);
  828. power = 1;
  829. while (power !== maxpower) {
  830. resb = data.val & data.position;
  831. data.position >>= 1;
  832. if (data.position === 0) {
  833. data.position = resetValue;
  834. data.val = getNextValue(data.index++);
  835. }
  836. bits |= (resb > 0 ? 1 : 0) * power;
  837. power <<= 1;
  838. }
  839. dictionary[dictSize++] = f(bits);
  840. c = dictSize - 1;
  841. enlargeIn--;
  842. break;
  843. case 1:
  844. bits = 0;
  845. maxpower = Math.pow(2, 16);
  846. power = 1;
  847. while (power !== maxpower) {
  848. resb = data.val & data.position;
  849. data.position >>= 1;
  850. if (data.position === 0) {
  851. data.position = resetValue;
  852. data.val = getNextValue(data.index++);
  853. }
  854. bits |= (resb > 0 ? 1 : 0) * power;
  855. power <<= 1;
  856. }
  857. dictionary[dictSize++] = f(bits);
  858. c = dictSize - 1;
  859. enlargeIn--;
  860. break;
  861. case 2:
  862. return result.join('');
  863. }
  864. if (enlargeIn === 0) {
  865. enlargeIn = Math.pow(2, numBits);
  866. numBits++;
  867. }
  868. if (dictionary[c]) {
  869. entry = dictionary[c];
  870. } else {
  871. if (c === dictSize) {
  872. entry = w + w.charAt(0);
  873. } else {
  874. return null;
  875. }
  876. }
  877. result.push(entry);
  878. // Add w+entry[0] to the dictionary.
  879. dictionary[dictSize++] = w + entry.charAt(0);
  880. enlargeIn--;
  881. w = entry;
  882. if (enlargeIn === 0) {
  883. enlargeIn = Math.pow(2, numBits);
  884. numBits++;
  885. }
  886. }
  887. }
  888. };
  889. return LZString;
  890. })();
  891. }
  892. }