mapshaper 0.7.12 → 0.7.14

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,76 +4321,384 @@
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 || {}));
4309
- }
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
+ }
4310
4702
 
4311
4703
  function noteTableFieldsWillChange(table, fields, detail) {
4312
4704
  notifyTransaction(activeTransaction, 'captureTableFieldsBefore', table, Object.assign({
@@ -4440,6 +4832,597 @@
4440
4832
  return Array.isArray(arr) ? arr.slice() : [arr];
4441
4833
  }
4442
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
+ }
5164
+ }
5165
+
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;
5177
+ }
5178
+
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
+ ];
5189
+ }
5190
+
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;
5197
+ }
5198
+
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};
5210
+ }
5211
+
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;
5219
+ }
5220
+
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;
5231
+ }
5232
+
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)));
5238
+ }
5239
+
5240
+ function getDefaultRasterScaling(grid, recipe) {
5241
+ return isEightBitPixelType(grid.pixelType) ? 'none' : 'percentile';
5242
+ }
5243
+
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;
5256
+ }
5257
+
5258
+ function getDisplayRange(scaleRange) {
5259
+ return [
5260
+ scaleRange[0] / 100 * 255,
5261
+ scaleRange[1] / 100 * 255
5262
+ ];
5263
+ }
5264
+
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;
5275
+ }
5276
+
5277
+ function isEightBitPixelType(pixelType) {
5278
+ return pixelType == 'uint8' || pixelType == 'int8';
5279
+ }
5280
+
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);
5288
+ }
5289
+
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('|');
5303
+ }
5304
+
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;
5311
+ }
5312
+
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;
5318
+ }
5319
+
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);
5325
+ }
5326
+
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;
5333
+ }
5334
+
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};
5368
+ }
5369
+
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;
5378
+ }
5379
+
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;
5388
+ }
5389
+
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);
5393
+ }
5394
+
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
+ }
5404
+ }
5405
+
5406
+ function isValidBandValue(val, noData) {
5407
+ return isFinite(val) && (noData === null || noData === undefined || val != noData);
5408
+ }
5409
+
5410
+ function getPercentileRank(count, pct) {
5411
+ return Math.max(0, Math.min(count - 1, Math.floor((count - 1) * pct / 100)));
5412
+ }
5413
+
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;
5420
+ }
5421
+
5422
+ function copyObjectOrArray(obj) {
5423
+ return obj && obj.concat ? obj.concat() : utils.extend({}, obj);
5424
+ }
5425
+
4443
5426
  function UndoTransaction(label) {
4444
5427
  this.label = label || '';
4445
5428
  this.units = [];
@@ -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']) {
@@ -5750,14 +6796,143 @@
5750
6796
  }
5751
6797
  }
5752
6798
 
5753
- async function loadGeoParquetLib() {
5754
- if (!window.modules || !window.modules.hyparquet || !window.modules['hyparquet-compressors'] || !window.modules['hyparquet-writer'] || !window.modules['@bokuweb/zstd-wasm']) {
5755
- if (!geoParquetPromise) {
5756
- geoParquetPromise = loadScript('geoparquet.js');
5757
- }
5758
- await geoParquetPromise;
5759
- geoParquetPromise = null;
6799
+ async function loadGeoParquetLib() {
6800
+ if (!window.modules || !window.modules.hyparquet || !window.modules['hyparquet-compressors'] || !window.modules['hyparquet-writer'] || !window.modules['@bokuweb/zstd-wasm']) {
6801
+ if (!geoParquetPromise) {
6802
+ geoParquetPromise = loadScript('geoparquet.js');
6803
+ }
6804
+ await geoParquetPromise;
6805
+ geoParquetPromise = null;
6806
+ }
6807
+ }
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
+ });
5760
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;
5761
6936
  }
5762
6937
 
5763
6938
  // @cb function(<FileList>)
@@ -6097,6 +7272,7 @@
6097
7272
  // gui.container.addClass('queued-files');
6098
7273
  El('#import-options').show();
6099
7274
  gui.container.classed('queued-files', queuedFiles.length > 0);
7275
+ updateSidebarTabZIndex();
6100
7276
  El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
6101
7277
  showQueuedFiles();
6102
7278
  updateGeoPackageLayerSelectionMenu();
@@ -6104,9 +7280,15 @@
6104
7280
 
6105
7281
  function hideImportMenu() {
6106
7282
  // gui.container.removeClass('queued-files');
7283
+ gui.container.removeClass('sidebar-tabs-over-popup');
6107
7284
  El('#import-options').hide();
6108
7285
  }
6109
7286
 
7287
+ function updateSidebarTabZIndex() {
7288
+ gui.container.classed('sidebar-tabs-over-popup',
7289
+ queuedFiles.length === 0 && !gui.sidebarPanelIsOpen());
7290
+ }
7291
+
6110
7292
  function getFileNames(files) {
6111
7293
  return Array.from(files).map(function(f) {return f.name;});
6112
7294
  }
@@ -6193,7 +7375,10 @@
6193
7375
  if (group.parquet) {
6194
7376
  await loadGeoParquetLib();
6195
7377
  }
6196
- 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) {
6197
7382
  dataset = await internal.importContentAsync(group, importOpts);
6198
7383
  } else {
6199
7384
  dataset = internal.importContent(group, importOpts);
@@ -6204,6 +7389,7 @@
6204
7389
  if (group.layername) {
6205
7390
  d.layers.forEach(lyr => lyr.name = group.layername);
6206
7391
  }
7392
+ await persistRasterSourceForDataset(d, group);
6207
7393
  // TODO: add popup here
6208
7394
  // save import options for use by repair control, etc.
6209
7395
  d.info.import_options = importOpts;
@@ -6224,7 +7410,8 @@
6224
7410
  function filesMayContainPaths(files) {
6225
7411
  return utils$1.some(files, function(f) {
6226
7412
  var type = internal.guessInputFileType(f.name);
6227
- 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);
6228
7415
  });
6229
7416
  }
6230
7417
 
@@ -6238,6 +7425,10 @@
6238
7425
  return /\.(shp|shx|dbf|prj|cpg)$/i.test(name);
6239
7426
  }
6240
7427
 
7428
+ function isRasterImagePart(name) {
7429
+ return /\.(png|jpg|jpeg|tfw|pgw|pngw|jgw|jpw|jpgw|jpegw|wld|prj)$/i.test(name);
7430
+ }
7431
+
6241
7432
  function readImportOpts() {
6242
7433
  var importOpts;
6243
7434
  if (useQuickView) {
@@ -6562,10 +7753,17 @@
6562
7753
  return data.some(d => fileKey(d) == shpKey);
6563
7754
  }
6564
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
+
6565
7763
  data.forEach(d => {
6566
7764
  var basename = fileBase(d);
6567
7765
  var type = fileType(d);
6568
- if (type == 'shp' || !isShapefilePart(d.name)) {
7766
+ if (type == 'shp') {
6569
7767
  d.group = key(basename, type);
6570
7768
  d.filename = d.name;
6571
7769
  } else if (hasShp(basename)) {
@@ -6573,6 +7771,12 @@
6573
7771
  } else if (type == 'dbf') {
6574
7772
  d.filename = d.name;
6575
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;
6576
7780
  } else {
6577
7781
  // shapefile part without a .shp file
6578
7782
  d.group = null;
@@ -6599,6 +7803,14 @@
6599
7803
  });
6600
7804
  return groups;
6601
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
+ }
6602
7814
  }
6603
7815
 
6604
7816
  function draggable(ref) {
@@ -6726,11 +7938,18 @@
6726
7938
 
6727
7939
  function message() {
6728
7940
  var msg = GUI.formatMessageArgs(arguments);
6729
- 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) {
6730
7948
  // Fallback for early messages before MessageControl is constructed
6731
7949
  gui.message(msg);
7950
+ } else {
7951
+ internal.logArgs(arguments);
6732
7952
  }
6733
- internal.logArgs(arguments);
6734
7953
  }
6735
7954
 
6736
7955
  // CLI warnings used to surface as modal alerts, which interrupt the user
@@ -6748,6 +7967,10 @@
6748
7967
  internal.setLoggingFunctions(message, error, stop, warn);
6749
7968
  }
6750
7969
 
