mapshaper 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,108 +1,5 @@
1
1
  (function () {
2
2
 
3
- var utils = /*#__PURE__*/Object.freeze({
4
- __proto__: null,
5
- get addThousandsSep () { return addThousandsSep; },
6
- get addslashes () { return addslashes; },
7
- get arrayToIndex () { return arrayToIndex; },
8
- get clamp () { return clamp; },
9
- get cleanNumericString () { return cleanNumericString; },
10
- get contains () { return contains; },
11
- get copyElements () { return copyElements; },
12
- get countValues () { return countValues; },
13
- get createBuffer () { return createBuffer; },
14
- get default () { return utils; },
15
- get defaults () { return defaults; },
16
- get difference () { return difference; },
17
- get endsWith () { return endsWith; },
18
- get every () { return every; },
19
- get expandoBuffer () { return expandoBuffer; },
20
- get extend () { return extend; },
21
- get extendBuffer () { return extendBuffer; },
22
- get find () { return find; },
23
- get findMedian () { return findMedian; },
24
- get findQuantile () { return findQuantile; },
25
- get findRankByValue () { return findRankByValue; },
26
- get findStringPrefix () { return findStringPrefix; },
27
- get findValueByPct () { return findValueByPct; },
28
- get findValueByRank () { return findValueByRank; },
29
- get forEach () { return forEach; },
30
- get forEachProperty () { return forEachProperty; },
31
- get format () { return format; },
32
- get formatDateISO () { return formatDateISO; },
33
- get formatIntlNumber () { return formatIntlNumber; },
34
- get formatNumber () { return formatNumber; },
35
- get formatNumberForDisplay () { return formatNumberForDisplay; },
36
- get formatVersionedName () { return formatVersionedName; },
37
- get formatter () { return formatter; },
38
- get genericSort () { return genericSort; },
39
- get getArrayBounds () { return getArrayBounds; },
40
- get getGenericComparator () { return getGenericComparator; },
41
- get getKeyComparator () { return getKeyComparator; },
42
- get getSortedIds () { return getSortedIds; },
43
- get getUniqueName () { return getUniqueName; },
44
- get groupBy () { return groupBy; },
45
- get htmlEscape () { return htmlEscape; },
46
- get indexOf () { return indexOf; },
47
- get indexOn () { return indexOn; },
48
- get inherit () { return inherit; },
49
- get initializeArray () { return initializeArray; },
50
- get intersection () { return intersection; },
51
- get isArray () { return isArray; },
52
- get isArrayLike () { return isArrayLike; },
53
- get isBoolean () { return isBoolean; },
54
- get isDate () { return isDate; },
55
- get isEven () { return isEven; },
56
- get isFiniteNumber () { return isFiniteNumber; },
57
- get isFunction () { return isFunction; },
58
- get isInteger () { return isInteger; },
59
- get isNonNegNumber () { return isNonNegNumber; },
60
- get isNumber () { return isNumber; },
61
- get isObject () { return isObject; },
62
- get isOdd () { return isOdd; },
63
- get isPromise () { return isPromise; },
64
- get isString () { return isString; },
65
- get isValidNumber () { return isValidNumber; },
66
- get lpad () { return lpad; },
67
- get ltrim () { return ltrim; },
68
- get mean () { return mean; },
69
- get merge () { return merge; },
70
- get mergeNames () { return mergeNames; },
71
- get numToStr () { return numToStr; },
72
- get parseIntlNumber () { return parseIntlNumber; },
73
- get parseNumber () { return parseNumber; },
74
- get parsePercent () { return parsePercent; },
75
- get parseString () { return parseString; },
76
- get pickOne () { return pickOne; },
77
- get pluck () { return pluck; },
78
- get pluralSuffix () { return pluralSuffix; },
79
- get promisify () { return promisify; },
80
- get quicksort () { return quicksort; },
81
- get quicksortPartition () { return quicksortPartition; },
82
- get range () { return range; },
83
- get reduceAsync () { return reduceAsync; },
84
- get regexEscape () { return regexEscape; },
85
- get reorderArray () { return reorderArray; },
86
- get repeat () { return repeat; },
87
- get repeatString () { return repeatString; },
88
- get replaceArray () { return replaceArray; },
89
- get rpad () { return rpad; },
90
- get rtrim () { return rtrim; },
91
- get shuffle () { return shuffle; },
92
- get some () { return some; },
93
- get sortArrayIndex () { return sortArrayIndex; },
94
- get sortOn () { return sortOn; },
95
- get splitLines () { return splitLines; },
96
- get sum () { return sum; },
97
- get toArray () { return toArray; },
98
- get toBuffer () { return toBuffer; },
99
- get trim () { return trim; },
100
- get trimQuotes () { return trimQuotes; },
101
- get uniq () { return uniq; },
102
- get uniqifyNames () { return uniqifyNames; },
103
- get wildcardToRegExp () { return wildcardToRegExp; }
104
- });
105
-
106
3
  var api = window.mapshaper; // assuming mapshaper is in global scope
107
4
  var mapshaper = api,
108
5
  utils$1 = api.utils,
@@ -113,7 +10,7 @@
113
10
  UserError$1 = internal.UserError,
114
11
  message$1 = internal.message, // stop, error and message are overridden in gui-proxy.js
115
12
  stop$1 = internal.stop,
116
- error$1 = internal.error;
13
+ error$2 = internal.error;
117
14
 
118
15
  api.enableLogging();
119
16
 
@@ -232,7 +129,7 @@
232
129
  evt = new EventData(this.type);
233
130
  evt.target = this.target;
234
131
  } else if (evt.target != this.target || evt.type != this.type) {
235
- error$1("[Handler] event target/type have changed.");
132
+ error$2("[Handler] event target/type have changed.");
236
133
  }
237
134
  this.callback.call(this.listener, evt);
238
135
  };
@@ -481,10 +378,10 @@
481
378
  try {
482
379
  els = root.querySelectorAll(selector);
483
380
  } catch (e) {
484
- error$1("Invalid selector:", selector);
381
+ error$2("Invalid selector:", selector);
485
382
  }
486
383
  } else {
487
- error$1("This browser doesn't support CSS query selectors");
384
+ error$2("This browser doesn't support CSS query selectors");
488
385
  }
489
386
  return utils$1.toArray(els);
490
387
  };
@@ -522,7 +419,7 @@
522
419
  };
523
420
 
524
421
  function El(ref) {
525
- if (!ref) error$1("Element() needs a reference");
422
+ if (!ref) error$2("Element() needs a reference");
526
423
  if (ref instanceof El) {
527
424
  return ref;
528
425
  }
@@ -543,7 +440,7 @@
543
440
  } else if (ref.tagName) {
544
441
  node = ref;
545
442
  }
546
- if (!node) error$1("Unmatched element selector:", ref);
443
+ if (!node) error$2("Unmatched element selector:", ref);
547
444
  this.el = node;
548
445
  }
549
446
 
@@ -1087,7 +984,7 @@
1087
984
  }
1088
985
  var val = utils$1.clamp(arg, _min, _max);
1089
986
  if (!_validator(val)) {
1090
- error$1("ClickText#value() invalid value:", arg);
987
+ error$2("ClickText#value() invalid value:", arg);
1091
988
  } else {
1092
989
  _value = val;
1093
990
  }
@@ -1418,6 +1315,7 @@
1418
1315
  //
1419
1316
  var options = getSaveFileOptions(filename);
1420
1317
  var handle;
1318
+ done = done || function() {};
1421
1319
  try {
1422
1320
  handle = await window.showSaveFilePicker(options);
1423
1321
  var writable = await handle.createWritable();
@@ -1519,13 +1417,36 @@
1519
1417
  // https://github.com/jakearchibald/idb-keyval
1520
1418
  var sessionId = getUniqId('session');
1521
1419
  var snapshotCount = 0;
1420
+ // IDs of snapshots created (and not removed) by this tab. Tracked in memory
1421
+ // so the pagehide handler can fire a single batched delMany() without first
1422
+ // awaiting idb.keys() -- the page may not survive the round trip.
1423
+ var ownSnapshotIds = new Set();
1424
+
1425
+ // Lifecycle constants for snapshot cleanup.
1426
+ // HEARTBEAT_INTERVAL_MS: how often this tab refreshes its localStorage entry.
1427
+ // STALE_THRESHOLD_MS: a session whose heartbeat is older than this is treated
1428
+ // as dead. Generous enough to tolerate backgrounded/throttled tabs.
1429
+ // BROADCAST_DISCOVERY_MS: how long startup waits for live tabs to identify
1430
+ // themselves over BroadcastChannel before deciding what to delete.
1431
+ var HEARTBEAT_INTERVAL_MS = 30 * 1000;
1432
+ var STALE_THRESHOLD_MS = 5 * 60 * 1000;
1433
+ var BROADCAST_DISCOVERY_MS = 200;
1434
+ var SESSION_DATA_KEY = 'session_data';
1435
+ var BROADCAST_CHANNEL_NAME = 'mapshaper-snapshots';
1522
1436
 
1523
1437
  function getUniqId(prefix) {
1524
1438
  return prefix + '_' + (Math.random() + 1).toString(36).substring(2,8);
1525
1439
  }
1526
1440
 
1441
+ function getSessionFromSnapshotId(snapshotId) {
1442
+ // Snapshot ids look like 'session_<6chars>_<NNN>'. The session id is the
1443
+ // 'session_<6chars>' prefix.
1444
+ var m = /^(session_[a-z0-9]+)_\d+$/.exec(snapshotId);
1445
+ return m ? m[1] : null;
1446
+ }
1447
+
1527
1448
  function isSnapshotId(str) {
1528
- return /^session_/.test(str);
1449
+ return getSessionFromSnapshotId(str) !== null;
1529
1450
  }
1530
1451
 
1531
1452
  function SessionSnapshots(gui) {
@@ -1543,13 +1464,20 @@
1543
1464
  return;
1544
1465
  }
1545
1466
  menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
1467
+ startLifecycle();
1546
1468
  await initialCleanup();
1547
1469
 
1548
- window.addEventListener('beforeunload', async function() {
1549
- // delete snapshot data
1550
- // This is not ideal, because the data gets deleted even if the user
1551
- // cancels the page close... but there's no apparent good alternative
1552
- await finalCleanup();
1470
+ // 'pagehide' is more reliable than 'unload' across modern browsers
1471
+ // (Chrome's bfcache rules increasingly suppress 'unload'). Sync work
1472
+ // (localStorage write, BroadcastChannel notice) always completes; the
1473
+ // best-effort delMany() below frequently completes in Chrome/Firefox and
1474
+ // sometimes in Safari, but is not relied upon -- the next session's
1475
+ // startup cleanup is the safety net.
1476
+ window.addEventListener('pagehide', function(e) {
1477
+ if (e.persisted) return; // bfcache: tab may come back, leave entry alive
1478
+ removeOwnSession();
1479
+ announceLeaving();
1480
+ attemptOwnDataDeletion();
1553
1481
  });
1554
1482
 
1555
1483
  btn.on('mouseenter', function() {
@@ -1573,12 +1501,6 @@
1573
1501
  var snapshots = await fetchSnapshotList();
1574
1502
 
1575
1503
  menu.empty();
1576
- addMenuLink({
1577
- slug: 'stash',
1578
- // label: 'save data snapshot',
1579
- label: 'create a snapshot',
1580
- action: saveSnapshot
1581
- });
1582
1504
 
1583
1505
  if (!gui.session.isEmpty()) {
1584
1506
  // Surface the console "history" command via the snapshot menu so users
@@ -1593,6 +1515,15 @@
1593
1515
  });
1594
1516
  }
1595
1517
 
1518
+ addMenuLink({
1519
+ slug: 'stash',
1520
+ // label: 'save data snapshot',
1521
+ label: 'create a snapshot',
1522
+ action: saveSnapshot
1523
+ });
1524
+
1525
+
1526
+
1596
1527
  // var available = await getAvailableStorage();
1597
1528
  // if (available) {
1598
1529
  // El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu);
@@ -1676,6 +1607,7 @@
1676
1607
  };
1677
1608
 
1678
1609
  await idb.set(entry.id, obj);
1610
+ ownSnapshotIds.add(entry.id);
1679
1611
  await addToIndex(entry);
1680
1612
  renderMenu();
1681
1613
  }
@@ -1690,7 +1622,7 @@
1690
1622
  }
1691
1623
 
