WME Utils - Google Link Enhancer

Adds some extra WME functionality related to Google place links.

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

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

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