mapshaper 0.7.13 → 0.7.15

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.
@@ -6,7 +6,7 @@
6
6
  cli = api.cli,
7
7
  geom = api.geom,
8
8
  internal = api.internal,
9
- Bounds = internal.Bounds,
9
+ Bounds$1 = internal.Bounds,
10
10
  UserError$1 = internal.UserError,
11
11
  message$1 = internal.message, // stop, error and message are overridden in gui-proxy.js
12
12
  stop$1 = internal.stop,
@@ -1396,6 +1396,32 @@
1396
1396
  }, 400);
1397
1397
  }
1398
1398
 
1399
+ function logStartupCleanup(opts) {
1400
+ opts = opts || {};
1401
+ var count = opts.count || 0;
1402
+ if (!count || typeof console == 'undefined' || !console.log) return;
1403
+ var sessionCount = opts.sessionCount || 0;
1404
+ var singular = opts.singular || 'item';
1405
+ var plural = opts.plural || singular + 's';
1406
+ var itemLabel = count == 1 ? singular : plural;
1407
+ var msg = '[mapshaper] startup cleanup reclaimed ' + count + ' ' + itemLabel;
1408
+ if (sessionCount > 0) {
1409
+ msg += ' from ' + sessionCount + ' stale session' + (sessionCount == 1 ? '' : 's');
1410
+ }
1411
+ if (opts.sizeBytes > 0) {
1412
+ msg += ' (' + formatCleanupSize(opts.sizeBytes) + ')';
1413
+ }
1414
+ console.log(msg);
1415
+ }
1416
+
1417
+ function formatCleanupSize(bytes) {
1418
+ var kb = Math.round(bytes / 1000);
1419
+ var mb = (bytes / 1e6).toFixed(1);
1420
+ if (!kb) return '';
1421
+ if (kb < 990) return kb + 'kB';
1422
+ return mb + 'MB';
1423
+ }
1424
+
1399
1425
  // Several dependencies are loaded via require()
1400
1426
  var f;
1401
1427
  if (typeof require == 'function') {
@@ -1412,10 +1438,10 @@
1412
1438
  }
1413
1439
  var require$1 = f;
1414
1440
 
1415
- var idb$1 = require$1('idb-keyval');
1441
+ var idb$2 = require$1('idb-keyval');
1416
1442
  // https://github.com/jakearchibald/idb
1417
1443
  // https://github.com/jakearchibald/idb-keyval
1418
- var sessionId = getUniqId$1('session');
1444
+ var sessionId$1 = getUniqId$1('session');
1419
1445
  var snapshotCount = 0;
1420
1446
  // IDs of snapshots created (and not removed) by this tab. Tracked in memory
1421
1447
  // so the pagehide handler can fire a single batched delMany() without first
@@ -1539,7 +1565,7 @@
1539
1565
  closeMenu(100);
1540
1566
  }).text('restore');
1541
1567
  El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1542
- var obj = await idb$1.get(item.id);
1568
+ var obj = await idb$2.get(item.id);
1543
1569
  await internal.compressSnapshotForExport(obj);
1544
1570
  var buf = internal.pack(obj);
1545
1571
  var fileName = `snapshot-${String(item.number).padStart(2, '0')}.msx`;
@@ -1590,43 +1616,35 @@
1590
1616
  // note: we don't know the size of unpacked snapshot objects
1591
1617
  // obj = internal.pack(obj);
1592
1618
  var entryId = String(++snapshotCount).padStart(3, '0');
1593
- var snapshotId = sessionId + '_' + entryId; // e.g. session_d89fw_001
1619
+ var snapshotId = sessionId$1 + '_' + entryId; // e.g. session_d89fw_001
1594
1620
  var size = obj.length;
1595
1621
  var entry = {
1596
1622
  created: Date.now(),
1597
- session: sessionId,
1623
+ session: sessionId$1,
1598
1624
  id: snapshotId,
1599
1625
  name: snapshotCount + '.',
1600
1626
  number: snapshotCount,
1601
1627
  size: size,
1602
- display_size: formatSize(size)
1628
+ display_size: formatCleanupSize(size)
1603
1629
  };
1604
1630
 
1605
- await idb$1.set(entry.id, obj);
1631
+ await idb$2.set(entry.id, obj);
1606
1632
  ownSnapshotIds.add(entry.id);
1607
1633
  await addToIndex(entry);
1608
1634
  renderMenu();
1609
1635
  }
1610
1636
  }
1611
1637
 
1612
- function formatSize(bytes) {
1613
- var kb = Math.round(bytes / 1000);
1614
- var mb = (bytes / 1e6).toFixed(1);
1615
- if (!kb) return '';
1616
- if (kb < 990) return kb + 'kB';
1617
- return mb + 'MB';
1618
- }
1619
-
1620
1638
  async function fetchSnapshotList() {
1621
1639
  await pruneIndexAgainstKeys();
1622
1640
  var index = await fetchIndex();
1623
1641
  var snapshots = index.snapshots;
1624
- snapshots = snapshots.filter(function(o) {return o.session == sessionId;});
1642
+ snapshots = snapshots.filter(function(o) {return o.session == sessionId$1;});
1625
1643
  return snapshots.sort(function(a, b) {b.created > a.created;});
1626
1644
  }
1627
1645
 