1692
1624
  async function fetchSnapshotList() {
1693
- await removeMissingSnapshots();
1625
+ await pruneIndexAgainstKeys();
1694
1626
  var index = await fetchIndex();
1695
1627
  var snapshots = index.snapshots;
1696
1628
  snapshots = snapshots.filter(function(o) {return o.session == sessionId;});
@@ -1699,6 +1631,7 @@
1699
1631
 
1700
1632
  async function removeSnapshotById(id, gui) {
1701
1633
  await idb.del(id);
1634
+ ownSnapshotIds.delete(id);
1702
1635
  return updateIndex(function(index) {
1703
1636
  index.snapshots = index.snapshots.filter(function(snap) {
1704
1637
  return snap.id != id;
@@ -1796,13 +1729,16 @@
1796
1729
  }
1797
1730
 
1798
1731
  async function addToIndex(obj) {
1799
- updateSessionData();
1732
+ touchOwnSession();
1800
1733
  return updateIndex(function(index) {
1801
1734
  index.snapshots.push(obj);
1802
1735
  });
1803
1736
  }
1804
1737
 
1805
- async function removeMissingSnapshots() {
1738
+ // Drop index entries whose underlying IndexedDB blob has gone missing
1739
+ // (e.g. cleared by another tab). Cheaper than reclaimDeadSessionData; used
1740
+ // before rendering the menu to keep stale entries out of the UI.
1741
+ async function pruneIndexAgainstKeys() {
1806
1742
  var keys = await idb.keys();
1807
1743
  return updateIndex(function(index) {
1808
1744
  index.snapshots = index.snapshots.filter(function(snap) {
@@ -1811,32 +1747,65 @@
1811
1747
  });
1812
1748
  }
1813
1749
 
1750
+ // Run on every fresh page load. Aggressively reclaim space by deleting any
1751
+ // snapshot whose owning session is no longer alive.
1814
1752
  async function initialCleanup() {
1815
- // (Safari workaround) remove any lingering data from past sessions
1816
- if (getSessionData().length === 0) {
1817
- await idb.clear();
1818
- }
1819
- // remove any snapshots that are not indexed
1753
+ touchOwnSession();
1754
+ pruneStaleSessionData();
1755
+ var liveSessions = await discoverLiveSessions();
1756
+ await reclaimDeadSessionData(liveSessions);
1757
+ }
1758
+
1759
+ // Delete every snapshot in IndexedDB whose session id is not in liveSessions,
1760
+ // and keep the on-disk index consistent with the actual key set.
1761
+ async function reclaimDeadSessionData(liveSessions) {
1820
1762
  var keys = await idb.keys();
1821
- var indexedIds = (await fetchIndex()).snapshots.map(function(snap) {return snap.id;});
1763
+ var doomedKeys = [];
1764
+ var doomedSessions = new Set();
1822
1765
  keys.forEach(function(key) {
1823
- if (isSnapshotId(key) && !indexedIds.includes(key)) {
1824
- idb.del(key);
1766
+ var sid = getSessionFromSnapshotId(key);
1767
+ if (sid && !liveSessions.has(sid)) {
1768
+ doomedKeys.push(key);
1769
+ doomedSessions.add(sid);
1825
1770
  }
1826
1771
  });
1827
- // remove old indexed snapshots
1772
+
1773
+ // Sum sizes from the index for an informative log message. We only know
1774
+ // the size of snapshots that still have an index entry; orphaned blobs
1775
+ // are counted but their sizes contribute 0.
1776
+ var sizeBytes = 0;
1777
+ if (doomedKeys.length) {
1778
+ var index = await fetchIndex();
1779
+ var doomedKeySet = new Set(doomedKeys);
1780
+ index.snapshots.forEach(function(snap) {
1781
+ if (doomedKeySet.has(snap.id) && typeof snap.size === 'number') {
1782
+ sizeBytes += snap.size;
1783
+ }
1784
+ });
1785
+ await Promise.all(doomedKeys.map(function(k) { return idb.del(k); }));
1786
+ }
1787
+
1788
+ // Drop index entries pointing to deleted snapshots, and any entries whose
1789
+ // session is dead even if the underlying key was already gone.
1790
+ var remainingKeys = await idb.keys();
1791
+ var keySet = new Set(remainingKeys);
1828
1792
  await updateIndex(function(index) {
1829
1793
  index.snapshots = index.snapshots.filter(function(snap) {
1830
- var msPerDay = 1000 * 60 * 60 * 24;
1831
- var daysOld = (Date.now() - snap.created) / msPerDay;
1832
- if (daysOld > 1) {
1833
- if (keys.includes(snap.id)) idb.del(snap.id);
1834
- return false;
1835
- }
1836
- return true;
1794
+ if (!keySet.has(snap.id)) return false;
1795
+ var sid = getSessionFromSnapshotId(snap.id);
1796
+ return sid && liveSessions.has(sid);
1837
1797
  });
1838
- return index;
1839
1798
  });
1799
+
1800
+ if (doomedKeys.length) {
1801
+ var msg = '[mapshaper] startup cleanup reclaimed ' +
1802
+ doomedKeys.length + ' snapshot' + (doomedKeys.length === 1 ? '' : 's') +
1803
+ ' from ' + doomedSessions.size + ' stale session' +
1804
+ (doomedSessions.size === 1 ? '' : 's');
1805
+ var sizeStr = sizeBytes > 0 ? formatSize(sizeBytes) : '';
1806
+ if (sizeStr) msg += ' (' + sizeStr + ')';
1807
+ console.log(msg);
1808
+ }
1840
1809
  }
1841
1810
 
1842
1811
  async function getAvailableStorage() {
@@ -1874,52 +1843,164 @@
1874
1843
  return target;
1875
1844
  }
1876
1845
 
1877
- // Clean up snapshot data (called just before browser tab is closed)
1878
- async function finalCleanup() {
1879
- // When called on 'beforeunload', idb.clear() seems to complete
1880
- // before tab is unloaded in Chrome and Firefox, but not in Safari.
1881
- // Calling idb.del(key) to selectively delete data for the current session
1882
- // does not seem to complete in any browser.
1883
- // So we wait until the last open session is ending at this URL, and delete
1884
- // data for all recently open sessions.
1885
- //
1886
- var sessions = getSessionData().filter(function(item) {
1887
- // remove current session
1888
- var daysOld = (Date.now() - item.timestamp) / (1000 * 60 * 60 * 24);
1889
- if (item.session == sessionId) return false;
1890
- // also remove any lingering old sessions (ordinarily this shouldn't be needed)
1891
- if (daysOld > 1) return false;
1892
- return true;
1846
+ // Heartbeat + BroadcastChannel state. The localStorage map and the channel
1847
+ // together let other tabs identify themselves on demand and let crashed tabs'
1848
+ // data be reclaimed safely.
1849
+ //
1850
+ // localStorage 'session_data' shape: { <sessionId>: <lastSeenMs>, ... }
1851
+ // (The previous build stored an array; old data is overwritten on first
1852
+ // touchOwnSession() call. Only used for cleanup heuristics, not user data.)
1853
+
1854
+ var _heartbeatTimer = null;
1855
+ var _channel = null;
1856
+
1857
+ function startLifecycle() {
1858
+ touchOwnSession();
1859
+ _heartbeatTimer = setInterval(touchOwnSession, HEARTBEAT_INTERVAL_MS);
1860
+ if (typeof BroadcastChannel == 'function') {
1861
+ try {
1862
+ _channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
1863
+ _channel.onmessage = function(e) {
1864
+ var msg = e.data;
1865
+ if (!msg || msg.from === sessionId) return;
1866
+ if (msg.type === 'whois') {
1867
+ try {
1868
+ _channel.postMessage({type: 'iam', from: sessionId});
1869
+ } catch (err) {} // channel can throw if tab is being torn down
1870
+ }
1871
+ };
1872
+ } catch (err) {
1873
+ _channel = null; // sandboxed contexts may forbid BroadcastChannel
1874
+ }
1875
+ }
1876
+ }
1877
+
1878
+ // Discover other live tabs. Returns a Set of session ids that should be
1879
+ // considered alive (always includes our own).
1880
+ function discoverLiveSessions() {
1881
+ return new Promise(function(resolve) {
1882
+ var live = new Set([sessionId]);
1883
+ // localStorage heartbeat data is the durable signal; it survives
1884
+ // backgrounded/throttled tabs that may not respond to BroadcastChannel
1885
+ // promptly.
1886
+ var data = readSessionData();
1887
+ Object.keys(data).forEach(function(sid) {
1888
+ if (Date.now() - data[sid] < STALE_THRESHOLD_MS) {
1889
+ live.add(sid);
1890
+ }
1891
+ });
1892
+ if (!_channel) {
1893
+ resolve(live);
1894
+ return;
1895
+ }
1896
+ // Broadcast 'whois' and add any tab that responds within the discovery
1897
+ // window. This is fast and authoritative for foreground tabs.
1898
+ var listener = function(e) {
1899
+ var msg = e.data;
1900
+ if (msg && msg.type === 'iam' && msg.from && msg.from !== sessionId) {
1901
+ live.add(msg.from);
1902
+ }
1903
+ };
1904
+ _channel.addEventListener('message', listener);
1905
+ try {
1906
+ _channel.postMessage({type: 'whois', from: sessionId});
1907
+ } catch (err) {}
1908
+ setTimeout(function() {
1909
+ _channel.removeEventListener('message', listener);
1910
+ resolve(live);
1911
+ }, BROADCAST_DISCOVERY_MS);
1893
1912
  });
1894
- setSessionData(sessions);
1895
- if (sessions.length === 0) {
1896
- await idb.clear();
1913
+ }
1914
+
1915
+ function announceLeaving() {
1916
+ if (!_channel) return;
1917
+ try {
1918
+ _channel.postMessage({type: 'leaving', from: sessionId});
1919
+ _channel.close();
1920
+ } catch (err) {}
1921
+ }
1922
+
1923
+ // Best-effort eager cleanup at end-of-session. Snapshots can be tens or
1924
+ // hundreds of MB, so we don't want to rely solely on the next session's
1925
+ // startup to reclaim space. A single delMany() transaction is cheap to
1926
+ // launch and frequently commits before the page is fully torn down,
1927
+ // especially in Chrome/Firefox; Safari is less reliable. Anything that
1928
+ // doesn't complete here will be reclaimed by the next session anyway.
1929
+ //
1930
+ // Important: do NOT await any of this. The browser doesn't await async work
1931
+ // in the unload path; we just want the IDB transaction to be queued before
1932
+ // the page is killed.
1933
+ function attemptOwnDataDeletion() {
1934
+ if (ownSnapshotIds.size === 0) return;
1935
+ var ids = Array.from(ownSnapshotIds);
1936
+ ownSnapshotIds.clear();
1937
+ // Delete the blobs in a single transaction.
1938
+ try {
1939
+ idb.delMany(ids).catch(function() {});
1940
+ } catch (err) {}
1941
+ // Drop our entries from the index in a separate (also fire-and-forget)
1942
+ // transaction. If only one of the two completes, startup cleanup will
1943
+ // reconcile -- reclaimDeadSessionData() handles both "key gone, index
1944
+ // entry remains" and "index entry gone, key remains".
1945
+ try {
1946
+ updateIndex(function(index) {
1947
+ index.snapshots = index.snapshots.filter(function(snap) {
1948
+ return getSessionFromSnapshotId(snap.id) !== sessionId;
1949
+ });
1950
+ }).catch(function() {});
1951
+ } catch (err) {}
1952
+ }
1953
+
1954
+ function touchOwnSession() {
1955
+ var data = readSessionData();
1956
+ data[sessionId] = Date.now();
1957
+ writeSessionData(data);
1958
+ }
1959
+
1960
+ function removeOwnSession() {
1961
+ var data = readSessionData();
1962
+ if (sessionId in data) {
1963
+ delete data[sessionId];
1964
+ writeSessionData(data);
1897
1965
  }
1898
1966
  }
1899
1967
 
1900
- function updateSessionData() {
1901
- // make sure the current session is added to the list of open sessions
1902
- var sessions = getSessionData();
1903
- if (sessions.find(o => o.session == sessionId)) return;
1904
- var entry = {
1905
- session: sessionId,
1906
- timestamp: Date.now()
1907
- };
1908
- setSessionData(sessions.concat([entry]));
1968
+ function pruneStaleSessionData() {
1969
+ var data = readSessionData();
1970
+ var now = Date.now();
1971
+ var changed = false;
1972
+ Object.keys(data).forEach(function(sid) {
1973
+ if (now - data[sid] > STALE_THRESHOLD_MS) {
1974
+ delete data[sid];
1975
+ changed = true;
1976
+ }
1977
+ });
1978
+ if (changed) writeSessionData(data);
1909
1979
  }
1910
1980
 
1911
- function getSessionData() {
1912
- var data = JSON.parse(window.localStorage.getItem('session_data'));
1913
- return data || [];
1981
+ function readSessionData() {
1982
+ try {
1983
+ var raw = window.localStorage.getItem(SESSION_DATA_KEY);
1984
+ var parsed = raw ? JSON.parse(raw) : null;
1985
+ // Tolerate the legacy array shape by ignoring it (next write replaces it).
1986
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1987
+ return parsed;
1988
+ }
1989
+ return {};
1990
+ } catch (e) {
1991
+ return {};
1992
+ }
1914
1993
  }
1915
1994
 
1916
- function setSessionData(arr) {
1917
- window.localStorage.setItem('session_data', JSON.stringify(arr));
1995
+ function writeSessionData(obj) {
1996
+ try {
1997
+ window.localStorage.setItem(SESSION_DATA_KEY, JSON.stringify(obj));
1998
+ } catch (e) {} // localStorage can throw on quota exceeded; non-fatal
1918
1999
  }
1919
2000
 
1920
2001
  async function isStorageEnabled() {
1921
2002
  try {
1922
- setSessionData(getSessionData());
2003
+ writeSessionData(readSessionData());
1923
2004
  await updateIndex(function() {});
1924
2005
  return true;
1925
2006
  } catch(e) {
@@ -2399,10 +2480,34 @@
2399
2480
  } else if (await importDataset(group, groupImportOpts)) {
2400
2481
  importCount++;
2401
2482
  gui.session.fileImported(group.filename, optStr);
2483
+ notifyMissingShapefileParts(group);
2402
2484
  }
2403
2485
  }
2404
2486
  }
2405
2487
 
2488
+ // Surface a passive warning if a .shp file came in without its .dbf or .prj
2489
+ // sibling. Both files are technically optional, but their absence has very
2490
+ // different consequences (no attribute data, no projection metadata) and
2491
+ // users frequently load just the .shp by accident. Mapshaper generally
2492
+ // doesn't need .shx, so we don't warn about that.
2493
+ function notifyMissingShapefileParts(group) {
2494
+ if (!group.shp || !gui.notify) return;
2495
+ var missing = [];
2496
+ if (!group.dbf) missing.push({ext: '.dbf', why: 'no attribute data'});
2497
+ if (!group.prj) missing.push({ext: '.prj', why: 'no projection metadata'});
2498
+ if (!missing.length) return;
2499
+ var base = internal.getFileBase(group.shp.filename);
2500
+ var parts = missing.map(function(m) { return m.ext; }).join(' and ');
2501
+ var consequences = missing.map(function(m) { return m.why; }).join(', ');
2502
+ gui.notify({
2503
+ severity: 'warn',
2504
+ body: base + '.shp was loaded without its ' + parts +
2505
+ (missing.length > 1 ? ' files' : ' file') +
2506
+ ' (' + consequences + ').',
2507
+ dedupKey: 'shp-missing:' + base
2508
+ });
2509
+ }
2510
+
2406
2511
  async function importDataset(group, importOpts) {
2407
2512
  var dataset;
2408
2513
  var datasets;
@@ -2943,15 +3048,25 @@
2943
3048
 
2944
3049
  function message() {
2945
3050
  var msg = GUI.formatMessageArgs(arguments);
2946
- gui.message(msg);
2947
- internal.logArgs(arguments);
3051
+ if (gui.notify) {
3052
+ gui.notify({severity: 'info', body: msg});
3053
+ } else {
3054
+ // Fallback for early messages before MessageControl is constructed
3055
+ gui.message(msg);
3056
+ internal.logArgs(arguments);
3057
+ }
2948
3058
  }
2949
3059
 
2950
- // GUI warning uses the alert popup, which replaces previous popup
2951
- // (unlike message) -- this allows for catching and handling errors
2952
- // by replacing the error popup with a warning.
3060
+ // CLI warnings used to surface as modal alerts, which interrupt the user
3061
+ // and replace any previous popup. They now go to the Messages inbox so
3062
+ // they can accumulate non-disruptively.
2953
3063
  function warn() {
2954
- gui.alert(GUI.formatMessageArgs(arguments));
3064
+ var msg = GUI.formatMessageArgs(arguments);
3065
+ if (gui.notify) {
3066
+ gui.notify({severity: 'warn', body: msg, dedupKey: 'warn:' + msg});
3067
+ } else {
3068
+ gui.alert(msg);
3069
+ }
2955
3070
  }
2956
3071
 
2957
3072
  internal.setLoggingFunctions(message, error, stop, warn);
@@ -2970,7 +3085,11 @@
2970
3085
  try {
2971
3086
  await utils$1.promisify(saveFilesToServer)(paths, data);
2972
3087
  if (files.length >= 1) {
2973
- gui.alert('<b>Saved</b><br>' + paths.join('<br>'));
3088
+ if (gui.notify) {
3089
+ gui.notify({severity: 'info', title: 'Saved', body: paths.join('\n')});
3090
+ } else {
3091
+ gui.alert('<b>Saved</b><br>' + paths.join('<br>'));
3092
+ }
2974
3093
  }
2975
3094
  } catch(err) {
2976
3095
  msg = "<b>Direct save failed</b><br>Reason: " + err.message + ".";
@@ -3811,7 +3930,7 @@
3811
3930
  slider.on('change', function(e) {
3812
3931
  var pct = fromSliderPct(e.pct);
3813
3932
  text.value(pct);
3814
- pct = utils$1.parsePercent(text.text()); // use rounded value (for consistency w/ cli)
3933
+ pct = internal.parsePercent(text.text()); // use rounded value (for consistency w/ cli)
3815
3934
  onChange(pct);
3816
3935
  });
3817
3936
  slider.on('start', function(e) {
@@ -4058,11 +4177,16 @@
4058
4177
  if (!_isOpen) {
4059
4178
  btn.addClass('active');
4060
4179
  _isOpen = true;
4061
- // use console for messages while open
4180
+ // Route logging output to the in-app console while it's open, so a
4181
+ // user typing CLI commands here sees the results inline -- the same
4182
+ // way a CLI user sees output in their terminal. warn() goes to a
4183
+ // distinct consoleWarn() rather than to the Messages inbox: warnings
4184
+ // emitted by command pipelines should land where the user typed,
4185
+ // not in a separate UI surface they might not notice.
4062
4186
  // TODO: find a solution for logging problem when switching between multiple
4063
4187
  // gui instances with the console open. E.g. console could close
4064
4188
  // when an instance loses focus.
4065
- internal.setLoggingFunctions(consoleMessage, consoleError, consoleStop);
4189
+ internal.setLoggingFunctions(consoleMessage, consoleError, consoleStop, consoleWarn);
4066
4190
  gui.container.addClass('console-open');
4067
4191
  el.show();
4068
4192
  gui.dispatchEvent('resize');
@@ -4460,6 +4584,15 @@
4460
4584
  toLog(msg, 'console-error');
4461
4585
  }
4462
4586
 
4587
+ // Routed from internal.warn() while the console is open. Distinct from
4588
+ // consoleWarning() (which is used for hard-stop errors) so the styling
4589
+ // can differ -- warnings shouldn't read as red "something went wrong",
4590
+ // they're just heads-ups about how the input was interpreted.
4591
+ function consoleWarn() {
4592
+ var msg = GUI.formatMessageArgs(arguments);
4593
+ toLog(msg, 'console-warn');
4594
+ }
4595
+
4463
4596
  function consoleMessage() {
4464
4597
  var msg = GUI.formatMessageArgs(arguments);
4465
4598
  if (internal.loggingEnabled()) {
@@ -4710,7 +4843,7 @@
4710
4843
  });
4711
4844
 
4712
4845
  internal.writeFiles = function() {
4713
- error$1(unsupportedMsg);
4846
+ error$2(unsupportedMsg);
4714
4847
  };
4715
4848
  return;
4716
4849
  }
@@ -6825,1356 +6958,1386 @@
6825
6958
  // Fall back to browserify's Buffer polyfill
6826
6959
  var B = typeof Buffer != 'undefined' ? Buffer : require$1('buffer').Buffer;
6827
6960
 
6828
- // This module provides a way for multiple jobs to run together asynchronously
6829
- // while keeping job-level context variables (like "defs") separate.
6830
-
6831
- var stash = {};
6832
-
6833
- function stashVar(key, val) {
6834
- if (key in stash) {
6835
- error('Tried to replace a stashed variable:', key);
6836
- }
6837
- stash[key] = val;
6961
+ // We do NOT import from mapshaper-logging here to avoid a circular dependency
6962
+ function error$1() {
6963
+ throw new Error(Array.prototype.slice.call(arguments).join(' '));
6838
6964
  }
6839
6965
 
6840
- function getStashedVar(key) {
6841
- if (key in stash === false) {
6842
- return undefined; // to support running commands in tests
6843
- // error('Tried to read a nonexistent variable from the stash:', key);
6844
- }
6845
- return stash[key];
6966
+ var uniqCount = 0;
6967
+ function getUniqueName(prefix) {
6968
+ return (prefix || "__id_") + (++uniqCount);
6846
6969
  }
6847
6970
 
6848
- function clearStash() {
6849
- stash = {};
6971
+ function isFunction(obj) {
6972
+ return typeof obj == 'function';
6850
6973
  }
6851
6974
 
6852
- var LOGGING = false;
6853
- var STDOUT = false; // use stdout for status messages
6854
- var _error, _stop, _message, _warn;
6975
+ function isPromise(arg) {
6976
+ return arg ? isFunction(arg.then) : false;
6977
+ }
6855
6978
 
6856
- var _interrupt = function() {
6857
- throw new NonFatalError(formatLogArgs(arguments));
6858
- };
6979
+ function isObject(obj) {
6980
+ return obj === Object(obj); // via underscore
6981
+ }
6859
6982
 
6860
- var onceMessages = [];
6983
+ function clamp(val, min, max) {
6984
+ return val < min ? min : (val > max ? max : val);
6985
+ }
6861
6986
 
6862
- setLoggingForCLI();
6987
+ function isArray(obj) {
6988
+ return Array.isArray(obj);
6989
+ }
6863
6990
 
6864
- function getLoggingSetter() {
6865
- var e = _error, s = _stop, m = _message, w = _warn;
6866
- return function() {
6867
- setLoggingFunctions(m, e, s, w);
6868
- };
6991
+ // Is obj a valid number or NaN? (test if obj is type number)
6992
+ function isNumber(obj) {
6993
+ return obj != null && obj.constructor == Number;
6869
6994
  }
6870
6995
 
6871
- function setLoggingForCLI() {
6872
- function stop() {
6873
- throw new UserError(formatLogArgs(arguments));
6874
- }
6996
+ function isValidNumber(val) {
6997
+ return isNumber(val) && !isNaN(val);
6998
+ }
6875
6999
 
6876
- function error() {
6877
- var msg = utils.toArray(arguments).join(' ');
6878
- throw new Error(msg);
6879
- }
7000
+ // Similar to isFinite() but does not coerce strings or other types
7001
+ function isFiniteNumber(val) {
7002
+ return isValidNumber(val) && val !== Infinity && val !== -Infinity;
7003
+ }
6880
7004
 
6881
- function message() {
6882
- logArgs(arguments);
6883
- }
7005
+ // This uses type conversion
7006
+ // export function isFiniteNumber(val) {
7007
+ // return val > -Infinity && val < Infinity;
7008
+ // }
6884
7009
 
6885
- // CLI warning is just a message (GUI behaves differently)
6886
- var warn = message;
7010
+ function isNonNegNumber(val) {
7011
+ return isNumber(val) && val >= 0;
7012
+ }
6887
7013
 
6888
- setLoggingFunctions(message, error, stop, warn);
7014
+ function isInteger(obj) {
7015
+ return isNumber(obj) && ((obj | 0) === obj);
6889
7016
  }
6890
7017
 
6891
- function enableLogging() {
6892
- LOGGING = true;
7018
+ function isEven(obj) {
7019
+ return (obj % 2) === 0;
6893
7020
  }
6894
7021
 
6895
- function disableLogging() {
6896
- LOGGING = false;
7022
+ function isOdd(obj) {
7023
+ return (obj % 2) === 1;
6897
7024
  }
6898
7025
 
6899
- function loggingEnabled() {
6900
- return !!LOGGING;
7026
+ function isString(obj) {
7027
+ return obj != null && obj.toString === String.prototype.toString;
7028
+ // TODO: replace w/ something better.
6901
7029
  }
6902
7030
 
6903
- // Handle an unexpected condition (internal error)
6904
- function error() {
6905
- _error.apply(null, utils.toArray(arguments));
7031
+ function isDate(obj) {
7032
+ return !!obj && obj.getTime === Date.prototype.getTime;
6906
7033
  }
6907
7034
 
6908
- // Handle an error caused by invalid input or misuse of API
6909
- function stop() {
6910
- // _stop.apply(null, utils.toArray(arguments));
6911
- _stop.apply(null, messageArgs(arguments));
7035
+ function isBoolean(obj) {
7036
+ return obj === true || obj === false;
6912
7037
  }
6913
7038
 
6914
- function interrupt() {
6915
- _interrupt.apply(null, utils.toArray(arguments));
7039
+ function formatDateISO(d) {
7040
+ if (!isDate(d)) return '';
7041
+ return d.toISOString().replace(':00.000Z', 'Z');
6916
7042
  }
6917
7043
 
6918
- // Print a status message
6919
- function message() {
6920
- _message.apply(null, messageArgs(arguments));
7044
+ // Convert an array-like object to an Array, or make a copy if @obj is an Array
7045
+ function toArray(obj) {
7046
+ var arr;
7047
+ if (!isArrayLike(obj)) error$1("toArray() requires an array-like object");
7048
+ try {
7049
+ arr = Array.prototype.slice.call(obj, 0); // breaks in ie8
7050
+ } catch(e) {
7051
+ // support ie8
7052
+ arr = [];
7053
+ for (var i=0, n=obj.length; i<n; i++) {
7054
+ arr[i] = obj[i];
7055
+ }
7056
+ }
7057
+ return arr;
6921
7058
  }
6922
7059
 
6923
- function warn() {
6924
- _warn.apply(null, messageArgs(arguments));
7060
+ // Array like: has length property, is numerically indexed and mutable.
7061
+ // TODO: try to detect objects with length property but no indexed data elements
7062
+ function isArrayLike(obj) {
7063
+ if (!obj) return false;
7064
+ if (isArray(obj)) return true;
7065
+ if (isString(obj)) return false;
7066
+ if (obj.length === 0 || obj.length > 0) return true;
7067
+ return false;
6925
7068
  }
6926
7069
 
6927
- function warnOnce() {
6928
- var str = formatLogArgs(arguments);
6929
- if (onceMessages.includes(str)) return;
6930
- onceMessages.push(str);
6931
- _warn.apply(null, messageArgs(arguments));
7070
+ // See https://raw.github.com/kvz/phpjs/master/functions/strings/addslashes.js
7071
+ function addslashes(str) {
7072
+ return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
6932
7073
  }
6933
7074
 
6934
- // A way for the GUI to replace the CLI logging functions
6935
- function setLoggingFunctions(message, error, stop, warn) {
6936
- _message = message;
6937
- _error = error;
6938
- _stop = stop;
6939
- _warn = warn;
7075
+ // Escape a literal string to use in a regexp.
7076
+ // Ref.: http://simonwillison.net/2006/Jan/20/escape/
7077
+ function regexEscape(str) {
7078
+ return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
6940
7079
  }
6941
7080
 
6942
- // get detailed error information from error stack (if available)
6943
- // Example stack string (Node.js):
6944
- /*
6945
- /Users/someuser/somescript.js:226
6946
- opacity: Math.round(weight * 5 / 5 // 0.2 0.4 0.6 etc
6947
- ^
6948
7081
 
6949
- SyntaxError: missing ) after argument list
6950
- at internalCompileFunction (node:internal/vm:73:18)
6951
- at wrapSafe (node:internal/modules/cjs/loader:1149:20)
6952
- at Module._compile (node:internal/modules/cjs/loader:1190:27)
6953
- ...
6954
- */
6955
- function getErrorDetail(e) {
6956
- var parts = (typeof e.stack == 'string') ? e.stack.split(/\n\s*\n/) : [];
6957
- if (parts.length > 1 || true) {
6958
- return '\nError details:\n' + parts[0];
6959
- }
6960
- return '';
7082
+ // See https://github.com/janl/mustache.js/blob/master/mustache.js
7083
+ var entityMap = {
7084
+ '&': '&amp;',
7085
+ '<': '&lt;',
7086
+ '>': '&gt;',
7087
+ '"': '&quot;',
7088
+ "'": '&#39;',
7089
+ '/': '&#x2F;'
7090
+ };
7091
+ function htmlEscape(s) {
7092
+ return String(s).replace(/[&<>"'/]/g, function(s) {
7093
+ return entityMap[s];
7094
+ });
6961
7095
  }
6962
7096
 
6963
- // print a message to stdout
6964
- function print() {
6965
- STDOUT = true; // tell logArgs() to print to stdout, not stderr
6966
- // calling message() adds the "[command name]" prefix
6967
- _message(utils.toArray(arguments));
6968
- STDOUT = false;
6969
- }
6970
7097
 
6971
- function verbose() {
6972
- // verbose can be set globally with the -verbose command or separately for each command
6973
- if (useVerbose()) {
6974
- message.apply(null, arguments);
7098
+ function defaults(dest) {
7099
+ for (var i=1, n=arguments.length; i<n; i++) {
7100
+ var src = arguments[i] || {};
7101
+ for (var key in src) {
7102
+ if (key in dest === false && src.hasOwnProperty(key)) {
7103
+ dest[key] = src[key];
7104
+ }
7105
+ }
6975
7106
  }
7107
+ return dest;
6976
7108
  }
6977
7109
 
6978
- function useVerbose() {
6979
- return getStashedVar('VERBOSE');
7110
+ function extend(o) {
7111
+ var dest = o || {},
7112
+ n = arguments.length,
7113
+ key, i, src;
7114
+ for (i=1; i<n; i++) {
7115
+ src = arguments[i] || {};
7116
+ for (key in src) {
7117
+ if (src.hasOwnProperty(key)) {
7118
+ dest[key] = src[key];
7119
+ }
7120
+ }
7121
+ }
7122
+ return dest;
6980
7123
  }
6981
7124
 
6982
- function useDebug() {
6983
- return getStashedVar('DEBUG');
6984
- }
7125
+ // Pseudoclassical inheritance
7126
+ //
7127
+ // Inherit from a Parent function:
7128
+ // inherit(Child, Parent);
7129
+ // Call parent's constructor (inside child constructor):
7130
+ // this.__super__([args...]);
7131
+ function inherit(targ, src) {
7132
+ var f = function() {
7133
+ if (this.__super__ == f) {
7134
+ // add __super__ of parent to front of lookup chain
7135
+ // so parent class constructor can call its parent using this.__super__
7136
+ this.__super__ = src.prototype.__super__;
7137
+ // call parent constructor function. this.__super__ now points to parent-of-parent
7138
+ src.apply(this, arguments);
7139
+ // remove temp __super__, expose targ.prototype.__super__ again
7140
+ delete this.__super__;
7141
+ }
7142
+ };
6985
7143
 
6986
- function debug() {
6987
- if (useDebug()) {
6988
- logArgs(arguments);
6989
- }
7144
+ f.prototype = src.prototype || src; // added || src to allow inheriting from objects as well as functions
7145
+ // Extend targ prototype instead of wiping it out --
7146
+ // in case inherit() is called after targ.prototype = {stuff}; statement
7147
+ targ.prototype = extend(new f(), targ.prototype); //
7148
+ targ.prototype.constructor = targ;
7149
+ targ.prototype.__super__ = f;
6990
7150
  }
6991
7151
 
6992
- function time(slug) {
6993
- if (useDebug()) {
6994
- console.time(slug);
6995
- }
7152
+ function promisify(asyncFn) {
7153
+ return function() {
7154
+ var args = toArray(arguments);
7155
+ return new Promise((resolve, reject) => {
7156
+ var cb = function(err, data) {
7157
+ if (err) reject(err);
7158
+ else resolve(data);
7159
+ };
7160
+ args.push(cb);
7161
+ asyncFn.apply(this, args);
7162
+ });
7163
+ };
6996
7164
  }
6997
7165
 
6998
- function timeEnd(slug) {
6999
- if (useDebug()) {
7000
- console.timeEnd(slug);
7166
+ function runAsync(fn, arg) {
7167
+ return new Promise((resolve, reject) => {
7168
+ fn(arg, function(err, data) {
7169
+ return err ? reject(err) : resolve(data);
7170
+ });
7171
+ });
7001
7172
  }
7002
- }
7003
7173
 
7004
- function printError(err) {
7005
- var msg;
7006
- if (!LOGGING) return;
7007
- if (utils.isString(err)) {
7008
- err = new UserError(err);
7009
- }
7010
- if (err.name == 'NonFatalError') {
7011
- console.error(messageArgs([err.message]).join(' '));
7012
- } else if (err.name == 'UserError') {
7013
- msg = err.message;
7014
- if (!/Error/.test(msg)) {
7015
- msg = "Error: " + msg;
7174
+ // Call @iter on each member of an array (similar to Array#reduce(iter))
7175
+ // iter: function(memo, item, callback)
7176
+ // Call @done when all members have been processed or if an error occurs
7177
+ // done: function(err, memo)
7178
+ // @memo: Initial value
7179
+ //
7180
+ function reduceAsync(arr, memo, iter, done) {
7181
+ var call = typeof setImmediate == 'undefined' ? setTimeout : setImmediate;
7182
+ var i=0;
7183
+ next(null, memo);
7184
+
7185
+ function next(err, memo) {
7186
+ // Detach next operation from call stack to prevent overflow
7187
+ // Don't use setTimeout(, 0) if setImmediate is available
7188
+ // (setTimeout() can introduce a long delay if previous operation was slow,
7189
+ // as of Node 0.10.32 -- a bug?)
7190
+ if (err) {
7191
+ return done(err, null);
7016
7192
  }
7017
- console.error(messageArgs([msg]).join(' '));
7018
- console.error("Run mapshaper -h to view help");
7019
- } else {
7020
- // not a user error (i.e. a bug in mapshaper)
7021
- console.error(err);
7022
- // throw err;
7193
+ call(function() {
7194
+ if (i < arr.length === false) {
7195
+ done(null, memo);
7196
+ } else {
7197
+ iter(memo, arr[i++], next);
7198
+ }
7199
+ }, 0);
7023
7200
  }
7024
7201
  }
7025
7202
 
7026
- function UserError(msg) {
7027
- var err = new Error(msg);
7028
- err.name = 'UserError';
7029
- return err;
7030
- }
7031
7203
 
7032
- function NonFatalError(msg) {
7033
- var err = new Error(msg);
7034
- err.name = 'NonFatalError';
7035
- return err;
7204
+ // Append elements of @src array to @dest array
7205
+ function merge(dest, src) {
7206
+ if (!isArray(dest) || !isArray(src)) {
7207
+ error$1("Usage: merge(destArray, srcArray);");
7208
+ }
7209
+ for (var i=0, n=src.length; i<n; i++) {
7210
+ dest.push(src[i]);
7211
+ }
7212
+ return dest;
7036
7213
  }
7037
7214
 
7038
- function formatColumns(arr, alignments) {
7039
- var widths = arr.reduce(function(memo, line) {
7040
- return line.map(function(str, i) {
7041
- return memo ? Math.max(memo[i], str.length) : str.length;
7042
- });
7043
- }, null);
7044
- return arr.map(function(line) {
7045
- line = line.map(function(str, i) {
7046
- var rt = alignments && alignments[i] == 'right';
7047
- var pad = (rt ? str.padStart : str.padEnd).bind(str);
7048
- return pad(widths[i], ' ');
7049
- });
7050
- return ' ' + line.join(' ');
7051
- }).join('\n');
7215
+ // Returns elements in arr and not in other
7216
+ // (similar to underscore diff)
7217
+ function difference(arr, other) {
7218
+ var index = arrayToIndex(other);
7219
+ return arr.filter(function(el) {
7220
+ return !Object.prototype.hasOwnProperty.call(index, el);
7221
+ });
7052
7222
  }
7053
7223
 
7054
- // Format an array of (preferably short) strings in columns for console logging.
7055
- function formatStringsAsGrid(arr, width) {
7056
- // TODO: variable column width
7057
- var longest = arr.reduce(function(len, str) {
7058
- return Math.max(len, str.length);
7059
- }, 0),
7060
- colWidth = longest + 2,
7061
- perLine = Math.floor((width || 80) / colWidth) || 1;
7062
- return arr.reduce(function(memo, name, i) {
7063
- var col = i % perLine;
7064
- if (i > 0 && col === 0) memo += '\n';
7065
- if (col < perLine - 1) { // right-pad all but rightmost column
7066
- name = utils.rpad(name, colWidth - 2, ' ');
7067
- }
7068
- return memo + ' ' + name;
7069
- }, '');
7224
+ // Return the intersection of two arrays
7225
+ function intersection(a, b) {
7226
+ return a.filter(function(el) {
7227
+ return b.includes(el);
7228
+ });
7070
7229
  }
7071
7230
 
7072
- // expose so GUI can use it
7073
- function formatLogArgs(args) {
7074
- return utils.toArray(args).join(' ');
7231
+ function indexOf(arr, item) {
7232
+ var nan = item !== item;
7233
+ for (var i = 0, len = arr.length || 0; i < len; i++) {
7234
+ if (arr[i] === item) return i;
7235
+ if (nan && arr[i] !== arr[i]) return i;
7236
+ }
7237
+ return -1;
7075
7238
  }
7076
7239
 
7077
- function messageArgs(args) {
7078
- var arr = utils.toArray(args);
7079
- var cmd = getStashedVar('current_command');
7080
- if (cmd && cmd != 'help') {
7081
- arr.unshift('[' + cmd + ']');
7240
+ // Test a string or array-like object for existence of substring or element
7241
+ function contains(container, item) {
7242
+ if (isString(container)) {
7243
+ return container.indexOf(item) != -1;
7082
7244
  }
7083
- return arr;
7245
+ else if (isArrayLike(container)) {
7246
+ return indexOf(container, item) != -1;
7247
+ }
7248
+ error$1("Expected Array or String argument");
7084
7249
  }
7085
7250
 
7086
- function logArgs(args) {
7087
- if (!LOGGING || getStashedVar('QUIET') || !utils.isArrayLike(args)) return;
7088
- var msg = formatLogArgs(args);
7089
- if (STDOUT) console.log(msg);
7090
- else console.error(msg);
7251
+ function some(arr, test) {
7252
+ return arr.reduce(function(val, item) {
7253
+ return val || test(item); // TODO: short-circuit?
7254
+ }, false);
7091
7255
  }
7092
7256
 
7093
- function truncateString(str, maxLen) {
7094
- maxLen = maxLen || 80;
7095
- if (str.length > maxLen) {
7096
- str = str.substring(0, maxLen - 3).trimEnd() + '...';
7097
- }
7098
- return str;
7257
+ function every(arr, test) {
7258
+ return arr.reduce(function(val, item) {
7259
+ return val && test(item);
7260
+ }, true);
7099
7261
  }
7100
7262
 
7101
- var uniqCount = 0;
7102
- function getUniqueName(prefix) {
7103
- return (prefix || "__id_") + (++uniqCount);
7263
+ function find(arr, test, ctx) {
7264
+ var matches = arr.filter(test, ctx);
7265
+ return matches.length === 0 ? null : matches[0];
7104
7266
  }
7105
7267
 
7106
- function isFunction(obj) {
7107
- return typeof obj == 'function';
7268
+ function range(len, start, inc) {
7269
+ var arr = [],
7270
+ v = start === void 0 ? 0 : start,
7271
+ i = inc === void 0 ? 1 : inc;
7272
+ while(len--) {
7273
+ arr.push(v);
7274
+ v += i;
7275
+ }
7276
+ return arr;
7108
7277
  }
7109
7278
 
7110
- function isPromise(arg) {
7111
- return arg ? isFunction(arg.then) : false;
7279
+ function repeat(times, func) {
7280
+ var values = [],
7281
+ val;
7282
+ for (var i=0; i<times; i++) {
7283
+ val = func(i);
7284
+ if (val !== void 0) {
7285
+ values[i] = val;
7286
+ }
7287
+ }
7288
+ return values.length > 0 ? values : void 0;
7112
7289
  }
7113
7290
 
7114
- function isObject(obj) {
7115
- return obj === Object(obj); // via underscore
7291
+ // Calc sum, skip falsy and NaN values
7292
+ // Assumes: no other non-numeric objects in array
7293
+ //
7294
+ function sum(arr, info) {
7295
+ if (!isArrayLike(arr)) error$1 ("sum() expects an array, received:", arr);
7296
+ var tot = 0,
7297
+ nan = 0,
7298
+ val;
7299
+ for (var i=0, n=arr.length; i<n; i++) {
7300
+ val = arr[i];
7301
+ if (val) {
7302
+ tot += val;
7303
+ } else if (isNaN(val)) {
7304
+ nan++;
7305
+ }
7306
+ }
7307
+ if (info) {
7308
+ info.nan = nan;
7309
+ }
7310
+ return tot;
7116
7311
  }
7117
7312
 
7118
- function clamp(val, min, max) {
7119
- return val < min ? min : (val > max ? max : val);
7313
+ // Calculate min and max values of an array, ignoring NaN values
7314
+ function getArrayBounds(arr) {
7315
+ var min = Infinity,
7316
+ max = -Infinity,
7317
+ nan = 0, val;
7318
+ for (var i=0, len=arr.length; i<len; i++) {
7319
+ val = arr[i];
7320
+ if (val !== val) nan++;
7321
+ if (val < min) min = val;
7322
+ if (val > max) max = val;
7323
+ }
7324
+ return {
7325
+ min: min,
7326
+ max: max,
7327
+ nan: nan
7328
+ };
7120
7329
  }
7121
7330
 
7122
- function isArray(obj) {
7123
- return Array.isArray(obj);
7124
- }
7331
+ // export function uniq(src) {
7332
+ // var index = {};
7333
+ // return src.reduce(function(memo, el) {
7334
+ // if (el in index === false) {
7335
+ // index[el] = true;
7336
+ // memo.push(el);
7337
+ // }
7338
+ // return memo;
7339
+ // }, []);
7340
+ // }
7125
7341
 
7126
- // Is obj a valid number or NaN? (test if obj is type number)
7127
- function isNumber(obj) {
7128
- return obj != null && obj.constructor == Number;
7342
+ function uniq(src) {
7343
+ var index = new Set();
7344
+ var arr = [];
7345
+ var item;
7346
+ for (var i=0, n=src.length; i<n; i++) {
7347
+ item = src[i];
7348
+ if (!index.has(item)) {
7349
+ arr.push(item);
7350
+ index.add(item);
7351
+ }
7352
+ }
7353
+ return arr;
7129
7354
  }
7130
7355
 
7131
- function isValidNumber(val) {
7132
- return isNumber(val) && !isNaN(val);
7356
+ function pluck(arr, key) {
7357
+ return arr.map(function(obj) {
7358
+ return obj[key];
7359
+ });
7133
7360
  }
7134
7361
 
7135
- // Similar to isFinite() but does not coerce strings or other types
7136
- function isFiniteNumber(val) {
7137
- return isValidNumber(val) && val !== Infinity && val !== -Infinity;
7362
+ function countValues(arr) {
7363
+ return arr.reduce(function(memo, val) {
7364
+ memo[val] = (val in memo) ? memo[val] + 1 : 1;
7365
+ return memo;
7366
+ }, {});
7138
7367
  }
7139
7368
 
7140
- // This uses type conversion
7141
- // export function isFiniteNumber(val) {
7142
- // return val > -Infinity && val < Infinity;
7143
- // }
7144
-
7145
- function isNonNegNumber(val) {
7146
- return isNumber(val) && val >= 0;
7369
+ function indexOn(arr, k) {
7370
+ return arr.reduce(function(index, o) {
7371
+ index[o[k]] = o;
7372
+ return index;
7373
+ }, {});
7147
7374
  }
7148
7375
 
7149
- function isInteger(obj) {
7150
- return isNumber(obj) && ((obj | 0) === obj);
7376
+ function groupBy(arr, k) {
7377
+ return arr.reduce(function(index, o) {
7378
+ var keyval = o[k];
7379
+ if (keyval in index) {
7380
+ index[keyval].push(o);
7381
+ } else {
7382
+ index[keyval] = [o];
7383
+ }
7384
+ return index;
7385
+ }, {});
7151
7386
  }
7152
7387
 
7153
- function isEven(obj) {
7154
- return (obj % 2) === 0;
7388
+ function arrayToIndex(arr, val) {
7389
+ var init = arguments.length > 1;
7390
+ return arr.reduce(function(index, key) {
7391
+ index[key] = init ? val : true;
7392
+ return index;
7393
+ }, {});
7155
7394
  }
7156
7395
 
7157
- function isOdd(obj) {
7158
- return (obj % 2) === 1;
7396
+ // Support for iterating over array-like objects, like typed arrays
7397
+ function forEach(arr, func, ctx) {
7398
+ if (!isArrayLike(arr)) {
7399
+ throw new Error("#forEach() takes an array-like argument. " + arr);
7400
+ }
7401
+ for (var i=0, n=arr.length; i < n; i++) {
7402
+ func.call(ctx, arr[i], i);
7403
+ }
7159
7404
  }
7160
7405
 
7161
- function isString(obj) {
7162
- return obj != null && obj.toString === String.prototype.toString;
7163
- // TODO: replace w/ something better.
7406
+ function forEachProperty(o, func, ctx) {
7407
+ Object.keys(o).forEach(function(key) {
7408
+ func.call(ctx, o[key], key);
7409
+ });
7164
7410
  }
7165
7411
 
7166
- function isDate(obj) {
7167
- return !!obj && obj.getTime === Date.prototype.getTime;
7412
+ function initializeArray(arr, init) {
7413
+ for (var i=0, len=arr.length; i<len; i++) {
7414
+ arr[i] = init;
7415
+ }
7416
+ return arr;
7168
7417
  }
7169
7418
 
7170
- function isBoolean(obj) {
7171
- return obj === true || obj === false;
7419
+ function replaceArray(arr, arr2) {
7420
+ arr.splice(0, arr.length);
7421
+ for (var i=0, n=arr2.length; i<n; i++) {
7422
+ arr.push(arr2[i]);
7423
+ }
7172
7424
  }
7173
7425
 
7174
- function formatDateISO(d) {
7175
- if (!isDate(d)) return '';
7176
- return d.toISOString().replace(':00.000Z', 'Z');
7426
+ function repeatString(src, n) {
7427
+ var str = "";
7428
+ for (var i=0; i<n; i++)
7429
+ str += src;
7430
+ return str;
7177
7431
  }
7178
7432
 
7179
- // Convert an array-like object to an Array, or make a copy if @obj is an Array
7180
- function toArray(obj) {
7181
- var arr;
7182
- if (!isArrayLike(obj)) error("toArray() requires an array-like object");
7183
- try {
7184
- arr = Array.prototype.slice.call(obj, 0); // breaks in ie8
7185
- } catch(e) {
7186
- // support ie8
7187
- arr = [];
7188
- for (var i=0, n=obj.length; i<n; i++) {
7189
- arr[i] = obj[i];
7190
- }
7191
- }
7192
- return arr;
7433
+ function splitLines(str) {
7434
+ return str.split(/\r?\n/);
7193
7435
  }
7194
7436
 
7195
- // Array like: has length property, is numerically indexed and mutable.
7196
- // TODO: try to detect objects with length property but no indexed data elements
7197
- function isArrayLike(obj) {
7198
- if (!obj) return false;
7199
- if (isArray(obj)) return true;
7200
- if (isString(obj)) return false;
7201
- if (obj.length === 0 || obj.length > 0) return true;
7202
- return false;
7437
+ function pluralSuffix(count) {
7438
+ return count != 1 ? 's' : '';
7203
7439
  }
7204
7440
 
7205
- // See https://raw.github.com/kvz/phpjs/master/functions/strings/addslashes.js
7206
- function addslashes(str) {
7207
- return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0');
7441
+ function endsWith(str, ending) {
7442
+ return str.indexOf(ending, str.length - ending.length) !== -1;
7208
7443
  }
7209
7444
 
7210
- // Escape a literal string to use in a regexp.
7211
- // Ref.: http://simonwillison.net/2006/Jan/20/escape/
7212
- function regexEscape(str) {
7213
- return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
7445
+ function lpad(str, size, pad) {
7446
+ pad = pad || ' ';
7447
+ str = String(str);
7448
+ return repeatString(pad, size - str.length) + str;
7214
7449
  }
7215
7450
 
7451
+ function rpad(str, size, pad) {
7452
+ pad = pad || ' ';
7453
+ str = String(str);
7454
+ return str + repeatString(pad, size - str.length);
7455
+ }
7216
7456
 
7217
- // See https://github.com/janl/mustache.js/blob/master/mustache.js
7218
- var entityMap = {
7219
- '&': '&amp;',
7220
- '<': '&lt;',
7221
- '>': '&gt;',
7222
- '"': '&quot;',
7223
- "'": '&#39;',
7224
- '/': '&#x2F;'
7225
- };
7226
- function htmlEscape(s) {
7227
- return String(s).replace(/[&<>"'/]/g, function(s) {
7228
- return entityMap[s];
7229
- });
7457
+ function trim(str) {
7458
+ return ltrim(rtrim(str));
7230
7459
  }
7231
7460
 
7461
+ var ltrimRxp = /^\s+/;
7462
+ function ltrim(str) {
7463
+ return str.replace(ltrimRxp, '');
7464
+ }
7232
7465
 
7233
- function defaults(dest) {
7234
- for (var i=1, n=arguments.length; i<n; i++) {
7235
- var src = arguments[i] || {};
7236
- for (var key in src) {
7237
- if (key in dest === false && src.hasOwnProperty(key)) {
7238
- dest[key] = src[key];
7239
- }
7240
- }
7241
- }
7242
- return dest;
7466
+ var rtrimRxp = /\s+$/;
7467
+ function rtrim(str) {
7468
+ return str.replace(rtrimRxp, '');
7243
7469
  }
7244
7470
 
7245
- function extend(o) {
7246
- var dest = o || {},
7247
- n = arguments.length,
7248
- key, i, src;
7249
- for (i=1; i<n; i++) {
7250
- src = arguments[i] || {};
7251
- for (key in src) {
7252
- if (src.hasOwnProperty(key)) {
7253
- dest[key] = src[key];
7254
- }
7255
- }
7471
+ function addThousandsSep(str) {
7472
+ var fmt = '',
7473
+ start = str[0] == '-' ? 1 : 0,
7474
+ dec = str.indexOf('.'),
7475
+ end = str.length,
7476
+ ins = (dec == -1 ? end : dec) - 3;
7477
+ while (ins > start) {
7478
+ fmt = ',' + str.substring(ins, end) + fmt;
7479
+ end = ins;
7480
+ ins -= 3;
7256
7481
  }
7257
- return dest;
7482
+ return str.substring(0, end) + fmt;
7258
7483
  }
7259
7484
 
7260
- // Pseudoclassical inheritance
7261
- //
7262
- // Inherit from a Parent function:
7263
- // inherit(Child, Parent);
7264
- // Call parent's constructor (inside child constructor):
7265
- // this.__super__([args...]);
7266
- function inherit(targ, src) {
7267
- var f = function() {
7268
- if (this.__super__ == f) {
7269
- // add __super__ of parent to front of lookup chain
7270
- // so parent class constructor can call its parent using this.__super__
7271
- this.__super__ = src.prototype.__super__;
7272
- // call parent constructor function. this.__super__ now points to parent-of-parent
7273
- src.apply(this, arguments);
7274
- // remove temp __super__, expose targ.prototype.__super__ again
7275
- delete this.__super__;
7276
- }
7277
- };
7278
-
7279
- f.prototype = src.prototype || src; // added || src to allow inheriting from objects as well as functions
7280
- // Extend targ prototype instead of wiping it out --
7281
- // in case inherit() is called after targ.prototype = {stuff}; statement
7282
- targ.prototype = extend(new f(), targ.prototype); //
7283
- targ.prototype.constructor = targ;
7284
- targ.prototype.__super__ = f;
7485
+ function numToStr(num, decimals) {
7486
+ return decimals >= 0 ? num.toFixed(decimals) : String(num);
7285
7487
  }
7286
7488
 
7287
- function promisify(asyncFn) {
7288
- return function() {
7289
- var args = toArray(arguments);
7290
- return new Promise((resolve, reject) => {
7291
- var cb = function(err, data) {
7292
- if (err) reject(err);
7293
- else resolve(data);
7294
- };
7295
- args.push(cb);
7296
- asyncFn.apply(this, args);
7297
- });
7298
- };
7489
+ function formatNumber(val) {
7490
+ return val + '';
7299
7491
  }
7300
7492
 
7301
- function runAsync(fn, arg) {
7302
- return new Promise((resolve, reject) => {
7303
- fn(arg, function(err, data) {
7304
- return err ? reject(err) : resolve(data);
7305
- });
7306
- });
7307
- }
7308
-
7309
- // Call @iter on each member of an array (similar to Array#reduce(iter))
7310
- // iter: function(memo, item, callback)
7311
- // Call @done when all members have been processed or if an error occurs
7312
- // done: function(err, memo)
7313
- // @memo: Initial value
7314
- //
7315
- function reduceAsync(arr, memo, iter, done) {
7316
- var call = typeof setImmediate == 'undefined' ? setTimeout : setImmediate;
7317
- var i=0;
7318
- next(null, memo);
7493
+ function formatIntlNumber(val) {
7494
+ var str = formatNumber(val);
7495
+ return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
7496
+ }
7319
7497
 
7320
- function next(err, memo) {
7321
- // Detach next operation from call stack to prevent overflow
7322
- // Don't use setTimeout(, 0) if setImmediate is available
7323
- // (setTimeout() can introduce a long delay if previous operation was slow,
7324
- // as of Node 0.10.32 -- a bug?)
7325
- if (err) {
7326
- return done(err, null);
7498
+ function formatNumberForDisplay(num, decimals, nullStr, showPos) {
7499
+ var fmt;
7500
+ if (isNaN(num)) {
7501
+ fmt = nullStr || '-';
7502
+ } else {
7503
+ fmt = numToStr(num, decimals);
7504
+ fmt = addThousandsSep(fmt);
7505
+ if (showPos && parseFloat(fmt) > 0) {
7506
+ fmt = "+" + fmt;
7327
7507
  }
7328
- call(function() {
7329
- if (i < arr.length === false) {
7330
- done(null, memo);
7331
- } else {
7332
- iter(memo, arr[i++], next);
7333
- }
7334
- }, 0);
7335
7508
  }
7509
+ return fmt;
7336
7510
  }
7337
7511
 
7338
-
7339
- // Append elements of @src array to @dest array
7340
- function merge(dest, src) {
7341
- if (!isArray(dest) || !isArray(src)) {
7342
- error("Usage: merge(destArray, srcArray);");
7343
- }
7344
- for (var i=0, n=src.length; i<n; i++) {
7345
- dest.push(src[i]);
7512
+ function shuffle(arr) {
7513
+ var tmp, i, j;
7514
+ for (i = arr.length - 1; i > 0; i--) {
7515
+ j = Math.floor(Math.random() * (i + 1));
7516
+ tmp = arr[i];
7517
+ arr[i] = arr[j];
7518
+ arr[j] = tmp;
7346
7519
  }
7347
- return dest;
7348
7520
  }
7349
7521
 
7350
- // Returns elements in arr and not in other
7351
- // (similar to underscore diff)
7352
- function difference(arr, other) {
7353
- var index = arrayToIndex(other);
7354
- return arr.filter(function(el) {
7355
- return !Object.prototype.hasOwnProperty.call(index, el);
7356
- });
7522
+ function pickOne(arr) {
7523
+ return arr[Math.floor(Math.random() * arr.length)];
7357
7524
  }
7358
7525
 
7359
- // Return the intersection of two arrays
7360
- function intersection(a, b) {
7361
- return a.filter(function(el) {
7362
- return b.includes(el);
7526
+ // Sort an array of objects based on one or more properties.
7527
+ // Usage: sortOn(array, key1, asc?[, key2, asc? ...])
7528
+ //
7529
+ function sortOn(arr) {
7530
+ var comparators = [];
7531
+ for (var i=1; i<arguments.length; i+=2) {
7532
+ comparators.push(getKeyComparator(arguments[i], arguments[i+1]));
7533
+ }
7534
+ arr.sort(function(a, b) {
7535
+ var cmp = 0,
7536
+ i = 0,
7537
+ n = comparators.length;
7538
+ while (i < n && cmp === 0) {
7539
+ cmp = comparators[i](a, b);
7540
+ i++;
7541
+ }
7542
+ return cmp;
7363
7543
  });
7544
+ return arr;
7364
7545
  }
7365
7546
 
7366
- function indexOf(arr, item) {
7367
- var nan = item !== item;
7368
- for (var i = 0, len = arr.length || 0; i < len; i++) {
7369
- if (arr[i] === item) return i;
7370
- if (nan && arr[i] !== arr[i]) return i;
7371
- }
7372
- return -1;
7547
+ // Sort array of values that can be compared with < > operators (strings, numbers)
7548
+ // null, undefined and NaN are sorted to the end of the array
7549
+ // default order is ascending
7550
+ //
7551
+ function genericSort(arr, ascending) {
7552
+ var compare = getGenericComparator(ascending);
7553
+ Array.prototype.sort.call(arr, compare);
7554
+ return arr;
7373
7555
  }
7374
7556
 
7375
- // Test a string or array-like object for existence of substring or element
7376
- function contains(container, item) {
7377
- if (isString(container)) {
7378
- return container.indexOf(item) != -1;
7379
- }
7380
- else if (isArrayLike(container)) {
7381
- return indexOf(container, item) != -1;
7382
- }
7383
- error("Expected Array or String argument");
7557
+ function getSortedIds(arr, asc) {
7558
+ var ids = range(arr.length);
7559
+ sortArrayIndex(ids, arr, asc);
7560
+ return ids;
7384
7561
  }
7385
7562
 
7386
- function some(arr, test) {
7387
- return arr.reduce(function(val, item) {
7388
- return val || test(item); // TODO: short-circuit?
7389
- }, false);
7563
+ function sortArrayIndex(ids, arr, asc) {
7564
+ var compare = getGenericComparator(asc);
7565
+ ids.sort(function(i, j) {
7566
+ // added i, j comparison to guarantee that sort is stable
7567
+ var cmp = compare(arr[i], arr[j]);
7568
+ return cmp > 0 || cmp === 0 && i > j ? 1 : -1;
7569
+ });
7390
7570
  }
7391
7571
 
7392
- function every(arr, test) {
7393
- return arr.reduce(function(val, item) {
7394
- return val && test(item);
7395
- }, true);
7572
+ function reorderArray(arr, idxs) {
7573
+ var len = idxs.length;
7574
+ var arr2 = [];
7575
+ for (var i=0; i<len; i++) {
7576
+ var idx = idxs[i];
7577
+ if (idx < 0 || idx >= len) error$1("Out-of-bounds array idx");
7578
+ arr2[i] = arr[idx];
7579
+ }
7580
+ replaceArray(arr, arr2);
7396
7581
  }
7397
7582
 
7398
- function find(arr, test, ctx) {
7399
- var matches = arr.filter(test, ctx);
7400
- return matches.length === 0 ? null : matches[0];
7583
+ function getKeyComparator(key, asc) {
7584
+ var compare = getGenericComparator(asc);
7585
+ return function(a, b) {
7586
+ return compare(a[key], b[key]);
7587
+ };
7401
7588
  }
7402
7589
 
7403
- function range(len, start, inc) {
7404
- var arr = [],
7405
- v = start === void 0 ? 0 : start,
7406
- i = inc === void 0 ? 1 : inc;
7407
- while(len--) {
7408
- arr.push(v);
7409
- v += i;
7410
- }
7411
- return arr;
7590
+ function getGenericComparator(asc) {
7591
+ asc = asc !== false && asc != 'descending'; // ascending is the default
7592
+ return function(a, b) {
7593
+ var retn = 0;
7594
+ if (b == null) {
7595
+ retn = a == null ? 0 : -1;
7596
+ } else if (a == null) {
7597
+ retn = 1;
7598
+ } else if (a < b) {
7599
+ retn = asc ? -1 : 1;
7600
+ } else if (a > b) {
7601
+ retn = asc ? 1 : -1;
7602
+ } else if (a !== a) {
7603
+ retn = 1;
7604
+ } else if (b !== b) {
7605
+ retn = -1;
7606
+ }
7607
+ return retn;
7608
+ };
7412
7609
  }
7413
7610
 
7414
- function repeat(times, func) {
7415
- var values = [],
7416
- val;
7417
- for (var i=0; i<times; i++) {
7418
- val = func(i);
7419
- if (val !== void 0) {
7420
- values[i] = val;
7421
- }
7422
- }
7423
- return values.length > 0 ? values : void 0;
7611
+
7612
+ // Generic in-place sort (null, NaN, undefined not handled)
7613
+ function quicksort(arr, asc) {
7614
+ quicksortPartition(arr, 0, arr.length-1);
7615
+ if (asc === false) Array.prototype.reverse.call(arr); // Works with typed arrays
7616
+ return arr;
7424
7617
  }
7425
7618
 
7426
- // Calc sum, skip falsy and NaN values
7427
- // Assumes: no other non-numeric objects in array
7428
- //
7429
- function sum(arr, info) {
7430
- if (!isArrayLike(arr)) error ("sum() expects an array, received:", arr);
7431
- var tot = 0,
7432
- nan = 0,
7433
- val;
7434
- for (var i=0, n=arr.length; i<n; i++) {
7435
- val = arr[i];
7436
- if (val) {
7437
- tot += val;
7438
- } else if (isNaN(val)) {
7439
- nan++;
7619
+ // Moved out of quicksort() (saw >100% speedup in Chrome with deep recursion)
7620
+ function quicksortPartition (a, lo, hi) {
7621
+ var i = lo,
7622
+ j = hi,
7623
+ pivot, tmp;
7624
+ while (i < hi) {
7625
+ pivot = a[lo + hi >> 1]; // avoid n^2 performance on sorted arrays
7626
+ while (i <= j) {
7627
+ while (a[i] < pivot) i++;
7628
+ while (a[j] > pivot) j--;
7629
+ if (i <= j) {
7630
+ tmp = a[i];
7631
+ a[i] = a[j];
7632
+ a[j] = tmp;
7633
+ i++;
7634
+ j--;
7635
+ }
7440
7636
  }
7637
+ if (lo < j) quicksortPartition(a, lo, j);
7638
+ lo = i;
7639
+ j = hi;
7441
7640
  }
7442
- if (info) {
7443
- info.nan = nan;
7444
- }
7445
- return tot;
7446
- }
7447
-
7448
- // Calculate min and max values of an array, ignoring NaN values
7449
- function getArrayBounds(arr) {
7450
- var min = Infinity,
7451
- max = -Infinity,
7452
- nan = 0, val;
7453
- for (var i=0, len=arr.length; i<len; i++) {
7454
- val = arr[i];
7455
- if (val !== val) nan++;
7456
- if (val < min) min = val;
7457
- if (val > max) max = val;
7458
- }
7459
- return {
7460
- min: min,
7461
- max: max,
7462
- nan: nan
7463
- };
7464
7641
  }
7465
7642
 
7466
- // export function uniq(src) {
7467
- // var index = {};
7468
- // return src.reduce(function(memo, el) {
7469
- // if (el in index === false) {
7470
- // index[el] = true;
7471
- // memo.push(el);
7472
- // }
7473
- // return memo;
7474
- // }, []);
7475
- // }
7476
7643
 
7477
- function uniq(src) {
7478
- var index = new Set();
7479
- var arr = [];
7480
- var item;
7481
- for (var i=0, n=src.length; i<n; i++) {
7482
- item = src[i];
7483
- if (!index.has(item)) {
7484
- arr.push(item);
7485
- index.add(item);
7486
- }
7644
+ function findRankByValue(arr, value) {
7645
+ if (isNaN(value)) return arr.length;
7646
+ var rank = 1;
7647
+ for (var i=0, n=arr.length; i<n; i++) {
7648
+ if (value > arr[i]) rank++;
7487
7649
  }
7488
- return arr;
7650
+ return rank;
7489
7651
  }
7490
7652
 
7491
- function pluck(arr, key) {
7492
- return arr.map(function(obj) {
7493
- return obj[key];
7494
- });
7653
+ function findValueByPct(arr, pct) {
7654
+ var rank = Math.ceil((1-pct) * (arr.length));
7655
+ return findValueByRank(arr, rank);
7495
7656
  }
7496
7657
 
7497
- function countValues(arr) {
7498
- return arr.reduce(function(memo, val) {
7499
- memo[val] = (val in memo) ? memo[val] + 1 : 1;
7500
- return memo;
7501
- }, {});
7502
- }
7658
+ // See http://ndevilla.free.fr/median/median/src/wirth.c
7659
+ // Elements of @arr are reordered
7660
+ //
7661
+ function findValueByRank(arr, rank) {
7662
+ if (!arr.length || rank < 1 || rank > arr.length) error$1("[findValueByRank()] invalid input");
7503
7663
 
7504
- function indexOn(arr, k) {
7505
- return arr.reduce(function(index, o) {
7506
- index[o[k]] = o;
7507
- return index;
7508
- }, {});
7664
+ rank = clamp(rank | 0, 1, arr.length);
7665
+ var k = rank - 1, // conv. rank to array index
7666
+ n = arr.length,
7667
+ l = 0,
7668
+ m = n - 1,
7669
+ i, j, val, tmp;
7670
+
7671
+ while (l < m) {
7672
+ val = arr[k];
7673
+ i = l;
7674
+ j = m;
7675
+ do {
7676
+ while (arr[i] < val) {i++;}
7677
+ while (val < arr[j]) {j--;}
7678
+ if (i <= j) {
7679
+ tmp = arr[i];
7680
+ arr[i] = arr[j];
7681
+ arr[j] = tmp;
7682
+ i++;
7683
+ j--;
7684
+ }
7685
+ } while (i <= j);
7686
+ if (j < k) l = i;
7687
+ if (k < i) m = j;
7688
+ }
7689
+ return arr[k];
7509
7690
  }
7510
7691
 
7511
- function groupBy(arr, k) {
7512
- return arr.reduce(function(index, o) {
7513
- var keyval = o[k];
7514
- if (keyval in index) {
7515
- index[keyval].push(o);
7516
- } else {
7517
- index[keyval] = [o];
7518
- }
7519
- return index;
7520
- }, {});
7692
+ function findMedian(arr) {
7693
+ return findQuantile(arr, 0.5);
7521
7694
  }
7522
7695
 
7523
- function arrayToIndex(arr, val) {
7524
- var init = arguments.length > 1;
7525
- return arr.reduce(function(index, key) {
7526
- index[key] = init ? val : true;
7527
- return index;
7528
- }, {});
7696
+ function findQuantile(arr, k) {
7697
+ var n = arr.length,
7698
+ i1 = Math.floor((n - 1) * k),
7699
+ i2 = Math.ceil((n - 1) * k);
7700
+ if (i1 < 0 || i2 >= n) return NaN;
7701
+ var v1 = findValueByRank(arr, i1 + 1);
7702
+ if (i1 == i2) return v1;
7703
+ var v2 = findValueByRank(arr, i2 + 1);
7704
+ // use linear interpolation
7705
+ var w1 = i2 / (n - 1) - k;
7706
+ var w2 = k - i1 / (n - 1);
7707
+ var v = (v1 * w1 + v2 * w2) * (n - 1);
7708
+ return v;
7529
7709
  }
7530
7710
 
7531
- // Support for iterating over array-like objects, like typed arrays
7532
- function forEach(arr, func, ctx) {
7533
- if (!isArrayLike(arr)) {
7534
- throw new Error("#forEach() takes an array-like argument. " + arr);
7535
- }
7536
- for (var i=0, n=arr.length; i < n; i++) {
7537
- func.call(ctx, arr[i], i);
7711
+ function mean(arr) {
7712
+ var count = 0,
7713
+ avg = NaN,
7714
+ val;
7715
+ for (var i=0, n=arr.length; i<n; i++) {
7716
+ val = arr[i];
7717
+ if (isNaN(val)) continue;
7718
+ avg = ++count == 1 ? val : val / count + (count - 1) / count * avg;
7538
7719
  }
7720
+ return avg;
7539
7721
  }
7540
7722
 
7541
- function forEachProperty(o, func, ctx) {
7542
- Object.keys(o).forEach(function(key) {
7543
- func.call(ctx, o[key], key);
7544
- });
7723
+
7724
+ /*
7725
+ A simplified version of printf formatting
7726
+ Format codes: %[flags][width][.precision]type
7727
+
7728
+ supported flags:
7729
+ + add '+' before positive numbers
7730
+ 0 left-pad with '0'
7731
+ ' Add thousands separator
7732
+ width: 1 to many
7733
+ precision: .(1 to many)
7734
+ type:
7735
+ s string
7736
+ di integers
7737
+ f decimal numbers
7738
+ xX hexidecimal (unsigned)
7739
+ % literal '%'
7740
+
7741
+ Examples:
7742
+ code val formatted
7743
+ %+d 1 '+1'
7744
+ %4i 32 ' 32'
7745
+ %04i 32 '0032'
7746
+ %x 255 'ff'
7747
+ %.2f 0.125 '0.13'
7748
+ %'f 1000 '1,000'
7749
+ */
7750
+
7751
+ // Usage: format(formatString, [values])
7752
+ // Tip: When reusing the same format many times, use formatter() for 5x - 10x better performance
7753
+ //
7754
+ function format(fmt) {
7755
+ var fn = formatter(fmt);
7756
+ var str = fn.apply(null, Array.prototype.slice.call(arguments, 1));
7757
+ return str;
7545
7758
  }
7546
7759
 
7547
- function initializeArray(arr, init) {
7548
- for (var i=0, len=arr.length; i<len; i++) {
7549
- arr[i] = init;
7760
+ function formatValue(val, matches) {
7761
+ var flags = matches[1];
7762
+ var padding = matches[2];
7763
+ var decimals = matches[3] ? parseInt(matches[3].substr(1)) : void 0;
7764
+ var type = matches[4];
7765
+ var isString = type == 's',
7766
+ isHex = type == 'x' || type == 'X',
7767
+ // isInt = type == 'd' || type == 'i',
7768
+ // isFloat = type == 'f',
7769
+ isNumber = !isString;
7770
+
7771
+ var sign = "",
7772
+ padDigits = 0,
7773
+ isZero = false,
7774
+ isNeg = false;
7775
+
7776
+ var str, padChar, padStr;
7777
+ if (isString) {
7778
+ str = String(val);
7779
+ }
7780
+ else if (isHex) {
7781
+ str = val.toString(16);
7782
+ if (type == 'X')
7783
+ str = str.toUpperCase();
7784
+ }
7785
+ else if (isNumber) {
7786
+ // str = formatNumberForDisplay(val, isInt ? 0 : decimals);
7787
+ str = numToStr(val, decimals);
7788
+ if (str[0] == '-') {
7789
+ isNeg = true;
7790
+ str = str.substr(1);
7791
+ }
7792
+ isZero = parseFloat(str) == 0;
7793
+ if (flags.indexOf("'") != -1 || flags.indexOf(',') != -1) {
7794
+ str = addThousandsSep(str);
7795
+ }
7796
+ if (!isZero) { // BUG: sign is added when num rounds to 0
7797
+ if (isNeg) {
7798
+ sign = "\u2212"; // U+2212
7799
+ } else if (flags.indexOf('+') != -1) {
7800
+ sign = '+';
7801
+ }
7802
+ }
7550
7803
  }
7551
- return arr;
7552
- }
7553
7804
 
7554
- function replaceArray(arr, arr2) {
7555
- arr.splice(0, arr.length);
7556
- for (var i=0, n=arr2.length; i<n; i++) {
7557
- arr.push(arr2[i]);
7805
+ if (padding) {
7806
+ var strLen = str.length + sign.length;
7807
+ var minWidth = parseInt(padding, 10);
7808
+ if (strLen < minWidth) {
7809
+ padDigits = minWidth - strLen;
7810
+ padChar = flags.indexOf('0') == -1 ? ' ' : '0';
7811
+ padStr = repeatString(padChar, padDigits);
7812
+ }
7558
7813
  }
7559
- }
7560
7814
 
7561
- function repeatString(src, n) {
7562
- var str = "";
7563
- for (var i=0; i<n; i++)
7564
- str += src;
7815
+ if (padDigits == 0) {
7816
+ str = sign + str;
7817
+ } else if (padChar == '0') {
7818
+ str = sign + padStr + str;
7819
+ } else {
7820
+ str = padStr + sign + str;
7821
+ }
7565
7822
  return str;
7566
7823
  }
7567
7824
 
7568
- function splitLines(str) {
7569
- return str.split(/\r?\n/);
7570
- }
7571
-
7572
- function pluralSuffix(count) {
7573
- return count != 1 ? 's' : '';
7574
- }
7825
+ // Get a function for interpolating formatted values into a string.
7826
+ function formatter(fmt) {
7827
+ var codeRxp = /%([',+0]*)([1-9]?)((?:\.[1-9])?)([sdifxX%])/g;
7828
+ var literals = [],
7829
+ formatCodes = [],
7830
+ startIdx = 0,
7831
+ prefix = "",
7832
+ matches = codeRxp.exec(fmt),
7833
+ literal;
7575
7834
 
7576
- function endsWith(str, ending) {
7577
- return str.indexOf(ending, str.length - ending.length) !== -1;
7578
- }
7835
+ while (matches) {
7836
+ literal = fmt.substring(startIdx, codeRxp.lastIndex - matches[0].length);
7837
+ if (matches[0] == '%%') {
7838
+ prefix += literal + '%';
7839
+ } else {
7840
+ literals.push(prefix + literal);
7841
+ prefix = '';
7842
+ formatCodes.push(matches);
7843
+ }
7844
+ startIdx = codeRxp.lastIndex;
7845
+ matches = codeRxp.exec(fmt);
7846
+ }
7847
+ literals.push(prefix + fmt.substr(startIdx));
7579
7848
 
7580
- function lpad(str, size, pad) {
7581
- pad = pad || ' ';
7582
- str = String(str);
7583
- return repeatString(pad, size - str.length) + str;
7849
+ return function() {
7850
+ var str = literals[0],
7851
+ n = arguments.length;
7852
+ if (n != formatCodes.length) {
7853
+ error$1("[format()] Data does not match format string; format:", fmt, "data:", arguments);
7854
+ }
7855
+ for (var i=0; i<n; i++) {
7856
+ str += formatValue(arguments[i], formatCodes[i]) + literals[i+1];
7857
+ }
7858
+ return str;
7859
+ };
7584
7860
  }
7585
7861
 
7586
- function rpad(str, size, pad) {
7587
- pad = pad || ' ';
7588
- str = String(str);
7589
- return str + repeatString(pad, size - str.length);
7862
+ function wildcardToRegExp(name) {
7863
+ var rxp = name.split('*').map(function(str) {
7864
+ return regexEscape(str);
7865
+ }).join('.*');
7866
+ return new RegExp('^' + rxp + '$');
7590
7867
  }
7591
7868
 
7592
- function trim(str) {
7593
- return ltrim(rtrim(str));
7869
+ function createBuffer(arg, arg2) {
7870
+ if (isInteger(arg)) {
7871
+ return B.allocUnsafe ? B.allocUnsafe(arg) : new B(arg);
7872
+ } else {
7873
+ // check allocUnsafe to make sure Buffer.from() will accept strings (it didn't before Node v5.10)
7874
+ return B.from && B.allocUnsafe ? B.from(arg, arg2) : new B(arg, arg2);
7875
+ }
7594
7876
  }
7595
7877
 
7596
- var ltrimRxp = /^\s+/;
7597
- function ltrim(str) {
7598
- return str.replace(ltrimRxp, '');
7878
+ function toBuffer(src) {
7879
+ if (src instanceof B) return src;
7880
+ if (src instanceof ArrayBuffer) return B.from(src);
7881
+ if (src instanceof Uint8Array) {
7882
+ return B.from(src.buffer, src.byteOffset, src.byteLength);
7883
+ }
7884
+ error$1('Unexpected argument type');
7599
7885
  }
7600
7886
 
7601
- var rtrimRxp = /\s+$/;
7602
- function rtrim(str) {
7603
- return str.replace(rtrimRxp, '');
7887
+ function expandoBuffer(constructor, rate) {
7888
+ var capacity = 0,
7889
+ k = rate >= 1 ? rate : 1.2,
7890
+ buf;
7891
+ return function(size) {
7892
+ if (size > capacity) {
7893
+ capacity = Math.ceil(size * k);
7894
+ buf = constructor ? new constructor(capacity) : createBuffer(capacity);
7895
+ }
7896
+ return buf;
7897
+ };
7604
7898
  }
7605
7899
 
7606
- function addThousandsSep(str) {
7607
- var fmt = '',
7608
- start = str[0] == '-' ? 1 : 0,
7609
- dec = str.indexOf('.'),
7610
- end = str.length,
7611
- ins = (dec == -1 ? end : dec) - 3;
7612
- while (ins > start) {
7613
- fmt = ',' + str.substring(ins, end) + fmt;
7614
- end = ins;
7615
- ins -= 3;
7900
+ function copyElements(src, i, dest, j, n, rev) {
7901
+ var same = src == dest || src.buffer && src.buffer == dest.buffer;
7902
+ var inc = 1,
7903
+ offs = 0,
7904
+ k;
7905
+ if (rev) {
7906
+ if (same) error$1('copy error');
7907
+ inc = -1;
7908
+ offs = n - 1;
7909
+ }
7910
+ if (same && j > i) {
7911
+ for (k=n-1; k>=0; k--) {
7912
+ dest[j + k] = src[i + k];
7913
+ }
7914
+ } else {
7915
+ for (k=0; k<n; k++, offs += inc) {
7916
+ dest[k + j] = src[i + offs];
7917
+ }
7616
7918
  }
7617
- return str.substring(0, end) + fmt;
7618
- }
7619
-
7620
- function numToStr(num, decimals) {
7621
- return decimals >= 0 ? num.toFixed(decimals) : String(num);
7622
7919
  }
7623
7920
 
7624
- function formatNumber(val) {
7625
- return val + '';
7921
+ function extendBuffer(src, newLen, copyLen) {
7922
+ var len = Math.max(src.length, newLen);
7923
+ var n = copyLen || src.length;
7924
+ var dest = new src.constructor(len);
7925
+ copyElements(src, 0, dest, 0, n);
7926
+ return dest;
7626
7927
  }
7627
7928
 
7628
- function formatIntlNumber(val) {
7629
- var str = formatNumber(val);
7630
- return '"' + str.replace('.', ',') + '"'; // need to quote if comma-delimited
7929
+ function mergeNames(name1, name2) {
7930
+ var merged;
7931
+ if (name1 && name2) {
7932
+ merged = findStringPrefix(name1, name2).replace(/[-_]$/, '');
7933
+ }
7934
+ return merged || '';
7631
7935
  }
7632
7936
 
7633
- function formatNumberForDisplay(num, decimals, nullStr, showPos) {
7634
- var fmt;
7635
- if (isNaN(num)) {
7636
- fmt = nullStr || '-';
7637
- } else {
7638
- fmt = numToStr(num, decimals);
7639
- fmt = addThousandsSep(fmt);
7640
- if (showPos && parseFloat(fmt) > 0) {
7641
- fmt = "+" + fmt;
7642
- }
7937
+ function findStringPrefix(a, b) {
7938
+ var i = 0;
7939
+ for (var n=a.length; i<n; i++) {
7940
+ if (a[i] !== b[i]) break;
7643
7941
  }
7644
- return fmt;
7942
+ return a.substr(0, i);
7645
7943
  }
7646
7944
 
7647
- function shuffle(arr) {
7648
- var tmp, i, j;
7649
- for (i = arr.length - 1; i > 0; i--) {
7650
- j = Math.floor(Math.random() * (i + 1));
7651
- tmp = arr[i];
7652
- arr[i] = arr[j];
7653
- arr[j] = tmp;
7945
+ function formatVersionedName(name, i) {
7946
+ var suffix = String(i);
7947
+ if (/[0-9]$/.test(name)) {
7948
+ suffix = '-' + suffix;
7654
7949
  }
7950
+ return name + suffix;
7655
7951
  }
7656
7952
 
7657
- function pickOne(arr) {
7658
- return arr[Math.floor(Math.random() * arr.length)];
7659
- }
7953
+ function uniqifyNames(names, formatter) {
7954
+ var counts = countValues(names),
7955
+ format = formatter || formatVersionedName,
7956
+ names2 = [];
7660
7957
 
7661
- // Sort an array of objects based on one or more properties.
7662
- // Usage: sortOn(array, key1, asc?[, key2, asc? ...])
7663
- //
7664
- function sortOn(arr) {
7665
- var comparators = [];
7666
- for (var i=1; i<arguments.length; i+=2) {
7667
- comparators.push(getKeyComparator(arguments[i], arguments[i+1]));
7668
- }
7669
- arr.sort(function(a, b) {
7670
- var cmp = 0,
7671
- i = 0,
7672
- n = comparators.length;
7673
- while (i < n && cmp === 0) {
7674
- cmp = comparators[i](a, b);
7958
+ names.forEach(function(name) {
7959
+ var i = 0,
7960
+ candidate = name,
7961
+ versionedName;
7962
+ while (
7963
+ names2.indexOf(candidate) > -1 || // candidate name has already been used
7964
+ candidate == name && counts[candidate] > 1 || // duplicate unversioned names
7965
+ candidate != name && counts[candidate] > 0) { // versioned name is a preexisting name
7675
7966
  i++;
7967
+ versionedName = format(name, i);
7968
+ if (!versionedName || versionedName == candidate) {
7969
+ throw new Error("Naming error"); // catch buggy versioning function
7970
+ }
7971
+ candidate = versionedName;
7676
7972
  }
7677
- return cmp;
7973
+ names2.push(candidate);
7678
7974
  });
7679
- return arr;
7975
+ return names2;
7680
7976
  }
7681
7977
 
7682
- // Sort array of values that can be compared with < > operators (strings, numbers)
7683
- // null, undefined and NaN are sorted to the end of the array
7684
- // default order is ascending
7685
- //
7686
- function genericSort(arr, ascending) {
7687
- var compare = getGenericComparator(ascending);
7688
- Array.prototype.sort.call(arr, compare);
7689
- return arr;
7690
- }
7691
7978
 
7692
- function getSortedIds(arr, asc) {
7693
- var ids = range(arr.length);
7694
- sortArrayIndex(ids, arr, asc);
7695
- return ids;
7979
+ // Assume: @raw is string, undefined or null
7980
+ function parseString(raw) {
7981
+ return raw ? raw : "";
7696
7982
  }
7697
7983
 
7698
- function sortArrayIndex(ids, arr, asc) {
7699
- var compare = getGenericComparator(asc);
7700
- ids.sort(function(i, j) {
7701
- // added i, j comparison to guarantee that sort is stable
7702
- var cmp = compare(arr[i], arr[j]);
7703
- return cmp > 0 || cmp === 0 && i > j ? 1 : -1;
7704
- });
7984
+ // Assume: @raw is string, undefined or null
7985
+ // Use null instead of NaN for unparsable values
7986
+ // (in part because if NaN is used, empty strings get converted to "NaN"
7987
+ // when re-exported).
7988
+ function parseNumber(raw) {
7989
+ return parseToNum(raw, cleanNumericString);
7705
7990
  }
7706
7991
 
7707
- function reorderArray(arr, idxs) {
7708
- var len = idxs.length;
7709
- var arr2 = [];
7710
- for (var i=0; i<len; i++) {
7711
- var idx = idxs[i];
7712
- if (idx < 0 || idx >= len) error("Out-of-bounds array idx");
7713
- arr2[i] = arr[idx];
7714
- }
7715
- replaceArray(arr, arr2);
7992
+ function parseIntlNumber(raw) {
7993
+ return parseToNum(raw, convertIntlNumString);
7716
7994
  }
7717
7995
 
7718
- function getKeyComparator(key, asc) {
7719
- var compare = getGenericComparator(asc);
7720
- return function(a, b) {
7721
- return compare(a[key], b[key]);
7722
- };
7996
+ function parseToNum(raw, clean) {
7997
+ var str = String(raw).trim();
7998
+ var parsed = str ? Number(clean(str)) : NaN;
7999
+ return isNaN(parsed) ? null : parsed;
7723
8000
  }
7724
8001
 
7725
- function getGenericComparator(asc) {
7726
- asc = asc !== false && asc != 'descending'; // ascending is the default
7727
- return function(a, b) {
7728
- var retn = 0;
7729
- if (b == null) {
7730
- retn = a == null ? 0 : -1;
7731
- } else if (a == null) {
7732
- retn = 1;
7733
- } else if (a < b) {
7734
- retn = asc ? -1 : 1;
7735
- } else if (a > b) {
7736
- retn = asc ? 1 : -1;
7737
- } else if (a !== a) {
7738
- retn = 1;
7739
- } else if (b !== b) {
7740
- retn = -1;
7741
- }
7742
- return retn;
7743
- };
8002
+ // Remove comma separators from strings
8003
+ function cleanNumericString(str) {
8004
+ return (str.indexOf(',') > 0) ? str.replace(/,([0-9]{3})/g, '$1') : str;
7744
8005
  }
7745
8006
 
7746
-
7747
- // Generic in-place sort (null, NaN, undefined not handled)
7748
- function quicksort(arr, asc) {
7749
- quicksortPartition(arr, 0, arr.length-1);
7750
- if (asc === false) Array.prototype.reverse.call(arr); // Works with typed arrays
7751
- return arr;
8007
+ function convertIntlNumString(str) {
8008
+ str = str.replace(/[ .]([0-9]{3})/g, '$1');
8009
+ return str.replace(',', '.');
7752
8010
  }
7753
8011
 
7754
- // Moved out of quicksort() (saw >100% speedup in Chrome with deep recursion)
7755
- function quicksortPartition (a, lo, hi) {
7756
- var i = lo,
7757
- j = hi,
7758
- pivot, tmp;
7759
- while (i < hi) {
7760
- pivot = a[lo + hi >> 1]; // avoid n^2 performance on sorted arrays
7761
- while (i <= j) {
7762
- while (a[i] < pivot) i++;
7763
- while (a[j] > pivot) j--;
7764
- if (i <= j) {
7765
- tmp = a[i];
7766
- a[i] = a[j];
7767
- a[j] = tmp;
7768
- i++;
7769
- j--;
7770
- }
8012
+ function trimQuotes(str) {
8013
+ var len = str.length, first, last;
8014
+ if (len >= 2) {
8015
+ first = str.charAt(0);
8016
+ last = str.charAt(len-1);
8017
+ // if (first == '"' && last == '"' && !str.includes('","') ||
8018
+ // first == "'" && last == "'" && !str.includes("','")) {
8019
+ // don't strip if there are unescaped quotes
8020
+ // e.g. expressions that start and end with quotes
8021
+ // e.g. comma-separated list of quoted values
8022
+ if (first == '"' && last == '"' && !/[^\\]"./.test(str) ||
8023
+ first == "'" && last == "'" && !/[^\\]'./.test(str)) {
8024
+ str = str.substr(1, len-2);
8025
+ // remove string escapes
8026
+ str = str.replace(first == '"' ? /\\(?=")/g : /\\(?=')/g, '');
7771
8027
  }
7772
- if (lo < j) quicksortPartition(a, lo, j);
7773
- lo = i;
7774
- j = hi;
7775
- }
7776
- }
7777
-
7778
-
7779
- function findRankByValue(arr, value) {
7780
- if (isNaN(value)) return arr.length;
7781
- var rank = 1;
7782
- for (var i=0, n=arr.length; i<n; i++) {
7783
- if (value > arr[i]) rank++;
7784
8028
  }
7785
- return rank;
8029
+ return str;
7786
8030
  }
7787
8031
 
7788
- function findValueByPct(arr, pct) {
7789
- var rank = Math.ceil((1-pct) * (arr.length));
7790
- return findValueByRank(arr, rank);
7791
- }
8032
+ // Default export so consumers can do `import utils from './mapshaper-utils'`
8033
+ // and call `utils.isObject(x)` etc. Listing the names explicitly (instead
8034
+ // of using `import * as utils from './mapshaper-utils'`) avoids a
8035
+ // self-import and the resulting Rollup circular-dependency warning.
8036
+ var utils = {
8037
+ addThousandsSep, addslashes, arrayToIndex,
8038
+ clamp, cleanNumericString, contains, copyElements, countValues, createBuffer,
8039
+ defaults, difference,
8040
+ endsWith, every, expandoBuffer, extend, extendBuffer,
8041
+ find, findMedian, findQuantile, findRankByValue, findStringPrefix,
8042
+ findValueByPct, findValueByRank, forEach, forEachProperty, format,
8043
+ formatDateISO, formatIntlNumber, formatNumber, formatNumberForDisplay,
8044
+ formatVersionedName, formatter,
8045
+ genericSort, getArrayBounds, getGenericComparator, getKeyComparator,
8046
+ getSortedIds, getUniqueName, groupBy,
8047
+ htmlEscape,
8048
+ indexOf, indexOn, inherit, initializeArray, intersection,
8049
+ isArray, isArrayLike, isBoolean, isDate, isEven, isFiniteNumber, isFunction,
8050
+ isInteger, isNonNegNumber, isNumber, isObject, isOdd, isPromise, isString,
8051
+ isValidNumber,
8052
+ lpad, ltrim,
8053
+ mean, merge, mergeNames,
8054
+ numToStr,
8055
+ parseIntlNumber, parseNumber, parseString, pickOne, pluck,
8056
+ pluralSuffix, promisify,
8057
+ quicksort, quicksortPartition,
8058
+ range, reduceAsync, regexEscape, reorderArray, repeat, repeatString,
8059
+ replaceArray, rpad, rtrim,
8060
+ shuffle, some, sortArrayIndex, sortOn, splitLines, sum,
8061
+ toArray, toBuffer, trim, trimQuotes,
8062
+ uniq, uniqifyNames,
8063
+ wildcardToRegExp
8064
+ };
7792
8065
 
7793
- // See http://ndevilla.free.fr/median/median/src/wirth.c
7794
- // Elements of @arr are reordered
8066
+ // This module provides a way for multiple jobs to run together asynchronously
8067
+ // while keeping job-level context variables (like "defs") separate.
7795
8068
  //
7796
- function findValueByRank(arr, rank) {
7797
- if (!arr.length || rank < 1 || rank > arr.length) error("[findValueByRank()] invalid input");
8069
+ // We deliberately do NOT import from mapshaper-logging here -- the logging
8070
+ // module imports from this one, and avoiding the back-edge keeps the
8071
+ // foundational dependency graph acyclic. (The single error path below is
8072
+ // an internal-bug guard, not a user-facing message, so a plain Error is
8073
+ // adequate.)
7798
8074
 
7799
- rank = clamp(rank | 0, 1, arr.length);
7800
- var k = rank - 1, // conv. rank to array index
7801
- n = arr.length,
7802
- l = 0,
7803
- m = n - 1,
7804
- i, j, val, tmp;
8075
+ var stash = {};
7805
8076
 
7806
- while (l < m) {
7807
- val = arr[k];
7808
- i = l;
7809
- j = m;
7810
- do {
7811
- while (arr[i] < val) {i++;}
7812
- while (val < arr[j]) {j--;}
7813
- if (i <= j) {
7814
- tmp = arr[i];
7815
- arr[i] = arr[j];
7816
- arr[j] = tmp;
7817
- i++;
7818
- j--;
7819
- }
7820
- } while (i <= j);
7821
- if (j < k) l = i;
7822
- if (k < i) m = j;
8077
+ function stashVar(key, val) {
8078
+ if (key in stash) {
8079
+ throw new Error('Tried to replace a stashed variable: ' + key);
7823
8080
  }
7824
- return arr[k];
8081
+ stash[key] = val;
7825
8082
  }
7826
8083
 
7827
- function findMedian(arr) {
7828
- return findQuantile(arr, 0.5);
8084
+ function getStashedVar(key) {
8085
+ if (key in stash === false) {
8086
+ return undefined; // to support running commands in tests
8087
+ // error('Tried to read a nonexistent variable from the stash:', key);
8088
+ }
8089
+ return stash[key];
7829
8090
  }
7830
8091
 
7831
- function findQuantile(arr, k) {
7832
- var n = arr.length,
7833
- i1 = Math.floor((n - 1) * k),
7834
- i2 = Math.ceil((n - 1) * k);
7835
- if (i1 < 0 || i2 >= n) return NaN;
7836
- var v1 = findValueByRank(arr, i1 + 1);
7837
- if (i1 == i2) return v1;
7838
- var v2 = findValueByRank(arr, i2 + 1);
7839
- // use linear interpolation
7840
- var w1 = i2 / (n - 1) - k;
7841
- var w2 = k - i1 / (n - 1);
7842
- var v = (v1 * w1 + v2 * w2) * (n - 1);
7843
- return v;
8092
+ function clearStash() {
8093
+ stash = {};
7844
8094
  }
7845
8095
 
7846
- function mean(arr) {
7847
- var count = 0,
7848
- avg = NaN,
7849
- val;
7850
- for (var i=0, n=arr.length; i<n; i++) {
7851
- val = arr[i];
7852
- if (isNaN(val)) continue;
7853
- avg = ++count == 1 ? val : val / count + (count - 1) / count * avg;
7854
- }
7855
- return avg;
8096
+ var LOGGING = false;
8097
+ var STDOUT = false; // use stdout for status messages
8098
+ var _error, _stop, _message, _warn;
8099
+
8100
+ var _interrupt = function() {
8101
+ throw new NonFatalError(formatLogArgs(arguments));
8102
+ };
8103
+
8104
+ var onceMessages = [];
8105
+
8106
+ setLoggingForCLI();
8107
+
8108
+ function getLoggingSetter() {
8109
+ var e = _error, s = _stop, m = _message, w = _warn;
8110
+ return function() {
8111
+ setLoggingFunctions(m, e, s, w);
8112
+ };
7856
8113
  }
7857
8114
 
8115
+ function setLoggingForCLI() {
8116
+ function stop() {
8117
+ throw new UserError(formatLogArgs(arguments));
8118
+ }
7858
8119
 
7859
- /*
7860
- A simplified version of printf formatting
7861
- Format codes: %[flags][width][.precision]type
8120
+ function error() {
8121
+ var msg = utils.toArray(arguments).join(' ');
8122
+ throw new Error(msg);
8123
+ }
7862
8124
 
7863
- supported flags:
7864
- + add '+' before positive numbers
7865
- 0 left-pad with '0'
7866
- ' Add thousands separator
7867
- width: 1 to many
7868
- precision: .(1 to many)
7869
- type:
7870
- s string
7871
- di integers
7872
- f decimal numbers
7873
- xX hexidecimal (unsigned)
7874
- % literal '%'
8125
+ function message() {
8126
+ logArgs(arguments);
8127
+ }
7875
8128
 
7876
- Examples:
7877
- code val formatted
7878
- %+d 1 '+1'
7879
- %4i 32 ' 32'
7880
- %04i 32 '0032'
7881
- %x 255 'ff'
7882
- %.2f 0.125 '0.13'
7883
- %'f 1000 '1,000'
7884
- */
8129
+ // CLI warning is just a message (GUI behaves differently)
8130
+ var warn = message;
7885
8131
 
7886
- // Usage: format(formatString, [values])
7887
- // Tip: When reusing the same format many times, use formatter() for 5x - 10x better performance
7888
- //
7889
- function format(fmt) {
7890
- var fn = formatter(fmt);
7891
- var str = fn.apply(null, Array.prototype.slice.call(arguments, 1));
7892
- return str;
8132
+ setLoggingFunctions(message, error, stop, warn);
7893
8133
  }
7894
8134
 
7895
- function formatValue(val, matches) {
7896
- var flags = matches[1];
7897
- var padding = matches[2];
7898
- var decimals = matches[3] ? parseInt(matches[3].substr(1)) : void 0;
7899
- var type = matches[4];
7900
- var isString = type == 's',
7901
- isHex = type == 'x' || type == 'X',
7902
- // isInt = type == 'd' || type == 'i',
7903
- // isFloat = type == 'f',
7904
- isNumber = !isString;
8135
+ function enableLogging() {
8136
+ LOGGING = true;
8137
+ }
7905
8138
 
7906
- var sign = "",
7907
- padDigits = 0,
7908
- isZero = false,
7909
- isNeg = false;
8139
+ function disableLogging() {
8140
+ LOGGING = false;
8141
+ }
7910
8142
 
7911
- var str, padChar, padStr;
7912
- if (isString) {
7913
- str = String(val);
7914
- }
7915
- else if (isHex) {
7916
- str = val.toString(16);
7917
- if (type == 'X')
7918
- str = str.toUpperCase();
7919
- }
7920
- else if (isNumber) {
7921
- // str = formatNumberForDisplay(val, isInt ? 0 : decimals);
7922
- str = numToStr(val, decimals);
7923
- if (str[0] == '-') {
7924
- isNeg = true;
7925
- str = str.substr(1);
7926
- }
7927
- isZero = parseFloat(str) == 0;
7928
- if (flags.indexOf("'") != -1 || flags.indexOf(',') != -1) {
7929
- str = addThousandsSep(str);
7930
- }
7931
- if (!isZero) { // BUG: sign is added when num rounds to 0
7932
- if (isNeg) {
7933
- sign = "\u2212"; // U+2212
7934
- } else if (flags.indexOf('+') != -1) {
7935
- sign = '+';
7936
- }
7937
- }
7938
- }
8143
+ function loggingEnabled() {
8144
+ return !!LOGGING;
8145
+ }
7939
8146
 
7940
- if (padding) {
7941
- var strLen = str.length + sign.length;
7942
- var minWidth = parseInt(padding, 10);
7943
- if (strLen < minWidth) {
7944
- padDigits = minWidth - strLen;
7945
- padChar = flags.indexOf('0') == -1 ? ' ' : '0';
7946
- padStr = repeatString(padChar, padDigits);
7947
- }
7948
- }
8147
+ // Handle an unexpected condition (internal error)
8148
+ function error() {
8149
+ _error.apply(null, utils.toArray(arguments));
8150
+ }
7949
8151
 
7950
- if (padDigits == 0) {
7951
- str = sign + str;
7952
- } else if (padChar == '0') {
7953
- str = sign + padStr + str;
7954
- } else {
7955
- str = padStr + sign + str;
7956
- }
7957
- return str;
8152
+ // Handle an error caused by invalid input or misuse of API
8153
+ function stop() {
8154
+ // _stop.apply(null, utils.toArray(arguments));
8155
+ _stop.apply(null, messageArgs(arguments));
7958
8156
  }
7959
8157
 
7960
- // Get a function for interpolating formatted values into a string.
7961
- function formatter(fmt) {
7962
- var codeRxp = /%([',+0]*)([1-9]?)((?:\.[1-9])?)([sdifxX%])/g;
7963
- var literals = [],
7964
- formatCodes = [],
7965
- startIdx = 0,
7966
- prefix = "",
7967
- matches = codeRxp.exec(fmt),
7968
- literal;
8158
+ function interrupt() {
8159
+ _interrupt.apply(null, utils.toArray(arguments));
8160
+ }
7969
8161
 
7970
- while (matches) {
7971
- literal = fmt.substring(startIdx, codeRxp.lastIndex - matches[0].length);
7972
- if (matches[0] == '%%') {
7973
- prefix += literal + '%';
7974
- } else {
7975
- literals.push(prefix + literal);
7976
- prefix = '';
7977
- formatCodes.push(matches);
7978
- }
7979
- startIdx = codeRxp.lastIndex;
7980
- matches = codeRxp.exec(fmt);
7981
- }
7982
- literals.push(prefix + fmt.substr(startIdx));
8162
+ // Print a status message
8163
+ function message() {
8164
+ _message.apply(null, messageArgs(arguments));
8165
+ }
7983
8166
 
7984
- return function() {
7985
- var str = literals[0],
7986
- n = arguments.length;
7987
- if (n != formatCodes.length) {
7988
- error("[format()] Data does not match format string; format:", fmt, "data:", arguments);
7989
- }
7990
- for (var i=0; i<n; i++) {
7991
- str += formatValue(arguments[i], formatCodes[i]) + literals[i+1];
7992
- }
7993
- return str;
7994
- };
8167
+ function warn() {
8168
+ _warn.apply(null, messageArgs(arguments));
7995
8169
  }
7996
8170
 
7997
- function wildcardToRegExp(name) {
7998
- var rxp = name.split('*').map(function(str) {
7999
- return regexEscape(str);
8000
- }).join('.*');
8001
- return new RegExp('^' + rxp + '$');
8171
+ function warnOnce() {
8172
+ var str = formatLogArgs(arguments);
8173
+ if (onceMessages.includes(str)) return;
8174
+ onceMessages.push(str);
8175
+ _warn.apply(null, messageArgs(arguments));
8002
8176
  }
8003
8177
 
8004
- function createBuffer(arg, arg2) {
8005
- if (isInteger(arg)) {
8006
- return B.allocUnsafe ? B.allocUnsafe(arg) : new B(arg);
8007
- } else {
8008
- // check allocUnsafe to make sure Buffer.from() will accept strings (it didn't before Node v5.10)
8009
- return B.from && B.allocUnsafe ? B.from(arg, arg2) : new B(arg, arg2);
8010
- }
8178
+ // A way for the GUI to replace the CLI logging functions
8179
+ function setLoggingFunctions(message, error, stop, warn) {
8180
+ _message = message;
8181
+ _error = error;
8182
+ _stop = stop;
8183
+ _warn = warn;
8011
8184
  }
8012
8185
 
8013
- function toBuffer(src) {
8014
- if (src instanceof B) return src;
8015
- if (src instanceof ArrayBuffer) return B.from(src);
8016
- if (src instanceof Uint8Array) {
8017
- return B.from(src.buffer, src.byteOffset, src.byteLength);
8186
+ // get detailed error information from error stack (if available)
8187
+ // Example stack string (Node.js):
8188
+ /*
8189
+ /Users/someuser/somescript.js:226
8190
+ opacity: Math.round(weight * 5 / 5 // 0.2 0.4 0.6 etc
8191
+ ^
8192
+
8193
+ SyntaxError: missing ) after argument list
8194
+ at internalCompileFunction (node:internal/vm:73:18)
8195
+ at wrapSafe (node:internal/modules/cjs/loader:1149:20)
8196
+ at Module._compile (node:internal/modules/cjs/loader:1190:27)
8197
+ ...
8198
+ */
8199
+ function getErrorDetail(e) {
8200
+ var parts = (typeof e.stack == 'string') ? e.stack.split(/\n\s*\n/) : [];
8201
+ if (parts.length > 1 || true) {
8202
+ return '\nError details:\n' + parts[0];
8018
8203
  }
8019
- error('Unexpected argument type');
8204
+ return '';
8020
8205
  }
8021
8206
 
8022
- function expandoBuffer(constructor, rate) {
8023
- var capacity = 0,
8024
- k = rate >= 1 ? rate : 1.2,
8025
- buf;
8026
- return function(size) {
8027
- if (size > capacity) {
8028
- capacity = Math.ceil(size * k);
8029
- buf = constructor ? new constructor(capacity) : createBuffer(capacity);
8030
- }
8031
- return buf;
8032
- };
8207
+ // print a message to stdout
8208
+ function print() {
8209
+ STDOUT = true; // tell logArgs() to print to stdout, not stderr
8210
+ // calling message() adds the "[command name]" prefix
8211
+ _message(utils.toArray(arguments));
8212
+ STDOUT = false;
8033
8213
  }
8034
8214
 
8035
- function copyElements(src, i, dest, j, n, rev) {
8036
- var same = src == dest || src.buffer && src.buffer == dest.buffer;
8037
- var inc = 1,
8038
- offs = 0,
8039
- k;
8040
- if (rev) {
8041
- if (same) error('copy error');
8042
- inc = -1;
8043
- offs = n - 1;
8044
- }
8045
- if (same && j > i) {
8046
- for (k=n-1; k>=0; k--) {
8047
- dest[j + k] = src[i + k];
8048
- }
8049
- } else {
8050
- for (k=0; k<n; k++, offs += inc) {
8051
- dest[k + j] = src[i + offs];
8052
- }
8215
+ function verbose() {
8216
+ // verbose can be set globally with the -verbose command or separately for each command
8217
+ if (useVerbose()) {
8218
+ message.apply(null, arguments);
8053
8219
  }
8054
8220
  }
8055
8221
 
8056
- function extendBuffer(src, newLen, copyLen) {
8057
- var len = Math.max(src.length, newLen);
8058
- var n = copyLen || src.length;
8059
- var dest = new src.constructor(len);
8060
- copyElements(src, 0, dest, 0, n);
8061
- return dest;
8222
+ function useVerbose() {
8223
+ return getStashedVar('VERBOSE');
8062
8224
  }
8063
8225
 
8064
- function mergeNames(name1, name2) {
8065
- var merged;
8066
- if (name1 && name2) {
8067
- merged = findStringPrefix(name1, name2).replace(/[-_]$/, '');
8068
- }
8069
- return merged || '';
8226
+ function useDebug() {
8227
+ return getStashedVar('DEBUG');
8070
8228
  }
8071
8229
 
8072
- function findStringPrefix(a, b) {
8073
- var i = 0;
8074
- for (var n=a.length; i<n; i++) {
8075
- if (a[i] !== b[i]) break;
8230
+ function debug() {
8231
+ if (useDebug()) {
8232
+ logArgs(arguments);
8076
8233
  }
8077
- return a.substr(0, i);
8078
8234
  }
8079
8235
 
8080
- function parsePercent(o) {
8081
- var str = String(o);
8082
- var isPct = str.indexOf('%') > 0;
8083
- var pct;
8084
- if (isPct) {
8085
- pct = Number(str.replace('%', '')) / 100;
8086
- } else {
8087
- pct = Number(str);
8088
- }
8089
- if (!(pct >= 0 && pct <= 1)) {
8090
- stop(format("Invalid percentage: %s", str));
8236
+ function time(slug) {
8237
+ if (useDebug()) {
8238
+ console.time(slug);
8091
8239
  }
8092
- return pct;
8093
8240
  }
8094
8241
 
8095
- function formatVersionedName(name, i) {
8096
- var suffix = String(i);
8097
- if (/[0-9]$/.test(name)) {
8098
- suffix = '-' + suffix;
8242
+ function timeEnd(slug) {
8243
+ if (useDebug()) {
8244
+ console.timeEnd(slug);
8099
8245
  }
8100
- return name + suffix;
8101
8246
  }
8102
8247
 
8103
- function uniqifyNames(names, formatter) {
8104
- var counts = countValues(names),
8105
- format = formatter || formatVersionedName,
8106
- names2 = [];
8107
-
8108
- names.forEach(function(name) {
8109
- var i = 0,
8110
- candidate = name,
8111
- versionedName;
8112
- while (
8113
- names2.indexOf(candidate) > -1 || // candidate name has already been used
8114
- candidate == name && counts[candidate] > 1 || // duplicate unversioned names
8115
- candidate != name && counts[candidate] > 0) { // versioned name is a preexisting name
8116
- i++;
8117
- versionedName = format(name, i);
8118
- if (!versionedName || versionedName == candidate) {
8119
- throw new Error("Naming error"); // catch buggy versioning function
8120
- }
8121
- candidate = versionedName;
8248
+ function printError(err) {
8249
+ var msg;
8250
+ if (!LOGGING) return;
8251
+ if (utils.isString(err)) {
8252
+ err = new UserError(err);
8253
+ }
8254
+ if (err.name == 'NonFatalError') {
8255
+ console.error(messageArgs([err.message]).join(' '));
8256
+ } else if (err.name == 'UserError') {
8257
+ msg = err.message;
8258
+ if (!/Error/.test(msg)) {
8259
+ msg = "Error: " + msg;
8122
8260
  }
8123
- names2.push(candidate);
8124
- });
8125
- return names2;
8261
+ console.error(messageArgs([msg]).join(' '));
8262
+ console.error("Run mapshaper -h to view help");
8263
+ } else {
8264
+ // not a user error (i.e. a bug in mapshaper)
8265
+ console.error(err);
8266
+ // throw err;
8267
+ }
8126
8268
  }
8127
8269
 
8270
+ function UserError(msg) {
8271
+ var err = new Error(msg);
8272
+ err.name = 'UserError';
8273
+ return err;
8274
+ }
8128
8275
 
8129
- // Assume: @raw is string, undefined or null
8130
- function parseString(raw) {
8131
- return raw ? raw : "";
8276
+ function NonFatalError(msg) {
8277
+ var err = new Error(msg);
8278
+ err.name = 'NonFatalError';
8279
+ return err;
8132
8280
  }
8133
8281
 
8134
- // Assume: @raw is string, undefined or null
8135
- // Use null instead of NaN for unparsable values
8136
- // (in part because if NaN is used, empty strings get converted to "NaN"
8137
- // when re-exported).
8138
- function parseNumber(raw) {
8139
- return parseToNum(raw, cleanNumericString);
8282
+ function formatColumns(arr, alignments) {
8283
+ var widths = arr.reduce(function(memo, line) {
8284
+ return line.map(function(str, i) {
8285
+ return memo ? Math.max(memo[i], str.length) : str.length;
8286
+ });
8287
+ }, null);
8288
+ return arr.map(function(line) {
8289
+ line = line.map(function(str, i) {
8290
+ var rt = alignments && alignments[i] == 'right';
8291
+ var pad = (rt ? str.padStart : str.padEnd).bind(str);
8292
+ return pad(widths[i], ' ');
8293
+ });
8294
+ return ' ' + line.join(' ');
8295
+ }).join('\n');
8140
8296
  }
8141
8297
 
8142
- function parseIntlNumber(raw) {
8143
- return parseToNum(raw, convertIntlNumString);
8298
+ // Format an array of (preferably short) strings in columns for console logging.
8299
+ function formatStringsAsGrid(arr, width) {
8300
+ // TODO: variable column width
8301
+ var longest = arr.reduce(function(len, str) {
8302
+ return Math.max(len, str.length);
8303
+ }, 0),
8304
+ colWidth = longest + 2,
8305
+ perLine = Math.floor((width || 80) / colWidth) || 1;
8306
+ return arr.reduce(function(memo, name, i) {
8307
+ var col = i % perLine;
8308
+ if (i > 0 && col === 0) memo += '\n';
8309
+ if (col < perLine - 1) { // right-pad all but rightmost column
8310
+ name = utils.rpad(name, colWidth - 2, ' ');
8311
+ }
8312
+ return memo + ' ' + name;
8313
+ }, '');
8144
8314
  }
8145
8315
 
8146
- function parseToNum(raw, clean) {
8147
- var str = String(raw).trim();
8148
- var parsed = str ? Number(clean(str)) : NaN;
8149
- return isNaN(parsed) ? null : parsed;
8316
+ // expose so GUI can use it
8317
+ function formatLogArgs(args) {
8318
+ return utils.toArray(args).join(' ');
8150
8319
  }
8151
8320
 
8152
- // Remove comma separators from strings
8153
- function cleanNumericString(str) {
8154
- return (str.indexOf(',') > 0) ? str.replace(/,([0-9]{3})/g, '$1') : str;
8321
+ function messageArgs(args) {
8322
+ var arr = utils.toArray(args);
8323
+ var cmd = getStashedVar('current_command');
8324
+ if (cmd && cmd != 'help') {
8325
+ arr.unshift('[' + cmd + ']');
8326
+ }
8327
+ return arr;
8155
8328
  }
8156
8329
 
8157
- function convertIntlNumString(str) {
8158
- str = str.replace(/[ .]([0-9]{3})/g, '$1');
8159
- return str.replace(',', '.');
8330
+ function logArgs(args) {
8331
+ if (!LOGGING || getStashedVar('QUIET') || !utils.isArrayLike(args)) return;
8332
+ var msg = formatLogArgs(args);
8333
+ if (STDOUT) console.log(msg);
8334
+ else console.error(msg);
8160
8335
  }
8161
8336
 
8162
- function trimQuotes(str) {
8163
- var len = str.length, first, last;
8164
- if (len >= 2) {
8165
- first = str.charAt(0);
8166
- last = str.charAt(len-1);
8167
- // if (first == '"' && last == '"' && !str.includes('","') ||
8168
- // first == "'" && last == "'" && !str.includes("','")) {
8169
- // don't strip if there are unescaped quotes
8170
- // e.g. expressions that start and end with quotes
8171
- // e.g. comma-separated list of quoted values
8172
- if (first == '"' && last == '"' && !/[^\\]"./.test(str) ||
8173
- first == "'" && last == "'" && !/[^\\]'./.test(str)) {
8174
- str = str.substr(1, len-2);
8175
- // remove string escapes
8176
- str = str.replace(first == '"' ? /\\(?=")/g : /\\(?=')/g, '');
8177
- }
8337
+ function truncateString(str, maxLen) {
8338
+ maxLen = maxLen || 80;
8339
+ if (str.length > maxLen) {
8340
+ str = str.substring(0, maxLen - 3).trimEnd() + '...';
8178
8341
  }
8179
8342
  return str;
8180
8343
  }
@@ -8415,7 +8578,7 @@
8415
8578
  } else if (geoType == 'polygon') {
8416
8579
  test = polygonTest;
8417
8580
  } else {
8418
- error$1("Unexpected geometry type:", geoType);
8581
+ error$2("Unexpected geometry type:", geoType);
8419
8582
  }
8420
8583
  return test;
8421
8584
 
@@ -11091,7 +11254,7 @@
11091
11254
  hit.on('drag', function(e) {
11092
11255
  if (!labelSelected(e) || activeId == -1) return;
11093
11256
  if (e.id != activeId) {
11094
- error$1("Mismatched hit ids:", e.id, activeId);
11257
+ error$2("Mismatched hit ids:", e.id, activeId);
11095
11258
  }
11096
11259
  var scale = ext.getSymbolScale() || 1;
11097
11260
  var symNode, textNode;
@@ -13467,9 +13630,12 @@
13467
13630
  // is noticeably slower during animations with multiple canvases.
13468
13631
  // Highlights are drawn on a separate canvas while hovering, because this
13469
13632
  // is generally faster than redrawing all of the shapes.
13633
+ // Fast-nav forces the same path: _mainCanv has a CSS transform but stale
13634
+ // pixels, so painting on it would visually go through that transform a
13635
+ // second time and detach the highlight from its feature.
13470
13636
  this.drawOverlayLayers = function(layers, action) {
13471
13637
  var canv;
13472
- if (action == 'hover') {
13638
+ if (action == 'hover' || _fastActive) {
13473
13639
  canv = _overlayCanv;
13474
13640
  _overlayCanv.prep(_ext);
13475
13641
  } else {
@@ -14649,7 +14815,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14649
14815
  ]
14650
14816
  });
14651
14817
  } else {
14652
- error$1('unsupported map style:', style);
14818
+ error$2('unsupported map style:', style);
14653
14819
  }
14654
14820
  }
14655
14821
 
@@ -14876,6 +15042,200 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14876
15042
  };
14877
15043
  }
14878
15044
 
15045
+ // Passive "Messages" inbox: a non-blocking alternative to modal alerts for
15046
+ // status messages and warnings. Backed by a header button (the envelope icon)
15047
+ // in #mode-buttons and a popup-dialog panel.
15048
+ //
15049
+ // Public API (attached to the gui instance):
15050
+ // gui.notify({severity, body, title?, dedupKey?}) add an entry
15051
+ // gui.notify(message) shorthand: info severity
15052
+ //
15053
+ // severity is one of 'info' | 'warn' | 'error' (default 'info').
15054
+ // If dedupKey is given and an entry with the same key already exists, the
15055
+ // existing entry's count is incremented and its timestamp updated instead of
15056
+ // adding a new row -- handy for repeated CLI warnings.
15057
+
15058
+
15059
+ var SEVERITIES = {info: true, warn: true, error: true};
15060
+ var PULSE_DURATION_MS = 1900; // matches the CSS animation total runtime
15061
+
15062
+ function MessageControl(gui) {
15063
+ var btn = gui.container.findChild('.messages-btn');
15064
+ var badge = btn.findChild('.messages-badge');
15065
+ var panel = gui.container.findChild('.messages-panel');
15066
+ var listEl = panel.findChild('.messages-list');
15067
+ var emptyEl = panel.findChild('.messages-empty');
15068
+ var clearBtn = new SimpleButton(panel.findChild('.messages-clear-btn'));
15069
+ var closeBtn = new SimpleButton(panel.findChild('.close2-btn'));
15070
+ var entries = [];
15071
+ var nextId = 1;
15072
+ var pulseTimer = null;
15073
+ var rendered = false;
15074
+
15075
+ // Start fully hidden until the first notification arrives, so the icon
15076
+ // doesn't take up header space when there's nothing to show.
15077
+ btn.addClass('hidden');
15078
+ gui.addMode('messages', turnOn, turnOff, btn);
15079
+
15080
+ closeBtn.on('click', function() {
15081
+ gui.clearMode();
15082
+ });
15083
+
15084
+ clearBtn.on('click', function() {
15085
+ if (entries.length === 0) return;
15086
+ entries = [];
15087
+ // Close the panel so the now-disabled envelope isn't holding open a
15088
+ // panel the user can no longer dismiss with a header click.
15089
+ gui.clearMode();
15090
+ renderList();
15091
+ updateBadge();
15092
+ });
15093
+
15094
+ // Public API
15095
+ gui.notify = function(opts) {
15096
+ if (typeof opts == 'string') {
15097
+ opts = {body: opts};
15098
+ }
15099
+ opts = opts || {};
15100
+ var severity = SEVERITIES[opts.severity] ? opts.severity : 'info';
15101
+ var body = opts.body == null ? '' : String(opts.body);
15102
+ var title = opts.title || null;
15103
+ if (!body && !title) return;
15104
+
15105
+ if (opts.dedupKey) {
15106
+ for (var i = 0; i < entries.length; i++) {
15107
+ if (entries[i].dedupKey === opts.dedupKey) {
15108
+ entries[i].count = (entries[i].count || 1) + 1;
15109
+ entries[i].time = new Date();
15110
+ // Promote severity if a later occurrence is more severe.
15111
+ if (severityRank(severity) > severityRank(entries[i].severity)) {
15112
+ entries[i].severity = severity;
15113
+ }
15114
+ renderList();
15115
+ pulseBadge();
15116
+ return;
15117
+ }
15118
+ }
15119
+ }
15120
+
15121
+ entries.unshift({
15122
+ id: nextId++,
15123
+ severity: severity,
15124
+ title: title,
15125
+ body: body,
15126
+ count: 1,
15127
+ time: new Date(),
15128
+ dedupKey: opts.dedupKey || null
15129
+ });
15130
+ renderList();
15131
+ updateBadge();
15132
+ pulseBadge();
15133
+
15134
+ // Mirror to the JS console so power users still see a record even if they
15135
+ // never open the panel. Use the matching console method per severity.
15136
+ var rec = (title ? title + ': ' : '') + body;
15137
+ if (severity === 'error') console.error(rec);
15138
+ else if (severity === 'warn') console.warn(rec);
15139
+ else if (typeof internal !== 'undefined' && internal.logArgs) {
15140
+ internal.logArgs([rec]);
15141
+ } else {
15142
+ console.log(rec);
15143
+ }
15144
+ };
15145
+
15146
+ function severityRank(s) {
15147
+ return s === 'error' ? 2 : s === 'warn' ? 1 : 0;
15148
+ }
15149
+
15150
+ function turnOn() {
15151
+ if (!rendered) renderList();
15152
+ panel.show();
15153
+ }
15154
+
15155
+ function turnOff() {
15156
+ panel.hide();
15157
+ }
15158
+
15159
+ function updateBadge() {
15160
+ var n = entries.length;
15161
+ var hasWarn = false;
15162
+ var hasError = false;
15163
+ for (var i = 0; i < entries.length; i++) {
15164
+ if (entries[i].severity === 'error') hasError = true;
15165
+ else if (entries[i].severity === 'warn') hasWarn = true;
15166
+ }
15167
+ badge.text(n > 99 ? '99+' : String(n));
15168
+ badge.classed('hidden', n === 0);
15169
+ badge.classed('warn', hasWarn && !hasError);
15170
+ badge.classed('error', hasError);
15171
+ btn.classed('hidden', n === 0);
15172
+ }
15173
+
15174
+ function pulseBadge() {
15175
+ btn.removeClass('pulse');
15176
+ // Force reflow so removing+adding the class restarts the CSS animation.
15177
+ void btn.node().offsetWidth;
15178
+ btn.addClass('pulse');
15179
+ if (pulseTimer) clearTimeout(pulseTimer);
15180
+ pulseTimer = setTimeout(function() {
15181
+ btn.removeClass('pulse');
15182
+ pulseTimer = null;
15183
+ }, PULSE_DURATION_MS);
15184
+ }
15185
+
15186
+ function renderList() {
15187
+ rendered = true;
15188
+ listEl.empty();
15189
+ if (entries.length === 0) {
15190
+ emptyEl.show();
15191
+ return;
15192
+ }
15193
+ emptyEl.hide();
15194
+ for (var i = 0; i < entries.length; i++) {
15195
+ listEl.node().appendChild(renderItem(entries[i]).node());
15196
+ }
15197
+ }
15198
+
15199
+ function renderItem(entry) {
15200
+ var item = El('div').addClass('message-item').addClass('severity-' + entry.severity);
15201
+ if (entry.title) {
15202
+ El('span').addClass('message-title').text(entry.title).appendTo(item);
15203
+ }
15204
+ var bodyText = entry.body || '';
15205
+ if (entry.count > 1) {
15206
+ bodyText += ' (\u00d7' + entry.count + ')';
15207
+ }
15208
+ El('div').addClass('message-body').text(bodyText).appendTo(item);
15209
+ El('span').addClass('message-time').text(formatTime(entry.time)).appendTo(item);
15210
+ var dismiss = El('span').addClass('message-dismiss').attr('title', 'Dismiss').text('\u00d7').appendTo(item);
15211
+ var entryId = entry.id;
15212
+ dismiss.on('click', function(e) {
15213
+ // Prevent the click from also triggering any panel-level handlers.
15214
+ if (e && e.stopPropagation) e.stopPropagation();
15215
+ entries = entries.filter(function(x) { return x.id !== entryId; });
15216
+ // If we just dismissed the last entry, close the panel for the same
15217
+ // reason as Clear all: the envelope will go disabled and the user
15218
+ // wouldn't be able to dismiss the panel by clicking the header again.
15219
+ if (entries.length === 0) gui.clearMode();
15220
+ renderList();
15221
+ updateBadge();
15222
+ });
15223
+ return item;
15224
+ }
15225
+
15226
+ function formatTime(d) {
15227
+ if (!(d instanceof Date)) return '';
15228
+ var hh = String(d.getHours()).padStart(2, '0');
15229
+ var mm = String(d.getMinutes()).padStart(2, '0');
15230
+ return hh + ':' + mm;
15231
+ }
15232
+
15233
+ return {
15234
+ notify: gui.notify,
15235
+ count: function() { return entries.length; }
15236
+ };
15237
+ }
15238
+
14879
15239
  // import { ProjectOptions } from './gui-project-control';
14880
15240
 
14881
15241
 
@@ -14897,6 +15257,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14897
15257
  gui.keyboard = new KeyboardEvents(gui);
14898
15258
  gui.buttons = new SidebarButtons(gui);
14899
15259
  gui.display = new DisplayOptions(gui);
15260
+ gui.messages = new MessageControl(gui);
14900
15261
  gui.basemap = new Basemap(gui);
14901
15262
  gui.session = new SessionHistory(gui);
14902
15263
  gui.contextMenu = new ContextMenu();
@@ -14988,6 +15349,25 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
14988
15349
  // This is the entry point for bundling mapshaper's web UI
14989
15350
 
14990
15351
 
15352
+ // Refresh detection for mapshaper-gui: if the previous incarnation of this
15353
+ // tab set the 'navigating away' marker on pagehide, then this page load is a
15354
+ // reload (not a fresh tab). Tell the server to cancel any pending /close,
15355
+ // otherwise the server's grace window can race past while the new page is
15356
+ // idle and terminate the process. sessionStorage is per-tab and survives a
15357
+ // refresh, but is gone after a real close -- exactly the signal we need.
15358
+ // This runs before onload to fire as early as possible in the page lifecycle.
15359
+ if (typeof window !== 'undefined' &&
15360
+ window.location.hostname === 'localhost' &&
15361
+ window.sessionStorage &&
15362
+ window.sessionStorage.getItem('mapshaper_navigating_away')) {
15363
+ window.sessionStorage.removeItem('mapshaper_navigating_away');
15364
+ if (navigator.sendBeacon) {
15365
+ navigator.sendBeacon('/cancel-close');
15366
+ } else {
15367
+ try { fetch('/cancel-close', {method: 'GET', keepalive: true}); } catch (err) {}
15368
+ }
15369
+ }
15370
+
14991
15371
  onload(function() {
14992
15372
  if (!GUI.browserIsSupported()) {
14993
15373
  El("#mshp-not-supported").show();
@@ -15031,10 +15411,9 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
15031
15411
  importOpts = getImportOpts(manifest),
15032
15412
  gui = new GuiInstance('body');
15033
15413
 
15034
- // TODO: re-enable the "blurb"
15035
- // if (manifest.blurb) {
15036
- // El('#splash-screen-blurb').text(manifest.blurb);
15037
- // }
15414
+ if (!importOpts.files?.length) {
15415
+ El('body').removeClass('mapshaper-preload');
15416
+ }
15038
15417
 
15039
15418
  new AlertControl(gui);
15040
15419
  new IntersectionControl(gui);
@@ -15056,12 +15435,23 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
15056
15435
  }
15057
15436
  });
15058
15437
 
15059
- window.addEventListener('unload', function(e) {
15060
- if (window.location.hostname == 'localhost') {
15061
- // send termination signal for mapshaper-gui
15062
- var req = new XMLHttpRequest();
15063
- req.open('GET', '/close');
15064
- req.send();
15438
+ // Send termination signal to mapshaper-gui when the tab is closed.
15439
+ // Use 'pagehide' rather than 'unload' (the latter is increasingly suppressed
15440
+ // by Chrome/Edge for bfcache reasons), and use sendBeacon / keepalive fetch
15441
+ // because async XHR in the unload path is not guaranteed to be sent.
15442
+ // The sessionStorage marker is the refresh-vs-close signal: if a new page
15443
+ // loads in the same tab, it'll see the marker and send /cancel-close to
15444
+ // abort the pending exit (see the top of this file).
15445
+ window.addEventListener('pagehide', function(e) {
15446
+ if (window.location.hostname != 'localhost') return;
15447
+ if (e.persisted) return; // page is being cached, not actually closed
15448
+ try { window.sessionStorage.setItem('mapshaper_navigating_away', '1'); } catch (err) {}
15449
+ if (navigator.sendBeacon) {
15450
+ navigator.sendBeacon('/close');
15451
+ } else {
15452
+ try {
15453
+ fetch('/close', {method: 'GET', keepalive: true});
15454
+ } catch (err) {}
15065
15455
  }
15066
15456
  });
15067
15457