WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

当前为 2018-04-10 提交的版本,查看 最新版本

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

  1. // ==UserScript==
  2. // @name WME Utils - Google Link Enhancer
  3. // @namespace WazeDev
  4. // @version 2018.04.10.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. _getSelectedFeatures(){
  291. if(!W.selectionManager.getSelectedFeatures)
  292. return W.selectionManager.selectedItems;
  293. return W.selectionManager.getSelectedFeatures();
  294. }
  295.  
  296. _formatLinkElements(a,b,c) {
  297. let existingLinks = this._getExistingLinks();
  298. $('#edit-panel').find(this.EXT_PROV_ELEM_QUERY).each((ix, childEl) => {
  299. let $childEl = $(childEl);
  300. let id = this._getIdFromElement($childEl);
  301. if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) {
  302. setTimeout(() => {
  303. $childEl.find('div.uuid').css({backgroundColor:'#FFA500'}).attr({'title':this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count)});
  304. }, 50);
  305. }
  306. this._addHoverEvent($(childEl));
  307.  
  308. let link = this._linkCache[id];
  309. if (link) {
  310. if (link.closed) {
  311. // A delay is needed to allow the UI to do its formatting so it doesn't overwrite ours.
  312. setTimeout(() => {
  313. $childEl.find('div.uuid').css({backgroundColor:'#FAA'}).attr('title',this.strings.closedPlace);
  314. }, 50);
  315. } else if (link.notFound) {
  316. setTimeout(() => {
  317. $childEl.find('div.uuid').css({backgroundColor:'#F0F'}).attr('title',this.strings.badLink);
  318. }, 50);
  319. } else {
  320. let venue = this._getSelectedFeatures()[0].model;
  321. if (this._isLinkTooFar(link, venue)) {
  322. setTimeout(() => {
  323. $childEl.find('div.uuid').css({backgroundColor:'#0FF'}).attr('title',this.strings.tooFar.replace('{0}',this.distanceLimit));
  324. }, 50);
  325. }
  326. }
  327. }
  328. });
  329. }
  330.  
  331. _getExistingLinks() {
  332. let existingLinks = {};
  333. let thisVenue;
  334. if (this._getSelectedFeatures().length) {
  335. thisVenue = this._getSelectedFeatures()[0].model;
  336. }
  337. W.model.venues.getObjectArray().forEach(venue => {
  338. let isThisVenue = venue === thisVenue;
  339. let thisPlaceIDs = [];
  340. venue.attributes.externalProviderIDs.forEach(provID => {
  341. let id = provID.attributes.uuid;
  342. if (thisPlaceIDs.indexOf(id) === -1) {
  343. thisPlaceIDs.push(id);
  344. let link = existingLinks[id];
  345. if (link) {
  346. link.count++;
  347. } else {
  348. link = {count: 1};
  349. existingLinks[id] = link;
  350. }
  351. link.isThisVenue = link.isThisVenue || isThisVenue;
  352. }
  353. });
  354. });
  355. return existingLinks;
  356. }
  357.  
  358. _onAjaxSuccess(event, jqXHR, ajaxOptions, data) {
  359. let url = ajaxOptions.url;
  360. let that = event.data;
  361.  
  362. if(/^\/maps\/api\/place\/autocomplete\/json?/i.test(url)) {
  363. // After an "autocomplete" api call...
  364.  
  365. // Get a list of already-linked id's
  366. let existingLinks = that._getExistingLinks();
  367.  
  368. // Examine the suggestions and format any that are already linked.
  369. $('#select2-drop ul li').each((idx, el) => {
  370. let $el = $(el);
  371. let linkData = $el.data('select2Data');
  372. if (linkData) {
  373. let link = existingLinks[linkData.id];
  374. if (link) {
  375. let title, bgColor, textColor, fontWeight;
  376. if (link.count > 1) {
  377. title = that.strings.multiLinked;
  378. textColor = '#000';
  379. bgColor = '#FFA500';
  380. } else {
  381. bgColor = '#ddd';
  382. if (link.isThisVenue) {
  383. title = that.strings.linkedToThisPlace;
  384. textColor = '#444';
  385. fontWeight = 600;
  386. } else {
  387. title = that.strings.linkedNearby;
  388. textColor = '#888';
  389. }
  390. }
  391. if (bgColor) $el.css({backgroundColor: bgColor});
  392. if (textColor) $el.css({color: textColor});
  393. if (fontWeight) $el.css({fontWeight: fontWeight});
  394. $el.attr('title',title);
  395. }
  396. $el.mouseover(function() {
  397. that._addPoint(linkData.id);
  398. }).mouseleave(() => that._destroyPoint()).bind('destroyed', () => that._destroyPoint()).mousedown(() => that._destroyPoint());
  399. }
  400. });
  401. } else if (/^\/maps\/api\/place\/details\/json?/i.test(url)) {
  402. //After a "details" api call...
  403.  
  404. // Cache location results. Note this isn't absolutely necessary because they're
  405. // cached when calling for them on mouseover. However, WME calls this api for each link
  406. // when the place is first loaded, so might as well take advantage of it.
  407.  
  408. let link = {};
  409. if (data.status === 'NOT_FOUND') {
  410. link.notFound = true;
  411. } else {
  412. link.loc = data.result.geometry.location;
  413. link.closed = data.result.permanently_closed;
  414. }
  415. var id = url.match(/placeid=(.*)&?/)[0];
  416. that._cacheLink(id, link);
  417. that._formatLinkElements();
  418. }
  419. }
  420.  
  421. // Remove the POI point from the map.
  422. _destroyPoint() {
  423. if (this._ptFeature) {
  424. this._ptFeature.destroy();
  425. this._ptFeature = null;
  426. this._lineFeature.destroy();
  427. this._lineFeature = null;
  428. }
  429. }
  430.  
  431. // Add the POI point to the map.
  432. _addPoint(id) {
  433. if (!id) return;
  434. let link = this._linkCache[id];
  435. if (link) {
  436. if (!link.notFound) {
  437. let coord = link.loc;
  438. let poiPt = new OL.Geometry.Point(coord.lng, coord.lat);
  439. poiPt.transform(W.map.displayProjection, W.map.projection);
  440. let placeGeom = this._getSelectedFeatures()[0].geometry.getCentroid();
  441. let placePt = new OL.Geometry.Point(placeGeom.x, placeGeom.y);
  442. let ext = W.map.getExtent();
  443. var lsBounds = new OL.Geometry.LineString([new OL.Geometry.Point(ext.left, ext.bottom), new OL.Geometry.Point(ext.left, ext.top),
  444. new OL.Geometry.Point(ext.right, ext.top),new OL.Geometry.Point(ext.right, ext.bottom),new OL.Geometry.Point(ext.left, ext.bottom)]);
  445. let lsLine = new OL.Geometry.LineString([placePt, poiPt]);
  446.  
  447. // If the line extends outside the bounds, split it so we don't draw a line across the world.
  448. let splits = lsLine.splitWith(lsBounds);
  449. let label = '';
  450. if (splits) {
  451. let splitPoints;
  452. splits.forEach(split => {
  453. split.components.forEach(component => {
  454. if (component.x === placePt.x && component.y === placePt.y) splitPoints = split;
  455. });
  456. });
  457. lsLine = new OL.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]);
  458. let distance = poiPt.distanceTo(placePt);
  459. let unitConversion, unit1, unit2;
  460. if (W.model.isImperial) {
  461. distance *= 3.28084;
  462. unitConversion = 5280;
  463. unit1 = ' ft';
  464. unit2 = ' mi';
  465. } else {
  466. unitConversion = 1000;
  467. unit1 = ' m';
  468. unit2 = ' km';
  469. }
  470. if (distance > unitConversion * 10) {
  471. label = Math.round(distance / unitConversion) + unit2;
  472. } else if (distance > 1000) {
  473. label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2;
  474. } else {
  475. label = Math.round(distance) + unit1;
  476. }
  477. }
  478.  
  479. this._destroyPoint(); // Just in case it still exists.
  480. this._ptFeature = new OL.Feature.Vector(poiPt,{poiCoord:true},{
  481. pointRadius: 6,
  482. strokeWidth: 30,
  483. strokeColor: '#FF0',
  484. fillColor: '#FF0',
  485. strokeOpacity: 0.5
  486. });
  487. this._lineFeature = new OL.Feature.Vector(lsLine, {}, {
  488. strokeWidth: 3,
  489. strokeDashstyle: '12 8',
  490. strokeColor: '#FF0',
  491. label: label,
  492. labelYOffset: 45,
  493. fontColor: '#FF0',
  494. fontWeight: 'bold',
  495. labelOutlineColor: "#000",
  496. labelOutlineWidth: 4,
  497. fontSize: '18'
  498. });
  499. W.map.getLayerByUniqueName('landmarks').addFeatures([this._ptFeature, this._lineFeature]);
  500. this._timeoutDestroyPoint();
  501. }
  502. } else {
  503. $.getJSON(this._urlOrigin + '/maps/api/place/details/json?&key=AIzaSyDf-q2MCay0AE7RF6oIMrDPrjBwxVtsUuI&placeid=' + id).then(json => {
  504. this._cacheLink(id, {loc:json.result.geometry.location,closed:json.result.permanently_closed});
  505. this._addPoint(id);
  506. });
  507. }
  508. }
  509.  
  510. // Destroy the point after some time, if it hasn't been destroyed already.
  511. _timeoutDestroyPoint() {
  512. if (this._timeoutID) clearTimeout(this._timeoutID);
  513. this._timeoutID = setTimeout(() => this._destroyPoint(), 4000);
  514. }
  515.  
  516. _getIdFromElement($el) {
  517. return $el.find('input.uuid').attr('value');
  518. }
  519.  
  520. _addHoverEvent($el) {
  521. $el.hover(() => this._addPoint(this._getIdFromElement($el)) , () => this._destroyPoint());
  522. }
  523.  
  524. _observeLinks() {
  525. this._linkObserver.observe($('#edit-panel')[0],{ childList: true, subtree: true });
  526. }
  527.  
  528. _initLZString() {
  529. // LZ Compressor
  530. // Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
  531. // This work is free. You can redistribute it and/or modify it
  532. // under the terms of the WTFPL, Version 2
  533. // LZ-based compression algorithm, version 1.4.4
  534. this._LZString = (function() {
  535. // private property
  536. var f = String.fromCharCode;
  537. var keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  538. var keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
  539. var baseReverseDic = {};
  540.  
  541. function getBaseValue(alphabet, character) {
  542. if (!baseReverseDic[alphabet]) {
  543. baseReverseDic[alphabet] = {};
  544. for (var i = 0; i < alphabet.length; i++) {
  545. baseReverseDic[alphabet][alphabet.charAt(i)] = i;
  546. }
  547. }
  548. return baseReverseDic[alphabet][character];
  549. }
  550. var LZString = {
  551. compressToBase64: function(input) {
  552. if (input === null) return "";
  553. var res = LZString._compress(input, 6, function(a) {
  554. return keyStrBase64.charAt(a);
  555. });
  556. switch (res.length % 4) { // To produce valid Base64
  557. default: // When could this happen ?
  558. case 0:
  559. return res;
  560. case 1:
  561. return res + "===";
  562. case 2:
  563. return res + "==";
  564. case 3:
  565. return res + "=";
  566. }
  567. },
  568. decompressFromBase64: function(input) {
  569. if (input === null) return "";
  570. if (input === "") return null;
  571. return LZString._decompress(input.length, 32, function(index) {
  572. return getBaseValue(keyStrBase64, input.charAt(index));
  573. });
  574. },
  575. compressToUTF16: function(input) {
  576. if (input === null) return "";
  577. return LZString._compress(input, 15, function(a) {
  578. return f(a + 32);
  579. }) + " ";
  580. },
  581. decompressFromUTF16: function(compressed) {
  582. if (compressed === null) return "";
  583. if (compressed === "") return null;
  584. return LZString._decompress(compressed.length, 16384, function(index) {
  585. return compressed.charCodeAt(index) - 32;
  586. });
  587. },
  588.  
  589. compress: function(uncompressed) {
  590. return LZString._compress(uncompressed, 16, function(a) {
  591. return f(a);
  592. });
  593. },
  594. _compress: function(uncompressed, bitsPerChar, getCharFromInt) {
  595. if (uncompressed === null) return "";
  596. var i, value,
  597. context_dictionary = {},
  598. context_dictionaryToCreate = {},
  599. context_c = "",
  600. context_wc = "",
  601. context_w = "",
  602. context_enlargeIn = 2, // Compensate for the first entry which should not count
  603. context_dictSize = 3,
  604. context_numBits = 2,
  605. context_data = [],
  606. context_data_val = 0,
  607. context_data_position = 0,
  608. ii;
  609. for (ii = 0; ii < uncompressed.length; ii += 1) {
  610. context_c = uncompressed.charAt(ii);
  611. if (!Object.prototype.hasOwnProperty.call(context_dictionary, context_c)) {
  612. context_dictionary[context_c] = context_dictSize++;
  613. context_dictionaryToCreate[context_c] = true;
  614. }
  615. context_wc = context_w + context_c;
  616. if (Object.prototype.hasOwnProperty.call(context_dictionary, context_wc)) {
  617. context_w = context_wc;
  618. } else {
  619. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  620. if (context_w.charCodeAt(0) < 256) {
  621. for (i = 0; i < context_numBits; i++) {
  622. context_data_val = (context_data_val << 1);
  623. if (context_data_position === bitsPerChar - 1) {
  624. context_data_position = 0;
  625. context_data.push(getCharFromInt(context_data_val));
  626. context_data_val = 0;
  627. } else {
  628. context_data_position++;
  629. }
  630. }
  631. value = context_w.charCodeAt(0);
  632. for (i = 0; i < 8; i++) {
  633. context_data_val = (context_data_val << 1) | (value & 1);
  634. if (context_data_position === bitsPerChar - 1) {
  635. context_data_position = 0;
  636. context_data.push(getCharFromInt(context_data_val));
  637. context_data_val = 0;
  638. } else {
  639. context_data_position++;
  640. }
  641. value = value >> 1;
  642. }
  643. } else {
  644. value = 1;
  645. for (i = 0; i < context_numBits; i++) {
  646. context_data_val = (context_data_val << 1) | value;
  647. if (context_data_position === bitsPerChar - 1) {
  648. context_data_position = 0;
  649. context_data.push(getCharFromInt(context_data_val));
  650. context_data_val = 0;
  651. } else {
  652. context_data_position++;
  653. }
  654. value = 0;
  655. }
  656. value = context_w.charCodeAt(0);
  657. for (i = 0; i < 16; i++) {
  658. context_data_val = (context_data_val << 1) | (value & 1);
  659. if (context_data_position === bitsPerChar - 1) {
  660. context_data_position = 0;
  661. context_data.push(getCharFromInt(context_data_val));
  662. context_data_val = 0;
  663. } else {
  664. context_data_position++;
  665. }
  666. value = value >> 1;
  667. }
  668. }
  669. context_enlargeIn--;
  670. if (context_enlargeIn === 0) {
  671. context_enlargeIn = Math.pow(2, context_numBits);
  672. context_numBits++;
  673. }
  674. delete context_dictionaryToCreate[context_w];
  675. } else {
  676. value = context_dictionary[context_w];
  677. for (i = 0; i < context_numBits; i++) {
  678. context_data_val = (context_data_val << 1) | (value & 1);
  679. if (context_data_position === bitsPerChar - 1) {
  680. context_data_position = 0;
  681. context_data.push(getCharFromInt(context_data_val));
  682. context_data_val = 0;
  683. } else {
  684. context_data_position++;
  685. }
  686. value = value >> 1;
  687. }
  688. }
  689. context_enlargeIn--;
  690. if (context_enlargeIn === 0) {
  691. context_enlargeIn = Math.pow(2, context_numBits);
  692. context_numBits++;
  693. }
  694. // Add wc to the dictionary.
  695. context_dictionary[context_wc] = context_dictSize++;
  696. context_w = String(context_c);
  697. }
  698. }
  699. // Output the code for w.
  700. if (context_w !== "") {
  701. if (Object.prototype.hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
  702. if (context_w.charCodeAt(0) < 256) {
  703. for (i = 0; i < context_numBits; i++) {
  704. context_data_val = (context_data_val << 1);
  705. if (context_data_position === bitsPerChar - 1) {
  706. context_data_position = 0;
  707. context_data.push(getCharFromInt(context_data_val));
  708. context_data_val = 0;
  709. } else {
  710. context_data_position++;
  711. }
  712. }
  713. value = context_w.charCodeAt(0);
  714. for (i = 0; i < 8; i++) {
  715. context_data_val = (context_data_val << 1) | (value & 1);
  716. if (context_data_position === bitsPerChar - 1) {
  717. context_data_position = 0;
  718. context_data.push(getCharFromInt(context_data_val));
  719. context_data_val = 0;
  720. } else {
  721. context_data_position++;
  722. }
  723. value = value >> 1;
  724. }
  725. } else {
  726. value = 1;
  727. for (i = 0; i < context_numBits; i++) {
  728. context_data_val = (context_data_val << 1) | value;
  729. if (context_data_position === bitsPerChar - 1) {
  730. context_data_position = 0;
  731. context_data.push(getCharFromInt(context_data_val));
  732. context_data_val = 0;
  733. } else {
  734. context_data_position++;
  735. }
  736. value = 0;
  737. }
  738. value = context_w.charCodeAt(0);
  739. for (i = 0; i < 16; i++) {
  740. context_data_val = (context_data_val << 1) | (value & 1);
  741. if (context_data_position === bitsPerChar - 1) {
  742. context_data_position = 0;
  743. context_data.push(getCharFromInt(context_data_val));
  744. context_data_val = 0;
  745. } else {
  746. context_data_position++;
  747. }
  748. value = value >> 1;
  749. }
  750. }
  751. context_enlargeIn--;
  752. if (context_enlargeIn === 0) {
  753. context_enlargeIn = Math.pow(2, context_numBits);
  754. context_numBits++;
  755. }
  756. delete context_dictionaryToCreate[context_w];
  757. } else {
  758. value = context_dictionary[context_w];
  759. for (i = 0; i < context_numBits; i++) {
  760. context_data_val = (context_data_val << 1) | (value & 1);
  761. if (context_data_position === bitsPerChar - 1) {
  762. context_data_position = 0;
  763. context_data.push(getCharFromInt(context_data_val));
  764. context_data_val = 0;
  765. } else {
  766. context_data_position++;
  767. }
  768. value = value >> 1;
  769. }
  770. }
  771. context_enlargeIn--;
  772. if (context_enlargeIn === 0) {
  773. context_enlargeIn = Math.pow(2, context_numBits);
  774. context_numBits++;
  775. }
  776. }
  777. // Mark the end of the stream
  778. value = 2;
  779. for (i = 0; i < context_numBits; i++) {
  780. context_data_val = (context_data_val << 1) | (value & 1);
  781. if (context_data_position === bitsPerChar - 1) {
  782. context_data_position = 0;
  783. context_data.push(getCharFromInt(context_data_val));
  784. context_data_val = 0;
  785. } else {
  786. context_data_position++;
  787. }
  788. value = value >> 1;
  789. }
  790. // Flush the last char
  791. while (true) {
  792. context_data_val = (context_data_val << 1);
  793. if (context_data_position === bitsPerChar - 1) {
  794. context_data.push(getCharFromInt(context_data_val));
  795. break;
  796. } else context_data_position++;
  797. }
  798. return context_data.join('');
  799. },
  800. decompress: function(compressed) {
  801. if (compressed === null) return "";
  802. if (compressed === "") return null;
  803. return LZString._decompress(compressed.length, 32768, function(index) {
  804. return compressed.charCodeAt(index);
  805. });
  806. },
  807. _decompress: function(length, resetValue, getNextValue) {
  808. var dictionary = [],
  809. next,
  810. enlargeIn = 4,
  811. dictSize = 4,
  812. numBits = 3,
  813. entry = "",
  814. result = [],
  815. i,
  816. w,
  817. bits, resb, maxpower, power,
  818. c,
  819. data = {
  820. val: getNextValue(0),
  821. position: resetValue,
  822. index: 1
  823. };
  824. for (i = 0; i < 3; i += 1) {
  825. dictionary[i] = i;
  826. }
  827. bits = 0;
  828. maxpower = Math.pow(2, 2);
  829. power = 1;
  830. while (power !== maxpower) {
  831. resb = data.val & data.position;
  832. data.position >>= 1;
  833. if (data.position === 0) {
  834. data.position = resetValue;
  835. data.val = getNextValue(data.index++);
  836. }
  837. bits |= (resb > 0 ? 1 : 0) * power;
  838. power <<= 1;
  839. }
  840. switch (next = bits) {
  841. case 0:
  842. bits = 0;
  843. maxpower = Math.pow(2, 8);
  844. power = 1;
  845. while (power !== maxpower) {
  846. resb = data.val & data.position;
  847. data.position >>= 1;
  848. if (data.position === 0) {
  849. data.position = resetValue;
  850. data.val = getNextValue(data.index++);
  851. }
  852. bits |= (resb > 0 ? 1 : 0) * power;
  853. power <<= 1;
  854. }
  855. c = f(bits);
  856. break;
  857. case 1:
  858. bits = 0;
  859. maxpower = Math.pow(2, 16);
  860. power = 1;
  861. while (power !== maxpower) {
  862. resb = data.val & data.position;
  863. data.position >>= 1;
  864. if (data.position === 0) {
  865. data.position = resetValue;
  866. data.val = getNextValue(data.index++);
  867. }
  868. bits |= (resb > 0 ? 1 : 0) * power;
  869. power <<= 1;
  870. }
  871. c = f(bits);
  872. break;
  873. case 2:
  874. return "";
  875. }
  876. dictionary[3] = c;
  877. w = c;
  878. result.push(c);
  879. while (true) {
  880. if (data.index > length) {
  881. return "";
  882. }
  883. bits = 0;
  884. maxpower = Math.pow(2, numBits);
  885. power = 1;
  886. while (power !== maxpower) {
  887. resb = data.val & data.position;
  888. data.position >>= 1;
  889. if (data.position === 0) {
  890. data.position = resetValue;
  891. data.val = getNextValue(data.index++);
  892. }
  893. bits |= (resb > 0 ? 1 : 0) * power;
  894. power <<= 1;
  895. }
  896. switch (c = bits) {
  897. case 0:
  898. bits = 0;
  899. maxpower = Math.pow(2, 8);
  900. power = 1;
  901. while (power !== maxpower) {
  902. resb = data.val & data.position;
  903. data.position >>= 1;
  904. if (data.position === 0) {
  905. data.position = resetValue;
  906. data.val = getNextValue(data.index++);
  907. }
  908. bits |= (resb > 0 ? 1 : 0) * power;
  909. power <<= 1;
  910. }
  911. dictionary[dictSize++] = f(bits);
  912. c = dictSize - 1;
  913. enlargeIn--;
  914. break;
  915. case 1:
  916. bits = 0;
  917. maxpower = Math.pow(2, 16);
  918. power = 1;
  919. while (power !== maxpower) {
  920. resb = data.val & data.position;
  921. data.position >>= 1;
  922. if (data.position === 0) {
  923. data.position = resetValue;
  924. data.val = getNextValue(data.index++);
  925. }
  926. bits |= (resb > 0 ? 1 : 0) * power;
  927. power <<= 1;
  928. }
  929. dictionary[dictSize++] = f(bits);
  930. c = dictSize - 1;
  931. enlargeIn--;
  932. break;
  933. case 2:
  934. return result.join('');
  935. }
  936. if (enlargeIn === 0) {
  937. enlargeIn = Math.pow(2, numBits);
  938. numBits++;
  939. }
  940. if (dictionary[c]) {
  941. entry = dictionary[c];
  942. } else {
  943. if (c === dictSize) {
  944. entry = w + w.charAt(0);
  945. } else {
  946. return null;
  947. }
  948. }
  949. result.push(entry);
  950. // Add w+entry[0] to the dictionary.
  951. dictionary[dictSize++] = w + entry.charAt(0);
  952. enlargeIn--;
  953. w = entry;
  954. if (enlargeIn === 0) {
  955. enlargeIn = Math.pow(2, numBits);
  956. numBits++;
  957. }
  958. }
  959. }
  960. };
  961. return LZString;
  962. })();
  963. }
  964. }