1628
1646
  async function removeSnapshotById(id, gui) {
1629
- await idb$1.del(id);
1647
+ await idb$2.del(id);
1630
1648
  ownSnapshotIds.delete(id);
1631
1649
  return updateIndex(function(index) {
1632
1650
  index.snapshots = index.snapshots.filter(function(snap) {
@@ -1638,7 +1656,7 @@
1638
1656
  async function restoreSnapshotById(id, gui) {
1639
1657
  var data;
1640
1658
  try {
1641
- data = await internal.restoreSessionData(await idb$1.get(id));
1659
+ data = await internal.restoreSessionData(await idb$2.get(id));
1642
1660
  } catch(e) {
1643
1661
  console.error(e);
1644
1662
  stop$1('Snapshot is not available');
@@ -1721,12 +1739,12 @@
1721
1739
  }
1722
1740
 
1723
1741
  async function fetchIndex() {
1724
- var index = await idb$1.get('msx_index');
1742
+ var index = await idb$2.get('msx_index');
1725
1743
  return index || {snapshots: []};
1726
1744
  }
1727
1745
 
1728
1746
  async function updateIndex(action) {
1729
- return idb$1.update('msx_index', function(index) {
1747
+ return idb$2.update('msx_index', function(index) {
1730
1748
  if (!index || !Array.isArray(index.snapshots)) {
1731
1749
  index = {snapshots: []};
1732
1750
  }
@@ -1746,7 +1764,7 @@
1746
1764
  // (e.g. cleared by another tab). Cheaper than reclaimDeadSessionData; used
1747
1765
  // before rendering the menu to keep stale entries out of the UI.
1748
1766
  async function pruneIndexAgainstKeys() {
1749
- var keys = await idb$1.keys();
1767
+ var keys = await idb$2.keys();
1750
1768
  return updateIndex(function(index) {
1751
1769
  index.snapshots = index.snapshots.filter(function(snap) {
1752
1770
  return keys.includes(snap.id);
@@ -1766,7 +1784,7 @@
1766
1784
  // Delete every snapshot in IndexedDB whose session id is not in liveSessions,
1767
1785
  // and keep the on-disk index consistent with the actual key set.
1768
1786
  async function reclaimDeadSessionData(liveSessions) {
1769
- var keys = await idb$1.keys();
1787
+ var keys = await idb$2.keys();
1770
1788
  var doomedKeys = [];
1771
1789
  var doomedSessions = new Set();
1772
1790
  keys.forEach(function(key) {
@@ -1789,12 +1807,12 @@
1789
1807
  sizeBytes += snap.size;
1790
1808
  }
1791
1809
  });
1792
- await Promise.all(doomedKeys.map(function(k) { return idb$1.del(k); }));
1810
+ await Promise.all(doomedKeys.map(function(k) { return idb$2.del(k); }));
1793
1811
  }
1794
1812
 
1795
1813
  // Drop index entries pointing to deleted snapshots, and any entries whose
1796
1814
  // session is dead even if the underlying key was already gone.
1797
- var remainingKeys = await idb$1.keys();
1815
+ var remainingKeys = await idb$2.keys();
1798
1816
  var keySet = new Set(remainingKeys);
1799
1817
  await updateIndex(function(index) {
1800
1818
  index.snapshots = index.snapshots.filter(function(snap) {
@@ -1805,13 +1823,13 @@
1805
1823
  });
1806
1824
 
1807
1825
  if (doomedKeys.length) {
1808
- var msg = '[mapshaper] startup cleanup reclaimed ' +
1809
- doomedKeys.length + ' snapshot' + (doomedKeys.length === 1 ? '' : 's') +
1810
- ' from ' + doomedSessions.size + ' stale session' +
1811
- (doomedSessions.size === 1 ? '' : 's');
1812
- var sizeStr = sizeBytes > 0 ? formatSize(sizeBytes) : '';
1813
- if (sizeStr) msg += ' (' + sizeStr + ')';
1814
- console.log(msg);
1826
+ logStartupCleanup({
1827
+ count: doomedKeys.length,
1828
+ sessionCount: doomedSessions.size,
1829
+ singular: 'snapshot',
1830
+ plural: 'snapshots',
1831
+ sizeBytes: sizeBytes
1832
+ });
1815
1833
  }
1816
1834
  }
1817
1835
 
@@ -1869,10 +1887,10 @@
1869
1887
  _channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
1870
1888
  _channel.onmessage = function(e) {
1871
1889
  var msg = e.data;
1872
- if (!msg || msg.from === sessionId) return;
1890
+ if (!msg || msg.from === sessionId$1) return;
1873
1891
  if (msg.type === 'whois') {
1874
1892
  try {
1875
- _channel.postMessage({type: 'iam', from: sessionId});
1893
+ _channel.postMessage({type: 'iam', from: sessionId$1});
1876
1894
  } catch (err) {} // channel can throw if tab is being torn down
1877
1895
  }
1878
1896
  };
@@ -1886,7 +1904,7 @@
1886
1904
  // considered alive (always includes our own).
1887
1905
  function discoverLiveSessions() {
1888
1906
  return new Promise(function(resolve) {
1889
- var live = new Set([sessionId]);
1907
+ var live = new Set([sessionId$1]);
1890
1908
  // localStorage heartbeat data is the durable signal; it survives
1891
1909
  // backgrounded/throttled tabs that may not respond to BroadcastChannel
1892
1910
  // promptly.
@@ -1904,13 +1922,13 @@
1904
1922
  // window. This is fast and authoritative for foreground tabs.
1905
1923
  var listener = function(e) {
1906
1924
  var msg = e.data;
1907
- if (msg && msg.type === 'iam' && msg.from && msg.from !== sessionId) {
1925
+ if (msg && msg.type === 'iam' && msg.from && msg.from !== sessionId$1) {
1908
1926
  live.add(msg.from);
1909
1927
  }
1910
1928
  };
1911
1929
  _channel.addEventListener('message', listener);
1912
1930
  try {
1913
- _channel.postMessage({type: 'whois', from: sessionId});
1931
+ _channel.postMessage({type: 'whois', from: sessionId$1});
1914
1932
  } catch (err) {}
1915
1933
  setTimeout(function() {
1916
1934
  _channel.removeEventListener('message', listener);
@@ -1922,7 +1940,7 @@
1922
1940
  function announceLeaving() {
1923
1941
  if (!_channel) return;
1924
1942
  try {
1925
- _channel.postMessage({type: 'leaving', from: sessionId});
1943
+ _channel.postMessage({type: 'leaving', from: sessionId$1});
1926
1944
  _channel.close();
1927
1945
  } catch (err) {}
1928
1946
  }
@@ -1943,7 +1961,7 @@
1943
1961
  ownSnapshotIds.clear();
1944
1962
  // Delete the blobs in a single transaction.
1945
1963
  try {
1946
- idb$1.delMany(ids).catch(function() {});
1964
+ idb$2.delMany(ids).catch(function() {});
1947
1965
  } catch (err) {}
1948
1966
  // Drop our entries from the index in a separate (also fire-and-forget)
1949
1967
  // transaction. If only one of the two completes, startup cleanup will
@@ -1952,7 +1970,7 @@
1952
1970
  try {
1953
1971
  updateIndex(function(index) {
1954
1972
  index.snapshots = index.snapshots.filter(function(snap) {
1955
- return getSessionFromSnapshotId(snap.id) !== sessionId;
1973
+ return getSessionFromSnapshotId(snap.id) !== sessionId$1;
1956
1974
  });
1957
1975
  }).catch(function() {});
1958
1976
  } catch (err) {}
@@ -1960,14 +1978,14 @@
1960
1978
 
1961
1979
  function touchOwnSession() {
1962
1980
  var data = readSessionData();
1963
- data[sessionId] = Date.now();
1981
+ data[sessionId$1] = Date.now();
1964
1982
  writeSessionData(data);
1965
1983
  }
1966
1984
 
1967
1985
  function removeOwnSession() {
1968
1986
  var data = readSessionData();
1969
- if (sessionId in data) {
1970
- delete data[sessionId];
1987
+ if (sessionId$1 in data) {
1988
+ delete data[sessionId$1];
1971
1989
  writeSessionData(data);
1972
1990
  }
1973
1991
  }
@@ -2015,7 +2033,116 @@
2015
2033
  }
2016
2034
  }
2017
2035
 
2018
- var idb = require$1('idb-keyval');
2036
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 30 * 1000;
2037
+ var DEFAULT_STALE_THRESHOLD_MS = 5 * 60 * 1000;
2038
+
2039
+ function createTempSessionLifecycle(opts) {
2040
+ opts = opts || {};
2041
+ var win = opts.window || getWindow$1();
2042
+ var sessionId = opts.sessionId || getUniqueSessionId(opts.prefix || 'tmp');
2043
+ var sessionKey = opts.sessionKey;
2044
+ var heartbeatInterval = opts.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL_MS;
2045
+ var staleThreshold = opts.staleThreshold || DEFAULT_STALE_THRESHOLD_MS;
2046
+ var heartbeatTimer = null;
2047
+ var started = false;
2048
+
2049
+ return {
2050
+ start: start,
2051
+ stop: stop,
2052
+ touch: touchOwnSession,
2053
+ removeOwnSession: removeOwnSession,
2054
+ getLiveSessions: getLiveSessions,
2055
+ getSessionId: function() { return sessionId; }
2056
+ };
2057
+
2058
+ function start(onPageHide) {
2059
+ if (started) return;
2060
+ started = true;
2061
+ touchOwnSession();
2062
+ if (win && win.setInterval) {
2063
+ heartbeatTimer = win.setInterval(touchOwnSession, heartbeatInterval);
2064
+ } else if (typeof setInterval == 'function') {
2065
+ heartbeatTimer = setInterval(touchOwnSession, heartbeatInterval);
2066
+ }
2067
+ if (win && win.addEventListener) {
2068
+ win.addEventListener('pagehide', function(e) {
2069
+ if (e.persisted) return;
2070
+ stop();
2071
+ if (onPageHide) onPageHide();
2072
+ });
2073
+ }
2074
+ }
2075
+
2076
+ function stop() {
2077
+ if (heartbeatTimer) {
2078
+ if (win && win.clearInterval) {
2079
+ win.clearInterval(heartbeatTimer);
2080
+ } else if (typeof clearInterval == 'function') {
2081
+ clearInterval(heartbeatTimer);
2082
+ }
2083
+ heartbeatTimer = null;
2084
+ }
2085
+ removeOwnSession();
2086
+ }
2087
+
2088
+ function touchOwnSession() {
2089
+ var sessions = readSessions();
2090
+ sessions[sessionId] = Date.now();
2091
+ writeSessions(sessions);
2092
+ }
2093
+
2094
+ function removeOwnSession() {
2095
+ var sessions = readSessions();
2096
+ if (sessions[sessionId]) {
2097
+ delete sessions[sessionId];
2098
+ writeSessions(sessions);
2099
+ }
2100
+ }
2101
+
2102
+ function getLiveSessions() {
2103
+ var sessions = readSessions();
2104
+ var live = {};
2105
+ var now = Date.now();
2106
+ live[sessionId] = true;
2107
+ Object.keys(sessions).forEach(function(sid) {
2108
+ if (now - sessions[sid] < staleThreshold) {
2109
+ live[sid] = true;
2110
+ }
2111
+ });
2112
+ return live;
2113
+ }
2114
+
2115
+ function readSessions() {
2116
+ var storage = win && win.localStorage;
2117
+ var raw, parsed;
2118
+ if (!storage || !sessionKey) return {};
2119
+ try {
2120
+ raw = storage.getItem(sessionKey);
2121
+ parsed = raw ? JSON.parse(raw) : null;
2122
+ return parsed && typeof parsed == 'object' && !Array.isArray(parsed) ? parsed : {};
2123
+ } catch(e) {
2124
+ return {};
2125
+ }
2126
+ }
2127
+
2128
+ function writeSessions(sessions) {
2129
+ var storage = win && win.localStorage;
2130
+ if (!storage || !sessionKey) return;
2131
+ try {
2132
+ storage.setItem(sessionKey, JSON.stringify(sessions));
2133
+ } catch(e) {}
2134
+ }
2135
+ }
2136
+
2137
+ function getWindow$1() {
2138
+ return typeof window == 'undefined' ? null : window;
2139
+ }
2140
+
2141
+ function getUniqueSessionId(prefix) {
2142
+ return prefix + '_' + (Math.random() + 1).toString(36).substring(2, 8);
2143
+ }
2144
+
2145
+ var idb$1 = require$1('idb-keyval');
2019
2146
  var DEFAULT_INDEX_KEY = 'msu_index';
2020
2147
  var DEFAULT_SESSION_KEY = 'mapshaper_undo_sessions';
2021
2148
  var DEFAULT_KEY_PREFIX = 'msu';
@@ -2032,6 +2159,13 @@
2032
2159
  var keyPrefix = opts.keyPrefix || DEFAULT_KEY_PREFIX;
2033
2160
  var heartbeatInterval = opts.heartbeatInterval || HEARTBEAT_INTERVAL_MS;
2034
2161
  var staleThreshold = opts.staleThreshold || STALE_THRESHOLD_MS;
2162
+ var lifecycle = createTempSessionLifecycle({
2163
+ window: win,
2164
+ sessionId: sessionId,
2165
+ sessionKey: sessionKey,
2166
+ heartbeatInterval: heartbeatInterval,
2167
+ staleThreshold: staleThreshold
2168
+ });
2035
2169
  var maxBytes = opts.maxBytes || 0;
2036
2170
  var maxPayloadBytes = opts.maxPayloadBytes || 0;
2037
2171
  var payloadCount = 0;
@@ -2039,7 +2173,6 @@
2039
2173
  var ownPayloadSizes = {};
2040
2174
  var ownPayloadItems = {};
2041
2175
  var ownBytes = 0;
2042
- var heartbeatTimer = null;
2043
2176
  var lifecycleStarted = false;
2044
2177
 
2045
2178
  return {
@@ -2069,7 +2202,7 @@
2069
2202
  validatePayloadSize(size);
2070
2203
  await backend.set(key, value);
2071
2204
  notePayloadStored(item);
2072
- touchOwnSession();
2205
+ lifecycle.touch();
2073
2206
  try {
2074
2207
  await updateIndex(function(index) {
2075
2208
  index.payloads.push(item);
@@ -2110,55 +2243,46 @@
2110
2243
  keys.forEach(notePayloadRemoved);
2111
2244
  await removeIndexKeys(keys);
2112
2245
  }
2113
- removeOwnSession();
2246
+ lifecycle.removeOwnSession();
2114
2247
  }
2115
2248
 
2116
2249
  function startLifecycle() {
2117
2250
  if (lifecycleStarted) return;
2118
2251
  lifecycleStarted = true;
2119
- touchOwnSession();
2120
- if (win && win.setInterval) {
2121
- heartbeatTimer = win.setInterval(touchOwnSession, heartbeatInterval);
2122
- } else if (typeof setInterval == 'function') {
2123
- heartbeatTimer = setInterval(touchOwnSession, heartbeatInterval);
2124
- }
2125
- if (win && win.addEventListener) {
2126
- win.addEventListener('pagehide', function(e) {
2127
- if (e.persisted) return;
2128
- stopLifecycle();
2129
- attemptOwnDataDeletion();
2130
- });
2131
- }
2252
+ lifecycle.start(attemptOwnDataDeletion);
2132
2253
  }
2133
2254
 
2134
2255
  async function cleanupStaleSessions() {
2135
- var liveSessions = getLiveSessions();
2256
+ var liveSessions = lifecycle.getLiveSessions();
2136
2257
  var keys = await backend.keys();
2258
+ var doomedSessions = new Set();
2259
+ var sizeBytes = 0;
2137
2260
  var doomedKeys = keys.filter(function(key) {
2138
- var sid = getSessionFromPayloadKey(key);
2139
- return sid && !liveSessions[sid];
2261
+ var sid = getSessionFromPayloadKey(key, keyPrefix);
2262
+ var stale = sid && !liveSessions[sid];
2263
+ if (stale) doomedSessions.add(sid);
2264
+ return stale;
2140
2265
  });
2141
2266
  if (doomedKeys.length > 0) {
2142
2267
  await backend.delMany(doomedKeys);
2143
2268
  }
2144
2269
  await updateIndex(function(index) {
2270
+ var doomedKeyIndex = keyArrayToIndex(doomedKeys);
2145
2271
  index.payloads = index.payloads.filter(function(item) {
2146
- return liveSessions[item.sessionId];
2272
+ if (!liveSessions[item.sessionId]) {
2273
+ if (doomedKeyIndex[item.key] && typeof item.size == 'number') {
2274
+ sizeBytes += item.size;
2275
+ }
2276
+ return false;
2277
+ }
2278
+ return true;
2147
2279
  });
2148
2280
  });
2149
- return doomedKeys;
2150
- }
2151
-
2152
- function stopLifecycle() {
2153
- if (heartbeatTimer) {
2154
- if (win && win.clearInterval) {
2155
- win.clearInterval(heartbeatTimer);
2156
- } else if (typeof clearInterval == 'function') {
2157
- clearInterval(heartbeatTimer);
2158
- }
2159
- heartbeatTimer = null;
2160
- }
2161
- removeOwnSession();
2281
+ return {
2282
+ keys: doomedKeys,
2283
+ sessionCount: doomedSessions.size,
2284
+ sizeBytes: sizeBytes
2285
+ };
2162
2286
  }
2163
2287
 
2164
2288
  function attemptOwnDataDeletion() {
@@ -2247,66 +2371,26 @@
2247
2371
  });
2248
2372
  }
2249
2373
 
2250
- function touchOwnSession() {
2251
- var sessions = readSessions();
2252
- sessions[sessionId] = Date.now();
2253
- writeSessions(sessions);
2254
- }
2255
-
2256
- function removeOwnSession() {
2257
- var sessions = readSessions();
2258
- if (sessions[sessionId]) {
2259
- delete sessions[sessionId];
2260
- writeSessions(sessions);
2261
- }
2262
- }
2263
-
2264
- function getLiveSessions() {
2265
- var sessions = readSessions();
2266
- var live = {};
2267
- var now = Date.now();
2268
- live[sessionId] = true;
2269
- Object.keys(sessions).forEach(function(sid) {
2270
- if (now - sessions[sid] < staleThreshold) {
2271
- live[sid] = true;
2272
- }
2273
- });
2274
- return live;
2275
- }
2276
-
2277
- function readSessions() {
2278
- var storage = win && win.localStorage;
2279
- var raw, parsed;
2280
- if (!storage) return {};
2281
- try {
2282
- raw = storage.getItem(sessionKey);
2283
- parsed = raw ? JSON.parse(raw) : null;
2284
- return parsed && typeof parsed == 'object' && !Array.isArray(parsed) ? parsed : {};
2285
- } catch(e) {
2286
- return {};
2287
- }
2288
- }
2289
-
2290
- function writeSessions(sessions) {
2291
- var storage = win && win.localStorage;
2292
- if (!storage) return;
2293
- try {
2294
- storage.setItem(sessionKey, JSON.stringify(sessions));
2295
- } catch(e) {}
2296
- }
2297
2374
  }
2298
2375
 
2299
2376
  function getPayloadKey(ref) {
2300
2377
  return ref && (ref.key || ref);
2301
2378
  }
2302
2379
 
2380
+ function keyArrayToIndex(keys) {
2381
+ return keys.reduce(function(memo, key) {
2382
+ memo[key] = true;
2383
+ return memo;
2384
+ }, {});
2385
+ }
2386
+
2303
2387
  function copyPayloadItem(item) {
2304
2388
  return Object.assign({}, item || {});
2305
2389
  }
2306
2390
 
2307
- function getSessionFromPayloadKey(key) {
2308
- var match = /^msu:([^:]+):\d+$/.exec(key);
2309
- return match ? match[1] : null;
2391
+ function getSessionFromPayloadKey(key, keyPrefix) {
2392
+ var parts = String(key).split(':');
2393
+ return parts.length == 3 && parts[0] == keyPrefix ? parts[1] : null;
2310
2394
  }
2311
2395
 
2312
2396
  function getUniqId(prefix) {
@@ -2318,7 +2402,7 @@
2318
2402
  }
2319
2403
 
2320
2404
  function createBackend(win) {
2321
- if (win && win.indexedDB && idb) {
2405
+ if (win && win.indexedDB && idb$1) {
2322
2406
  return createIdbBackend();
2323
2407
  }
2324
2408
  return createMemoryBackend();
@@ -2327,13 +2411,13 @@
2327
2411
  function createIdbBackend() {
2328
2412
  return {
2329
2413
  persistent: true,
2330
- get: idb.get,
2331
- set: idb.set,
2332
- del: idb.del,
2333
- keys: idb.keys,
2414
+ get: idb$1.get,
2415
+ set: idb$1.set,
2416
+ del: idb$1.del,
2417
+ keys: idb$1.keys,
2334
2418
  delMany: function(keys) {
2335
- return idb.delMany ? idb.delMany(keys) :
2336
- Promise.all(keys.map(function(key) { return idb.del(key); }));
2419
+ return idb$1.delMany ? idb$1.delMany(keys) :
2420
+ Promise.all(keys.map(function(key) { return idb$1.del(key); }));
2337
2421
  }
2338
2422
  };
2339
2423
  }
@@ -4237,207 +4321,1106 @@
4237
4321
  return maxCount;
4238
4322
  }
4239
4323
 
4240
- // Lightweight hooks that make in-place mutations observable to GUI undo.
4241
- // Normal CLI runs leave activeTransaction unset, so capture hooks are no-ops.
4324
+ function Transform() {
4325
+ this.mx = this.my = 1;
4326
+ this.bx = this.by = 0;
4327
+ }
4242
4328
 
4243
- var activeTransaction = null;
4244
- var nextUndoId = 1;
4245
- var objectMetadata = new WeakMap();
4329
+ Transform.prototype.isNull = function() {
4330
+ return !this.mx || !this.my || isNaN(this.bx) || isNaN(this.by);
4331
+ };
4246
4332
 
4247
- function getActiveUndoTransaction() {
4248
- return activeTransaction;
4249
- }
4333
+ Transform.prototype.invert = function() {
4334
+ var inv = new Transform();
4335
+ inv.mx = 1 / this.mx;
4336
+ inv.my = 1 / this.my;
4337
+ //inv.bx = -this.bx * inv.mx;
4338
+ //inv.by = -this.by * inv.my;
4339
+ inv.bx = -this.bx / this.mx;
4340
+ inv.by = -this.by / this.my;
4341
+ return inv;
4342
+ };
4250
4343
 
4251
- function setActiveUndoTransaction(tx) {
4252
- activeTransaction = tx || null;
4253
- }
4254
4344
 
4255
- function clearActiveUndoTransaction(tx) {
4256
- if (!tx || activeTransaction == tx) {
4257
- activeTransaction = null;
4258
- }
4259
- }
4345
+ Transform.prototype.transform = function(x, y, xy) {
4346
+ xy = xy || [];
4347
+ xy[0] = x * this.mx + this.bx;
4348
+ xy[1] = y * this.my + this.by;
4349
+ return xy;
4350
+ };
4260
4351
 
4261
- function withActiveUndoTransaction(tx, cb) {
4262
- var prev = activeTransaction;
4263
- activeTransaction = tx || null;
4264
- try {
4265
- return cb();
4266
- } finally {
4267
- activeTransaction = prev;
4268
- }
4269
- }
4352
+ Transform.prototype.toString = function() {
4353
+ return JSON.stringify(Object.assign({}, this));
4354
+ };
4270
4355
 
4271
- function getUndoMetadata(obj) {
4272
- var meta = objectMetadata.get(obj);
4273
- if (!meta) {
4274
- meta = {
4275
- id: 'u' + nextUndoId++,
4276
- revision: 0
4277
- };
4278
- objectMetadata.set(obj, meta);
4356
+ function Bounds() {
4357
+ if (arguments.length > 0) {
4358
+ this.setBounds.apply(this, arguments);
4279
4359
  }
4280
- return meta;
4281
4360
  }
4282
4361
 
4283
- function getUndoId(obj) {
4284
- return getUndoMetadata(obj).id;
4285
- }
4362
+ Bounds.from = function() {
4363
+ var b = new Bounds();
4364
+ return b.setBounds.apply(b, arguments);
4365
+ };
4286
4366
 
4287
- function getUndoRevision(obj) {
4288
- return getUndoMetadata(obj).revision;
4289
- }
4367
+ Bounds.prototype.toString = function() {
4368
+ return JSON.stringify({
4369
+ xmin: this.xmin,
4370
+ xmax: this.xmax,
4371
+ ymin: this.ymin,
4372
+ ymax: this.ymax
4373
+ });
4374
+ };
4290
4375
 
4291
- function markChanged(obj, detail) {
4292
- var tx = activeTransaction;
4293
- var meta = objectMetadata.get(obj);
4294
- if (!tx && !meta) return 0;
4295
- if (!meta) meta = getUndoMetadata(obj);
4296
- meta.revision++;
4297
- notifyTransaction(tx, 'markChanged', obj, detail);
4298
- return meta.revision;
4299
- }
4376
+ Bounds.prototype.toArray = function() {
4377
+ return this.hasBounds() ? [this.xmin, this.ymin, this.xmax, this.ymax] : [];
4378
+ };
4300
4379
 
4301
- function noteTableWillChange(table, detail) {
4302
- notifyTransaction(activeTransaction, 'captureTableBefore', table, detail || {});
4303
- }
4380
+ Bounds.prototype.hasBounds = function() {
4381
+ return this.xmin <= this.xmax && this.ymin <= this.ymax;
4382
+ };
4304
4383
 
4305
- function noteTableRecordsWillChange(table, ids, detail) {
4306
- notifyTransaction(activeTransaction, 'captureTableRecordsBefore', table, Object.assign({
4307
- ids: normalizeIds(ids)
4308
- }, detail || {}));
4384
+ Bounds.prototype.sameBounds =
4385
+ Bounds.prototype.equals = function(bb) {
4386
+ return bb && this.xmin === bb.xmin && this.xmax === bb.xmax &&
4387
+ this.ymin === bb.ymin && this.ymax === bb.ymax;
4388
+ };
4389
+
4390
+ Bounds.prototype.width = function() {
4391
+ return (this.xmax - this.xmin) || 0;
4392
+ };
4393
+
4394
+ Bounds.prototype.height = function() {
4395
+ return (this.ymax - this.ymin) || 0;
4396
+ };
4397
+
4398
+ Bounds.prototype.area = function() {
4399
+ return this.width() * this.height() || 0;
4400
+ };
4401
+
4402
+ Bounds.prototype.empty = function() {
4403
+ this.xmin = this.ymin = this.xmax = this.ymax = void 0;
4404
+ return this;
4405
+ };
4406
+
4407
+ Bounds.prototype.setBounds = function(a, b, c, d) {
4408
+ if (arguments.length == 1) {
4409
+ // assume first arg is a Bounds or array
4410
+ if (utils.isArrayLike(a)) {
4411
+ b = a[1];
4412
+ c = a[2];
4413
+ d = a[3];
4414
+ a = a[0];
4415
+ } else {
4416
+ b = a.ymin;
4417
+ c = a.xmax;
4418
+ d = a.ymax;
4419
+ a = a.xmin;
4420
+ }
4421
+ }
4422
+
4423
+ this.xmin = a;
4424
+ this.ymin = b;
4425
+ this.xmax = c;
4426
+ this.ymax = d;
4427
+ if (a > c || b > d) this.update();
4428
+ return this;
4429
+ };
4430
+
4431
+
4432
+ Bounds.prototype.centerX = function() {
4433
+ var x = (this.xmin + this.xmax) * 0.5;
4434
+ return x;
4435
+ };
4436
+
4437
+ Bounds.prototype.centerY = function() {
4438
+ var y = (this.ymax + this.ymin) * 0.5;
4439
+ return y;
4440
+ };
4441
+
4442
+ Bounds.prototype.containsPoint = function(x, y) {
4443
+ if (x >= this.xmin && x <= this.xmax &&
4444
+ y <= this.ymax && y >= this.ymin) {
4445
+ return true;
4446
+ }
4447
+ return false;
4448
+ };
4449
+
4450
+ // intended to speed up slightly bubble symbol detection; could use intersects() instead
4451
+ // TODO: fix false positive where circle is just outside a corner of the box
4452
+ Bounds.prototype.containsBufferedPoint =
4453
+ Bounds.prototype.containsCircle = function(x, y, buf) {
4454
+ if ( x + buf > this.xmin && x - buf < this.xmax ) {
4455
+ if ( y - buf < this.ymax && y + buf > this.ymin ) {
4456
+ return true;
4457
+ }
4458
+ }
4459
+ return false;
4460
+ };
4461
+
4462
+ Bounds.prototype.intersects = function(bb) {
4463
+ if (bb.xmin <= this.xmax && bb.xmax >= this.xmin &&
4464
+ bb.ymax >= this.ymin && bb.ymin <= this.ymax) {
4465
+ return true;
4466
+ }
4467
+ return false;
4468
+ };
4469
+
4470
+ Bounds.prototype.contains = function(bb) {
4471
+ if (bb.xmin >= this.xmin && bb.ymax <= this.ymax &&
4472
+ bb.xmax <= this.xmax && bb.ymin >= this.ymin) {
4473
+ return true;
4474
+ }
4475
+ return false;
4476
+ };
4477
+
4478
+ Bounds.prototype.shift = function(x, y) {
4479
+ this.setBounds(this.xmin + x,
4480
+ this.ymin + y, this.xmax + x, this.ymax + y);
4481
+ };
4482
+
4483
+ Bounds.prototype.padBounds = function(a, b, c, d) {
4484
+ this.xmin -= a;
4485
+ this.ymin -= b;
4486
+ this.xmax += c;
4487
+ this.ymax += d;
4488
+ };
4489
+
4490
+ // Rescale the bounding box by a fraction. TODO: implement focus.
4491
+ // @param {number} pct Fraction of original extents
4492
+ // @param {number} pctY Optional amount to scale Y
4493
+ //
4494
+ Bounds.prototype.scale = function(pct, pctY) { /*, focusX, focusY*/
4495
+ var halfWidth = (this.xmax - this.xmin) * 0.5;
4496
+ var halfHeight = (this.ymax - this.ymin) * 0.5;
4497
+ var kx = pct - 1;
4498
+ var ky = pctY === undefined ? kx : pctY - 1;
4499
+ this.xmin -= halfWidth * kx;
4500
+ this.ymin -= halfHeight * ky;
4501
+ this.xmax += halfWidth * kx;
4502
+ this.ymax += halfHeight * ky;
4503
+ return this;
4504
+ };
4505
+
4506
+ // Return a bounding box with the same extent as this one.
4507
+ Bounds.prototype.cloneBounds = // alias so child classes can override clone()
4508
+ Bounds.prototype.clone = function() {
4509
+ return new Bounds(this.xmin, this.ymin, this.xmax, this.ymax);
4510
+ };
4511
+
4512
+ Bounds.prototype.clearBounds = function() {
4513
+ this.setBounds(new Bounds());
4514
+ };
4515
+
4516
+ Bounds.prototype.mergePoint = function(x, y) {
4517
+ if (this.xmin === void 0) {
4518
+ this.setBounds(x, y, x, y);
4519
+ } else {
4520
+ // this works even if x,y are NaN
4521
+ if (x < this.xmin) this.xmin = x;
4522
+ else if (x > this.xmax) this.xmax = x;
4523
+
4524
+ if (y < this.ymin) this.ymin = y;
4525
+ else if (y > this.ymax) this.ymax = y;
4526
+ }
4527
+ };
4528
+
4529
+ // expands either x or y dimension to match @aspect (width/height ratio)
4530
+ // @focusX, @focusY (optional): expansion focus, as a fraction of width and height
4531
+ Bounds.prototype.fillOut = function(aspect, focusX, focusY) {
4532
+ if (arguments.length < 3) {
4533
+ focusX = 0.5;
4534
+ focusY = 0.5;
4535
+ }
4536
+ var w = this.width(),
4537
+ h = this.height(),
4538
+ currAspect = w / h,
4539
+ pad;
4540
+ if (isNaN(aspect) || aspect <= 0) {
4541
+ // error condition; don't pad
4542
+ } else if (currAspect < aspect) { // fill out x dimension
4543
+ pad = h * aspect - w;
4544
+ this.xmin -= (1 - focusX) * pad;
4545
+ this.xmax += focusX * pad;
4546
+ } else {
4547
+ pad = w / aspect - h;
4548
+ this.ymin -= (1 - focusY) * pad;
4549
+ this.ymax += focusY * pad;
4550
+ }
4551
+ return this;
4552
+ };
4553
+
4554
+ Bounds.prototype.update = function() {
4555
+ var tmp;
4556
+ if (this.xmin > this.xmax) {
4557
+ tmp = this.xmin;
4558
+ this.xmin = this.xmax;
4559
+ this.xmax = tmp;
4560
+ }
4561
+ if (this.ymin > this.ymax) {
4562
+ tmp = this.ymin;
4563
+ this.ymin = this.ymax;
4564
+ this.ymax = tmp;
4565
+ }
4566
+ };
4567
+
4568
+ Bounds.prototype.transform = function(t) {
4569
+ this.xmin = this.xmin * t.mx + t.bx;
4570
+ this.xmax = this.xmax * t.mx + t.bx;
4571
+ this.ymin = this.ymin * t.my + t.by;
4572
+ this.ymax = this.ymax * t.my + t.by;
4573
+ this.update();
4574
+ return this;
4575
+ };
4576
+
4577
+ // Returns a Transform object for mapping this onto Bounds @b2
4578
+ // @flipY (optional) Flip y-axis coords, for converting to/from pixel coords
4579
+ //
4580
+ Bounds.prototype.getTransform = function(b2, flipY) {
4581
+ var t = new Transform();
4582
+ t.mx = b2.width() / this.width() || 1; // TODO: better handling of 0 w,h
4583
+ t.bx = b2.xmin - t.mx * this.xmin;
4584
+ if (flipY) {
4585
+ t.my = -b2.height() / this.height() || 1;
4586
+ t.by = b2.ymax - t.my * this.ymin;
4587
+ } else {
4588
+ t.my = b2.height() / this.height() || 1;
4589
+ t.by = b2.ymin - t.my * this.ymin;
4590
+ }
4591
+ return t;
4592
+ };
4593
+
4594
+ Bounds.prototype.mergeCircle = function(x, y, r) {
4595
+ if (r < 0) r = -r;
4596
+ this.mergeBounds([x - r, y - r, x + r, y + r]);
4597
+ };
4598
+
4599
+ Bounds.prototype.mergeBounds = function(bb) {
4600
+ var a, b, c, d;
4601
+ if (bb instanceof Bounds) {
4602
+ a = bb.xmin;
4603
+ b = bb.ymin;
4604
+ c = bb.xmax;
4605
+ d = bb.ymax;
4606
+ } else if (arguments.length == 4) {
4607
+ a = arguments[0];
4608
+ b = arguments[1];
4609
+ c = arguments[2];
4610
+ d = arguments[3];
4611
+ } else if (bb.length == 4) {
4612
+ // assume array: [xmin, ymin, xmax, ymax]
4613
+ a = bb[0];
4614
+ b = bb[1];
4615
+ c = bb[2];
4616
+ d = bb[3];
4617
+ } else {
4618
+ error("Bounds#mergeBounds() invalid argument:", bb);
4619
+ }
4620
+
4621
+ if (this.xmin === void 0) {
4622
+ this.setBounds(a, b, c, d);
4623
+ } else {
4624
+ if (a < this.xmin) this.xmin = a;
4625
+ if (b < this.ymin) this.ymin = b;
4626
+ if (c > this.xmax) this.xmax = c;
4627
+ if (d > this.ymax) this.ymax = d;
4628
+ }
4629
+ return this;
4630
+ };
4631
+
4632
+ // Lightweight hooks that make in-place mutations observable to GUI undo.
4633
+ // Normal CLI runs leave activeTransaction unset, so capture hooks are no-ops.
4634
+
4635
+ var activeTransaction = null;
4636
+ var nextUndoId = 1;
4637
+ var objectMetadata = new WeakMap();
4638
+
4639
+ function getActiveUndoTransaction() {
4640
+ return activeTransaction;
4641
+ }
4642
+
4643
+ function setActiveUndoTransaction(tx) {
4644
+ activeTransaction = tx || null;
4645
+ }
4646
+
4647
+ function clearActiveUndoTransaction(tx) {
4648
+ if (!tx || activeTransaction == tx) {
4649
+ activeTransaction = null;
4650
+ }
4651
+ }
4652
+
4653
+ function withActiveUndoTransaction(tx, cb) {
4654
+ var prev = activeTransaction;
4655
+ activeTransaction = tx || null;
4656
+ try {
4657
+ return cb();
4658
+ } finally {
4659
+ activeTransaction = prev;
4660
+ }
4661
+ }
4662
+
4663
+ function getUndoMetadata(obj) {
4664
+ var meta = objectMetadata.get(obj);
4665
+ if (!meta) {
4666
+ meta = {
4667
+ id: 'u' + nextUndoId++,
4668
+ revision: 0
4669
+ };
4670
+ objectMetadata.set(obj, meta);
4671
+ }
4672
+ return meta;
4673
+ }
4674
+
4675
+ function getUndoId(obj) {
4676
+ return getUndoMetadata(obj).id;
4677
+ }
4678
+
4679
+ function getUndoRevision(obj) {
4680
+ return getUndoMetadata(obj).revision;
4681
+ }
4682
+
4683
+ function markChanged(obj, detail) {
4684
+ var tx = activeTransaction;
4685
+ var meta = objectMetadata.get(obj);
4686
+ if (!tx && !meta) return 0;
4687
+ if (!meta) meta = getUndoMetadata(obj);
4688
+ meta.revision++;
4689
+ notifyTransaction(tx, 'markChanged', obj, detail);
4690
+ return meta.revision;
4691
+ }
4692
+
4693
+ function noteTableWillChange(table, detail) {
4694
+ notifyTransaction(activeTransaction, 'captureTableBefore', table, detail || {});
4695
+ }
4696
+
4697
+ function noteTableRecordsWillChange(table, ids, detail) {
4698
+ notifyTransaction(activeTransaction, 'captureTableRecordsBefore', table, Object.assign({
4699
+ ids: normalizeIds(ids)
4700
+ }, detail || {}));
4701
+ }
4702
+
4703
+ function noteTableFieldsWillChange(table, fields, detail) {
4704
+ notifyTransaction(activeTransaction, 'captureTableFieldsBefore', table, Object.assign({
4705
+ fields: normalizeStrings(fields)
4706
+ }, detail || {}));
4707
+ }
4708
+
4709
+ function noteTableOrderWillChange(table, ids, detail) {
4710
+ notifyTransaction(activeTransaction, 'captureTableOrderBefore', table, Object.assign({
4711
+ ids: normalizeIds(ids)
4712
+ }, detail || {}));
4713
+ }
4714
+
4715
+ function noteTableSchemaWillChange(table, detail) {
4716
+ notifyTransaction(activeTransaction, 'captureTableSchemaBefore', table, detail || {});
4717
+ }
4718
+
4719
+ function markTableChanged(table, detail) {
4720
+ return markChanged(table, Object.assign({type: 'table'}, detail || {}));
4721
+ }
4722
+
4723
+ function markTableRecordsChanged(table, ids, detail) {
4724
+ return markTableChanged(table, Object.assign({
4725
+ granularity: 'records',
4726
+ ids: normalizeIds(ids)
4727
+ }, detail || {}));
4728
+ }
4729
+
4730
+ function markTableFieldsChanged(table, fields, detail) {
4731
+ return markTableChanged(table, Object.assign({
4732
+ granularity: 'fields',
4733
+ fields: normalizeStrings(fields)
4734
+ }, detail || {}));
4735
+ }
4736
+
4737
+ function markTableSchemaChanged(table, detail) {
4738
+ return markTableChanged(table, Object.assign({
4739
+ granularity: 'schema'
4740
+ }, detail || {}));
4741
+ }
4742
+
4743
+ function markTableOrderChanged(table, ids, detail) {
4744
+ return markTableChanged(table, Object.assign({
4745
+ granularity: 'order',
4746
+ ids: normalizeIds(ids)
4747
+ }, detail || {}));
4748
+ }
4749
+
4750
+ function noteArcsWillChange(arcs, detail) {
4751
+ notifyTransaction(activeTransaction, 'captureArcsBefore', arcs, detail || {});
4752
+ }
4753
+
4754
+ function noteArcsSimplificationWillChange$1(arcs, detail) {
4755
+ notifyTransaction(activeTransaction, 'captureArcsSimplificationBefore', arcs, detail || {});
4756
+ }
4757
+
4758
+ function markArcsChanged(arcs, detail) {
4759
+ return markChanged(arcs, Object.assign({type: 'arcs'}, detail || {}));
4760
+ }
4761
+
4762
+ function markArcsSimplificationChanged$1(arcs, detail) {
4763
+ return markArcsChanged(arcs, Object.assign({granularity: 'simplification'}, detail || {}));
4764
+ }
4765
+
4766
+ function noteCatalogWillChange(catalog, detail) {
4767
+ notifyTransaction(activeTransaction, 'captureCatalogBefore', catalog, detail || {});
4768
+ }
4769
+
4770
+ function markCatalogChanged(catalog, detail) {
4771
+ return markChanged(catalog, Object.assign({type: 'catalog'}, detail || {}));
4772
+ }
4773
+
4774
+ function noteDatasetWillChange(dataset, detail) {
4775
+ notifyTransaction(activeTransaction, 'captureDatasetBefore', dataset, detail || {});
4776
+ }
4777
+
4778
+ function noteDatasetInfoWillChange(dataset, detail) {
4779
+ notifyTransaction(activeTransaction, 'captureDatasetInfoBefore', dataset, detail || {});
4780
+ }
4781
+
4782
+ function markDatasetChanged(dataset, detail) {
4783
+ return markChanged(dataset, Object.assign({type: 'dataset'}, detail || {}));
4784
+ }
4785
+
4786
+ function markDatasetInfoChanged(dataset, detail) {
4787
+ return markDatasetChanged(dataset, Object.assign({granularity: 'info'}, detail || {}));
4788
+ }
4789
+
4790
+ function noteLayerWillChange(layer, detail) {
4791
+ notifyTransaction(activeTransaction, 'captureLayerBefore', layer, detail || {});
4792
+ }
4793
+
4794
+ function noteLayerMetadataWillChange(layer, detail) {
4795
+ notifyTransaction(activeTransaction, 'captureLayerMetadataBefore', layer, detail || {});
4796
+ }
4797
+
4798
+ function noteLayerOrderWillChange(layer, ids, detail) {
4799
+ notifyTransaction(activeTransaction, 'captureLayerOrderBefore', layer, Object.assign({
4800
+ ids: normalizeIds(ids)
4801
+ }, detail || {}));
4802
+ }
4803
+
4804
+ function markLayerChanged(layer, detail) {
4805
+ return markChanged(layer, Object.assign({type: 'layer'}, detail || {}));
4806
+ }
4807
+
4808
+ function markLayerMetadataChanged(layer, detail) {
4809
+ return markLayerChanged(layer, Object.assign({granularity: 'metadata'}, detail || {}));
4810
+ }
4811
+
4812
+ function markLayerOrderChanged(layer, ids, detail) {
4813
+ return markLayerChanged(layer, Object.assign({
4814
+ granularity: 'order',
4815
+ ids: normalizeIds(ids)
4816
+ }, detail || {}));
4817
+ }
4818
+
4819
+ function notifyTransaction(tx, method, obj, detail) {
4820
+ if (tx && typeof tx[method] == 'function') {
4821
+ tx[method](obj, detail || {});
4822
+ }
4823
+ }
4824
+
4825
+ function normalizeIds(ids) {
4826
+ if (ids == null) return [];
4827
+ return Array.isArray(ids) ? ids.slice() : [ids];
4828
+ }
4829
+
4830
+ function normalizeStrings(arr) {
4831
+ if (arr == null) return [];
4832
+ return Array.isArray(arr) ? arr.slice() : [arr];
4833
+ }
4834
+
4835
+ var DEFAULT_MAX_PREVIEW_PIXELS = 4e6;
4836
+
4837
+ function getRasterGrid(raster) {
4838
+ return raster && (raster.grid || raster);
4839
+ }
4840
+
4841
+ function getRasterView(raster) {
4842
+ return raster && (raster.view || raster);
4843
+ }
4844
+
4845
+ function getRasterPreview(raster) {
4846
+ var view = getRasterView(raster);
4847
+ return view && (view.preview || raster.preview);
4848
+ }
4849
+
4850
+ function getRasterBBox(raster) {
4851
+ var grid = getRasterGrid(raster);
4852
+ return grid && grid.bbox || raster && raster.bbox || null;
4853
+ }
4854
+
4855
+ function getRasterTransform(raster) {
4856
+ var grid = getRasterGrid(raster);
4857
+ return grid && grid.transform || raster && raster.transform || null;
4858
+ }
4859
+
4860
+ function getRasterWidth(raster) {
4861
+ var grid = getRasterGrid(raster);
4862
+ return grid && grid.width || 0;
4863
+ }
4864
+
4865
+ function getRasterHeight(raster) {
4866
+ var grid = getRasterGrid(raster);
4867
+ return grid && grid.height || 0;
4868
+ }
4869
+
4870
+ function getRasterBandCount(raster) {
4871
+ var grid = getRasterGrid(raster);
4872
+ return grid && grid.bands || 0;
4873
+ }
4874
+
4875
+ function getRasterPixelType(raster) {
4876
+ var grid = getRasterGrid(raster);
4877
+ return grid && grid.pixelType || null;
4878
+ }
4879
+
4880
+ function copyRasterData(raster) {
4881
+ var copy = utils.extend({}, raster);
4882
+ if (raster.grid) {
4883
+ copy.grid = copyRasterGrid(raster.grid);
4884
+ }
4885
+ if (raster.view) {
4886
+ copy.view = copyRasterView(raster.view);
4887
+ }
4888
+ if (raster.derivation) {
4889
+ copy.derivation = utils.extend({}, raster.derivation);
4890
+ if (raster.derivation.bands) copy.derivation.bands = copyObjectOrArray(raster.derivation.bands);
4891
+ }
4892
+ if (raster.source) copy.source = utils.extend({}, raster.source);
4893
+
4894
+ // Back-compat for the first raster model.
4895
+ if (raster.pixels) copy.pixels = copyTypedArray(raster.pixels);
4896
+ if (raster.preview) copy.preview = copyRasterPreview(raster.preview);
4897
+ if (raster.bbox) copy.bbox = raster.bbox.concat();
4898
+ if (raster.transform) copy.transform = copyObjectOrArray(raster.transform);
4899
+ return copy;
4900
+ }
4901
+
4902
+ function copyRasterGrid(grid) {
4903
+ var copy = utils.extend({}, grid);
4904
+ if (grid.samples) copy.samples = copyTypedArray(grid.samples);
4905
+ if (grid.sampleBands) copy.sampleBands = grid.sampleBands.concat();
4906
+ if (grid.bbox) copy.bbox = grid.bbox.concat();
4907
+ if (grid.transform) copy.transform = copyObjectOrArray(grid.transform);
4908
+ return copy;
4909
+ }
4910
+
4911
+ function copyRasterView(view) {
4912
+ var copy = utils.extend({}, view);
4913
+ if (view.recipe) copy.recipe = copyObjectOrArray(view.recipe);
4914
+ if (view.preview) copy.preview = copyRasterPreview(view.preview);
4915
+ if (view.scalingStats) copy.scalingStats = copyObjectOrArray(view.scalingStats);
4916
+ return copy;
4917
+ }
4918
+
4919
+ function copyRasterPreview(preview) {
4920
+ var copy = utils.extend({}, preview);
4921
+ delete copy.canvas;
4922
+ if (preview.pixels) copy.pixels = copyTypedArray(preview.pixels);
4923
+ return copy;
4924
+ }
4925
+
4926
+ function copyTypedArray(arr) {
4927
+ if (Array.isArray(arr)) return arr.map(copyTypedArray);
4928
+ return arr && arr.slice ? arr.slice() : arr;
4929
+ }
4930
+
4931
+ function createRasterPreview(raster, opts) {
4932
+ opts = opts || {};
4933
+ var grid = getRasterGrid(raster);
4934
+ var recipe = getRasterViewRecipe(grid, raster.view && raster.view.recipe, opts);
4935
+ var stats = getRasterViewScalingStats(raster, recipe);
4936
+ var maxPixels = opts.maxPixels || opts.raster_max_pixels || opts.rasterMaxPixels || DEFAULT_MAX_PREVIEW_PIXELS;
4937
+ var scale = Math.min(1, Math.sqrt(maxPixels / (grid.width * grid.height)));
4938
+ var width = Math.max(1, Math.round(grid.width * scale));
4939
+ var height = Math.max(1, Math.round(grid.height * scale));
4940
+ return renderRasterPreview(grid, recipe, width, height, stats);
4941
+ }
4942
+
4943
+ function getRasterViewRecipe(grid, recipeArg, opts) {
4944
+ var recipe = Object.assign({}, recipeArg || {});
4945
+ var scaling = opts && opts.scaling || recipe.scaling || getDefaultRasterScaling(grid, recipe);
4946
+ var scaleRange = opts && (opts.scale_range || opts.scaleRange) || recipe.scaleRange;
4947
+ var percentileRange = opts && (opts.percentile_range || opts.percentileRange) || recipe.percentileRange;
4948
+ if (scaling != 'none' && scaling != 'minmax' && scaling != 'percentile') {
4949
+ stop('Unsupported raster scaling method:', scaling);
4950
+ }
4951
+ return Object.assign(recipe, {
4952
+ type: recipe.type || (grid.bands >= 3 ? 'rgb' : 'gray'),
4953
+ bands: recipe.bands || grid.sampleBands || null,
4954
+ scaling: scaling,
4955
+ scaleRange: parseRangeOption(scaleRange, [0, 100], 'scale-range'),
4956
+ percentileRange: parseRangeOption(percentileRange, [2, 98], 'percentile-range')
4957
+ });
4958
+ }
4959
+
4960
+ function renderRasterPreview(grid, recipe, width, height, statsArg) {
4961
+ recipe = getRasterViewRecipe(grid, recipe);
4962
+ if (useFastRawEightBitRendering(grid, recipe)) {
4963
+ return renderRawEightBitPreview(grid, width, height, null);
4964
+ }
4965
+ return renderRasterGridPreview(grid, recipe, width, height, statsArg || getRasterScalingStats(grid, recipe), null);
4966
+ }
4967
+
4968
+ function renderRasterViewportPreview(grid, recipe, bbox, width, height, statsArg) {
4969
+ recipe = getRasterViewRecipe(grid, recipe);
4970
+ if (!intersectBboxes(grid.bbox, bbox)) return null;
4971
+ if (useFastRawEightBitRendering(grid, recipe)) {
4972
+ return Object.assign(renderRawEightBitPreview(grid, width, height, bbox), {
4973
+ bbox: bbox.concat()
4974
+ });
4975
+ }
4976
+ return Object.assign(renderRasterGridPreview(grid, recipe, width, height, statsArg || getRasterScalingStats(grid, recipe), bbox), {
4977
+ bbox: bbox.concat()
4978
+ });
4979
+ }
4980
+
4981
+ function getRasterScalingStats(grid, recipe) {
4982
+ recipe = getRasterViewRecipe(grid, recipe);
4983
+ return getScalingStats(grid.samples, grid.bands, grid.nodata, recipe);
4984
+ }
4985
+
4986
+ function getRasterViewScalingStats(raster, recipeArg) {
4987
+ var grid = getRasterGrid(raster);
4988
+ var view = getRasterView(raster);
4989
+ var recipe = getRasterViewRecipe(grid, recipeArg || view && view.recipe);
4990
+ var key, cached, stats;
4991
+ if (recipe.scaling == 'none') return null;
4992
+ key = getRasterScalingStatsKey(grid, recipe);
4993
+ cached = view && view.scalingStats;
4994
+ if (cached && cached.key == key) return cached.stats;
4995
+ stats = getRasterScalingStats(grid, recipe);
4996
+ if (view) {
4997
+ view.scalingStats = {
4998
+ key: key,
4999
+ stats: stats
5000
+ };
5001
+ }
5002
+ return stats;
5003
+ }
5004
+
5005
+ function renderRasterGridPreview(grid, recipe, width, height, statsArg, sourceBbox) {
5006
+ var samples = grid.samples;
5007
+ var bands = grid.bands;
5008
+ var noData = grid.nodata;
5009
+ var pixels = new Uint8ClampedArray(width * height * 4);
5010
+ var stats = statsArg || getScalingStats(samples, bands, noData, recipe);
5011
+ var sourceRange = recipe.scaling == 'none' ? getPixelTypeRange(grid.pixelType) : null;
5012
+ var displayRange = getDisplayRange(recipe.scaleRange);
5013
+ var src, dest, val, isNoData, j;
5014
+ for (var y = 0; y < height; y++) {
5015
+ for (var x = 0; x < width; x++) {
5016
+ src = getPreviewSourceOffset(grid, sourceBbox, x, y, width, height, bands);
5017
+ dest = (y * width + x) * 4;
5018
+ isNoData = noData !== null && noData !== undefined && allSamplesAreNoData(samples, src, bands, noData);
5019
+ if (bands == 1) {
5020
+ val = scaleSample(samples[src], stats && stats[0], sourceRange, displayRange);
5021
+ pixels[dest] = val;
5022
+ pixels[dest + 1] = val;
5023
+ pixels[dest + 2] = val;
5024
+ pixels[dest + 3] = isNoData ? 0 : 255;
5025
+ } else {
5026
+ for (j = 0; j < 3; j++) {
5027
+ pixels[dest + j] = scaleSample(samples[src + j], stats && stats[j], sourceRange, displayRange);
5028
+ }
5029
+ pixels[dest + 3] = isNoData ? 0 : bands >= 4 ? scaleSample(samples[src + 3], stats && stats[3], sourceRange, [0, 255]) : 255;
5030
+ }
5031
+ }
5032
+ }
5033
+ return {
5034
+ width: width,
5035
+ height: height,
5036
+ bands: 4,
5037
+ pixelType: 'uint8',
5038
+ colorModel: 'rgba',
5039
+ pixels: pixels
5040
+ };
5041
+ }
5042
+
5043
+ function renderRawEightBitPreview(grid, width, height, sourceBbox) {
5044
+ var samples = grid.samples;
5045
+ var bands = grid.bands;
5046
+ var pixels = new Uint8ClampedArray(width * height * 4);
5047
+ var src, dest, val;
5048
+ for (var y = 0; y < height; y++) {
5049
+ for (var x = 0; x < width; x++) {
5050
+ src = getPreviewSourceOffset(grid, sourceBbox, x, y, width, height, bands);
5051
+ dest = (y * width + x) * 4;
5052
+ if (bands == 1) {
5053
+ val = samples[src];
5054
+ pixels[dest] = val;
5055
+ pixels[dest + 1] = val;
5056
+ pixels[dest + 2] = val;
5057
+ pixels[dest + 3] = 255;
5058
+ } else {
5059
+ pixels[dest] = samples[src];
5060
+ pixels[dest + 1] = samples[src + 1];
5061
+ pixels[dest + 2] = samples[src + 2];
5062
+ pixels[dest + 3] = bands >= 4 ? samples[src + 3] : 255;
5063
+ }
5064
+ }
5065
+ }
5066
+ return {
5067
+ width: width,
5068
+ height: height,
5069
+ bands: 4,
5070
+ pixelType: 'uint8',
5071
+ colorModel: 'rgba',
5072
+ pixels: pixels
5073
+ };
5074
+ }
5075
+
5076
+ function getPreviewSourceOffset(grid, sourceBbox, x, y, width, height, bands) {
5077
+ var sx, sy, mapX, mapY, rb;
5078
+ if (!sourceBbox) {
5079
+ sy = Math.min(grid.height - 1, Math.floor((y + 0.5) * grid.height / height));
5080
+ sx = Math.min(grid.width - 1, Math.floor((x + 0.5) * grid.width / width));
5081
+ } else {
5082
+ rb = grid.bbox;
5083
+ mapX = sourceBbox[0] + (x + 0.5) / width * (sourceBbox[2] - sourceBbox[0]);
5084
+ mapY = sourceBbox[3] - (y + 0.5) / height * (sourceBbox[3] - sourceBbox[1]);
5085
+ sx = Math.max(0, Math.min(grid.width - 1, Math.floor((mapX - rb[0]) / (rb[2] - rb[0]) * grid.width)));
5086
+ sy = Math.max(0, Math.min(grid.height - 1, Math.floor((rb[3] - mapY) / (rb[3] - rb[1]) * grid.height)));
5087
+ }
5088
+ return (sy * grid.width + sx) * bands;
5089
+ }
5090
+
5091
+ function clipRasterToBBox(lyr, bbox, opts) {
5092
+ var raster = lyr.raster;
5093
+ var grid = getRasterGrid(raster);
5094
+ var clipBbox = intersectBboxes(grid.bbox, bbox);
5095
+ if (!clipBbox) {
5096
+ warn('Raster clipping rectangle does not intersect the raster layer');
5097
+ return false;
5098
+ }
5099
+ var crop = getRasterCrop(grid, clipBbox);
5100
+ if (crop.width === 0 || crop.height === 0) {
5101
+ warn('Raster clipping rectangle does not intersect the raster layer');
5102
+ return false;
5103
+ }
5104
+ noteLayerWillChange(lyr, {operation: 'clipRasterToBBox', unit: 'raster'});
5105
+ raster.grid = cropRasterGrid(grid, crop, clipBbox);
5106
+ raster.view = raster.view || {};
5107
+ raster.view.preview = createRasterPreview(raster, opts || {});
5108
+ clearLegacyRasterFields(raster);
5109
+ markLayerChanged(lyr, {operation: 'clipRasterToBBox', unit: 'raster'});
5110
+ return true;
5111
+ }
5112
+
5113
+ function intersectBboxes(a, b) {
5114
+ var bbox = [
5115
+ Math.max(a[0], b[0]),
5116
+ Math.max(a[1], b[1]),
5117
+ Math.min(a[2], b[2]),
5118
+ Math.min(a[3], b[3])
5119
+ ];
5120
+ return bbox[2] <= bbox[0] || bbox[3] <= bbox[1] ? null : bbox;
5121
+ }
5122
+
5123
+ function getRasterCrop(grid, bbox) {
5124
+ var rb = grid.bbox;
5125
+ var x0 = Math.max(0, Math.floor((bbox[0] - rb[0]) / (rb[2] - rb[0]) * grid.width));
5126
+ var x1 = Math.min(grid.width, Math.ceil((bbox[2] - rb[0]) / (rb[2] - rb[0]) * grid.width));
5127
+ var y0 = Math.max(0, Math.floor((rb[3] - bbox[3]) / (rb[3] - rb[1]) * grid.height));
5128
+ var y1 = Math.min(grid.height, Math.ceil((rb[3] - bbox[1]) / (rb[3] - rb[1]) * grid.height));
5129
+ return {
5130
+ x: x0,
5131
+ y: y0,
5132
+ width: Math.max(0, x1 - x0),
5133
+ height: Math.max(0, y1 - y0)
5134
+ };
5135
+ }
5136
+
5137
+ function cropRasterGrid(grid, crop, bbox) {
5138
+ var samples = new grid.samples.constructor(crop.width * crop.height * grid.bands);
5139
+ var rowCount = crop.width * grid.bands;
5140
+ var src, dest;
5141
+ for (var y = 0; y < crop.height; y++) {
5142
+ src = ((crop.y + y) * grid.width + crop.x) * grid.bands;
5143
+ dest = y * rowCount;
5144
+ samples.set(grid.samples.subarray(src, src + rowCount), dest);
5145
+ }
5146
+ return Object.assign({}, grid, {
5147
+ width: crop.width,
5148
+ height: crop.height,
5149
+ samples: samples,
5150
+ bbox: bbox,
5151
+ transform: updateTransformForBBox(grid.transform, bbox, crop.width, crop.height)
5152
+ });
5153
+ }
5154
+
5155
+ function getRasterLayerBounds(lyr) {
5156
+ var bbox = lyr.raster && getRasterBBox(lyr.raster);
5157
+ return bbox && bbox.length == 4 ? new Bounds(bbox) : null;
5158
+ }
5159
+
5160
+ function requireRasterLayer(lyr) {
5161
+ if (!lyr || !lyr.raster_type || !lyr.raster) {
5162
+ stop('Expected a raster layer');
5163
+ }
4309
5164
  }
4310
5165
 
4311
- function noteTableFieldsWillChange(table, fields, detail) {
4312
- notifyTransaction(activeTransaction, 'captureTableFieldsBefore', table, Object.assign({
4313
- fields: normalizeStrings(fields)
4314
- }, detail || {}));
5166
+ function clearLegacyRasterFields(raster) {
5167
+ delete raster.width;
5168
+ delete raster.height;
5169
+ delete raster.bands;
5170
+ delete raster.pixelType;
5171
+ delete raster.nodata;
5172
+ delete raster.bbox;
5173
+ delete raster.transform;
5174
+ delete raster.colorModel;
5175
+ delete raster.preview;
5176
+ delete raster.pixels;
4315
5177
  }
4316
5178
 
4317
- function noteTableOrderWillChange(table, ids, detail) {
4318
- notifyTransaction(activeTransaction, 'captureTableOrderBefore', table, Object.assign({
4319
- ids: normalizeIds(ids)
4320
- }, detail || {}));
5179
+ function updateTransformForBBox(transform, bbox, width, height) {
5180
+ if (!transform) return null;
5181
+ return [
5182
+ (bbox[2] - bbox[0]) / width,
5183
+ 0,
5184
+ bbox[0],
5185
+ 0,
5186
+ -(bbox[3] - bbox[1]) / height,
5187
+ bbox[3]
5188
+ ];
4321
5189
  }
4322
5190
 
4323
- function noteTableSchemaWillChange(table, detail) {
4324
- notifyTransaction(activeTransaction, 'captureTableSchemaBefore', table, detail || {});
5191
+ function getBandStats(data, bands, noData) {
5192
+ var stats = [];
5193
+ for (var band = 0; band < bands; band++) {
5194
+ stats[band] = getSingleBandStats(data, bands, band, noData);
5195
+ }
5196
+ return stats;
4325
5197
  }
4326
5198
 
4327
- function markTableChanged(table, detail) {
4328
- return markChanged(table, Object.assign({type: 'table'}, detail || {}));
5199
+ function getSingleBandStats(data, bands, band, noData) {
5200
+ var min = Infinity;
5201
+ var max = -Infinity;
5202
+ var val;
5203
+ for (var i = band; i < data.length; i += bands) {
5204
+ val = data[i];
5205
+ if (!isValidBandValue(val, noData)) continue;
5206
+ if (val < min) min = val;
5207
+ if (val > max) max = val;
5208
+ }
5209
+ return min < Infinity && max > min ? {min: min, max: max} : {min: 0, max: 255};
4329
5210
  }
4330
5211
 
4331
- function markTableRecordsChanged(table, ids, detail) {
4332
- return markTableChanged(table, Object.assign({
4333
- granularity: 'records',
4334
- ids: normalizeIds(ids)
4335
- }, detail || {}));
5212
+ function getSharedBandStats(data, bands, noData) {
5213
+ var stats = getBandStats(data, bands, noData);
5214
+ var min = Math.min(stats[0].min, stats[1].min, stats[2].min);
5215
+ var max = Math.max(stats[0].max, stats[1].max, stats[2].max);
5216
+ var shared = max > min ? {min: min, max: max} : {min: 0, max: 255};
5217
+ stats[0] = stats[1] = stats[2] = shared;
5218
+ return stats;
4336
5219
  }
4337
5220
 
4338
- function markTableFieldsChanged(table, fields, detail) {
4339
- return markTableChanged(table, Object.assign({
4340
- granularity: 'fields',
4341
- fields: normalizeStrings(fields)
4342
- }, detail || {}));
5221
+ function getScalingStats(data, bands, noData, recipe) {
5222
+ var shared = recipe.type == 'rgb' && bands >= 3;
5223
+ if (recipe.scaling == 'minmax') {
5224
+ return shared ? getSharedBandStats(data, bands, noData) : getBandStats(data, bands, noData);
5225
+ }
5226
+ if (recipe.scaling == 'percentile') {
5227
+ return shared ? getSharedPercentileStats(data, bands, noData, recipe.percentileRange) :
5228
+ getBandPercentileStats(data, bands, noData, recipe.percentileRange);
5229
+ }
5230
+ return null;
4343
5231
  }
4344
5232
 
4345
- function markTableSchemaChanged(table, detail) {
4346
- return markTableChanged(table, Object.assign({
4347
- granularity: 'schema'
4348
- }, detail || {}));
5233
+ function scaleSample(val, stats, sourceRange, displayRange) {
5234
+ var range = stats || sourceRange || {min: 0, max: 255};
5235
+ var pct = range.max > range.min ? (val - range.min) / (range.max - range.min) : 0;
5236
+ var scaled = displayRange[0] + pct * (displayRange[1] - displayRange[0]);
5237
+ return Math.max(0, Math.min(255, Math.round(scaled)));
4349
5238
  }
4350
5239
 
4351
- function markTableOrderChanged(table, ids, detail) {
4352
- return markTableChanged(table, Object.assign({
4353
- granularity: 'order',
4354
- ids: normalizeIds(ids)
4355
- }, detail || {}));
5240
+ function getDefaultRasterScaling(grid, recipe) {
5241
+ return isEightBitPixelType(grid.pixelType) ? 'none' : 'percentile';
4356
5242
  }
4357
5243
 
4358
- function noteArcsWillChange(arcs, detail) {
4359
- notifyTransaction(activeTransaction, 'captureArcsBefore', arcs, detail || {});
5244
+ function parseRangeOption(val, def, name) {
5245
+ var range;
5246
+ if (val == null || val === '') return def;
5247
+ range = Array.isArray(val) ? val : String(val).split(',');
5248
+ if (range.length != 2) {
5249
+ stop('Expected ' + name + '= to contain two comma-separated numbers');
5250
+ }
5251
+ range = range.map(Number);
5252
+ if (!isFinite(range[0]) || !isFinite(range[1]) || range[0] < 0 || range[1] > 100 || range[1] < range[0]) {
5253
+ stop('Expected ' + name + '= values between 0 and 100');
5254
+ }
5255
+ return range;
4360
5256
  }
4361
5257
 
4362
- function noteArcsSimplificationWillChange$1(arcs, detail) {
4363
- notifyTransaction(activeTransaction, 'captureArcsSimplificationBefore', arcs, detail || {});
5258
+ function getDisplayRange(scaleRange) {
5259
+ return [
5260
+ scaleRange[0] / 100 * 255,
5261
+ scaleRange[1] / 100 * 255
5262
+ ];
4364
5263
  }
4365
5264
 
4366
- function markArcsChanged(arcs, detail) {
4367
- return markChanged(arcs, Object.assign({type: 'arcs'}, detail || {}));
5265
+ function getPixelTypeRange(pixelType) {
5266
+ switch (pixelType) {
5267
+ case 'uint8': return {min: 0, max: 255};
5268
+ case 'uint16': return {min: 0, max: 65535};
5269
+ case 'uint32': return {min: 0, max: 4294967295};
5270
+ case 'int8': return {min: -128, max: 127};
5271
+ case 'int16': return {min: -32768, max: 32767};
5272
+ case 'int32': return {min: -2147483648, max: 2147483647};
5273
+ }
5274
+ return null;
4368
5275
  }
4369
5276
 
4370
- function markArcsSimplificationChanged$1(arcs, detail) {
4371
- return markArcsChanged(arcs, Object.assign({granularity: 'simplification'}, detail || {}));
5277
+ function isEightBitPixelType(pixelType) {
5278
+ return pixelType == 'uint8' || pixelType == 'int8';
4372
5279
  }
4373
5280
 
4374
- function noteCatalogWillChange(catalog, detail) {
4375
- notifyTransaction(activeTransaction, 'captureCatalogBefore', catalog, detail || {});
5281
+ function useFastRawEightBitRendering(grid, recipe) {
5282
+ return grid.pixelType == 'uint8' &&
5283
+ recipe.scaling == 'none' &&
5284
+ recipe.scaleRange[0] === 0 &&
5285
+ recipe.scaleRange[1] === 100 &&
5286
+ (recipe.type == 'gray' && grid.bands == 1 || recipe.type == 'rgb' && grid.bands >= 3) &&
5287
+ (grid.nodata === null || grid.nodata === undefined);
4376
5288
  }
4377
5289
 
4378
- function markCatalogChanged(catalog, detail) {
4379
- return markChanged(catalog, Object.assign({type: 'catalog'}, detail || {}));
5290
+ function getRasterScalingStatsKey(grid, recipe) {
5291
+ return [
5292
+ grid.width,
5293
+ grid.height,
5294
+ grid.bands,
5295
+ grid.pixelType,
5296
+ grid.samples && grid.samples.length,
5297
+ grid.nodata,
5298
+ recipe.type,
5299
+ recipe.scaling,
5300
+ recipe.scaleRange && recipe.scaleRange.join(','),
5301
+ recipe.percentileRange && recipe.percentileRange.join(',')
5302
+ ].join('|');
4380
5303
  }
4381
5304
 
4382
- function noteDatasetWillChange(dataset, detail) {
4383
- notifyTransaction(activeTransaction, 'captureDatasetBefore', dataset, detail || {});
5305
+ function getBandPercentileStats(data, bands, noData, range) {
5306
+ var stats = [];
5307
+ for (var band = 0; band < bands; band++) {
5308
+ stats[band] = getPercentileStats(data, bands, [band], noData, range);
5309
+ }
5310
+ return stats;
4384
5311
  }
4385
5312
 
4386
- function noteDatasetInfoWillChange(dataset, detail) {
4387
- notifyTransaction(activeTransaction, 'captureDatasetInfoBefore', dataset, detail || {});
5313
+ function getSharedPercentileStats(data, bands, noData, range) {
5314
+ var stats = getBandPercentileStats(data, bands, noData, range);
5315
+ var shared = getPercentileStats(data, bands, [0, 1, 2], noData, range);
5316
+ stats[0] = stats[1] = stats[2] = shared;
5317
+ return stats;
4388
5318
  }
4389
5319
 
4390
- function markDatasetChanged(dataset, detail) {
4391
- return markChanged(dataset, Object.assign({type: 'dataset'}, detail || {}));
5320
+ function getPercentileStats(data, bands, bandIds, noData, range) {
5321
+ var integerRange = getSmallIntegerRange(data);
5322
+ return integerRange ?
5323
+ getHistogramPercentileStats(data, bands, bandIds, noData, range, integerRange) :
5324
+ getApproxHistogramPercentileStats(data, bands, bandIds, noData, range);
4392
5325
  }
4393
5326
 
4394
- function markDatasetInfoChanged(dataset, detail) {
4395
- return markDatasetChanged(dataset, Object.assign({granularity: 'info'}, detail || {}));
5327
+ function getSmallIntegerRange(data) {
5328
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) return {min: 0, max: 255};
5329
+ if (data instanceof Int8Array) return {min: -128, max: 127};
5330
+ if (data instanceof Uint16Array) return {min: 0, max: 65535};
5331
+ if (data instanceof Int16Array) return {min: -32768, max: 32767};
5332
+ return null;
4396
5333
  }
4397
5334
 
4398
- function noteLayerWillChange(layer, detail) {
4399
- notifyTransaction(activeTransaction, 'captureLayerBefore', layer, detail || {});
5335
+ function getHistogramPercentileStats(data, bands, bandIds, noData, range, integerRange) {
5336
+ var counts = new Uint32Array(integerRange.max - integerRange.min + 1);
5337
+ var count = 0, val;
5338
+ forEachBandValue(data, bands, bandIds, noData, function(v) {
5339
+ val = v - integerRange.min;
5340
+ counts[val]++;
5341
+ count++;
5342
+ });
5343
+ return count > 0 ? {
5344
+ min: getHistogramPercentileValue(counts, integerRange.min, count, range[0]),
5345
+ max: getHistogramPercentileValue(counts, integerRange.min, count, range[1])
5346
+ } : {min: 0, max: 255};
5347
+ }
5348
+
5349
+ function getApproxHistogramPercentileStats(data, bands, bandIds, noData, range) {
5350
+ var bounds = getBandValueBounds(data, bands, bandIds, noData);
5351
+ var binCount = 65536;
5352
+ var counts, count, scale, maxBin;
5353
+ if (!bounds || bounds.max <= bounds.min) {
5354
+ return bounds || {min: 0, max: 255};
5355
+ }
5356
+ counts = new Uint32Array(binCount);
5357
+ count = 0;
5358
+ scale = (binCount - 1) / (bounds.max - bounds.min);
5359
+ maxBin = binCount - 1;
5360
+ forEachBandValue(data, bands, bandIds, noData, function(val) {
5361
+ counts[Math.max(0, Math.min(maxBin, Math.floor((val - bounds.min) * scale)))]++;
5362
+ count++;
5363
+ });
5364
+ return count > 0 ? {
5365
+ min: getApproxHistogramPercentileValue(counts, bounds, count, range[0]),
5366
+ max: getApproxHistogramPercentileValue(counts, bounds, count, range[1])
5367
+ } : {min: 0, max: 255};
4400
5368
  }
4401
5369
 
4402
- function noteLayerMetadataWillChange(layer, detail) {
4403
- notifyTransaction(activeTransaction, 'captureLayerMetadataBefore', layer, detail || {});
5370
+ function getBandValueBounds(data, bands, bandIds, noData) {
5371
+ var min = Infinity;
5372
+ var max = -Infinity;
5373
+ forEachBandValue(data, bands, bandIds, noData, function(val) {
5374
+ if (val < min) min = val;
5375
+ if (val > max) max = val;
5376
+ });
5377
+ return min < Infinity ? {min: min, max: max} : null;
4404
5378
  }
4405
5379
 
4406
- function noteLayerOrderWillChange(layer, ids, detail) {
4407
- notifyTransaction(activeTransaction, 'captureLayerOrderBefore', layer, Object.assign({
4408
- ids: normalizeIds(ids)
4409
- }, detail || {}));
5380
+ function getHistogramPercentileValue(counts, offset, count, pct) {
5381
+ var target = getPercentileRank(count, pct);
5382
+ var sum = 0;
5383
+ for (var i = 0; i < counts.length; i++) {
5384
+ sum += counts[i];
5385
+ if (sum > target) return i + offset;
5386
+ }
5387
+ return counts.length - 1 + offset;
4410
5388
  }
4411
5389
 
4412
- function markLayerChanged(layer, detail) {
4413
- return markChanged(layer, Object.assign({type: 'layer'}, detail || {}));
5390
+ function getApproxHistogramPercentileValue(counts, bounds, count, pct) {
5391
+ var bin = getHistogramPercentileValue(counts, 0, count, pct);
5392
+ return bounds.min + bin / (counts.length - 1) * (bounds.max - bounds.min);
4414
5393
  }
4415
5394
 
4416
- function markLayerMetadataChanged(layer, detail) {
4417
- return markLayerChanged(layer, Object.assign({granularity: 'metadata'}, detail || {}));
5395
+ function forEachBandValue(data, bands, bandIds, noData, cb) {
5396
+ var val;
5397
+ for (var i = 0; i < data.length; i += bands) {
5398
+ for (var j = 0; j < bandIds.length; j++) {
5399
+ val = data[i + bandIds[j]];
5400
+ if (!isValidBandValue(val, noData)) continue;
5401
+ cb(val);
5402
+ }
5403
+ }
4418
5404
  }
4419
5405
 
4420
- function markLayerOrderChanged(layer, ids, detail) {
4421
- return markLayerChanged(layer, Object.assign({
4422
- granularity: 'order',
4423
- ids: normalizeIds(ids)
4424
- }, detail || {}));
5406
+ function isValidBandValue(val, noData) {
5407
+ return isFinite(val) && (noData === null || noData === undefined || val != noData);
4425
5408
  }
4426
5409
 
4427
- function notifyTransaction(tx, method, obj, detail) {
4428
- if (tx && typeof tx[method] == 'function') {
4429
- tx[method](obj, detail || {});
4430
- }
5410
+ function getPercentileRank(count, pct) {
5411
+ return Math.max(0, Math.min(count - 1, Math.floor((count - 1) * pct / 100)));
4431
5412
  }
4432
5413
 
4433
- function normalizeIds(ids) {
4434
- if (ids == null) return [];
4435
- return Array.isArray(ids) ? ids.slice() : [ids];
5414
+ function allSamplesAreNoData(data, offset, bands, noData) {
5415
+ var n = Math.min(bands, 3);
5416
+ for (var i = 0; i < n; i++) {
5417
+ if (data[offset + i] != noData) return false;
5418
+ }
5419
+ return true;
4436
5420
  }
4437
5421
 
4438
- function normalizeStrings(arr) {
4439
- if (arr == null) return [];
4440
- return Array.isArray(arr) ? arr.slice() : [arr];
5422
+ function copyObjectOrArray(obj) {
5423
+ return obj && obj.concat ? obj.concat() : utils.extend({}, obj);
4441
5424
  }
4442
5425
 
4443
5426
  function UndoTransaction(label) {
@@ -4652,6 +5635,8 @@
4652
5635
  detail: copyDetail(detail),
4653
5636
  name: layer.name,
4654
5637
  geometry_type: layer.geometry_type,
5638
+ raster_type: layer.raster_type || null,
5639
+ raster: layer.raster ? copyRasterData(layer.raster) : null,
4655
5640
  shapes: layer.shapes ? cloneShapes(layer.shapes) : null,
4656
5641
  data: layer.data || null
4657
5642
  });
@@ -4945,6 +5930,8 @@
4945
5930
  revision: getUndoRevision(unit.target),
4946
5931
  name: unit.target.name,
4947
5932
  geometry_type: unit.target.geometry_type,
5933
+ raster_type: unit.target.raster_type || null,
5934
+ raster: unit.target.raster ? copyRasterData(unit.target.raster) : null,
4948
5935
  shapes: unit.target.shapes ? cloneShapes(unit.target.shapes) : null,
4949
5936
  data: unit.target.data || null
4950
5937
  });
@@ -5071,6 +6058,8 @@
5071
6058
  function restoreLayer(unit) {
5072
6059
  unit.target.name = unit.name;
5073
6060
  unit.target.geometry_type = unit.geometry_type;
6061
+ unit.target.raster_type = unit.raster_type || null;
6062
+ unit.target.raster = unit.raster ? copyRasterData(unit.raster) : null;
5074
6063
  unit.target.shapes = unit.shapes ? cloneShapes(unit.shapes) : null;
5075
6064
  unit.target.data = unit.data;
5076
6065
  }
@@ -5177,7 +6166,7 @@
5177
6166
  'table-schema': ['fields'],
5178
6167
  arcs: ['nn', 'xx', 'yy', 'zz', 'zlimit'],
5179
6168
  'arcs-simplification': ['zz'],
5180
- layer: ['shapes']
6169
+ layer: ['shapes', 'raster']
5181
6170
  };
5182
6171
 
5183
6172
  async function storeUndoUnits(units, store, entryId, role) {
@@ -5282,9 +6271,19 @@
5282
6271
  }
5283
6272
  });
5284
6273
  if (!hasPayload) return null;
6274
+ if (unit.type == 'layer') return packLayerPayload(payload);
5285
6275
  return unit.type == 'table' ? packTablePayload(payload) : payload;
5286
6276
  }
5287
6277
 
6278
+ async function packLayerPayload(payload) {
6279
+ if (payload.raster) {
6280
+ payload = Object.assign({}, payload, {
6281
+ raster: packRasterUndoPayload(payload.raster)
6282
+ });
6283
+ }
6284
+ return payload;
6285
+ }
6286
+
5288
6287
  async function packTablePayload(payload) {
5289
6288
  return {
5290
6289
  packedRecords: packRecordsAsColumns(payload.records)
@@ -5297,9 +6296,46 @@
5297
6296
  records: unpackRecordsFromColumns(payload.packedRecords)
5298
6297
  };
5299
6298
  }
6299
+ if (unit.type == 'layer' && payload.raster) {
6300
+ return Object.assign({}, payload, {
6301
+ raster: unpackRasterUndoPayload(payload.raster)
6302
+ });
6303
+ }
5300
6304
  return payload;
5301
6305
  }
5302
6306
 
6307
+ function packRasterUndoPayload(raster) {
6308
+ var copy = Object.assign({}, raster);
6309
+ if (raster.view) {
6310
+ copy.view = Object.assign({}, raster.view);
6311
+ if (raster.view.preview) {
6312
+ copy.view.preview = stripPreviewPixels(raster.view.preview);
6313
+ }
6314
+ }
6315
+ if (raster.preview) {
6316
+ copy.preview = stripPreviewPixels(raster.preview);
6317
+ }
6318
+ return copy;
6319
+ }
6320
+
6321
+ function unpackRasterUndoPayload(raster) {
6322
+ var copy = Object.assign({}, raster);
6323
+ if (raster.view) {
6324
+ copy.view = Object.assign({}, raster.view);
6325
+ if (raster.view.preview && !raster.view.preview.pixels && raster.grid && raster.grid.samples) {
6326
+ copy.view.preview = renderRasterPreview(raster.grid, raster.view.recipe, raster.view.preview.width, raster.view.preview.height);
6327
+ }
6328
+ }
6329
+ return copy;
6330
+ }
6331
+
6332
+ function stripPreviewPixels(preview) {
6333
+ var copy = Object.assign({}, preview);
6334
+ delete copy.canvas;
6335
+ delete copy.pixels;
6336
+ return copy;
6337
+ }
6338
+
5303
6339
  function packRecordsAsColumns(records) {
5304
6340
  var fields = getRecordFields(records);
5305
6341
  return {
@@ -5458,7 +6494,15 @@
5458
6494
  if (!gui.undoPayloadStore) {
5459
6495
  gui.undoPayloadStore = createUndoPayloadStore(getUndoPayloadStoreOptions());
5460
6496
  gui.undoPayloadStore.startLifecycle();
5461
- gui.undoPayloadStore.cleanupStaleSessions();
6497
+ gui.undoPayloadStore.cleanupStaleSessions().then(function(result) {
6498
+ logStartupCleanup({
6499
+ count: result.keys.length,
6500
+ sessionCount: result.sessionCount,
6501
+ singular: 'undo payload',
6502
+ plural: 'undo payloads',
6503
+ sizeBytes: result.sizeBytes
6504
+ });
6505
+ }).catch(function() {});
5462
6506
  }
5463
6507
  return gui.undoPayloadStore;
5464
6508
  }
@@ -5496,6 +6540,7 @@
5496
6540
  delete copy.zz;
5497
6541
  delete copy.zlimit;
5498
6542
  delete copy.shapes;
6543
+ delete copy.raster;
5499
6544
  if (copy.type != 'table-records') delete copy.records;
5500
6545
  if (copy.type != 'table-fields') delete copy.columns;
5501
6546
  delete copy.fields;
@@ -5717,6 +6762,7 @@
5717
6762
 
5718
6763
  var geopackagePromise = null;
5719
6764
  var geoParquetPromise = null;
6765
+ var geoTIFFPromise = null;
5720
6766
 
5721
6767
  async function loadGeopackageLib() {
5722
6768
  if (!window.modules || !window.modules['@ngageoint/geopackage']) {
@@ -5760,6 +6806,135 @@
5760
6806
  }
5761
6807
  }
5762
6808
 
6809
+ async function loadGeoTIFFLib() {
6810
+ if (!window.modules || !window.modules.geotiff) {
6811
+ if (!geoTIFFPromise) {
6812
+ geoTIFFPromise = loadScript('geotiff.js');
6813
+ }
6814
+ await geoTIFFPromise;
6815
+ geoTIFFPromise = null;
6816
+ }
6817
+ }
6818
+
6819
+ var idb = require$1('idb-keyval');
6820
+ var KEY_PREFIX = 'msr';
6821
+ var SESSION_KEY = 'mapshaper_raster_source_sessions';
6822
+ var lifecycle = createTempSessionLifecycle({
6823
+ prefix: 'raster',
6824
+ sessionKey: SESSION_KEY
6825
+ });
6826
+ var sessionId = lifecycle.getSessionId();
6827
+ var ownKeys = new Set();
6828
+ var sourceCount = 0;
6829
+ var sampleCount = 0;
6830
+ var lifecycleStarted = false;
6831
+
6832
+ async function persistRasterSourceForDataset(dataset, group) {
6833
+ if (!idb) return;
6834
+ var source = getRasterSourceFile(group);
6835
+ var sourceKey = source && source.content ?
6836
+ await persistRasterSourceBytes(dataset, source) : null;
6837
+ await persistRasterLayerSamples(dataset);
6838
+ lifecycle.touch();
6839
+ return sourceKey;
6840
+ }
6841
+
6842
+ function getRasterSourceFile(group) {
6843
+ return group && (group.geotiff || group.png || group.jpeg) || null;
6844
+ }
6845
+
6846
+ function startRasterSourceStoreLifecycle() {
6847
+ if (lifecycleStarted || !idb) return;
6848
+ lifecycleStarted = true;
6849
+ lifecycle.start(attemptOwnSourceDeletion);
6850
+ cleanupStaleRasterSources().then(function(result) {
6851
+ logStartupCleanup({
6852
+ count: result.keys.length,
6853
+ sessionCount: result.sessionCount,
6854
+ singular: 'raster temp file',
6855
+ plural: 'raster temp files'
6856
+ });
6857
+ }).catch(function() {});
6858
+ }
6859
+
6860
+ async function cleanupStaleRasterSources() {
6861
+ if (!idb) return {keys: [], sessionCount: 0};
6862
+ var liveSessions = lifecycle.getLiveSessions();
6863
+ var keys = await idb.keys();
6864
+ var doomedSessions = new Set();
6865
+ var doomedKeys = keys.filter(function(key) {
6866
+ var sid = getSessionFromKey(key);
6867
+ var stale = isRasterSourceKey(key) && !liveSessions[sid];
6868
+ if (stale && sid) doomedSessions.add(sid);
6869
+ return stale;
6870
+ });
6871
+ if (doomedKeys.length > 0) {
6872
+ await idb.delMany(doomedKeys);
6873
+ }
6874
+ return {
6875
+ keys: doomedKeys,
6876
+ sessionCount: doomedSessions.size
6877
+ };
6878
+ }
6879
+
6880
+ function makeRasterSourceKey(filename) {
6881
+ sourceCount++;
6882
+ return [KEY_PREFIX, sessionId, 'source', sourceCount, filename || 'raster'].join(':');
6883
+ }
6884
+
6885
+ function makeRasterSamplesKey(layerName) {
6886
+ sampleCount++;
6887
+ return [KEY_PREFIX, sessionId, 'samples', sampleCount, layerName || 'raster'].join(':');
6888
+ }
6889
+
6890
+ async function persistRasterSourceBytes(dataset, sourceFile) {
6891
+ var key = makeRasterSourceKey(sourceFile.filename);
6892
+ await idb.set(key, sourceFile.content);
6893
+ ownKeys.add(key);
6894
+ if (dataset.info && dataset.info.raster_sources) {
6895
+ dataset.info.raster_sources.forEach(function(source) {
6896
+ source.storage = 'indexeddb';
6897
+ source.key = key;
6898
+ });
6899
+ }
6900
+ return key;
6901
+ }
6902
+
6903
+ async function persistRasterLayerSamples(dataset) {
6904
+ var promises = [];
6905
+ dataset.layers.forEach(function(lyr) {
6906
+ var grid = lyr.raster && getRasterGrid(lyr.raster);
6907
+ var key;
6908
+ if (!grid || !grid.samples) return;
6909
+ key = makeRasterSamplesKey(lyr.name);
6910
+ ownKeys.add(key);
6911
+ grid.storage = {
6912
+ type: 'indexeddb',
6913
+ key: key
6914
+ };
6915
+ promises.push(idb.set(key, grid.samples));
6916
+ });
6917
+ await Promise.all(promises);
6918
+ }
6919
+
6920
+ function attemptOwnSourceDeletion() {
6921
+ var keys = Array.from(ownKeys);
6922
+ ownKeys.clear();
6923
+ if (keys.length === 0) return;
6924
+ try {
6925
+ idb.delMany(keys).catch(function() {});
6926
+ } catch(e) {}
6927
+ }
6928
+
6929
+ function isRasterSourceKey(key) {
6930
+ return typeof key == 'string' && key.indexOf(KEY_PREFIX + ':') === 0;
6931
+ }
6932
+
6933
+ function getSessionFromKey(key) {
6934
+ var parts = String(key).split(':');
6935
+ return parts.length > 4 && parts[0] == KEY_PREFIX ? parts[1] : null;
6936
+ }
6937
+
5763
6938
  // @cb function(<FileList>)
5764
6939
  function DropControl(gui, el, cb) {
5765
6940
  var area = El(el);
@@ -6200,7 +7375,10 @@
6200
7375
  if (group.parquet) {
6201
7376
  await loadGeoParquetLib();
6202
7377
  }
6203
- if (group.gpkg || group.fgb || group.parquet) {
7378
+ if (group.geotiff) {
7379
+ await loadGeoTIFFLib();
7380
+ }
7381
+ if (group.gpkg || group.fgb || group.parquet || group.geotiff || group.png || group.jpeg) {
6204
7382
  dataset = await internal.importContentAsync(group, importOpts);
6205
7383
  } else {
6206
7384
  dataset = internal.importContent(group, importOpts);
@@ -6211,6 +7389,7 @@
6211
7389
  if (group.layername) {
6212
7390
  d.layers.forEach(lyr => lyr.name = group.layername);
6213
7391
  }
7392
+ await persistRasterSourceForDataset(d, group);
6214
7393
  // TODO: add popup here
6215
7394
  // save import options for use by repair control, etc.
6216
7395
  d.info.import_options = importOpts;
@@ -6231,7 +7410,8 @@
6231
7410
  function filesMayContainPaths(files) {
6232
7411
  return utils$1.some(files, function(f) {
6233
7412
  var type = internal.guessInputFileType(f.name);
6234
- return type == 'shp' || type == 'json' || type == 'gpkg' || type == 'parquet' || internal.isZipFile(f.name);
7413
+ return type == 'shp' || type == 'json' || type == 'gpkg' || type == 'parquet' ||
7414
+ type == 'png' || type == 'jpeg' || internal.isZipFile(f.name);
6235
7415
  });
6236
7416
  }
6237
7417
 
@@ -6245,6 +7425,10 @@
6245
7425
  return /\.(shp|shx|dbf|prj|cpg)$/i.test(name);
6246
7426
  }
6247
7427
 
7428
+ function isRasterImagePart(name) {
7429
+ return /\.(png|jpg|jpeg|tfw|pgw|pngw|jgw|jpw|jpgw|jpegw|wld|prj)$/i.test(name);
7430
+ }
7431
+
6248
7432
  function readImportOpts() {
6249
7433
  var importOpts;
6250
7434
  if (useQuickView) {
@@ -6569,10 +7753,17 @@
6569
7753
  return data.some(d => fileKey(d) == shpKey);
6570
7754
  }
6571
7755
 
7756
+ function hasRasterImage(basename) {
7757
+ return data.some(d => {
7758
+ var type = fileType(d);
7759
+ return (type == 'png' || type == 'jpeg') && fileBase(d) == basename;
7760
+ });
7761
+ }
7762
+
6572
7763
  data.forEach(d => {
6573
7764
  var basename = fileBase(d);
6574
7765
  var type = fileType(d);
6575
- if (type == 'shp' || !isShapefilePart(d.name)) {
7766
+ if (type == 'shp') {
6576
7767
  d.group = key(basename, type);
6577
7768
  d.filename = d.name;
6578
7769
  } else if (hasShp(basename)) {
@@ -6580,6 +7771,12 @@
6580
7771
  } else if (type == 'dbf') {
6581
7772
  d.filename = d.name;
6582
7773
  d.group = key(basename, 'dbf');
7774
+ } else if ((type == 'png' || type == 'jpeg') || isRasterImagePart(d.name) && hasRasterImage(basename)) {
7775
+ d.group = key(basename, type == 'png' || type == 'jpeg' ? type : getRasterImageGroupType(data, basename));
7776
+ if (type == 'png' || type == 'jpeg') d.filename = d.name;
7777
+ } else if (!isShapefilePart(d.name)) {
7778
+ d.group = key(basename, type);
7779
+ d.filename = d.name;
6583
7780
  } else {
6584
7781
  // shapefile part without a .shp file
6585
7782
  d.group = null;
@@ -6606,6 +7803,14 @@
6606
7803
  });
6607
7804
  return groups;
6608
7805
  }
7806
+
7807
+ function getRasterImageGroupType(data, basename) {
7808
+ var match = data.find(d => {
7809
+ var type = fileType(d);
7810
+ return (type == 'png' || type == 'jpeg') && fileBase(d) == basename;
7811
+ });
7812
+ return match ? fileType(match) : null;
7813
+ }
6609
7814
  }
6610
7815
 
6611
7816
  function draggable(ref) {
@@ -6733,11 +7938,18 @@
6733
7938
 
6734
7939
  function message() {
6735
7940
  var msg = GUI.formatMessageArgs(arguments);
6736
- if (!gui.notify) {
7941
+ if (gui.notify && messageShouldGoToInbox(msg)) {
7942
+ gui.notify({
7943
+ severity: 'info',
7944
+ body: msg,
7945
+ dedupKey: 'info:' + msg
7946
+ });
7947
+ } else if (!gui.notify) {
6737
7948
  // Fallback for early messages before MessageControl is constructed
6738
7949
  gui.message(msg);
7950
+ } else {
7951
+ internal.logArgs(arguments);
6739
7952
  }
6740
- internal.logArgs(arguments);
6741
7953
  }
6742
7954
 
6743
7955
  // CLI warnings used to surface as modal alerts, which interrupt the user
@@ -6755,6 +7967,10 @@
6755
7967
  internal.setLoggingFunctions(message, error, stop, warn);
6756
7968
  }
6757
7969
 
7970
+ function messageShouldGoToInbox(msg) {
7971
+ return /^GeoTIFF renditions:/.test(msg);
7972
+ }
7973
+
6758
7974
  function WriteFilesProxy(gui) {
6759
7975
  // replace CLI version of writeFiles()
6760
7976
  internal.replaceWriteFiles(async function(files, opts) {
@@ -7199,7 +8415,7 @@
7199
8415
  var sourceCRS;
7200
8416
  var emptyArcs;
7201
8417
 
7202
- if (displayCRS && layer.geometry_type) {
8418
+ if (displayCRS && (layer.geometry_type || internal.layerHasRaster(layer))) {
7203
8419
  var crsInfo = getDatasetCrsInfo(dataset);
7204
8420
  if (crsInfo.error) {
7205
8421
  // unprojectable dataset -- return empty layer
@@ -7240,6 +8456,15 @@
7240
8456
 
7241
8457
  if (gui.unprojectable) {
7242
8458
  gui.displayLayer = {shapes: []}; // TODO: improve
8459
+ } else if (internal.layerHasRaster(layer)) {
8460
+ gui.geographic = true;
8461
+ gui.displayLayer = layer;
8462
+ if (needReprojectionForDisplay(sourceCRS, displayCRS)) {
8463
+ // Raster warping is not supported yet; hide rather than misplace pixels.
8464
+ gui.unprojectable = true;
8465
+ gui.displayLayer = {shapes: []};
8466
+ notifyRasterReprojectionBlocked(layer, opts);
8467
+ }
7243
8468
  } else if (layer.geometry_type) {
7244
8469
  gui.geographic = true;
7245
8470
  gui.displayLayer = layer;
@@ -7271,8 +8496,19 @@
7271
8496
  layer.gui = gui;
7272
8497
  }
7273
8498
 
8499
+ function notifyRasterReprojectionBlocked(layer, opts) {
8500
+ if (!opts.notify) return;
8501
+ opts.notify({
8502
+ severity: 'warn',
8503
+ title: 'Raster layer hidden',
8504
+ body: 'The raster layer "' + (layer.name || '[unnamed]') +
8505
+ '" cannot be displayed in the current map projection because raster reprojection is not supported yet.',
8506
+ dedupKey: 'raster-reprojection-blocked:' + (layer.menu_id || layer.name || '')
8507
+ });
8508
+ }
8509
+
7274
8510
  function getDisplayBounds(lyr, arcs) {
7275
- var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
8511
+ var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds$1();
7276
8512
  if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
7277
8513
  // if a point layer has no extent (e.g. contains only a single point),
7278
8514
  // then merge with arc bounds, to place the point in context.
@@ -8165,8 +9401,8 @@
8165
9401
  } else if (kc == 40) {
8166
9402
  forward();
8167
9403
  } else if (kc == 32 && (!typing || (inputText === '' && typingInConsole))) {
8168
- // space bar toggles the sidebar if nothing has been typed
8169
- gui.toggleSidebar();
9404
+ // space bar toggles the console tab if nothing has been typed
9405
+ gui.toggleSidebarPanel('console');
8170
9406
  } else if (!typing && e.target != input.node() && !metaKey(e)) {
8171
9407
  // typing returns focus, unless a meta key is down (to allow Cmd-C copy)
8172
9408
  // or user is typing in a different input area somewhere
@@ -8183,9 +9419,9 @@
8183
9419
 
8184
9420
  // various shortcuts (while not typing in an input field or editable el)
8185
9421
  } else if (!typing) {
8186
- if (kc == 32) { // space bar toggles the sidebar
9422
+ if (kc == 32) { // space bar toggles the console tab
8187
9423
  capture = true;
8188
- gui.toggleSidebar();
9424
+ gui.toggleSidebarPanel('console');
8189
9425
  // } else if (kc == 73) { // letter i opens inspector
8190
9426
  // gui.dispatchEvent('interaction_toggle');
8191
9427
  } else if (kc == 72) { // letter h resets map extent
@@ -8650,7 +9886,9 @@
8650
9886
  title.textContent = 'Layer: ' + (info.layer_name || '[unnamed layer]');
8651
9887
  section.appendChild(title);
8652
9888
  section.appendChild(renderKeyValueTable(getInfoRows(info), 'console-info-table'));
8653
- section.appendChild(renderAttributeInfoTable(info.attribute_data));
9889
+ if (!info.raster_type) {
9890
+ section.appendChild(renderAttributeInfoTable(info.attribute_data));
9891
+ }
8654
9892
  container.appendChild(section);
8655
9893
  });
8656
9894
  return container;
@@ -8658,13 +9896,18 @@
8658
9896
 
8659
9897
  function getInfoRows(info) {
8660
9898
  var rows = [
8661
- ['Type', info.geometry_type || 'tabular data'],
8662
- ['Records', utils$1.format('%,d', info.feature_count)]
9899
+ ['Type', info.raster_type ? 'raster data' : info.geometry_type || 'tabular data']
8663
9900
  ];
9901
+ if (info.raster_type) {
9902
+ rows.push(['Size', utils$1.format('%,d x %,d x %,d', info.raster_width, info.raster_height, info.raster_bands)]);
9903
+ rows.push(['Data', info.raster_pixel_type || 'unknown']);
9904
+ } else {
9905
+ rows.push(['Records', utils$1.format('%,d', info.feature_count)]);
9906
+ }
8664
9907
  if (info.null_shape_count > 0) {
8665
9908
  rows.push(['Nulls', utils$1.format("%'d", info.null_shape_count)]);
8666
9909
  }
8667
- if (info.geometry_type && info.feature_count > info.null_shape_count) {
9910
+ if (info.raster_type || info.geometry_type && info.feature_count > info.null_shape_count) {
8668
9911
  rows.push(['Bounds', info.bbox.join(',')]);
8669
9912
  rows.push(['CRS', info.proj4]);
8670
9913
  }
@@ -8782,7 +10025,7 @@
8782
10025
  }
8783
10026
 
8784
10027
  function getBoundsIntersection(a, b) {
8785
- var c = new Bounds();
10028
+ var c = new Bounds$1();
8786
10029
  if (a.intersects(b)) {
8787
10030
  c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
8788
10031
  Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
@@ -9065,11 +10308,7 @@
9065
10308
  } else {
9066
10309
  // stack seems to change if Error is logged directly
9067
10310
  console.error(err.stack);
9068
- var msg = 'Export failed for an unknown reason';
9069
- if (err.name == 'UserError') {
9070
- msg = err.message;
9071
- }
9072
- gui.alert(msg, 'Export failed');
10311
+ gui.alert(getExportErrorMessage(err), 'Export failed');
9073
10312
  }
9074
10313
  }).finally(function() {
9075
10314
  gui.clearProgressMessage();
@@ -9077,6 +10316,11 @@
9077
10316
  }, 20);
9078
10317
  }
9079
10318
 
10319
+ function getExportErrorMessage(err) {
10320
+ if (err && err.message) return err.message;
10321
+ return 'Export failed for an unknown reason';
10322
+ }
10323
+
9080
10324
  function getExportOpts() {
9081
10325
  return GUI.parseFreeformOptions(getExportOptsAsString(), 'o');
9082
10326
  }
@@ -9232,9 +10476,11 @@
9232
10476
  }
9233
10477
 
9234
10478
  function getDefaultExportFormat() {
9235
- var dataset = model.getActiveLayer().dataset;
10479
+ var active = model.getActiveLayer();
10480
+ var dataset = active.dataset;
9236
10481
  var inputFmt = dataset.info && dataset.info.input_formats &&
9237
10482
  dataset.info.input_formats[0];
10483
+ if (active.layer && internal.layerHasRaster(active.layer)) return 'svg';
9238
10484
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
9239
10485
  }
9240
10486
 
@@ -9542,17 +10788,25 @@
9542
10788
  headerBtn.on('click', function() {
9543
10789
  toggle();
9544
10790
  }).on('keydown', function(e) {
9545
- if (e.key == 'Enter' || e.key == ' ') {
10791
+ if (e.key == 'Enter') {
9546
10792
  e.preventDefault();
9547
10793
  e.stopPropagation();
9548
10794
  toggle();
10795
+ } else if (e.key == ' ') {
10796
+ e.preventDefault();
10797
+ e.stopPropagation();
10798
+ gui.toggleSidebarPanel('console');
9549
10799
  }
9550
10800
  });
9551
10801
  tab.on('click', toggle).on('keydown', function(e) {
9552
- if (e.key == 'Enter' || e.key == ' ') {
10802
+ if (e.key == 'Enter') {
9553
10803
  e.preventDefault();
9554
10804
  e.stopPropagation();
9555
10805
  toggle();
10806
+ } else if (e.key == ' ') {
10807
+ e.preventDefault();
10808
+ e.stopPropagation();
10809
+ gui.toggleSidebarPanel('console');
9556
10810
  }
9557
10811
  });
9558
10812
 
@@ -9913,9 +11167,13 @@
9913
11167
  type = 'data record';
9914
11168
  } else if (lyr.geometry_type) {
9915
11169
  type = lyr.geometry_type + ' feature';
11170
+ } else if (internal.layerHasRaster(lyr)) {
11171
+ type = 'raster layer';
9916
11172
  }
9917
11173
  if (isFrame) {
9918
11174
  str = 'map frame';
11175
+ } else if (internal.layerHasRaster(lyr)) {
11176
+ str = utils$1.format('%,d x %,d %s', getRasterWidth(lyr.raster), getRasterHeight(lyr.raster), type);
9919
11177
  } else if (type) {
9920
11178
  str = utils$1.format('%,d %s%s', n, type, utils$1.pluralSuffix(n));
9921
11179
  } else {
@@ -9950,7 +11208,7 @@
9950
11208
  }
9951
11209
 
9952
11210
  function isPinnable(lyr) {
9953
- return internal.layerIsGeometric(lyr) || internal.layerHasFurniture(lyr);
11211
+ return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
9954
11212
  }
9955
11213
 
9956
11214
  function rowHTML(c1, c2, cname) {
@@ -11043,6 +12301,7 @@
11043
12301
  rectangles: ['info', 'selection', 'box', 'rectangles', 'edit_polygons'],
11044
12302
  lines: ['info', 'selection', 'box', 'edit_lines'], // 'snip_lines'
11045
12303
  table: ['info', 'selection'],
12304
+ raster: ['box'],
11046
12305
  labels: ['info', 'selection', 'box', 'labels', 'edit_points'],
11047
12306
  points: ['info', 'selection', 'box', 'edit_points'] // , 'add-points'
11048
12307
  };
@@ -11163,6 +12422,9 @@
11163
12422
  if (!o || !o.layer) {
11164
12423
  return menus.empty; // TODO: more sensible handling of missing layer
11165
12424
  }
12425
+ if (internal.layerHasRaster(o.layer)) {
12426
+ return menus.raster;
12427
+ }
11166
12428
  if (!o.layer.geometry_type) {
11167
12429
  return menus.table;
11168
12430
  }
@@ -13577,7 +14839,7 @@
13577
14839
 
13578
14840
  // @box Bounds with pixels from t,l corner of map area.
13579
14841
  function zoomToBbox(bbox) {
13580
- var bounds = new Bounds(bbox),
14842
+ var bounds = new Bounds$1(bbox),
13581
14843
  pct = Math.max(bounds.width() / ext.width(), bounds.height() / ext.height()),
13582
14844
  fx = bounds.centerX() / ext.width() * (1 + pct) - pct / 2,
13583
14845
  fy = bounds.centerY() / ext.height() * (1 + pct) - pct / 2;
@@ -15727,7 +16989,7 @@
15727
16989
  // Get params for converting geographic coords to pixel coords
15728
16990
  this.getTransform = function(pixScale) {
15729
16991
  // get transform (y-flipped);
15730
- var viewBounds = new Bounds(0, 0, _position.width(), _position.height());
16992
+ var viewBounds = new Bounds$1(0, 0, _position.width(), _position.height());
15731
16993
  if (pixScale) {
15732
16994
  viewBounds.xmax *= pixScale;
15733
16995
  viewBounds.ymax *= pixScale;
@@ -15737,7 +16999,7 @@
15737
16999
 
15738
17000
  // k scales the size of the bbox (used by gui to control fp error when zoomed very far)
15739
17001
  this.getBounds = function(k) {
15740
- if (!_fullBounds) return new Bounds();
17002
+ if (!_fullBounds) return new Bounds$1();
15741
17003
  return calcBounds(_cx, _cy, _scale / (k || 1));
15742
17004
  };
15743
17005
 
@@ -15752,7 +17014,7 @@
15752
17014
  var b = _fullBounds = fullBounds;
15753
17015
  if (!b.hasBounds()) return; // kludge
15754
17016
  if (strictBounds) {
15755
- _strictBounds = Array.isArray(strictBounds) ? new Bounds(strictBounds) : strictBounds;
17017
+ _strictBounds = Array.isArray(strictBounds) ? new Bounds$1(strictBounds) : strictBounds;
15756
17018
  } else {
15757
17019
  _strictBounds = null;
15758
17020
  }
@@ -15782,7 +17044,7 @@
15782
17044
 
15783
17045
  this.getSymbolScale = function() {
15784
17046
  if (!_frame) return 1;
15785
- var bounds = new Bounds(_frame.bbox);
17047
+ var bounds = new Bounds$1(_frame.bbox);
15786
17048
  var bounds2 = bounds.clone().transform(this.getTransform());
15787
17049
  return bounds2.width() / _frame.width;
15788
17050
  };
@@ -15853,12 +17115,12 @@
15853
17115
  }
15854
17116
  var w = full.width() / scale;
15855
17117
  var h = full.height() / scale;
15856
- return new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
17118
+ return new Bounds$1(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
15857
17119
  }
15858
17120
 
15859
17121
  // Calculate viewport bounds from frame data
15860
17122
  function fillOutFrameBounds(frame) {
15861
- var bounds = new Bounds(frame.bbox);
17123
+ var bounds = new Bounds$1(frame.bbox);
15862
17124
  var kx = _position.width() / frame.width;
15863
17125
  var ky = _position.height() / frame.height;
15864
17126
  bounds.scale(kx, ky);
@@ -15870,7 +17132,7 @@
15870
17132
  hpix = _position.height() - 2 * marginpix,
15871
17133
  xpad, ypad, b2;
15872
17134
  if (wpix <= 0 || hpix <= 0) {
15873
- return new Bounds(0, 0, 0, 0);
17135
+ return new Bounds$1(0, 0, 0, 0);
15874
17136
  }
15875
17137
  b = b.clone();
15876
17138
  b2 = b.clone();
@@ -15908,6 +17170,198 @@
15908
17170
 
15909
17171
  utils$1.inherit(MapExtent, EventDispatcher);
15910
17172
 
17173
+ var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
17174
+ var cache = new WeakMap();
17175
+ var requestId = 0;
17176
+
17177
+ function getCachedRasterViewportPreview(layer, ext) {
17178
+ var entry = cache.get(layer);
17179
+ var params = getRasterViewportPreviewParams(layer, ext);
17180
+ if (!entry || !params || entry.key != params.key) return null;
17181
+ return entry.preview;
17182
+ }
17183
+
17184
+ function scheduleRasterViewportPreview(layer, ext, onReady) {
17185
+ var params = getRasterViewportPreviewParams(layer, ext);
17186
+ var entry = cache.get(layer);
17187
+ var id, stats, timing, preview;
17188
+ if (!params || !params.needed) return;
17189
+ if (entry && entry.key == params.key) return;
17190
+ id = ++requestId;
17191
+ cache.set(layer, {key: params.key, pending: id});
17192
+ setTimeout(function() {
17193
+ var current = cache.get(layer);
17194
+ if (!current || current.pending != id) return;
17195
+ timing = {};
17196
+ stats = getCachedRasterScalingStats(params, timing);
17197
+ timing.renderStart = getTimer();
17198
+ preview = renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
17199
+ timing.renderMs = getTimer() - timing.renderStart;
17200
+ logRasterPreviewTiming(params, timing);
17201
+ current = cache.get(layer);
17202
+ if (!preview || !current || current.pending != id) return;
17203
+ cache.set(layer, {
17204
+ key: params.key,
17205
+ preview: preview
17206
+ });
17207
+ onReady();
17208
+ }, 0);
17209
+ }
17210
+
17211
+ function invalidateRasterViewportPreview(layer) {
17212
+ cache.delete(layer);
17213
+ }
17214
+
17215
+ function getRasterViewportPreviewParams(layer, ext) {
17216
+ var raster = layer && layer.raster;
17217
+ var grid = getRasterGrid(raster);
17218
+ var rasterBbox = getRasterBBox(raster);
17219
+ var mapBbox = ext.getBounds().toArray();
17220
+ var visibleBbox = rasterBbox && intersectBboxes(rasterBbox, mapBbox);
17221
+ var preview = getRasterPreview(raster);
17222
+ var recipe, pixelRatio, t, p1, p2, displayWidth, displayHeight, crop, width, height, scale, key, needed;
17223
+ if (!grid || !grid.samples || !visibleBbox || !grid.bbox) return null;
17224
+ if (!supportsNorthUpRaster(grid)) return null;
17225
+ crop = getRasterSourceWindow(grid, visibleBbox);
17226
+ if (!crop) return null;
17227
+ visibleBbox = crop.bbox;
17228
+ recipe = getRasterViewRecipe(grid, raster.view && raster.view.recipe);
17229
+ pixelRatio = GUI.getPixelRatio();
17230
+ t = ext.getTransform(pixelRatio);
17231
+ p1 = t.transform(mapBbox[0], mapBbox[3]);
17232
+ p2 = t.transform(mapBbox[2], mapBbox[1]);
17233
+ displayWidth = Math.max(1, Math.round(Math.abs(p2[0] - p1[0])));
17234
+ displayHeight = Math.max(1, Math.round(Math.abs(p2[1] - p1[1])));
17235
+ width = Math.min(displayWidth, crop.width);
17236
+ height = Math.min(displayHeight, crop.height);
17237
+ scale = Math.min(1, Math.sqrt(MAX_VIEWPORT_PREVIEW_PIXELS / (width * height)));
17238
+ width = Math.max(1, Math.round(width * scale));
17239
+ height = Math.max(1, Math.round(height * scale));
17240
+ needed = viewportPreviewIsSharper(preview, grid, visibleBbox, width, height);
17241
+ key = [
17242
+ visibleBbox.map(roundKeyPart).join(','),
17243
+ width,
17244
+ height,
17245
+ pixelRatio,
17246
+ grid.width,
17247
+ grid.height,
17248
+ grid.samples && grid.samples.length,
17249
+ recipe.type,
17250
+ recipe.scaling,
17251
+ recipe.scaleRange && recipe.scaleRange.join(','),
17252
+ recipe.percentileRange && recipe.percentileRange.join(',')
17253
+ ].join('|');
17254
+ return {
17255
+ key: key,
17256
+ needed: needed,
17257
+ raster: raster,
17258
+ grid: grid,
17259
+ recipe: recipe,
17260
+ bbox: visibleBbox,
17261
+ width: width,
17262
+ height: height,
17263
+ sourceWidth: crop.width,
17264
+ sourceHeight: crop.height
17265
+ };
17266
+ }
17267
+
17268
+ function getCachedRasterScalingStats(params, timing) {
17269
+ var cached = params.raster.view && params.raster.view.scalingStats;
17270
+ var key;
17271
+ if (params.recipe.scaling == 'none') {
17272
+ timing.statsMs = 0;
17273
+ timing.statsSource = 'none';
17274
+ return null;
17275
+ }
17276
+ key = getRasterScalingStatsKey(params.grid, params.recipe);
17277
+ if (cached && cached.key == key) {
17278
+ timing.statsMs = 0;
17279
+ timing.statsSource = 'raster-view-cache';
17280
+ return cached.stats;
17281
+ }
17282
+ timing.statsSource = 'computed';
17283
+ timing.statsStart = getTimer();
17284
+ cached = getRasterViewScalingStats(params.raster, params.recipe);
17285
+ timing.statsMs = getTimer() - timing.statsStart;
17286
+ return cached;
17287
+ }
17288
+
17289
+ function logRasterPreviewTiming(params, timing) {
17290
+ if (!rasterDebugIsOn()) return;
17291
+ console.log([
17292
+ 'Raster viewport preview:',
17293
+ params.width + 'x' + params.height,
17294
+ 'from',
17295
+ params.sourceWidth + 'x' + params.sourceHeight,
17296
+ 'source px,',
17297
+ 'stats=' + formatMs(timing.statsMs),
17298
+ '(' + timing.statsSource + '),',
17299
+ 'render=' + formatMs(timing.renderMs) + ',',
17300
+ 'total=' + formatMs(timing.statsMs + timing.renderMs) + ',',
17301
+ 'scaling=' + params.recipe.scaling,
17302
+ 'type=' + params.recipe.type
17303
+ ].join(' '));
17304
+ }
17305
+
17306
+ function rasterDebugIsOn() {
17307
+ var vars = GUI.getUrlVars();
17308
+ return vars['raster-debug'] === true || vars['raster-debug'] == '1' || vars.raster_debug === true || vars.raster_debug == '1';
17309
+ }
17310
+
17311
+ function getTimer() {
17312
+ return typeof performance != 'undefined' && performance.now ? performance.now() : Date.now();
17313
+ }
17314
+
17315
+ function formatMs(ms) {
17316
+ return Math.round(ms * 10) / 10 + 'ms';
17317
+ }
17318
+
17319
+ function supportsNorthUpRaster(grid) {
17320
+ var t = grid.transform;
17321
+ return !t || t[1] === 0 && t[3] === 0;
17322
+ }
17323
+
17324
+ function getRasterSourcePixelSize(grid, bbox) {
17325
+ var crop = getRasterSourceWindow(grid, bbox);
17326
+ return crop ? {width: crop.width, height: crop.height} : {width: 0, height: 0};
17327
+ }
17328
+
17329
+ function getRasterSourceWindow(grid, bbox) {
17330
+ var rb = grid.bbox;
17331
+ var dx = (rb[2] - rb[0]) / grid.width;
17332
+ var dy = (rb[3] - rb[1]) / grid.height;
17333
+ var x0 = Math.max(0, Math.floor((bbox[0] - rb[0]) / dx));
17334
+ var x1 = Math.min(grid.width, Math.ceil((bbox[2] - rb[0]) / dx));
17335
+ var y0 = Math.max(0, Math.floor((rb[3] - bbox[3]) / dy));
17336
+ var y1 = Math.min(grid.height, Math.ceil((rb[3] - bbox[1]) / dy));
17337
+ if (x1 <= x0 || y1 <= y0) return null;
17338
+ return {
17339
+ x: x0,
17340
+ y: y0,
17341
+ width: x1 - x0,
17342
+ height: y1 - y0,
17343
+ bbox: [
17344
+ rb[0] + x0 * dx,
17345
+ rb[3] - y1 * dy,
17346
+ rb[0] + x1 * dx,
17347
+ rb[3] - y0 * dy
17348
+ ]
17349
+ };
17350
+ }
17351
+
17352
+ function viewportPreviewIsSharper(preview, grid, bbox, width, height) {
17353
+ var crop = getRasterSourcePixelSize(grid, bbox);
17354
+ var previewWidth = preview && preview.width || 0;
17355
+ var previewHeight = preview && preview.height || 0;
17356
+ var fallbackWidth = crop.width * previewWidth / grid.width;
17357
+ var fallbackHeight = crop.height * previewHeight / grid.height;
17358
+ return width > fallbackWidth * 1.25 || height > fallbackHeight * 1.25;
17359
+ }
17360
+
17361
+ function roundKeyPart(val) {
17362
+ return Math.round(val * 1e9) / 1e9;
17363
+ }
17364
+
15911
17365
  var hatches = {}; // cached patterns
15912
17366
 
15913
17367
  function getCanvasFillEffect(ctx, shp, arcs, ext, style) {
@@ -16110,7 +17564,7 @@
16110
17564
  function getShapeFilter(arcs, shapes, ext) {
16111
17565
  if (ext.scale() < 1.1) return null;
16112
17566
  var view = ext.getBounds();
16113
- var b = new Bounds();
17567
+ var b = new Bounds$1();
16114
17568
  return function(i) {
16115
17569
  var shp = shapes[i];
16116
17570
  if (!shp) return false;
@@ -16163,6 +17617,22 @@
16163
17617
  _ext = extent;
16164
17618
  };
16165
17619
 
17620
+ _self.drawRasterLayer = function(layer, opts) {
17621
+ var raster = layer.raster;
17622
+ var options = opts || {};
17623
+ var preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
17624
+ var bbox = preview && preview.bbox;
17625
+ if (!preview) {
17626
+ preview = raster && getRasterPreview(raster);
17627
+ bbox = raster && getRasterBBox(raster);
17628
+ if (options.action != 'nav' && options.onViewportPreviewReady) {
17629
+ scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
17630
+ }
17631
+ }
17632
+ if (!preview || !preview.pixels || !bbox) return;
17633
+ drawRasterPreview(preview, bbox);
17634
+ };
17635
+
16166
17636
  /*
16167
17637
  // Original function, not optimized
16168
17638
  _self.drawStyledPaths = function(shapes, arcs, style) {
@@ -16359,6 +17829,15 @@
16359
17829
  }
16360
17830
  }
16361
17831
 
17832
+ function drawRasterPreview(preview, bbox) {
17833
+ var img = getRasterCanvas(preview);
17834
+ var t = _ext.getTransform(GUI.getPixelRatio());
17835
+ var p1 = t.transform(bbox[0], bbox[3]);
17836
+ var p2 = t.transform(bbox[2], bbox[1]);
17837
+ _ctx.imageSmoothingEnabled = true;
17838
+ _ctx.drawImage(img, p1[0], p1[1], p2[0] - p1[0], p2[1] - p1[1]);
17839
+ }
17840
+
16362
17841
  // TODO: consider using drawStyledPaths(), which draws paths in batches
16363
17842
  // for faster Canvas rendering. Downside: changes stacking order, which
16364
17843
  // is bad if circles are graduated.
@@ -16474,6 +17953,19 @@
16474
17953
  };
16475
17954
  }
16476
17955
 
17956
+ function getRasterCanvas(preview) {
17957
+ if (preview.canvas) return preview.canvas;
17958
+ var canvas = document.createElement('canvas');
17959
+ var ctx = canvas.getContext('2d');
17960
+ var imageData;
17961
+ canvas.width = preview.width;
17962
+ canvas.height = preview.height;
17963
+ imageData = new ImageData(preview.pixels, preview.width, preview.height);
17964
+ ctx.putImageData(imageData, 0, 0);
17965
+ preview.canvas = canvas;
17966
+ return canvas;
17967
+ }
17968
+
16477
17969
  function drawCircle(x, y, radius, ctx) {
16478
17970
  if (radius > 0) {
16479
17971
  ctx.moveTo(x + radius, y);
@@ -16774,6 +18266,7 @@
16774
18266
  var _settleTimer = null;
16775
18267
  var _redrawPending = false;
16776
18268
  var _settleRequested = false;
18269
+ var _rasterNavRefreshPending = false;
16777
18270
 
16778
18271
  // 'map_interaction_end' may arrive before the in-flight 'nav' draw runs,
16779
18272
  // because drawLayers() schedules its work via requestAnimationFrame.
@@ -16782,6 +18275,8 @@
16782
18275
  gui.on('map_interaction_end', function() {
16783
18276
  if (_redrawPending) {
16784
18277
  settleNow();
18278
+ } else if (_rasterNavRefreshPending) {
18279
+ settleRasterNow();
16785
18280
  } else {
16786
18281
  _settleRequested = true;
16787
18282
  }
@@ -16792,6 +18287,11 @@
16792
18287
 
16793
18288
  this.drawMainLayers = function(layers, action) {
16794
18289
  if (skipMainLayerRedraw(action)) return;
18290
+ if (action == 'nav' && layersHaveRasters(layers)) {
18291
+ _rasterNavRefreshPending = true;
18292
+ } else if (action != 'nav') {
18293
+ _rasterNavRefreshPending = false;
18294
+ }
16795
18295
  if (action == 'nav' && shouldUseFastNav()) {
16796
18296
  applyFastTransform();
16797
18297
  // SVG symbol reposition is already cheap; keep labels/symbols accurate
@@ -16821,7 +18321,7 @@
16821
18321
  } else if (isSvgLayer) {
16822
18322
  _svg.drawLayer(lyr, 'symbol');
16823
18323
  } else {
16824
- drawCanvasLayer(lyr, _mainCanv);
18324
+ drawCanvasLayer(lyr, _mainCanv, action);
16825
18325
  }
16826
18326
  });
16827
18327
  // Force synchronous rasterization so performance.now() reflects the true
@@ -16830,6 +18330,10 @@
16830
18330
  flushCanvas(_mainCanv);
16831
18331
  _lastFrameMs = performance.now() - startTime;
16832
18332
  captureSnapshot();
18333
+ if (action == 'nav' && _settleRequested && _rasterNavRefreshPending) {
18334
+ _settleRequested = false;
18335
+ settleRasterNow();
18336
+ }
16833
18337
  };
16834
18338
 
16835
18339
  // Draw highlight effect for hover and selection
@@ -16850,7 +18354,7 @@
16850
18354
  _overlayCanv.hide();
16851
18355
  }
16852
18356
  layers.forEach(function(lyr) {
16853
- drawCanvasLayer(lyr, canv);
18357
+ drawCanvasLayer(lyr, canv, action);
16854
18358
  });
16855
18359
  };
16856
18360
 
@@ -16875,9 +18379,22 @@
16875
18379
  return action == 'hover' && _overlayCanv.visible();
16876
18380
  }
16877
18381
 
16878
- function drawCanvasLayer(lyr, canv) {
18382
+ function layersHaveRasters(layers) {
18383
+ return layers.some(function(lyr) {
18384
+ return lyr && lyr.gui && internal.layerHasRaster(lyr.gui.displayLayer);
18385
+ });
18386
+ }
18387
+
18388
+ function drawCanvasLayer(lyr, canv, action) {
16879
18389
  if (!lyr) return;
16880
- if (lyr.gui.style.type == 'outline') {
18390
+ if (internal.layerHasRaster(lyr.gui.displayLayer)) {
18391
+ canv.drawRasterLayer(lyr.gui.displayLayer, {
18392
+ action: action,
18393
+ onViewportPreviewReady: function() {
18394
+ gui.dispatchEvent('map-needs-refresh');
18395
+ }
18396
+ });
18397
+ } else if (lyr.gui.style.type == 'outline') {
16881
18398
  drawOutlineLayerToCanvas(lyr, canv, ext);
16882
18399
  } else {
16883
18400
  drawStyledLayerToCanvas(lyr, canv, ext);
@@ -16980,6 +18497,11 @@
16980
18497
  gui.dispatchEvent('map-needs-refresh');
16981
18498
  }
16982
18499
 
18500
+ function settleRasterNow() {
18501
+ _rasterNavRefreshPending = false;
18502
+ gui.dispatchEvent('map-needs-refresh');
18503
+ }
18504
+
16983
18505
  function cancelSettle() {
16984
18506
  if (_settleTimer) {
16985
18507
  clearTimeout(_settleTimer);
@@ -16995,6 +18517,9 @@
16995
18517
  var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
16996
18518
  var popup = gui.container.findChild('.box-tool-options');
16997
18519
  var coords = popup.findChild('.box-coords').hide();
18520
+ var selectBtn = popup.findChild('.select-btn');
18521
+ var clipBtn = popup.findChild('.clip-btn');
18522
+ var eraseBtn = popup.findChild('.erase-btn');
16998
18523
  var _on = false;
16999
18524
  var instructionsShown = false;
17000
18525
  var alert;
@@ -17007,7 +18532,7 @@
17007
18532
  reset();
17008
18533
  });
17009
18534
 
17010
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
18535
+ new SimpleButton(selectBtn).on('click', function() {
17011
18536
  var coords = box.getDataCoords();
17012
18537
  if (!coords || noData()) return;
17013
18538
  gui.enterMode('selection_tool');
@@ -17022,11 +18547,12 @@
17022
18547
  return !gui.model.getActiveLayer();
17023
18548
  }
17024
18549
 
17025
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
18550
+ new SimpleButton(clipBtn).on('click', function() {
17026
18551
  runCommand('-clip bbox=' + box.getDataCoords().join(','));
17027
18552
  });
17028
18553
 
17029
- new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
18554
+ new SimpleButton(eraseBtn).on('click', function() {
18555
+ if (activeLayerIsRaster()) return;
17030
18556
  runCommand('-erase bbox=' + box.getDataCoords().join(','));
17031
18557
  });
17032
18558
 
@@ -17069,6 +18595,7 @@
17069
18595
  box.on('dragend', function(e) {
17070
18596
  if (_on) {
17071
18597
  hideInstructions();
18598
+ updateOptionsForActiveLayer();
17072
18599
  popup.show();
17073
18600
  }
17074
18601
  });
@@ -17122,6 +18649,7 @@
17122
18649
 
17123
18650
  function turnOn() {
17124
18651
  box.turnOn();
18652
+ updateOptionsForActiveLayer();
17125
18653
  _on = true;
17126
18654
  }
17127
18655
 
@@ -17142,6 +18670,27 @@
17142
18670
  hideCoords();
17143
18671
  }
17144
18672
 
18673
+ function updateOptionsForActiveLayer() {
18674
+ var raster = activeLayerIsRaster();
18675
+ setButtonVisible(selectBtn, !raster);
18676
+ setButtonVisible(clipBtn, true);
18677
+ setButtonVisible(eraseBtn, !raster);
18678
+ }
18679
+
18680
+ function activeLayerIsRaster() {
18681
+ var active = gui.model.getActiveLayer();
18682
+ return !!(active && internal.layerHasRaster(active.layer));
18683
+ }
18684
+
18685
+ function setButtonVisible(btn, visible) {
18686
+ btn.classed('hidden', !visible);
18687
+ if (visible) {
18688
+ btn.show();
18689
+ } else {
18690
+ btn.hide();
18691
+ }
18692
+ }
18693
+
17145
18694
  function openAddFramePopup(gui, bbox) {
17146
18695
  var popup = showPopupAlert('', 'Add a map frame');
17147
18696
  var el = popup.container();
@@ -17511,7 +19060,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17511
19060
 
17512
19061
  function getDisplayOptions() {
17513
19062
  return {
17514
- crs: _dynamicCRS
19063
+ crs: _dynamicCRS,
19064
+ notify: gui.notify
17515
19065
  };
17516
19066
  }
17517
19067
 
@@ -17528,7 +19078,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17528
19078
  }
17529
19079
 
17530
19080
  function getContentLayerBounds() {
17531
- var b = new Bounds();
19081
+ var b = new Bounds$1();
17532
19082
  var layers = getContentLayers();
17533
19083
  layers.forEach(function(lyr) {
17534
19084
  b.mergeBounds(lyr.gui.bounds);
@@ -17545,7 +19095,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17545
19095
  function calcFullBounds() {
17546
19096
  var b;
17547
19097
  if (isPreviewView()) {
17548
- b = new Bounds(getFrameLayerData().bbox);
19098
+ b = new Bounds$1(getFrameLayerData().bbox);
17549
19099
  } else {
17550
19100
  b = getContentLayerBounds();
17551
19101
  }
@@ -18510,6 +20060,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
18510
20060
  var clearMsg;
18511
20061
 
18512
20062
  initModeRules(gui);
20063
+ startRasterSourceStoreLifecycle();
18513
20064
  gui.map.init();
18514
20065
 
18515
20066
  if (opts.saveControl) {