7970
+ function messageShouldGoToInbox(msg) {
7971
+ return /^GeoTIFF renditions:/.test(msg);
7972
+ }
7973
+
6751
7974
  function WriteFilesProxy(gui) {
6752
7975
  // replace CLI version of writeFiles()
6753
7976
  internal.replaceWriteFiles(async function(files, opts) {
@@ -7192,7 +8415,7 @@
7192
8415
  var sourceCRS;
7193
8416
  var emptyArcs;
7194
8417
 
7195
- if (displayCRS && layer.geometry_type) {
8418
+ if (displayCRS && (layer.geometry_type || internal.layerHasRaster(layer))) {
7196
8419
  var crsInfo = getDatasetCrsInfo(dataset);
7197
8420
  if (crsInfo.error) {
7198
8421
  // unprojectable dataset -- return empty layer
@@ -7233,6 +8456,15 @@
7233
8456
 
7234
8457
  if (gui.unprojectable) {
7235
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
+ }
7236
8468
  } else if (layer.geometry_type) {
7237
8469
  gui.geographic = true;
7238
8470
  gui.displayLayer = layer;
@@ -7264,8 +8496,19 @@
7264
8496
  layer.gui = gui;
7265
8497
  }
7266
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
+
7267
8510
  function getDisplayBounds(lyr, arcs) {
7268
- var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds();
8511
+ var bounds = internal.getLayerBounds(lyr, arcs) || new Bounds$1();
7269
8512
  if (lyr.geometry_type == 'point' && arcs && bounds.hasBounds() && bounds.area() > 0 === false) {
7270
8513
  // if a point layer has no extent (e.g. contains only a single point),
7271
8514
  // then merge with arc bounds, to place the point in context.
@@ -7927,7 +9170,7 @@
7927
9170
  function Console(gui) {
7928
9171
  var model = gui.model;
7929
9172
  var CURSOR = '$ ';
7930
- var PROMPT = 'Enter mapshaper commands or type "tips" for examples and console help';
9173
+ var PROMPT = 'Enter mapshaper commands or type "tips" for console help.';
7931
9174
  var el = gui.container.findChild('.console').hide();
7932
9175
  var content = el.findChild('.console-buffer');
7933
9176
  var log = El('div').appendTo(content);
@@ -7948,6 +9191,7 @@
7948
9191
  .on('keydown', function(e) {
7949
9192
  if (e.key == 'Enter' || e.key == ' ') {
7950
9193
  e.preventDefault();
9194
+ e.stopPropagation();
7951
9195
  toggle();
7952
9196
  }
7953
9197
  });
@@ -7963,7 +9207,7 @@
7963
9207
  this.runCommand = function(str) {
7964
9208
  str = str.trim();
7965
9209
  if (!str) return;
7966
- turnOn();
9210
+ gui.setSidebarPanel('console');
7967
9211
  submit(str);
7968
9212
  };
7969
9213
 
@@ -7981,10 +9225,17 @@
7981
9225
  });
7982
9226
 
7983
9227
  function toggle() {
7984
- if (_isOpen) turnOff();
7985
- else turnOn();
9228
+ gui.toggleSidebarPanel('console');
7986
9229
  }
7987
9230
 
9231
+ gui.on('sidebar', function(e) {
9232
+ if (e.name == 'console') {
9233
+ turnOn();
9234
+ } else if (e.prev == 'console') {
9235
+ turnOff();
9236
+ }
9237
+ });
9238
+
7988
9239
  function getHistory() {
7989
9240
  return GUI.getSavedValue('console_history') || [];
7990
9241
  }
@@ -8004,10 +9255,30 @@
8004
9255
  scrollDown();
8005
9256
  }
8006
9257
 
9258
+ function toLogNode(node, cname) {
9259
+ var msg = El('div').appendTo(log);
9260
+ if (cname) {
9261
+ msg.addClass(cname);
9262
+ }
9263
+ msg.node().appendChild(node);
9264
+ scrollDown();
9265
+ }
9266
+
9267
+ function getStructuredConsoleMessage(args) {
9268
+ var arr = utils$1.toArray(args);
9269
+ var msg;
9270
+ // print() sends its arguments as one array argument.
9271
+ if (arr.length == 1 && Array.isArray(arr[0])) {
9272
+ arr = arr[0];
9273
+ }
9274
+ msg = arr[arr.length - 1];
9275
+ return msg && msg.type && /^mapshaper-console-/.test(msg.type) ? msg : null;
9276
+ }
9277
+
8007
9278
  function turnOn() {
8008
9279
  // if (!_isOpen && !model.isEmpty()) {
8009
9280
  if (!_isOpen) {
8010
- btn.addClass('active');
9281
+ btn.addClass('active').attr('aria-expanded', 'true');
8011
9282
  _isOpen = true;
8012
9283
  // Route logging output to the in-app console while it's open, so a
8013
9284
  // user typing CLI commands here sees the results inline -- the same
@@ -8019,9 +9290,7 @@
8019
9290
  // gui instances with the console open. E.g. console could close
8020
9291
  // when an instance loses focus.
8021
9292
  internal.setLoggingFunctions(consoleMessage, consoleError, consoleStop, consoleWarn);
8022
- gui.container.addClass('console-open');
8023
9293
  el.show();
8024
- gui.dispatchEvent('resize');
8025
9294
  input.node().focus();
8026
9295
  history = getHistory();
8027
9296
  }
@@ -8029,7 +9298,7 @@
8029
9298
 
8030
9299
  function turnOff() {
8031
9300
  if (_isOpen) {
8032
- btn.removeClass('active');
9301
+ btn.removeClass('active').attr('aria-expanded', 'false');
8033
9302
  _isOpen = false;
8034
9303
  if (GUI.isActiveInstance(gui)) {
8035
9304
  setLoggingForGUI(gui); // reset stop, message and error functions
@@ -8037,8 +9306,6 @@
8037
9306
  el.hide();
8038
9307
  input.node().blur();
8039
9308
  saveHistory();
8040
- gui.container.removeClass('console-open');
8041
- gui.dispatchEvent('resize');
8042
9309
  }
8043
9310
  }
8044
9311
 
@@ -8098,7 +9365,7 @@
8098
9365
  if (gui.getMode()) {
8099
9366
  gui.clearMode(); // esc closes any open panels
8100
9367
  } else {
8101
- turnOff();
9368
+ gui.setSidebarPanel(null);
8102
9369
  }
8103
9370
  capture = true;
8104
9371
 
@@ -8134,8 +9401,8 @@
8134
9401
  } else if (kc == 40) {
8135
9402
  forward();
8136
9403
  } else if (kc == 32 && (!typing || (inputText === '' && typingInConsole))) {
8137
- // space bar closes if nothing has been typed
8138
- turnOff();
9404
+ // space bar toggles the sidebar if nothing has been typed
9405
+ gui.toggleSidebar();
8139
9406
  } else if (!typing && e.target != input.node() && !metaKey(e)) {
8140
9407
  // typing returns focus, unless a meta key is down (to allow Cmd-C copy)
8141
9408
  // or user is typing in a different input area somewhere
@@ -8152,9 +9419,9 @@
8152
9419
 
8153
9420
  // various shortcuts (while not typing in an input field or editable el)
8154
9421
  } else if (!typing) {
8155
- if (kc == 32) { // space bar opens console
9422
+ if (kc == 32) { // space bar toggles the sidebar
8156
9423
  capture = true;
8157
- turnOn();
9424
+ gui.toggleSidebar();
8158
9425
  // } else if (kc == 73) { // letter i opens inspector
8159
9426
  // gui.dispatchEvent('interaction_toggle');
8160
9427
  } else if (kc == 72) { // letter h resets map extent
@@ -8535,9 +9802,15 @@
8535
9802
  }
8536
9803
 
8537
9804
  function consoleMessage() {
8538
- var msg = GUI.formatMessageArgs(arguments);
9805
+ var structured = getStructuredConsoleMessage(arguments);
9806
+ var msg;
8539
9807
  if (internal.loggingEnabled()) {
8540
- toLog(msg, 'console-message');
9808
+ if (structured) {
9809
+ toLogNode(renderStructuredConsoleMessage(structured), 'console-message');
9810
+ } else {
9811
+ msg = GUI.formatMessageArgs(arguments);
9812
+ toLog(msg, 'console-message');
9813
+ }
8541
9814
  }
8542
9815
  }
8543
9816
 
@@ -8562,6 +9835,144 @@
8562
9835
  printExample("Clear the console", "$ clear");
8563
9836
  }
8564
9837
 
9838
+ function renderStructuredConsoleMessage(msg) {
9839
+ if (msg.type == 'mapshaper-console-help') {
9840
+ return renderHelpMessage(msg.lines);
9841
+ }
9842
+ if (msg.type == 'mapshaper-console-info') {
9843
+ return renderInfoMessage(msg.layers);
9844
+ }
9845
+ return document.createTextNode('');
9846
+ }
9847
+
9848
+ function renderHelpMessage(lines) {
9849
+ var container = document.createElement('div');
9850
+ var rows = [];
9851
+ container.className = 'console-help';
9852
+ (lines || []).forEach(function(line) {
9853
+ if (Array.isArray(line)) {
9854
+ rows.push(line);
9855
+ } else {
9856
+ flushRows();
9857
+ appendHelpLine(container, line);
9858
+ }
9859
+ });
9860
+ flushRows();
9861
+ return container;
9862
+
9863
+ function flushRows() {
9864
+ if (rows.length > 0) {
9865
+ container.appendChild(renderKeyValueTable(rows, 'console-help-table'));
9866
+ rows = [];
9867
+ }
9868
+ }
9869
+ }
9870
+
9871
+ function appendHelpLine(container, line) {
9872
+ var div = document.createElement('div');
9873
+ div.className = line ? 'console-help-line' : 'console-help-spacer';
9874
+ div.textContent = line || '';
9875
+ container.appendChild(div);
9876
+ }
9877
+
9878
+ function renderInfoMessage(layers) {
9879
+ var container = document.createElement('div');
9880
+ container.className = 'console-info';
9881
+ (layers || []).forEach(function(info) {
9882
+ var section = document.createElement('div');
9883
+ var title = document.createElement('div');
9884
+ section.className = 'console-info-layer';
9885
+ title.className = 'console-info-title';
9886
+ title.textContent = 'Layer: ' + (info.layer_name || '[unnamed layer]');
9887
+ section.appendChild(title);
9888
+ section.appendChild(renderKeyValueTable(getInfoRows(info), 'console-info-table'));
9889
+ if (!info.raster_type) {
9890
+ section.appendChild(renderAttributeInfoTable(info.attribute_data));
9891
+ }
9892
+ container.appendChild(section);
9893
+ });
9894
+ return container;
9895
+ }
9896
+
9897
+ function getInfoRows(info) {
9898
+ var rows = [
9899
+ ['Type', info.raster_type ? 'raster data' : info.geometry_type || 'tabular data']
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
+ }
9907
+ if (info.null_shape_count > 0) {
9908
+ rows.push(['Nulls', utils$1.format("%'d", info.null_shape_count)]);
9909
+ }
9910
+ if (info.raster_type || info.geometry_type && info.feature_count > info.null_shape_count) {
9911
+ rows.push(['Bounds', info.bbox.join(',')]);
9912
+ rows.push(['CRS', info.proj4]);
9913
+ }
9914
+ rows.push(['Source', info.source_file || 'n/a']);
9915
+ return rows;
9916
+ }
9917
+
9918
+ function renderAttributeInfoTable(fields) {
9919
+ var wrapper = document.createElement('div');
9920
+ var title = document.createElement('div');
9921
+ wrapper.className = 'console-attribute-info';
9922
+ title.className = 'console-info-subtitle';
9923
+ title.textContent = 'Attribute data';
9924
+ wrapper.appendChild(title);
9925
+ if (!fields) {
9926
+ var none = document.createElement('div');
9927
+ none.className = 'console-info-empty';
9928
+ none.textContent = '[none]';
9929
+ wrapper.appendChild(none);
9930
+ return wrapper;
9931
+ }
9932
+ wrapper.appendChild(renderDataTable(
9933
+ [['Field', 'First value']].concat(fields.map(function(o) {
9934
+ var valKey = 'first_value' in o ? 'first_value' : 'value';
9935
+ return [o.field, formatInfoValue(o[valKey])];
9936
+ })),
9937
+ 'console-attribute-table'
9938
+ ));
9939
+ return wrapper;
9940
+ }
9941
+
9942
+ function renderKeyValueTable(rows, className) {
9943
+ return renderDataTable(rows, className + ' console-key-value-table');
9944
+ }
9945
+
9946
+ function renderDataTable(rows, className) {
9947
+ var table = document.createElement('table');
9948
+ var tbody = document.createElement('tbody');
9949
+ table.className = className;
9950
+ rows.forEach(function(row, i) {
9951
+ var tr = document.createElement('tr');
9952
+ row.forEach(function(val) {
9953
+ var cell = document.createElement(i === 0 && rows.length > 1 && className == 'console-attribute-table' ? 'th' : 'td');
9954
+ cell.textContent = val == null ? '' : String(val);
9955
+ tr.appendChild(cell);
9956
+ });
9957
+ tbody.appendChild(tr);
9958
+ });
9959
+ table.appendChild(tbody);
9960
+ return table;
9961
+ }
9962
+
9963
+ function formatInfoValue(val) {
9964
+ if (val === null || val === undefined) return '';
9965
+ if (utils$1.isString(val)) {
9966
+ return "'" + val.replace(/[\r\t\n]/g, function(c) {
9967
+ return c == '\n' ? '\\n' : c == '\r' ? '\\r' : '\\t';
9968
+ }) + "'";
9969
+ }
9970
+ if (utils$1.isNumber(val) || val === true || val === false) {
9971
+ return String(val);
9972
+ }
9973
+ return JSON.stringify(val);
9974
+ }
9975
+
8565
9976
  function saveRuntimeContext() {
8566
9977
  var json = stringifyRuntimeStateContext(gui);
8567
9978
  saveBlobToLocalFile2('mapshaper-runtime-context.json', new Blob([json], {type: 'application/json'}));
@@ -8614,7 +10025,7 @@
8614
10025
  }
8615
10026
 
8616
10027
  function getBoundsIntersection(a, b) {
8617
- var c = new Bounds();
10028
+ var c = new Bounds$1();
8618
10029
  if (a.intersects(b)) {
8619
10030
  c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
8620
10031
  Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
@@ -8897,11 +10308,7 @@
8897
10308
  } else {
8898
10309
  // stack seems to change if Error is logged directly
8899
10310
  console.error(err.stack);
8900
- var msg = 'Export failed for an unknown reason';
8901
- if (err.name == 'UserError') {
8902
- msg = err.message;
8903
- }
8904
- gui.alert(msg, 'Export failed');
10311
+ gui.alert(getExportErrorMessage(err), 'Export failed');
8905
10312
  }
8906
10313
  }).finally(function() {
8907
10314
  gui.clearProgressMessage();
@@ -8909,6 +10316,11 @@
8909
10316
  }, 20);
8910
10317
  }
8911
10318
 
10319
+ function getExportErrorMessage(err) {
10320
+ if (err && err.message) return err.message;
10321
+ return 'Export failed for an unknown reason';
10322
+ }
10323
+
8912
10324
  function getExportOpts() {
8913
10325
  return GUI.parseFreeformOptions(getExportOptsAsString(), 'o');
8914
10326
  }
@@ -9064,9 +10476,11 @@
9064
10476
  }
9065
10477
 
9066
10478
  function getDefaultExportFormat() {
9067
- var dataset = model.getActiveLayer().dataset;
10479
+ var active = model.getActiveLayer();
10480
+ var dataset = active.dataset;
9068
10481
  var inputFmt = dataset.info && dataset.info.input_formats &&
9069
10482
  dataset.info.input_formats[0];
10483
+ if (active.layer && internal.layerHasRaster(active.layer)) return 'svg';
9070
10484
  return getExportFormats().includes(inputFmt) ? inputFmt : 'geojson';
9071
10485
  }
9072
10486
 
@@ -9148,6 +10562,7 @@
9148
10562
  }
9149
10563
 
9150
10564
  var openMenu;
10565
+ var openMenuId;
9151
10566
 
9152
10567
  document.addEventListener('mousedown', function(e) {
9153
10568
  if (e.target.classList.contains('contextmenu-item')) {
@@ -9156,17 +10571,23 @@
9156
10571
  closeOpenMenu();
9157
10572
  });
9158
10573
 
9159
- function closeOpenMenu() {
10574
+ function closeOpenMenu(immediate) {
9160
10575
  if (openMenu) {
9161
- openMenu.close();
10576
+ openMenu.close(immediate);
9162
10577
  openMenu = null;
10578
+ openMenuId = null;
9163
10579
  }
9164
10580
  }
9165
10581
 
9166
10582
  function openContextMenu(e, lyr, parent) {
9167
10583
  var menu = new ContextMenu(parent);
9168
- closeOpenMenu();
10584
+ if (e.contextMenuId && e.contextMenuId == openMenuId) {
10585
+ closeOpenMenu(true);
10586
+ return;
10587
+ }
10588
+ closeOpenMenu(true);
9169
10589
  menu.open(e, lyr);
10590
+ openMenuId = e.contextMenuId || null;
9170
10591
  }
9171
10592
 
9172
10593
  function ContextMenu(parentArg) {
@@ -9183,9 +10604,14 @@
9183
10604
 
9184
10605
  this.close = close;
9185
10606
 
9186
- function close() {
10607
+ function close(immediate) {
9187
10608
  var count = _openCount;
9188
10609
  if (!_open) return;
10610
+ if (immediate) {
10611
+ menu.hide();
10612
+ _open = false;
10613
+ return;
10614
+ }
9189
10615
  setTimeout(function() {
9190
10616
  if (count == _openCount) {
9191
10617
  menu.hide();
@@ -9346,8 +10772,10 @@
9346
10772
  function LayerControl(gui) {
9347
10773
  var model = gui.model;
9348
10774
  var map = gui.map;
9349
- var el = gui.container.findChild(".layer-control").on('click', GUI.handleDirectEvent(gui.clearMode));
10775
+ var el = gui.container.findChild(".layer-control");
9350
10776
  var btn = gui.container.findChild('.layer-control-btn');
10777
+ var headerBtn = btn.findChild('.active-layer-label');
10778
+ var tab = gui.container.findChild('.layer-tab');
9351
10779
  var isOpen = false;
9352
10780
  var cache = new DomCache();
9353
10781
  var pinAll = el.findChild('.pin-all'); // button for toggling layer visibility
@@ -9357,7 +10785,34 @@
9357
10785
  var dragging = false;
9358
10786
  var layerOrderSlug;
9359
10787
 
9360
- gui.addMode('layer_menu', turnOn, turnOff, btn.findChild('.header-btn'));
10788
+ headerBtn.on('click', function() {
10789
+ toggle();
10790
+ }).on('keydown', function(e) {
10791
+ if (e.key == 'Enter' || e.key == ' ') {
10792
+ e.preventDefault();
10793
+ e.stopPropagation();
10794
+ toggle();
10795
+ }
10796
+ });
10797
+ tab.on('click', toggle).on('keydown', function(e) {
10798
+ if (e.key == 'Enter' || e.key == ' ') {
10799
+ e.preventDefault();
10800
+ e.stopPropagation();
10801
+ toggle();
10802
+ }
10803
+ });
10804
+
10805
+ function toggle() {
10806
+ gui.toggleSidebarPanel('layers');
10807
+ }
10808
+
10809
+ gui.on('sidebar', function(e) {
10810
+ if (e.name == 'layers') {
10811
+ turnOn();
10812
+ } else if (e.prev == 'layers') {
10813
+ turnOff();
10814
+ }
10815
+ });
9361
10816
 
9362
10817
  // kludge to show menu button after initial import dialog is dismissed
9363
10818
  gui.on('mode', function(e) {
@@ -9443,53 +10898,33 @@
9443
10898
  }
9444
10899
 
9445
10900
  function turnOn() {
10901
+ if (isOpen) return;
9446
10902
  isOpen = true;
9447
- el.findChild('div.info-box-scrolled').css('max-height', El('body').height() - 80);
10903
+ tab.addClass('active').attr('aria-expanded', 'true');
9448
10904
  render();
9449
10905
  el.show();
9450
10906
  }
9451
10907
 
9452
10908
  function turnOff() {
10909
+ if (!isOpen) return;
9453
10910
  stopDragging();
9454
10911
  isOpen = false;
10912
+ tab.removeClass('active').attr('aria-expanded', 'false');
9455
10913
  el.hide();
9456
10914
  }
9457
10915
 
9458
10916
  function updateMenuBtn() {
9459
10917
  var lyr = model.getActiveLayer()?.layer;
9460
10918
  var lyrName = lyr?.name || '';
9461
- var menuTitle = lyrName || lyr && '[unnamed layer]' || '[no data]';
9462
10919
  var pageTitle = lyrName || 'mapshaper';
9463
- btn.classed('active', 'true').findChild('.layer-name').html(menuTitle + " &nbsp;&#9660;");
10920
+ btn.classed('active', !!lyr);
10921
+ headerBtn.text(lyr ? 'Active: ' + formatLayerNameForDisplay(lyr.name) : '');
9464
10922
  window.document.title = pageTitle;
9465
10923
  }
9466
10924
 
9467
10925
  function render() {
9468
10926
  renderLayerList();
9469
- renderSourceFileList();
9470
- }
9471
-
9472
- function renderSourceFileList() {
9473
10927
  el.findChild('.no-layer-note').classed('hidden', model.getActiveLayer());
9474
- el.findChild('.source-file-section').classed('hidden', !model.getActiveLayer());
9475
- var list = el.findChild('.file-list');
9476
- var files = [];
9477
- list.empty();
9478
- model.forEachLayer(function(lyr, dataset) {
9479
- var file = internal.getLayerSourceFile(lyr, dataset);
9480
- if (!file || files.includes(file)) return;
9481
- files.push(file);
9482
- var warnings = getWarnings(lyr, dataset);
9483
- var html = '<div class="layer-item">';
9484
- html += rowHTML('name', file);
9485
- if (warnings) {
9486
- // html += rowHTML('problems', warnings, 'layer-problems');
9487
- html += rowHTML('', warnings, 'layer-problems');
9488
- }
9489
- html += '</div>';
9490
- list.appendChild(El('div').html(html).firstChild());
9491
- });
9492
-
9493
10928
  }
9494
10929
 
9495
10930
  function renderLayerList() {
@@ -9550,10 +10985,10 @@
9550
10985
  html = '<!-- ' + lyr.menu_id + '--><div class="' + classes + '">';
9551
10986
  html += rowHTML('name', '<span class="layer-name colored-text dot-underline">' + formatLayerNameForDisplay(lyr.name) + '</span>', 'row1');
9552
10987
  html += rowHTML('contents', describeLyr(lyr, dataset));
9553
- // html += '<img class="close-btn" draggable="false" src="images/close.png">';
10988
+ html += '<span class="more-btn layer-btn" role="button" tabindex="0" aria-label="More layer options"></span>';
9554
10989
  if (opts.pinnable) {
9555
- html += '<img class="eye-btn black-eye" draggable="false" src="images/eye.png">';
9556
- html += '<img class="eye-btn green-eye" draggable="false" src="images/eye2.png">';
10990
+ html += '<img class="eye-btn black-eye layer-btn" draggable="false" src="images/eye.png">';
10991
+ html += '<img class="eye-btn green-eye layer-btn" draggable="false" src="images/eye2.png">';
9557
10992
  }
9558
10993
  html += '</div>';
9559
10994
  return html;
@@ -9561,8 +10996,10 @@
9561
10996
 
9562
10997
  function initMouseEvents(entry, id, pinnable) {
9563
10998
  entry.on('mouseover', init);
10999
+ entry.on('focusin', init);
9564
11000
  function init() {
9565
11001
  entry.removeEventListener('mouseover', init);
11002
+ entry.removeEventListener('focusin', init);
9566
11003
  initMouseEvents2(entry, id, pinnable);
9567
11004
  }
9568
11005
  }
@@ -9578,10 +11015,6 @@
9578
11015
  }
9579
11016
  // start dragging when button is first pressed
9580
11017
  if (e.buttons && !dragTargetId) {
9581
- // don't start dragging if pointer is over the close button
9582
- // (before, clicking this button wqs finicky -- the mouse had to remain
9583
- // perfectly still between mousedown and mouseup)
9584
- if (El(e.target).hasClass('close-btn')) return;
9585
11018
  dragTargetId = id;
9586
11019
  entry.addClass('drag-target');
9587
11020
  }
@@ -9606,6 +11039,7 @@
9606
11039
  }
9607
11040
 
9608
11041
  function initMouseEvents2(entry, id, pinnable) {
11042
+ var moreBtn = entry.findChild('.more-btn');
9609
11043
  initLayerDragging(entry, id);
9610
11044
 
9611
11045
  function deleteLayer() {
@@ -9630,7 +11064,7 @@
9630
11064
  }
9631
11065
  }
9632
11066
 
9633
- function selectLayer(closeMenu) {
11067
+ function selectLayer() {
9634
11068
  var target = findLayerById(id);
9635
11069
  // don't select if user is typing or dragging
9636
11070
  if (GUI.textIsSelected() || dragging) return;
@@ -9639,17 +11073,24 @@
9639
11073
  if (!map.isActiveLayer(target.layer)) {
9640
11074
  model.selectLayer(target.layer, target.dataset);
9641
11075
  }
9642
- // close menu after a delay
9643
- if (closeMenu === true) setTimeout(function() {
9644
- gui.clearMode();
9645
- }, 230);
9646
11076
  }
9647
11077
 
9648
- // init delete button
9649
- // GUI.onClick(entry.findChild('img.close-btn'), function(e) {
9650
- // e.stopPropagation();
9651
- // deleteLayer();
9652
- // });
11078
+ function openLayerMenu(e) {
11079
+ var menuEvent = e;
11080
+ e.stopPropagation();
11081
+ if (!isFinite(e.pageX) || !isFinite(e.pageY)) {
11082
+ var rect = moreBtn.node().getBoundingClientRect();
11083
+ menuEvent = {
11084
+ pageX: rect.right,
11085
+ pageY: rect.top + rect.height / 2
11086
+ };
11087
+ }
11088
+ menuEvent.deleteLayer = deleteLayer;
11089
+ menuEvent.selectLayer = selectLayer;
11090
+ menuEvent.contextMenuId = 'layer-' + id;
11091
+ openContextMenu(menuEvent, null, null);
11092
+ }
11093
+
9653
11094
 
9654
11095
  if (pinnable) {
9655
11096
  // init pin button
@@ -9692,18 +11133,22 @@
9692
11133
  renameLayer(target, str);
9693
11134
  });
9694
11135
 
11136
+ moreBtn.on('mousedown', function(e) {
11137
+ e.stopPropagation();
11138
+ });
11139
+ GUI.onClick(moreBtn, openLayerMenu);
11140
+ moreBtn.on('keydown', function(e) {
11141
+ if (e.key == 'Enter' || e.key == ' ') {
11142
+ e.preventDefault();
11143
+ openLayerMenu(e);
11144
+ }
11145
+ });
11146
+
9695
11147
  // init click-to-select
9696
11148
  GUI.onClick(entry, function() {
9697
- selectLayer(true);
11149
+ selectLayer();
9698
11150
  });
9699
11151
 
9700
- GUI.onContextClick(entry, function(e) {
9701
- e.deleteLayer = deleteLayer;
9702
- e.selectLayer = selectLayer;
9703
- // contextMenu.open(e);
9704
- // openContextMenu(e, null, entry.node())
9705
- openContextMenu(e, null, null);
9706
- });
9707
11152
  }
9708
11153
 
9709
11154
  function describeLyr(lyr, dataset) {
@@ -9714,9 +11159,13 @@
9714
11159
  type = 'data record';
9715
11160
  } else if (lyr.geometry_type) {
9716
11161
  type = lyr.geometry_type + ' feature';
11162
+ } else if (internal.layerHasRaster(lyr)) {
11163
+ type = 'raster layer';
9717
11164
  }
9718
11165
  if (isFrame) {
9719
11166
  str = 'map frame';
11167
+ } else if (internal.layerHasRaster(lyr)) {
11168
+ str = utils$1.format('%,d x %,d %s', getRasterWidth(lyr.raster), getRasterHeight(lyr.raster), type);
9720
11169
  } else if (type) {
9721
11170
  str = utils$1.format('%,d %s%s', n, type, utils$1.pluralSuffix(n));
9722
11171
  } else {
@@ -9750,32 +11199,8 @@
9750
11199
  }
9751
11200
  }
9752
11201
 
9753
- function getWarnings(lyr, dataset) {
9754
- var file = internal.getLayerSourceFile(lyr, dataset);
9755
- var missing = [];
9756
- var msg;
9757
- // show missing file warning for first layer in dataset
9758
- // (assuming it represents the content of the original file)
9759
- if (utils$1.endsWith(file, '.shp') && lyr == dataset.layers[0]) {
9760
- if (!lyr.data) {
9761
- missing.push('.dbf');
9762
- }
9763
- if (!dataset.info.wkt1 && !dataset.info.crs) {
9764
- missing.push('.prj');
9765
- }
9766
- }
9767
- if (missing.length) {
9768
- msg = 'missing ' + missing.join(' and ') + ' data';
9769
- }
9770
- return msg;
9771
- }
9772
-
9773
- function describeSrc(lyr, dataset) {
9774
- return internal.getLayerSourceFile(lyr, dataset);
9775
- }
9776
-
9777
11202
  function isPinnable(lyr) {
9778
- return internal.layerIsGeometric(lyr) || internal.layerHasFurniture(lyr);
11203
+ return internal.layerIsGeometric(lyr) || internal.layerHasRaster(lyr) || internal.layerHasFurniture(lyr);
9779
11204
  }
9780
11205
 
9781
11206
  function rowHTML(c1, c2, cname) {
@@ -9970,7 +11395,7 @@
9970
11395
  function getRestoreDataNote(gui) {
9971
11396
  var stats = getUndoPayloadStats(gui);
9972
11397
  var bytes = stats ? stats.ownBytes || 0 : 0;
9973
- return 'estimated on-disk restore data: ' + formatBytes(bytes);
11398
+ return 'restore data stored on-disk: ' + formatBytes(bytes);
9974
11399
  }
9975
11400
 
9976
11401
  function getUndoPayloadStats(gui) {
@@ -10868,6 +12293,7 @@
10868
12293
  rectangles: ['info', 'selection', 'box', 'rectangles', 'edit_polygons'],
10869
12294
  lines: ['info', 'selection', 'box', 'edit_lines'], // 'snip_lines'
10870
12295
  table: ['info', 'selection'],
12296
+ raster: ['box'],
10871
12297
  labels: ['info', 'selection', 'box', 'labels', 'edit_points'],
10872
12298
  points: ['info', 'selection', 'box', 'edit_points'] // , 'add-points'
10873
12299
  };
@@ -10988,6 +12414,9 @@
10988
12414
  if (!o || !o.layer) {
10989
12415
  return menus.empty; // TODO: more sensible handling of missing layer
10990
12416
  }
12417
+ if (internal.layerHasRaster(o.layer)) {
12418
+ return menus.raster;
12419
+ }
10991
12420
  if (!o.layer.geometry_type) {
10992
12421
  return menus.table;
10993
12422
  }
@@ -11991,8 +13420,8 @@
11991
13420
  // DELETE key
11992
13421
  // delete pinned feature
11993
13422
  // to help protect against inadvertent deletion, don't delete
11994
- // when console is open or a popup menu is open
11995
- if (!gui.getMode() && !gui.consoleIsOpen()) {
13423
+ // when a sidebar panel or popup menu is open
13424
+ if (!gui.getMode() && !gui.sidebarPanelIsOpen()) {
11996
13425
  internal.deleteFeatureById(targetLayer, pinnedId());
11997
13426
  self.clearSelection();
11998
13427
  gui.model.updated({flags: 'filter'}); // signal map to update
@@ -13402,7 +14831,7 @@
13402
14831
 
13403
14832
  // @box Bounds with pixels from t,l corner of map area.
13404
14833
  function zoomToBbox(bbox) {
13405
- var bounds = new Bounds(bbox),
14834
+ var bounds = new Bounds$1(bbox),
13406
14835
  pct = Math.max(bounds.width() / ext.width(), bounds.height() / ext.height()),
13407
14836
  fx = bounds.centerX() / ext.width() * (1 + pct) - pct / 2,
13408
14837
  fy = bounds.centerY() / ext.height() * (1 + pct) - pct / 2;
@@ -15552,7 +16981,7 @@
15552
16981
  // Get params for converting geographic coords to pixel coords
15553
16982
  this.getTransform = function(pixScale) {
15554
16983
  // get transform (y-flipped);
15555
- var viewBounds = new Bounds(0, 0, _position.width(), _position.height());
16984
+ var viewBounds = new Bounds$1(0, 0, _position.width(), _position.height());
15556
16985
  if (pixScale) {
15557
16986
  viewBounds.xmax *= pixScale;
15558
16987
  viewBounds.ymax *= pixScale;
@@ -15562,7 +16991,7 @@
15562
16991
 
15563
16992
  // k scales the size of the bbox (used by gui to control fp error when zoomed very far)
15564
16993
  this.getBounds = function(k) {
15565
- if (!_fullBounds) return new Bounds();
16994
+ if (!_fullBounds) return new Bounds$1();
15566
16995
  return calcBounds(_cx, _cy, _scale / (k || 1));
15567
16996
  };
15568
16997
 
@@ -15577,7 +17006,7 @@
15577
17006
  var b = _fullBounds = fullBounds;
15578
17007
  if (!b.hasBounds()) return; // kludge
15579
17008
  if (strictBounds) {
15580
- _strictBounds = Array.isArray(strictBounds) ? new Bounds(strictBounds) : strictBounds;
17009
+ _strictBounds = Array.isArray(strictBounds) ? new Bounds$1(strictBounds) : strictBounds;
15581
17010
  } else {
15582
17011
  _strictBounds = null;
15583
17012
  }
@@ -15607,7 +17036,7 @@
15607
17036
 
15608
17037
  this.getSymbolScale = function() {
15609
17038
  if (!_frame) return 1;
15610
- var bounds = new Bounds(_frame.bbox);
17039
+ var bounds = new Bounds$1(_frame.bbox);
15611
17040
  var bounds2 = bounds.clone().transform(this.getTransform());
15612
17041
  return bounds2.width() / _frame.width;
15613
17042
  };
@@ -15678,12 +17107,12 @@
15678
17107
  }
15679
17108
  var w = full.width() / scale;
15680
17109
  var h = full.height() / scale;
15681
- return new Bounds(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
17110
+ return new Bounds$1(cx - w/2, cy - h/2, cx + w/2, cy + h/2);
15682
17111
  }
15683
17112
 
15684
17113
  // Calculate viewport bounds from frame data
15685
17114
  function fillOutFrameBounds(frame) {
15686
- var bounds = new Bounds(frame.bbox);
17115
+ var bounds = new Bounds$1(frame.bbox);
15687
17116
  var kx = _position.width() / frame.width;
15688
17117
  var ky = _position.height() / frame.height;
15689
17118
  bounds.scale(kx, ky);
@@ -15695,7 +17124,7 @@
15695
17124
  hpix = _position.height() - 2 * marginpix,
15696
17125
  xpad, ypad, b2;
15697
17126
  if (wpix <= 0 || hpix <= 0) {
15698
- return new Bounds(0, 0, 0, 0);
17127
+ return new Bounds$1(0, 0, 0, 0);
15699
17128
  }
15700
17129
  b = b.clone();
15701
17130
  b2 = b.clone();
@@ -15733,6 +17162,198 @@
15733
17162
 
15734
17163
  utils$1.inherit(MapExtent, EventDispatcher);
15735
17164
 
17165
+ var MAX_VIEWPORT_PREVIEW_PIXELS = 6e6;
17166
+ var cache = new WeakMap();
17167
+ var requestId = 0;
17168
+
17169
+ function getCachedRasterViewportPreview(layer, ext) {
17170
+ var entry = cache.get(layer);
17171
+ var params = getRasterViewportPreviewParams(layer, ext);
17172
+ if (!entry || !params || entry.key != params.key) return null;
17173
+ return entry.preview;
17174
+ }
17175
+
17176
+ function scheduleRasterViewportPreview(layer, ext, onReady) {
17177
+ var params = getRasterViewportPreviewParams(layer, ext);
17178
+ var entry = cache.get(layer);
17179
+ var id, stats, timing, preview;
17180
+ if (!params || !params.needed) return;
17181
+ if (entry && entry.key == params.key) return;
17182
+ id = ++requestId;
17183
+ cache.set(layer, {key: params.key, pending: id});
17184
+ setTimeout(function() {
17185
+ var current = cache.get(layer);
17186
+ if (!current || current.pending != id) return;
17187
+ timing = {};
17188
+ stats = getCachedRasterScalingStats(params, timing);
17189
+ timing.renderStart = getTimer();
17190
+ preview = renderRasterViewportPreview(params.grid, params.recipe, params.bbox, params.width, params.height, stats);
17191
+ timing.renderMs = getTimer() - timing.renderStart;
17192
+ logRasterPreviewTiming(params, timing);
17193
+ current = cache.get(layer);
17194
+ if (!preview || !current || current.pending != id) return;
17195
+ cache.set(layer, {
17196
+ key: params.key,
17197
+ preview: preview
17198
+ });
17199
+ onReady();
17200
+ }, 0);
17201
+ }
17202
+
17203
+ function invalidateRasterViewportPreview(layer) {
17204
+ cache.delete(layer);
17205
+ }
17206
+
17207
+ function getRasterViewportPreviewParams(layer, ext) {
17208
+ var raster = layer && layer.raster;
17209
+ var grid = getRasterGrid(raster);
17210
+ var rasterBbox = getRasterBBox(raster);
17211
+ var mapBbox = ext.getBounds().toArray();
17212
+ var visibleBbox = rasterBbox && intersectBboxes(rasterBbox, mapBbox);
17213
+ var preview = getRasterPreview(raster);
17214
+ var recipe, pixelRatio, t, p1, p2, displayWidth, displayHeight, crop, width, height, scale, key, needed;
17215
+ if (!grid || !grid.samples || !visibleBbox || !grid.bbox) return null;
17216
+ if (!supportsNorthUpRaster(grid)) return null;
17217
+ crop = getRasterSourceWindow(grid, visibleBbox);
17218
+ if (!crop) return null;
17219
+ visibleBbox = crop.bbox;
17220
+ recipe = getRasterViewRecipe(grid, raster.view && raster.view.recipe);
17221
+ pixelRatio = GUI.getPixelRatio();
17222
+ t = ext.getTransform(pixelRatio);
17223
+ p1 = t.transform(mapBbox[0], mapBbox[3]);
17224
+ p2 = t.transform(mapBbox[2], mapBbox[1]);
17225
+ displayWidth = Math.max(1, Math.round(Math.abs(p2[0] - p1[0])));
17226
+ displayHeight = Math.max(1, Math.round(Math.abs(p2[1] - p1[1])));
17227
+ width = Math.min(displayWidth, crop.width);
17228
+ height = Math.min(displayHeight, crop.height);
17229
+ scale = Math.min(1, Math.sqrt(MAX_VIEWPORT_PREVIEW_PIXELS / (width * height)));
17230
+ width = Math.max(1, Math.round(width * scale));
17231
+ height = Math.max(1, Math.round(height * scale));
17232
+ needed = viewportPreviewIsSharper(preview, grid, visibleBbox, width, height);
17233
+ key = [
17234
+ visibleBbox.map(roundKeyPart).join(','),
17235
+ width,
17236
+ height,
17237
+ pixelRatio,
17238
+ grid.width,
17239
+ grid.height,
17240
+ grid.samples && grid.samples.length,
17241
+ recipe.type,
17242
+ recipe.scaling,
17243
+ recipe.scaleRange && recipe.scaleRange.join(','),
17244
+ recipe.percentileRange && recipe.percentileRange.join(',')
17245
+ ].join('|');
17246
+ return {
17247
+ key: key,
17248
+ needed: needed,
17249
+ raster: raster,
17250
+ grid: grid,
17251
+ recipe: recipe,
17252
+ bbox: visibleBbox,
17253
+ width: width,
17254
+ height: height,
17255
+ sourceWidth: crop.width,
17256
+ sourceHeight: crop.height
17257
+ };
17258
+ }
17259
+
17260
+ function getCachedRasterScalingStats(params, timing) {
17261
+ var cached = params.raster.view && params.raster.view.scalingStats;
17262
+ var key;
17263
+ if (params.recipe.scaling == 'none') {
17264
+ timing.statsMs = 0;
17265
+ timing.statsSource = 'none';
17266
+ return null;
17267
+ }
17268
+ key = getRasterScalingStatsKey(params.grid, params.recipe);
17269
+ if (cached && cached.key == key) {
17270
+ timing.statsMs = 0;
17271
+ timing.statsSource = 'raster-view-cache';
17272
+ return cached.stats;
17273
+ }
17274
+ timing.statsSource = 'computed';
17275
+ timing.statsStart = getTimer();
17276
+ cached = getRasterViewScalingStats(params.raster, params.recipe);
17277
+ timing.statsMs = getTimer() - timing.statsStart;
17278
+ return cached;
17279
+ }
17280
+
17281
+ function logRasterPreviewTiming(params, timing) {
17282
+ if (!rasterDebugIsOn()) return;
17283
+ console.log([
17284
+ 'Raster viewport preview:',
17285
+ params.width + 'x' + params.height,
17286
+ 'from',
17287
+ params.sourceWidth + 'x' + params.sourceHeight,
17288
+ 'source px,',
17289
+ 'stats=' + formatMs(timing.statsMs),
17290
+ '(' + timing.statsSource + '),',
17291
+ 'render=' + formatMs(timing.renderMs) + ',',
17292
+ 'total=' + formatMs(timing.statsMs + timing.renderMs) + ',',
17293
+ 'scaling=' + params.recipe.scaling,
17294
+ 'type=' + params.recipe.type
17295
+ ].join(' '));
17296
+ }
17297
+
17298
+ function rasterDebugIsOn() {
17299
+ var vars = GUI.getUrlVars();
17300
+ return vars['raster-debug'] === true || vars['raster-debug'] == '1' || vars.raster_debug === true || vars.raster_debug == '1';
17301
+ }
17302
+
17303
+ function getTimer() {
17304
+ return typeof performance != 'undefined' && performance.now ? performance.now() : Date.now();
17305
+ }
17306
+
17307
+ function formatMs(ms) {
17308
+ return Math.round(ms * 10) / 10 + 'ms';
17309
+ }
17310
+
17311
+ function supportsNorthUpRaster(grid) {
17312
+ var t = grid.transform;
17313
+ return !t || t[1] === 0 && t[3] === 0;
17314
+ }
17315
+
17316
+ function getRasterSourcePixelSize(grid, bbox) {
17317
+ var crop = getRasterSourceWindow(grid, bbox);
17318
+ return crop ? {width: crop.width, height: crop.height} : {width: 0, height: 0};
17319
+ }
17320
+
17321
+ function getRasterSourceWindow(grid, bbox) {
17322
+ var rb = grid.bbox;
17323
+ var dx = (rb[2] - rb[0]) / grid.width;
17324
+ var dy = (rb[3] - rb[1]) / grid.height;
17325
+ var x0 = Math.max(0, Math.floor((bbox[0] - rb[0]) / dx));
17326
+ var x1 = Math.min(grid.width, Math.ceil((bbox[2] - rb[0]) / dx));
17327
+ var y0 = Math.max(0, Math.floor((rb[3] - bbox[3]) / dy));
17328
+ var y1 = Math.min(grid.height, Math.ceil((rb[3] - bbox[1]) / dy));
17329
+ if (x1 <= x0 || y1 <= y0) return null;
17330
+ return {
17331
+ x: x0,
17332
+ y: y0,
17333
+ width: x1 - x0,
17334
+ height: y1 - y0,
17335
+ bbox: [
17336
+ rb[0] + x0 * dx,
17337
+ rb[3] - y1 * dy,
17338
+ rb[0] + x1 * dx,
17339
+ rb[3] - y0 * dy
17340
+ ]
17341
+ };
17342
+ }
17343
+
17344
+ function viewportPreviewIsSharper(preview, grid, bbox, width, height) {
17345
+ var crop = getRasterSourcePixelSize(grid, bbox);
17346
+ var previewWidth = preview && preview.width || 0;
17347
+ var previewHeight = preview && preview.height || 0;
17348
+ var fallbackWidth = crop.width * previewWidth / grid.width;
17349
+ var fallbackHeight = crop.height * previewHeight / grid.height;
17350
+ return width > fallbackWidth * 1.25 || height > fallbackHeight * 1.25;
17351
+ }
17352
+
17353
+ function roundKeyPart(val) {
17354
+ return Math.round(val * 1e9) / 1e9;
17355
+ }
17356
+
15736
17357
  var hatches = {}; // cached patterns
15737
17358
 
15738
17359
  function getCanvasFillEffect(ctx, shp, arcs, ext, style) {
@@ -15935,7 +17556,7 @@
15935
17556
  function getShapeFilter(arcs, shapes, ext) {
15936
17557
  if (ext.scale() < 1.1) return null;
15937
17558
  var view = ext.getBounds();
15938
- var b = new Bounds();
17559
+ var b = new Bounds$1();
15939
17560
  return function(i) {
15940
17561
  var shp = shapes[i];
15941
17562
  if (!shp) return false;
@@ -15988,6 +17609,22 @@
15988
17609
  _ext = extent;
15989
17610
  };
15990
17611
 
17612
+ _self.drawRasterLayer = function(layer, opts) {
17613
+ var raster = layer.raster;
17614
+ var options = opts || {};
17615
+ var preview = options.action == 'nav' ? null : getCachedRasterViewportPreview(layer, _ext);
17616
+ var bbox = preview && preview.bbox;
17617
+ if (!preview) {
17618
+ preview = raster && getRasterPreview(raster);
17619
+ bbox = raster && getRasterBBox(raster);
17620
+ if (options.action != 'nav' && options.onViewportPreviewReady) {
17621
+ scheduleRasterViewportPreview(layer, _ext, options.onViewportPreviewReady);
17622
+ }
17623
+ }
17624
+ if (!preview || !preview.pixels || !bbox) return;
17625
+ drawRasterPreview(preview, bbox);
17626
+ };
17627
+
15991
17628
  /*
15992
17629
  // Original function, not optimized
15993
17630
  _self.drawStyledPaths = function(shapes, arcs, style) {
@@ -16184,6 +17821,15 @@
16184
17821
  }
16185
17822
  }
16186
17823
 
17824
+ function drawRasterPreview(preview, bbox) {
17825
+ var img = getRasterCanvas(preview);
17826
+ var t = _ext.getTransform(GUI.getPixelRatio());
17827
+ var p1 = t.transform(bbox[0], bbox[3]);
17828
+ var p2 = t.transform(bbox[2], bbox[1]);
17829
+ _ctx.imageSmoothingEnabled = true;
17830
+ _ctx.drawImage(img, p1[0], p1[1], p2[0] - p1[0], p2[1] - p1[1]);
17831
+ }
17832
+
16187
17833
  // TODO: consider using drawStyledPaths(), which draws paths in batches
16188
17834
  // for faster Canvas rendering. Downside: changes stacking order, which
16189
17835
  // is bad if circles are graduated.
@@ -16299,6 +17945,19 @@
16299
17945
  };
16300
17946
  }
16301
17947
 
17948
+ function getRasterCanvas(preview) {
17949
+ if (preview.canvas) return preview.canvas;
17950
+ var canvas = document.createElement('canvas');
17951
+ var ctx = canvas.getContext('2d');
17952
+ var imageData;
17953
+ canvas.width = preview.width;
17954
+ canvas.height = preview.height;
17955
+ imageData = new ImageData(preview.pixels, preview.width, preview.height);
17956
+ ctx.putImageData(imageData, 0, 0);
17957
+ preview.canvas = canvas;
17958
+ return canvas;
17959
+ }
17960
+
16302
17961
  function drawCircle(x, y, radius, ctx) {
16303
17962
  if (radius > 0) {
16304
17963
  ctx.moveTo(x + radius, y);
@@ -16599,6 +18258,7 @@
16599
18258
  var _settleTimer = null;
16600
18259
  var _redrawPending = false;
16601
18260
  var _settleRequested = false;
18261
+ var _rasterNavRefreshPending = false;
16602
18262
 
16603
18263
  // 'map_interaction_end' may arrive before the in-flight 'nav' draw runs,
16604
18264
  // because drawLayers() schedules its work via requestAnimationFrame.
@@ -16607,6 +18267,8 @@
16607
18267
  gui.on('map_interaction_end', function() {
16608
18268
  if (_redrawPending) {
16609
18269
  settleNow();
18270
+ } else if (_rasterNavRefreshPending) {
18271
+ settleRasterNow();
16610
18272
  } else {
16611
18273
  _settleRequested = true;
16612
18274
  }
@@ -16617,6 +18279,11 @@
16617
18279
 
16618
18280
  this.drawMainLayers = function(layers, action) {
16619
18281
  if (skipMainLayerRedraw(action)) return;
18282
+ if (action == 'nav' && layersHaveRasters(layers)) {
18283
+ _rasterNavRefreshPending = true;
18284
+ } else if (action != 'nav') {
18285
+ _rasterNavRefreshPending = false;
18286
+ }
16620
18287
  if (action == 'nav' && shouldUseFastNav()) {
16621
18288
  applyFastTransform();
16622
18289
  // SVG symbol reposition is already cheap; keep labels/symbols accurate
@@ -16646,7 +18313,7 @@
16646
18313
  } else if (isSvgLayer) {
16647
18314
  _svg.drawLayer(lyr, 'symbol');
16648
18315
  } else {
16649
- drawCanvasLayer(lyr, _mainCanv);
18316
+ drawCanvasLayer(lyr, _mainCanv, action);
16650
18317
  }
16651
18318
  });
16652
18319
  // Force synchronous rasterization so performance.now() reflects the true
@@ -16655,6 +18322,10 @@
16655
18322
  flushCanvas(_mainCanv);
16656
18323
  _lastFrameMs = performance.now() - startTime;
16657
18324
  captureSnapshot();
18325
+ if (action == 'nav' && _settleRequested && _rasterNavRefreshPending) {
18326
+ _settleRequested = false;
18327
+ settleRasterNow();
18328
+ }
16658
18329
  };
16659
18330
 
16660
18331
  // Draw highlight effect for hover and selection
@@ -16675,7 +18346,7 @@
16675
18346
  _overlayCanv.hide();
16676
18347
  }
16677
18348
  layers.forEach(function(lyr) {
16678
- drawCanvasLayer(lyr, canv);
18349
+ drawCanvasLayer(lyr, canv, action);
16679
18350
  });
16680
18351
  };
16681
18352
 
@@ -16700,9 +18371,22 @@
16700
18371
  return action == 'hover' && _overlayCanv.visible();
16701
18372
  }
16702
18373
 
16703
- function drawCanvasLayer(lyr, canv) {
18374
+ function layersHaveRasters(layers) {
18375
+ return layers.some(function(lyr) {
18376
+ return lyr && lyr.gui && internal.layerHasRaster(lyr.gui.displayLayer);
18377
+ });
18378
+ }
18379
+
18380
+ function drawCanvasLayer(lyr, canv, action) {
16704
18381
  if (!lyr) return;
16705
- if (lyr.gui.style.type == 'outline') {
18382
+ if (internal.layerHasRaster(lyr.gui.displayLayer)) {
18383
+ canv.drawRasterLayer(lyr.gui.displayLayer, {
18384
+ action: action,
18385
+ onViewportPreviewReady: function() {
18386
+ gui.dispatchEvent('map-needs-refresh');
18387
+ }
18388
+ });
18389
+ } else if (lyr.gui.style.type == 'outline') {
16706
18390
  drawOutlineLayerToCanvas(lyr, canv, ext);
16707
18391
  } else {
16708
18392
  drawStyledLayerToCanvas(lyr, canv, ext);
@@ -16805,6 +18489,11 @@
16805
18489
  gui.dispatchEvent('map-needs-refresh');
16806
18490
  }
16807
18491
 
18492
+ function settleRasterNow() {
18493
+ _rasterNavRefreshPending = false;
18494
+ gui.dispatchEvent('map-needs-refresh');
18495
+ }
18496
+
16808
18497
  function cancelSettle() {
16809
18498
  if (_settleTimer) {
16810
18499
  clearTimeout(_settleTimer);
@@ -16820,6 +18509,9 @@
16820
18509
  var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
16821
18510
  var popup = gui.container.findChild('.box-tool-options');
16822
18511
  var coords = popup.findChild('.box-coords').hide();
18512
+ var selectBtn = popup.findChild('.select-btn');
18513
+ var clipBtn = popup.findChild('.clip-btn');
18514
+ var eraseBtn = popup.findChild('.erase-btn');
16823
18515
  var _on = false;
16824
18516
  var instructionsShown = false;
16825
18517
  var alert;
@@ -16832,7 +18524,7 @@
16832
18524
  reset();
16833
18525
  });
16834
18526
 
16835
- new SimpleButton(popup.findChild('.select-btn')).on('click', function() {
18527
+ new SimpleButton(selectBtn).on('click', function() {
16836
18528
  var coords = box.getDataCoords();
16837
18529
  if (!coords || noData()) return;
16838
18530
  gui.enterMode('selection_tool');
@@ -16847,11 +18539,12 @@
16847
18539
  return !gui.model.getActiveLayer();
16848
18540
  }
16849
18541
 
16850
- new SimpleButton(popup.findChild('.clip-btn')).on('click', function() {
18542
+ new SimpleButton(clipBtn).on('click', function() {
16851
18543
  runCommand('-clip bbox=' + box.getDataCoords().join(','));
16852
18544
  });
16853
18545
 
16854
- new SimpleButton(popup.findChild('.erase-btn')).on('click', function() {
18546
+ new SimpleButton(eraseBtn).on('click', function() {
18547
+ if (activeLayerIsRaster()) return;
16855
18548
  runCommand('-erase bbox=' + box.getDataCoords().join(','));
16856
18549
  });
16857
18550
 
@@ -16894,6 +18587,7 @@
16894
18587
  box.on('dragend', function(e) {
16895
18588
  if (_on) {
16896
18589
  hideInstructions();
18590
+ updateOptionsForActiveLayer();
16897
18591
  popup.show();
16898
18592
  }
16899
18593
  });
@@ -16947,6 +18641,7 @@
16947
18641
 
16948
18642
  function turnOn() {
16949
18643
  box.turnOn();
18644
+ updateOptionsForActiveLayer();
16950
18645
  _on = true;
16951
18646
  }
16952
18647
 
@@ -16967,6 +18662,27 @@
16967
18662
  hideCoords();
16968
18663
  }
16969
18664
 
18665
+ function updateOptionsForActiveLayer() {
18666
+ var raster = activeLayerIsRaster();
18667
+ setButtonVisible(selectBtn, !raster);
18668
+ setButtonVisible(clipBtn, true);
18669
+ setButtonVisible(eraseBtn, !raster);
18670
+ }
18671
+
18672
+ function activeLayerIsRaster() {
18673
+ var active = gui.model.getActiveLayer();
18674
+ return !!(active && internal.layerHasRaster(active.layer));
18675
+ }
18676
+
18677
+ function setButtonVisible(btn, visible) {
18678
+ btn.classed('hidden', !visible);
18679
+ if (visible) {
18680
+ btn.show();
18681
+ } else {
18682
+ btn.hide();
18683
+ }
18684
+ }
18685
+
16970
18686
  function openAddFramePopup(gui, bbox) {
16971
18687
  var popup = showPopupAlert('', 'Add a map frame');
16972
18688
  var el = popup.container();
@@ -17336,7 +19052,8 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17336
19052
 
17337
19053
  function getDisplayOptions() {
17338
19054
  return {
17339
- crs: _dynamicCRS
19055
+ crs: _dynamicCRS,
19056
+ notify: gui.notify
17340
19057
  };
17341
19058
  }
17342
19059
 
@@ -17353,7 +19070,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17353
19070
  }
17354
19071
 
17355
19072
  function getContentLayerBounds() {
17356
- var b = new Bounds();
19073
+ var b = new Bounds$1();
17357
19074
  var layers = getContentLayers();
17358
19075
  layers.forEach(function(lyr) {
17359
19076
  b.mergeBounds(lyr.gui.bounds);
@@ -17370,7 +19087,7 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
17370
19087
  function calcFullBounds() {
17371
19088
  var b;
17372
19089
  if (isPreviewView()) {
17373
- b = new Bounds(getFrameLayerData().bbox);
19090
+ b = new Bounds$1(getFrameLayerData().bbox);
17374
19091
  } else {
17375
19092
  b = getContentLayerBounds();
17376
19093
  }
@@ -18326,11 +20043,16 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
18326
20043
 
18327
20044
 
18328
20045
  gui.state = {};
20046
+ var sidebarPanel = null;
20047
+ var lastSidebarPanel = 'console';
20048
+ var sidebarWidth = GUI.getSavedValue('sidebar_width') || 0;
20049
+ var sidebarResizeFrame = null;
18329
20050
 
18330
20051
  var msgCount = 0;
18331
20052
  var clearMsg;
18332
20053
 
18333
20054
  initModeRules(gui);
20055
+ startRasterSourceStoreLifecycle();
18334
20056
  gui.map.init();
18335
20057
 
18336
20058
  if (opts.saveControl) {
@@ -18366,10 +20088,50 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
18366
20088
  // if (gui.progressMessage) gui.progressMessage.hide();
18367
20089
  };
18368
20090
 
20091
+ if (sidebarWidth) {
20092
+ setSidebarWidth(sidebarWidth);
20093
+ }
20094
+
20095
+ gui.getSidebarPanel = function() {
20096
+ return sidebarPanel;
20097
+ };
20098
+
20099
+ gui.setSidebarPanel = function(name) {
20100
+ var prev = sidebarPanel;
20101
+ sidebarPanel = name || null;
20102
+ if (sidebarPanel && gui.getMode()) {
20103
+ gui.clearMode();
20104
+ }
20105
+ if (sidebarPanel == prev) return;
20106
+ if (sidebarPanel) {
20107
+ lastSidebarPanel = sidebarPanel;
20108
+ }
20109
+ gui.container
20110
+ .classed('sidebar-open', !!sidebarPanel)
20111
+ .classed('layers-open', sidebarPanel == 'layers')
20112
+ .classed('console-open', sidebarPanel == 'console');
20113
+ gui.dispatchEvent('sidebar', {name: sidebarPanel, prev: prev});
20114
+ gui.dispatchEvent('resize');
20115
+ };
20116
+
20117
+ gui.toggleSidebarPanel = function(name) {
20118
+ gui.setSidebarPanel(sidebarPanel == name ? null : name);
20119
+ };
20120
+
20121
+ gui.toggleSidebar = function() {
20122
+ gui.setSidebarPanel(sidebarPanel ? null : lastSidebarPanel);
20123
+ };
20124
+
20125
+ gui.sidebarPanelIsOpen = function() {
20126
+ return !!sidebarPanel;
20127
+ };
20128
+
18369
20129
  gui.consoleIsOpen = function() {
18370
- return gui.container.hasClass('console-open');
20130
+ return sidebarPanel == 'console';
18371
20131
  };
18372
20132
 
20133
+ initSidebarResizing();
20134
+
18373
20135
  gui.getRuntimeStateContext = function() {
18374
20136
  return getRuntimeStateContext(gui);
18375
20137
  };
@@ -18411,6 +20173,50 @@ GUI and setting the size and crop of SVG output.</p><div><input type="text" clas
18411
20173
  }
18412
20174
 
18413
20175
  return gui;
20176
+
20177
+ function initSidebarResizing() {
20178
+ var handle = gui.container.findChild('.sidebar-resize-handle');
20179
+ if (!handle) return;
20180
+ handle.on('mousedown', function(e) {
20181
+ if (!gui.sidebarPanelIsOpen()) return;
20182
+ e.preventDefault();
20183
+ e.stopPropagation();
20184
+ gui.container.addClass('sidebar-resizing');
20185
+ window.addEventListener('mousemove', onMove);
20186
+ window.addEventListener('mouseup', onRelease);
20187
+ });
20188
+
20189
+ function onMove(e) {
20190
+ setSidebarWidth(e.pageX);
20191
+ scheduleSidebarResize();
20192
+ }
20193
+
20194
+ function onRelease() {
20195
+ window.removeEventListener('mousemove', onMove);
20196
+ window.removeEventListener('mouseup', onRelease);
20197
+ gui.container.removeClass('sidebar-resizing');
20198
+ GUI.setSavedValue('sidebar_width', sidebarWidth);
20199
+ scheduleSidebarResize();
20200
+ }
20201
+ }
20202
+
20203
+ function setSidebarWidth(width) {
20204
+ sidebarWidth = clampSidebarWidth(width);
20205
+ gui.container.node().style.setProperty('--left-sidebar-width', sidebarWidth + 'px');
20206
+ }
20207
+
20208
+ function clampSidebarWidth(width) {
20209
+ var max = Math.min(720, Math.round(window.innerWidth * 0.6));
20210
+ return Math.max(220, Math.min(max, Math.round(width)));
20211
+ }
20212
+
20213
+ function scheduleSidebarResize() {
20214
+ if (sidebarResizeFrame) return;
20215
+ sidebarResizeFrame = requestAnimationFrame(function() {
20216
+ sidebarResizeFrame = null;
20217
+ gui.dispatchEvent('resize');
20218
+ });
20219
+ }
18414
20220
  }
18415
20221
 
18416
20222
  function createUndoTestApi(gui) {