WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

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

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

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