mapshaper 0.6.18 → 0.6.20

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.
@@ -27,6 +27,7 @@
27
27
  get defaults () { return defaults; },
28
28
  get extend () { return extend; },
29
29
  get inherit () { return inherit; },
30
+ get promisify () { return promisify; },
30
31
  get reduceAsync () { return reduceAsync; },
31
32
  get merge () { return merge; },
32
33
  get difference () { return difference; },
@@ -1106,6 +1107,609 @@
1106
1107
 
1107
1108
  // utils.inherit(SimpleButton, EventDispatcher);
1108
1109
 
1110
+ function showPopupAlert(msg, title) {
1111
+ var self = {}, html = '';
1112
+ var _cancel, _close;
1113
+ var warningRxp = /^Warning: /;
1114
+ var el = El('div').appendTo('body').addClass('error-wrapper');
1115
+ var infoBox = El('div').appendTo(el).addClass('error-box info-box selectable');
1116
+ if (!title && warningRxp.test(msg)) {
1117
+ title = 'Warning';
1118
+ msg = msg.replace(warningRxp, '');
1119
+ }
1120
+ if (title) {
1121
+ html += `<div class="error-title">${title}</div>`;
1122
+ }
1123
+ html += `<p class="error-message">${msg}</p>`;
1124
+ El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
1125
+ if (_cancel) _cancel();
1126
+ self.close();
1127
+ });
1128
+ El('div').appendTo(infoBox).addClass('error-content').html(html);
1129
+
1130
+ self.onCancel = function(cb) {
1131
+ _cancel = cb;
1132
+ return self;
1133
+ };
1134
+
1135
+ self.onClose = function(cb) {
1136
+ _close = cb;
1137
+ return self;
1138
+ };
1139
+
1140
+ self.button = function(label, cb) {
1141
+ El('div')
1142
+ .addClass("btn dialog-btn alert-btn")
1143
+ .appendTo(infoBox)
1144
+ .html(label)
1145
+ .on('click', function() {
1146
+ self.close();
1147
+ cb();
1148
+ });
1149
+ return self;
1150
+ };
1151
+
1152
+ self.close = function() {
1153
+ if (el) el.remove();
1154
+ if (_close) _close();
1155
+ el = _cancel = _close = null;
1156
+ };
1157
+ return self;
1158
+ }
1159
+
1160
+ function AlertControl(gui) {
1161
+ var openAlert; // error popup
1162
+ var openPopup; // any popup
1163
+
1164
+ gui.addMode('alert', function() {}, closePopup);
1165
+
1166
+ gui.alert = function(str, title) {
1167
+ closePopup();
1168
+ openAlert = openPopup = showPopupAlert(str, title);
1169
+ // alert.button('close', gui.clearMode);
1170
+ openAlert.onClose(gui.clearMode);
1171
+ gui.enterMode('alert');
1172
+ };
1173
+
1174
+ gui.message = function(str, title) {
1175
+ if (openPopup) return; // don't stomp on another popup
1176
+ openPopup = showPopupAlert(str, title);
1177
+ openPopup.onClose(function() {openPopup = null;});
1178
+ };
1179
+
1180
+ function closePopup() {
1181
+ if (openPopup) openPopup.close();
1182
+ openPopup = openAlert = null;
1183
+ }
1184
+ }
1185
+
1186
+ function saveZipFile(zipfileName, files, done) {
1187
+ internal.zipAsync(files, function(err, buf) {
1188
+ if (err) {
1189
+ done(errorMessage(err));
1190
+ } else {
1191
+ saveBlobToLocalFile(zipfileName, new Blob([buf]), done);
1192
+ }
1193
+ });
1194
+
1195
+ function errorMessage(err) {
1196
+ var str = "Error creating Zip file";
1197
+ if (err.message) {
1198
+ str += ": " + err.message;
1199
+ }
1200
+ return str;
1201
+ }
1202
+ }
1203
+
1204
+ function saveFilesToServer(paths, data, done) {
1205
+ var i = -1;
1206
+ next();
1207
+ function next(err) {
1208
+ i++;
1209
+ if (err) return done(err);
1210
+ if (i >= data.length) return done();
1211
+ saveBlobToServer(paths[i], new Blob([data[i]]), next);
1212
+ }
1213
+ }
1214
+
1215
+ function saveBlobToServer(path, blob, done) {
1216
+ var q = '?file=' + encodeURIComponent(path);
1217
+ var url = window.location.origin + '/save' + q;
1218
+ window.fetch(url, {
1219
+ method: 'POST',
1220
+ credentials: 'include',
1221
+ body: blob
1222
+ }).then(function(resp) {
1223
+ if (resp.status == 400) {
1224
+ return resp.text();
1225
+ }
1226
+ }).then(function(err) {
1227
+ done(err);
1228
+ }).catch(function(resp) {
1229
+ done('connection to server was lost');
1230
+ });
1231
+ }
1232
+
1233
+ async function saveBlobToLocalFile(filename, blob, done) {
1234
+ done = done || function() {};
1235
+ if (window.showSaveFilePicker) {
1236
+ saveBlobToSelectedFile(filename, blob, done);
1237
+ } else {
1238
+ saveBlobToDownloadsFolder(filename, blob, done);
1239
+ }
1240
+ }
1241
+
1242
+ function showSaveDialog(filename, blob, done) {
1243
+ showPopupAlert(`Save ${filename} to:`)
1244
+ .button('selected folder', function() {
1245
+ saveBlobToSelectedFile(filename, blob, done);
1246
+ })
1247
+ .button('downloads', function() {
1248
+ saveBlobToDownloadsFolder(filename, blob, done);
1249
+ })
1250
+ .onCancel(done);
1251
+ }
1252
+
1253
+ async function saveBlobToSelectedFile(filename, blob, done) {
1254
+ // see: https://developer.chrome.com/articles/file-system-access/
1255
+ // note: saving multiple files to a directory using showDirectoryPicker()
1256
+ // does not work well (in Chrome). User is prompted for permission each time,
1257
+ // and some directories (like Downloads and Documents) are blocked.
1258
+ //
1259
+ var options = getSaveFileOptions(filename);
1260
+ var handle;
1261
+ try {
1262
+ handle = await window.showSaveFilePicker(options);
1263
+ var writable = await handle.createWritable();
1264
+ await writable.write(blob);
1265
+ await writable.close();
1266
+ } catch(e) {
1267
+ if (e.name == 'SecurityError') {
1268
+ // assuming this is a timeout error, with message like:
1269
+ // "Must be handling a user gesture to show a file picker."
1270
+ showSaveDialog(filename, blob, done);
1271
+ } else if (e.name == 'AbortError') {
1272
+ // fired if user clicks a cancel button (normal, no error message)
1273
+ // BUT: this kind of erro rmay also get fired when saving to a protected folder
1274
+ // (should show error message)
1275
+ done();
1276
+ } else {
1277
+ console.error(e.name, e.message, e);
1278
+ done('Save failed for an unknown reason');
1279
+ }
1280
+ return;
1281
+ }
1282
+
1283
+ done();
1284
+ }
1285
+
1286
+ function getSaveFileOptions(filename) {
1287
+ // see: https://wicg.github.io/file-system-access/#api-filepickeroptions
1288
+ var type = internal.guessInputFileType(filename);
1289
+ var ext = internal.getFileExtension(filename).toLowerCase();
1290
+ var accept = {};
1291
+ if (ext == 'kml') {
1292
+ accept['application/vnd.google-earth.kml+xml'] = ['.kml'];
1293
+ } else if (ext == 'svg') {
1294
+ accept['image/svg+xml'] = ['.svg'];
1295
+ } else if (ext == 'zip') {
1296
+ accept['application/zip'] == ['.zip'];
1297
+ } else if (type == 'text') {
1298
+ accept['text/csv'] = ['.csv', '.tsv', '.tab', '.txt'];
1299
+ } else if (type == 'json') {
1300
+ accept['application/json'] = ['.json', '.geojson', '.topojson'];
1301
+ } else {
1302
+ accept['application/octet-stream'] = ['.' + ext];
1303
+ }
1304
+ return {
1305
+ suggestedName: filename,
1306
+ // If startIn is given, Chrome will always start there
1307
+ // Default is to start in the previously selected dir (better)
1308
+ // // startIn: 'downloads', // or: desktop, documents, [file handle], [directory handle]
1309
+ types: [{
1310
+ description: 'Files',
1311
+ accept: accept
1312
+ }]
1313
+ };
1314
+ }
1315
+
1316
+
1317
+ function saveBlobToDownloadsFolder(filename, blob, done) {
1318
+ var anchor, blobUrl;
1319
+ try {
1320
+ blobUrl = URL.createObjectURL(blob);
1321
+ } catch(e) {
1322
+ done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
1323
+ return;
1324
+ }
1325
+ anchor = El('a').attr('href', '#').appendTo('body').node();
1326
+ anchor.href = blobUrl;
1327
+ anchor.download = filename;
1328
+ var clickEvent = document.createEvent("MouseEvent");
1329
+ clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
1330
+ false, false, false, 0, null);
1331
+ anchor.dispatchEvent(clickEvent);
1332
+ setTimeout(function() {
1333
+ // Revoke blob url to release memory; timeout needed in firefox
1334
+ URL.revokeObjectURL(blobUrl);
1335
+ anchor.parentNode.removeChild(anchor);
1336
+ done();
1337
+ }, 400);
1338
+ }
1339
+
1340
+ var idb = require('idb-keyval');
1341
+ // https://github.com/jakearchibald/idb
1342
+ // https://github.com/jakearchibald/idb-keyval
1343
+ var sessionId = getUniqId('session');
1344
+ var snapshotCount = 0;
1345
+
1346
+ function getUniqId(prefix) {
1347
+ return prefix + '_' + (Math.random() + 1).toString(36).substring(2,8);
1348
+ }
1349
+
1350
+ function isSnapshotId(str) {
1351
+ return /^session_/.test(str);
1352
+ }
1353
+
1354
+ function SessionSnapshots(gui) {
1355
+ var _menuOpen = false;
1356
+ var _menuTimeout;
1357
+ var btn, menu;
1358
+
1359
+ init();
1360
+
1361
+ async function init() {
1362
+ var enabled = await isStorageEnabled();
1363
+ if (!enabled) return;
1364
+
1365
+ btn = gui.buttons.addButton('#ribbon-icon').addClass('menu-btn save-btn');
1366
+ menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
1367
+ await initialCleanup();
1368
+
1369
+ window.addEventListener('beforeunload', async function() {
1370
+ // delete snapshot data
1371
+ // This is not ideal, because the data gets deleted even if the user
1372
+ // cancels the page close... but there's no apparent good alternative
1373
+ await finalCleanup();
1374
+ });
1375
+
1376
+ btn.on('mouseenter', function() {
1377
+ btn.addClass('hover');
1378
+ clearTimeout(_menuTimeout); // prevent timed closing
1379
+ if (!_menuOpen) {
1380
+ openMenu();
1381
+ }
1382
+ });
1383
+
1384
+ btn.on('mouseleave', function() {
1385
+ if (!_menuOpen) {
1386
+ btn.removeClass('hover');
1387
+ } else {
1388
+ closeMenu(200);
1389
+ }
1390
+ });
1391
+ }
1392
+
1393
+ async function renderMenu() {
1394
+ var snapshots = await fetchSnapshotList();
1395
+
1396
+ menu.empty();
1397
+ addMenuLink({
1398
+ slug: 'stash',
1399
+ // label: 'save data snapshot',
1400
+ label: 'take a snapshot',
1401
+ action: saveSnapshot
1402
+ });
1403
+
1404
+ // var available = await getAvailableStorage();
1405
+ // if (available) {
1406
+ // El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu);
1407
+ // }
1408
+
1409
+ // if (snapshots.length > 0) {
1410
+ // El('div').addClass('save-menu-entry').text('snapshots').appendTo(menu);
1411
+ // }
1412
+
1413
+ snapshots.forEach(function(item, i) {
1414
+ var id = i + 1;
1415
+ var line = El('div').appendTo(menu).addClass('save-menu-item');
1416
+ El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
1417
+ // show snapshot size
1418
+ El('span').appendTo(line).html(` <span class="save-item-size">${item.display_size}</span>`);
1419
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1420
+ await restoreSnapshotById(item.id, gui);
1421
+ closeMenu(100);
1422
+ }).text('restore');
1423
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1424
+ var buf = await idb.get(item.id);
1425
+ saveBlobToLocalFile('mapshaper_snapshot.msx', new Blob([buf]));
1426
+ }).text('export');
1427
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1428
+ await removeSnapshotById(item.id);
1429
+ closeMenu(300);
1430
+ renderMenu();
1431
+ }).text('remove');
1432
+ });
1433
+
1434
+ // if (snapshots.length >= 1) {
1435
+ // addMenuLink({
1436
+ // slug: 'clear',
1437
+ // label: 'remove all',
1438
+ // action: clearData
1439
+ // });
1440
+ // }
1441
+ }
1442
+
1443
+ function addMenuLink(item) {
1444
+ var line = El('div').appendTo(menu);
1445
+ var link = El('div').addClass('save-menu-link save-menu-entry').attr('data-name', item.slug).text(item.label).appendTo(line);
1446
+ link.on('click', async function(e) {
1447
+ await item.action(gui);
1448
+ e.stopPropagation();
1449
+ });
1450
+ }
1451
+
1452
+ function openMenu() {
1453
+ clearTimeout(_menuTimeout);
1454
+ if (!_menuOpen) {
1455
+ btn.addClass('open');
1456
+ _menuOpen = true;
1457
+ renderMenu();
1458
+ }
1459
+ }
1460
+
1461
+ function closeMenu(delay) {
1462
+ if (!_menuOpen) return;
1463
+ clearTimeout(_menuTimeout);
1464
+ _menuTimeout = setTimeout(function() {
1465
+ _menuOpen = false;
1466
+ btn.removeClass('open');
1467
+ btn.removeClass('hover');
1468
+ }, delay || 0);
1469
+ }
1470
+
1471
+ async function saveSnapshot(gui) {
1472
+ var buf = await captureSnapshot(gui);
1473
+ var entryId = String(++snapshotCount).padStart(3, '0');
1474
+ var snapshotId = sessionId + '_' + entryId; // e.g. session_d89fw_001
1475
+ var entry = {
1476
+ created: Date.now(),
1477
+ session: sessionId,
1478
+ id: snapshotId,
1479
+ name: snapshotCount + '.',
1480
+ number: snapshotCount,
1481
+ size: buf.length,
1482
+ display_size: formatSize(buf.length)
1483
+ };
1484
+
1485
+ await idb.set(entry.id, buf);
1486
+ await addToIndex(entry);
1487
+ renderMenu();
1488
+ }
1489
+ }
1490
+
1491
+ function formatSize(bytes) {
1492
+ var kb = Math.round(bytes / 1000);
1493
+ var mb = (bytes / 1e6).toFixed(1);
1494
+ if (kb < 990) {
1495
+ return kb + 'kB';
1496
+ }
1497
+ return mb + 'MB';
1498
+ }
1499
+
1500
+ async function fetchSnapshotList() {
1501
+ await removeMissingSnapshots();
1502
+ var index = await fetchIndex();
1503
+ var snapshots = index.snapshots;
1504
+ snapshots = snapshots.filter(function(o) {return o.session == sessionId;});
1505
+ return snapshots.sort(function(a, b) {b.created > a.created;});
1506
+ }
1507
+
1508
+ async function removeSnapshotById(id, gui) {
1509
+ await idb.del(id);
1510
+ return updateIndex(function(index) {
1511
+ index.snapshots = index.snapshots.filter(function(snap) {
1512
+ return snap.id != id;
1513
+ });
1514
+ });
1515
+ }
1516
+
1517
+ async function restoreSnapshotById(id, gui) {
1518
+ var buf = await idb.get(id);
1519
+ if (!buf) {
1520
+ console.log('Snapshot not available:', id);
1521
+ return;
1522
+ }
1523
+ var data = await internal.unpackSession(buf);
1524
+ buf = null;
1525
+ gui.model.clear();
1526
+ importDatasets(data.datasets, gui);
1527
+ gui.clearMode();
1528
+ }
1529
+
1530
+ // Add datasets to the current project
1531
+ // TODO: figure out if interface data should be imported (e.g. should
1532
+ // visibility flag of imported layers be imported)
1533
+ async function importSessionData(buf, gui) {
1534
+ if (buf instanceof ArrayBuffer) {
1535
+ buf = new Uint8Array(buf);
1536
+ }
1537
+ var data = await internal.unpackSession(buf);
1538
+ importDatasets(data.datasets, gui);
1539
+ }
1540
+
1541
+ function importDatasets(datasets, gui) {
1542
+ gui.model.addDatasets(datasets);
1543
+ var target = findTargetLayer(datasets);
1544
+ gui.model.setDefaultTarget(target.layers, target.dataset);
1545
+ gui.model.updated({select: true, arc_count: true}); // arc_count to refresh display shapes
1546
+ }
1547
+
1548
+ async function captureSnapshot(gui) {
1549
+ var datasets = gui.model.getDatasets();
1550
+ var lyr = gui.model.getActiveLayer().layer;
1551
+ // compact: true applies compression to vector coordinates, for ~30% reduction
1552
+ // in file size in a typical polygon or polyline file, but longer processing time
1553
+ var opts = {compact: false};
1554
+ lyr.active = true;
1555
+ // console.time('msx');
1556
+ var obj = await internal.exportDatasetsToPack(datasets, opts);
1557
+ // console.timeEnd('msx')
1558
+ delete lyr.active;
1559
+ obj.gui = getGuiState(gui);
1560
+ return internal.pack(obj);
1561
+ }
1562
+
1563
+ // TODO: capture gui state information to allow restoring more of the UI
1564
+ function getGuiState(gui) {
1565
+ return null;
1566
+ }
1567
+
1568
+ async function fetchIndex() {
1569
+ var index = await idb.get('msx_index');
1570
+ return index || {snapshots: []};
1571
+ }
1572
+
1573
+ async function updateIndex(action) {
1574
+ return idb.update('msx_index', function(index) {
1575
+ if (!index || !Array.isArray(index.snapshots)) {
1576
+ index = {snapshots: []};
1577
+ }
1578
+ action(index);
1579
+ return index;
1580
+ });
1581
+ }
1582
+
1583
+ async function addToIndex(obj) {
1584
+ updateSessionData();
1585
+ return updateIndex(function(index) {
1586
+ index.snapshots.push(obj);
1587
+ });
1588
+ }
1589
+
1590
+ async function removeMissingSnapshots() {
1591
+ var keys = await idb.keys();
1592
+ return updateIndex(function(index) {
1593
+ index.snapshots = index.snapshots.filter(function(snap) {
1594
+ return keys.includes(snap.id);
1595
+ });
1596
+ });
1597
+ }
1598
+
1599
+ async function initialCleanup() {
1600
+ // (Safari workaround) remove any lingering data from past sessions
1601
+ if (getSessionData().length === 0) {
1602
+ await idb.clear();
1603
+ }
1604
+ // remove any snapshots that are not indexed
1605
+ var keys = await idb.keys();
1606
+ var indexedIds = (await fetchIndex()).snapshots.map(function(snap) {return snap.id;});
1607
+ keys.forEach(function(key) {
1608
+ if (isSnapshotId(key) && !indexedIds.includes(key)) {
1609
+ idb.del(key);
1610
+ }
1611
+ });
1612
+ // remove old indexed snapshots
1613
+ await updateIndex(function(index) {
1614
+ index.snapshots = index.snapshots.filter(function(snap) {
1615
+ var msPerDay = 1000 * 60 * 60 * 24;
1616
+ var daysOld = (Date.now() - snap.created) / msPerDay;
1617
+ if (daysOld > 1) {
1618
+ if (keys.includes(snap.id)) idb.del(snap.id);
1619
+ return false;
1620
+ }
1621
+ return true;
1622
+ });
1623
+ return index;
1624
+ });
1625
+ }
1626
+
1627
+ async function getAvailableStorage() {
1628
+ var bytes;
1629
+ try {
1630
+ var estimate = await navigator.storage.estimate();
1631
+ bytes = (estimate.quota - estimate.usage);
1632
+ } catch(e) {
1633
+ return null;
1634
+ }
1635
+ var str = (bytes / 1e6).toFixed(1) + 'MB';
1636
+ if (str.length > 7) {
1637
+ str = (bytes / 1e9).toFixed(1) + 'GB';
1638
+ }
1639
+ if (str.length > 7) {
1640
+ str = (bytes / 1e12).toFixed(1) + 'TB';
1641
+ }
1642
+ if (parseFloat(str) >= 10) {
1643
+ str = str.replace(/\../, '');
1644
+ }
1645
+ return str;
1646
+ }
1647
+
1648
+ function findTargetLayer(datasets) {
1649
+ var target;
1650
+ datasets.forEach(function(dataset) {
1651
+ var lyr = dataset.layers.find(function(lyr) { return !!lyr.active; });
1652
+ if (lyr) target = {dataset: dataset, layers: [lyr]};
1653
+ });
1654
+ if (!target) {
1655
+ target = {dataset: datasets[0], layers: [datasets[0].layers[0]]};
1656
+ }
1657
+ return target;
1658
+ }
1659
+
1660
+ // Clean up snapshot data (called just before browser tab is closed)
1661
+ async function finalCleanup() {
1662
+ // When called on 'beforeunload', idb.clear() seems to complete
1663
+ // before tab is unloaded in Chrome and Firefox, but not in Safari.
1664
+ // Calling idb.del(key) to selectively delete data for the current session
1665
+ // does not seem to complete in any browser.
1666
+ // So we wait until the last open session is ending at this URL, and delete
1667
+ // data for all recently open sessions.
1668
+ //
1669
+ var sessions = getSessionData().filter(function(item) {
1670
+ // remove current session
1671
+ var daysOld = (Date.now() - item.timestamp) / (1000 * 60 * 60 * 24);
1672
+ if (item.session == sessionId) return false;
1673
+ // also remove any lingering old sessions (ordinarily this shouldn't be needed)
1674
+ if (daysOld > 1) return false;
1675
+ return true;
1676
+ });
1677
+ setSessionData(sessions);
1678
+ if (sessions.length === 0) {
1679
+ await idb.clear();
1680
+ }
1681
+ }
1682
+
1683
+ function updateSessionData() {
1684
+ // make sure the current session is added to the list of open sessions
1685
+ var sessions = getSessionData();
1686
+ if (sessions.find(o => o.session == sessionId)) return;
1687
+ var entry = {
1688
+ session: sessionId,
1689
+ timestamp: Date.now()
1690
+ };
1691
+ setSessionData(sessions.concat([entry]));
1692
+ }
1693
+
1694
+ function getSessionData() {
1695
+ var data = JSON.parse(window.localStorage.getItem('session_data'));
1696
+ return data || [];
1697
+ }
1698
+
1699
+ function setSessionData(arr) {
1700
+ window.localStorage.setItem('session_data', JSON.stringify(arr));
1701
+ }
1702
+
1703
+ async function isStorageEnabled() {
1704
+ try {
1705
+ setSessionData(getSessionData());
1706
+ await updateIndex(function() {});
1707
+ return true;
1708
+ } catch(e) {
1709
+ return false;
1710
+ }
1711
+ }
1712
+
1109
1713
  // @cb function(<FileList>)
1110
1714
  function DropControl(gui, el, cb) {
1111
1715
  var area = El(el);
@@ -1391,18 +1995,24 @@
1391
1995
  async function importFiles(fileData) {
1392
1996
  var importOpts = readImportOpts();
1393
1997
  var groups = groupFilesForImport(fileData, importOpts);
1998
+ var optStr = GUI.formatCommandOptions(importOpts);
1394
1999
  fileData = null;
1395
2000
  for (var group of groups) {
1396
2001
  if (group.size > 4e7) {
1397
2002
  gui.showProgressMessage('Importing');
1398
2003
  await wait(35);
1399
2004
  }
1400
- importDataset(group, importOpts);
2005
+ if (group[internal.PACKAGE_EXT]) {
2006
+ await importSessionData(group[internal.PACKAGE_EXT].content, gui);
2007
+ } else {
2008
+ importDataset(group, importOpts);
2009
+ }
2010
+ importCount++;
2011
+ gui.session.fileImported(group.filename, optStr);
1401
2012
  }
1402
2013
  }
1403
2014
 
1404
2015
  function importDataset(group, importOpts) {
1405
- var optStr = GUI.formatCommandOptions(importOpts);
1406
2016
  var dataset = internal.importContent(group, importOpts);
1407
2017
  if (datasetIsEmpty(dataset)) return;
1408
2018
  if (group.layername) {
@@ -1411,8 +2021,6 @@
1411
2021
  // save import options for use by repair control, etc.
1412
2022
  dataset.info.import_options = importOpts;
1413
2023
  model.addDataset(dataset);
1414
- importCount++;
1415
- gui.session.fileImported(group.filename, optStr);
1416
2024
  }
1417
2025
 
1418
2026
  function addEmptyLayer() {
@@ -1643,348 +2251,119 @@
1643
2251
  content: d.content
1644
2252
  };
1645
2253
  // kludge: stash import name for session history
1646
- if (d.filename) g.filename = d.filename;
1647
- });
1648
- return groups;
1649
- }
1650
- }
1651
-
1652
- function draggable(ref) {
1653
- var xdown, ydown;
1654
- var el = El(ref),
1655
- dragging = false,
1656
- obj = new EventDispatcher();
1657
- undraggable(el.node());
1658
- el.on('mousedown', function(e) {
1659
- xdown = e.pageX;
1660
- ydown = e.pageY;
1661
- window.addEventListener('mousemove', onmove);
1662
- window.addEventListener('mouseup', onrelease);
1663
- });
1664
-
1665
- function onrelease(e) {
1666
- window.removeEventListener('mousemove', onmove);
1667
- window.removeEventListener('mouseup', onrelease);
1668
- if (dragging) {
1669
- dragging = false;
1670
- obj.dispatchEvent('dragend');
1671
- }
1672
- }
1673
-
1674
- function onmove(e) {
1675
- if (!dragging) {
1676
- dragging = true;
1677
- obj.dispatchEvent('dragstart');
1678
- }
1679
- obj.dispatchEvent('drag', {dx: e.pageX - xdown, dy: e.pageY - ydown});
1680
- }
1681
- return obj;
1682
- }
1683
-
1684
- function Slider(ref, opts) {
1685
- var _self = this;
1686
- var defaults = {
1687
- space: 7
1688
- };
1689
- opts = utils$1.extend(defaults, opts);
1690
-
1691
- var _pct = 0;
1692
- var _track,
1693
- _handle,
1694
- _handleLeft = opts.space;
1695
-
1696
- function size() {
1697
- return _track ? _track.width() - opts.space * 2 : 0;
1698
- }
1699
-
1700
- this.track = function(ref) {
1701
- if (ref && !_track) {
1702
- _track = El(ref);
1703
- _handleLeft = _track.el.offsetLeft + opts.space;
1704
- updateHandlePos();
1705
- }
1706
- return _track;
1707
- };
1708
-
1709
- this.handle = function(ref) {
1710
- var startX;
1711
- if (ref && !_handle) {
1712
- _handle = El(ref);
1713
- draggable(_handle)
1714
- .on('drag', function(e) {
1715
- setHandlePos(startX + e.dx);
1716
- })
1717
- .on('dragstart', function(e) {
1718
- startX = position();
1719
- _self.dispatchEvent('start');
1720
- })
1721
- .on('dragend', function(e) {
1722
- _self.dispatchEvent('end');
1723
- });
1724
- updateHandlePos();
1725
- }
1726
- return _handle;
1727
- };
1728
-
1729
- function position() {
1730
- return Math.round(_pct * size());
1731
- }
1732
-
1733
- this.pct = function(pct) {
1734
- if (pct >= 0 && pct <= 1) {
1735
- _pct = pct;
1736
- updateHandlePos();
1737
- }
1738
- return _pct;
1739
- };
1740
-
1741
- function setHandlePos(x) {
1742
- x = utils$1.clamp(x, 0, size());
1743
- var pct = x / size();
1744
- if (pct != _pct) {
1745
- _pct = pct;
1746
- _handle.css('left', _handleLeft + x);
1747
- _self.dispatchEvent('change', {pct: _pct});
1748
- }
1749
- }
1750
-
1751
- function updateHandlePos() {
1752
- var x = _handleLeft + Math.round(position());
1753
- if (_handle) _handle.css('left', x);
1754
- }
1755
- }
1756
-
1757
- utils$1.inherit(Slider, EventDispatcher);
1758
-
1759
- function showPopupAlert(msg, title) {
1760
- var self = {}, html = '';
1761
- var _cancel, _close;
1762
- var warningRxp = /^Warning: /;
1763
- var el = El('div').appendTo('body').addClass('error-wrapper');
1764
- var infoBox = El('div').appendTo(el).addClass('error-box info-box selectable');
1765
- if (!title && warningRxp.test(msg)) {
1766
- title = 'Warning';
1767
- msg = msg.replace(warningRxp, '');
1768
- }
1769
- if (title) {
1770
- html += `<div class="error-title">${title}</div>`;
1771
- }
1772
- html += `<p class="error-message">${msg}</p>`;
1773
- El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
1774
- if (_cancel) _cancel();
1775
- self.close();
1776
- });
1777
- El('div').appendTo(infoBox).addClass('error-content').html(html);
1778
-
1779
- self.onCancel = function(cb) {
1780
- _cancel = cb;
1781
- return self;
1782
- };
1783
-
1784
- self.onClose = function(cb) {
1785
- _close = cb;
1786
- return self;
1787
- };
1788
-
1789
- self.button = function(label, cb) {
1790
- El('div')
1791
- .addClass("btn dialog-btn alert-btn")
1792
- .appendTo(infoBox)
1793
- .html(label)
1794
- .on('click', function() {
1795
- self.close();
1796
- cb();
1797
- });
1798
- return self;
1799
- };
1800
-
1801
- self.close = function() {
1802
- if (el) el.remove();
1803
- if (_close) _close();
1804
- el = _cancel = _close = null;
1805
- };
1806
- return self;
1807
- }
1808
-
1809
- function AlertControl(gui) {
1810
- var openAlert; // error popup
1811
- var openPopup; // any popup
1812
-
1813
- gui.addMode('alert', function() {}, closePopup);
1814
-
1815
- gui.alert = function(str, title) {
1816
- closePopup();
1817
- openAlert = openPopup = showPopupAlert(str, title);
1818
- // alert.button('close', gui.clearMode);
1819
- openAlert.onClose(gui.clearMode);
1820
- gui.enterMode('alert');
1821
- };
1822
-
1823
- gui.message = function(str, title) {
1824
- if (openPopup) return; // don't stomp on another popup
1825
- openPopup = showPopupAlert(str, title);
1826
- openPopup.onClose(function() {openPopup = null;});
1827
- };
1828
-
1829
- function closePopup() {
1830
- if (openPopup) openPopup.close();
1831
- openPopup = openAlert = null;
2254
+ if (d.filename) g.filename = d.filename;
2255
+ });
2256
+ return groups;
1832
2257
  }
1833
2258
  }
1834
2259
 
1835
- function saveZipFile(zipfileName, files, done) {
1836
- internal.zipAsync(files, function(err, buf) {
1837
- if (err) {
1838
- done(errorMessage(err));
1839
- } else {
1840
- saveBlobToLocalFile(zipfileName, new Blob([buf]), done);
1841
- }
2260
+ function draggable(ref) {
2261
+ var xdown, ydown;
2262
+ var el = El(ref),
2263
+ dragging = false,
2264
+ obj = new EventDispatcher();
2265
+ undraggable(el.node());
2266
+ el.on('mousedown', function(e) {
2267
+ xdown = e.pageX;
2268
+ ydown = e.pageY;
2269
+ window.addEventListener('mousemove', onmove);
2270
+ window.addEventListener('mouseup', onrelease);
1842
2271
  });
1843
2272
 
1844
- function errorMessage(err) {
1845
- var str = "Error creating Zip file";
1846
- if (err.message) {
1847
- str += ": " + err.message;
2273
+ function onrelease(e) {
2274
+ window.removeEventListener('mousemove', onmove);
2275
+ window.removeEventListener('mouseup', onrelease);
2276
+ if (dragging) {
2277
+ dragging = false;
2278
+ obj.dispatchEvent('dragend');
1848
2279
  }
1849
- return str;
1850
2280
  }
1851
- }
1852
2281
 
1853
- function saveFilesToServer(paths, data, done) {
1854
- var i = -1;
1855
- next();
1856
- function next(err) {
1857
- i++;
1858
- if (err) return done(err);
1859
- if (i >= data.length) return done();
1860
- saveBlobToServer(paths[i], new Blob([data[i]]), next);
2282
+ function onmove(e) {
2283
+ if (!dragging) {
2284
+ dragging = true;
2285
+ obj.dispatchEvent('dragstart');
2286
+ }
2287
+ obj.dispatchEvent('drag', {dx: e.pageX - xdown, dy: e.pageY - ydown});
1861
2288
  }
2289
+ return obj;
1862
2290
  }
1863
2291
 
1864
- function saveBlobToServer(path, blob, done) {
1865
- var q = '?file=' + encodeURIComponent(path);
1866
- var url = window.location.origin + '/save' + q;
1867
- window.fetch(url, {
1868
- method: 'POST',
1869
- credentials: 'include',
1870
- body: blob
1871
- }).then(function(resp) {
1872
- if (resp.status == 400) {
1873
- return resp.text();
1874
- }
1875
- }).then(function(err) {
1876
- done(err);
1877
- }).catch(function(resp) {
1878
- done('connection to server was lost');
1879
- });
1880
- }
2292
+ function Slider(ref, opts) {
2293
+ var _self = this;
2294
+ var defaults = {
2295
+ space: 7
2296
+ };
2297
+ opts = utils$1.extend(defaults, opts);
1881
2298
 
1882
- async function saveBlobToLocalFile(filename, blob, done) {
1883
- if (window.showSaveFilePicker) {
1884
- saveBlobToSelectedFile(filename, blob, done);
1885
- } else {
1886
- saveBlobToDownloadsFolder(filename, blob, done);
1887
- }
1888
- }
2299
+ var _pct = 0;
2300
+ var _track,
2301
+ _handle,
2302
+ _handleLeft = opts.space;
1889
2303
 
1890
- function showSaveDialog(filename, blob, done) {
1891
- showPopupAlert(`Save ${filename} to:`)
1892
- .button('selected folder', function() {
1893
- saveBlobToSelectedFile(filename, blob, done);
1894
- })
1895
- .button('downloads', function() {
1896
- saveBlobToDownloadsFolder(filename, blob, done);
1897
- })
1898
- .onCancel(done);
1899
- }
2304
+ function size() {
2305
+ return _track ? _track.width() - opts.space * 2 : 0;
2306
+ }
1900
2307
 
1901
- async function saveBlobToSelectedFile(filename, blob, done) {
1902
- // see: https://developer.chrome.com/articles/file-system-access/
1903
- // note: saving multiple files to a directory using showDirectoryPicker()
1904
- // does not work well (in Chrome). User is prompted for permission each time,
1905
- // and some directories (like Downloads and Documents) are blocked.
1906
- //
1907
- var options = getSaveFileOptions(filename);
1908
- var handle;
1909
- try {
1910
- handle = await window.showSaveFilePicker(options);
1911
- var writable = await handle.createWritable();
1912
- await writable.write(blob);
1913
- await writable.close();
1914
- } catch(e) {
1915
- if (e.name == 'SecurityError') {
1916
- // assuming this is a timeout error, with message like:
1917
- // "Must be handling a user gesture to show a file picker."
1918
- showSaveDialog(filename, blob, done);
1919
- } else if (e.name == 'AbortError') {
1920
- // fired if user clicks a cancel button (normal, no error message)
1921
- // BUT: this kind of erro rmay also get fired when saving to a protected folder
1922
- // (should show error message)
1923
- done();
1924
- } else {
1925
- console.error(e.name, e.message, e);
1926
- done('Save failed for an unknown reason');
2308
+ this.track = function(ref) {
2309
+ if (ref && !_track) {
2310
+ _track = El(ref);
2311
+ _handleLeft = _track.el.offsetLeft + opts.space;
2312
+ updateHandlePos();
1927
2313
  }
1928
- return;
1929
- }
2314
+ return _track;
2315
+ };
1930
2316
 
1931
- done();
1932
- }
2317
+ this.handle = function(ref) {
2318
+ var startX;
2319
+ if (ref && !_handle) {
2320
+ _handle = El(ref);
2321
+ draggable(_handle)
2322
+ .on('drag', function(e) {
2323
+ setHandlePos(startX + e.dx);
2324
+ })
2325
+ .on('dragstart', function(e) {
2326
+ startX = position();
2327
+ _self.dispatchEvent('start');
2328
+ })
2329
+ .on('dragend', function(e) {
2330
+ _self.dispatchEvent('end');
2331
+ });
2332
+ updateHandlePos();
2333
+ }
2334
+ return _handle;
2335
+ };
1933
2336
 
1934
- function getSaveFileOptions(filename) {
1935
- // see: https://wicg.github.io/file-system-access/#api-filepickeroptions
1936
- var type = internal.guessInputFileType(filename);
1937
- var ext = internal.getFileExtension(filename).toLowerCase();
1938
- var accept = {};
1939
- if (ext == 'kml') {
1940
- accept['application/vnd.google-earth.kml+xml'] = ['.kml'];
1941
- } else if (ext == 'svg') {
1942
- accept['image/svg+xml'] = ['.svg'];
1943
- } else if (ext == 'zip') {
1944
- accept['application/zip'] == ['.zip'];
1945
- } else if (type == 'text') {
1946
- accept['text/csv'] = ['.csv', '.tsv', '.tab', '.txt'];
1947
- } else if (type == 'json') {
1948
- accept['application/json'] = ['.json', '.geojson', '.topojson'];
1949
- } else {
1950
- accept['application/octet-stream'] = ['.' + ext];
2337
+ function position() {
2338
+ return Math.round(_pct * size());
1951
2339
  }
1952
- return {
1953
- suggestedName: filename,
1954
- // If startIn is given, Chrome will always start there
1955
- // Default is to start in the previously selected dir (better)
1956
- // // startIn: 'downloads', // or: desktop, documents, [file handle], [directory handle]
1957
- types: [{
1958
- description: 'Files',
1959
- accept: accept
1960
- }]
2340
+
2341
+ this.pct = function(pct) {
2342
+ if (pct >= 0 && pct <= 1) {
2343
+ _pct = pct;
2344
+ updateHandlePos();
2345
+ }
2346
+ return _pct;
1961
2347
  };
1962
- }
1963
2348
 
2349
+ function setHandlePos(x) {
2350
+ x = utils$1.clamp(x, 0, size());
2351
+ var pct = x / size();
2352
+ if (pct != _pct) {
2353
+ _pct = pct;
2354
+ _handle.css('left', _handleLeft + x);
2355
+ _self.dispatchEvent('change', {pct: _pct});
2356
+ }
2357
+ }
1964
2358
 
1965
- function saveBlobToDownloadsFolder(filename, blob, done) {
1966
- var anchor, blobUrl;
1967
- try {
1968
- blobUrl = URL.createObjectURL(blob);
1969
- } catch(e) {
1970
- done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
1971
- return;
2359
+ function updateHandlePos() {
2360
+ var x = _handleLeft + Math.round(position());
2361
+ if (_handle) _handle.css('left', x);
1972
2362
  }
1973
- anchor = El('a').attr('href', '#').appendTo('body').node();
1974
- anchor.href = blobUrl;
1975
- anchor.download = filename;
1976
- var clickEvent = document.createEvent("MouseEvent");
1977
- clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
1978
- false, false, false, 0, null);
1979
- anchor.dispatchEvent(clickEvent);
1980
- setTimeout(function() {
1981
- // Revoke blob url to release memory; timeout needed in firefox
1982
- URL.revokeObjectURL(blobUrl);
1983
- anchor.parentNode.removeChild(anchor);
1984
- done();
1985
- }, 400);
1986
2363
  }
1987
2364
 
2365
+ utils$1.inherit(Slider, EventDispatcher);
2366
+
1988
2367
  // replace default error, stop and message functions
1989
2368
  function setLoggingForGUI(gui) {
1990
2369
  function stop() {
@@ -2488,7 +2867,7 @@
2488
2867
  return copy;
2489
2868
  }
2490
2869
 
2491
- var wgs84 = internal.getCRS('wgs84');
2870
+ var wgs84 = internal.parseCrsString('wgs84');
2492
2871
  var toWGS84 = internal.getProjTransform2(src, wgs84);
2493
2872
  var fromWGS84 = internal.getProjTransform2(wgs84, dest);
2494
2873
 
@@ -2597,7 +2976,7 @@
2597
2976
  function setDisplayProjection(gui, cmd) {
2598
2977
  var arg = cmd.replace(/^projd[ ]*/, '');
2599
2978
  if (arg) {
2600
- gui.map.setDisplayCRS(internal.getCRS(arg));
2979
+ gui.map.setDisplayCRS(internal.parseCrsString(arg));
2601
2980
  } else {
2602
2981
  gui.map.setDisplayCRS(null);
2603
2982
  }
@@ -3130,7 +3509,7 @@
3130
3509
  flags.arc_count || flags.snap || flags.affine || flags.points || flags['merge-layers'];
3131
3510
  if (intersectionsMayHaveChanged) {
3132
3511
  // delete any cached intersection data, to trigger re-calculation
3133
- e.dataset.info.intersections = null;
3512
+ e.dataset._intersections = null;
3134
3513
  updateAsync();
3135
3514
  } else if (flags.select) {
3136
3515
  // new active layer, but no editing commands were run -- use cached intersections (if available)
@@ -3179,11 +3558,11 @@
3179
3558
  XX = internal.findSegmentIntersections(arcs, opts);
3180
3559
  showBtn = XX.length > 0;
3181
3560
  } else { // no simplification
3182
- XX = dataset.info.intersections;
3561
+ XX = dataset._intersections;
3183
3562
  if (!XX) {
3184
3563
  // cache intersections at 0 simplification, to avoid recalculating
3185
3564
  // every time the simplification slider is set to 100% or the layer is selected at 100%
3186
- XX = dataset.info.intersections = internal.findSegmentIntersections(arcs, opts);
3565
+ XX = dataset._intersections = internal.findSegmentIntersections(arcs, opts);
3187
3566
  }
3188
3567
  showBtn = false;
3189
3568
  }
@@ -3200,7 +3579,7 @@
3200
3579
 
3201
3580
  function dismiss() {
3202
3581
  var dataset = model.getActiveLayer().dataset;
3203
- dataset.info.intersections = null;
3582
+ dataset._intersections = null;
3204
3583
  dataset.info.no_intersections = true;
3205
3584
  reset();
3206
3585
  }
@@ -3265,8 +3644,6 @@
3265
3644
  return layers.reverse();
3266
3645
  }
3267
3646
 
3268
- // import { groupLayersByDataset } from '../dataset/mapshaper-target-utils';
3269
-
3270
3647
  // Export buttons and their behavior
3271
3648
  var ExportControl = function(gui) {
3272
3649
  var model = gui.model;
@@ -3318,16 +3695,15 @@
3318
3695
  gui.clearMode();
3319
3696
  gui.showProgressMessage('Exporting');
3320
3697
  setTimeout(function() {
3321
- exportMenuSelection(layers, function(err) {
3322
- if (err) {
3323
- if (utils$1.isString(err)) {
3324
- gui.alert(err);
3325
- } else {
3326
- // stack seems to change if Error is logged directly
3327
- console.error(err.stack);
3328
- gui.alert('Export failed for an unknown reason');
3329
- }
3698
+ exportMenuSelection(layers).catch(function(err) {
3699
+ if (utils$1.isString(err)) {
3700
+ gui.alert(err);
3701
+ } else {
3702
+ // stack seems to change if Error is logged directly
3703
+ console.error(err.stack);
3704
+ gui.alert('Export failed for an unknown reason');
3330
3705
  }
3706
+ }).finally(function() {
3331
3707
  gui.clearProgressMessage();
3332
3708
  });
3333
3709
  }, 20);
@@ -3349,17 +3725,12 @@
3349
3725
  }
3350
3726
 
3351
3727
  // done: function(string|Error|null)
3352
- function exportMenuSelection(layers, done) {
3353
- var opts, files;
3354
- try {
3355
- opts = getExportOpts();
3728
+ async function exportMenuSelection(layers) {
3729
+ var opts = getExportOpts();
3356
3730
  // note: command line "target" option gets ignored
3357
- files = internal.exportTargetLayers(layers, opts);
3358
- gui.session.layersExported(getTargetLayerIds(), getExportOptsAsString());
3359
- } catch(e) {
3360
- return done(e);
3361
- }
3362
- internal.writeFiles(files, opts, done);
3731
+ var files = await internal.exportTargetLayers(layers, opts);
3732
+ gui.session.layersExported(getTargetLayerIds(), getExportOptsAsString());
3733
+ await utils$1.promisify(internal.writeFiles)(files, opts);
3363
3734
  }
3364
3735
 
3365
3736
  function initLayerItem(o, i) {
@@ -3479,7 +3850,7 @@
3479
3850
  }
3480
3851
 
3481
3852
  function initFormatMenu() {
3482
- var defaults = ['shapefile', 'geojson', 'topojson', 'json', 'dsv', 'kml', 'svg'];
3853
+ var defaults = ['shapefile', 'geojson', 'topojson', 'json', 'dsv', 'kml', 'svg', internal.PACKAGE_EXT];
3483
3854
  var formats = utils$1.uniq(defaults.concat(getInputFormats()));
3484
3855
  var items = formats.map(function(fmt) {
3485
3856
  return utils$1.format('<div><label><input type="radio" name="format" value="%s"' +
@@ -3607,7 +3978,7 @@
3607
3978
  clearClass('insert-below');
3608
3979
  dragTargetId = layerOrderSlug = null;
3609
3980
  if (dragging) {
3610
- render(); // in case menu changed...
3981
+ renderLayerList(); // in case menu changed...
3611
3982
  dragging = false;
3612
3983
  }
3613
3984
  }
@@ -3644,6 +4015,32 @@
3644
4015
  }
3645
4016
 
3646
4017
  function render() {
4018
+ renderLayerList();
4019
+ renderFileList();
4020
+ }
4021
+
4022
+ function renderFileList() {
4023
+ var list = el.findChild('.file-list');
4024
+ var files = [];
4025
+ list.empty();
4026
+ model.forEachLayer(function(lyr, dataset) {
4027
+ var file = internal.getLayerSourceFile(lyr, dataset);
4028
+ if (!file || files.includes(file)) return;
4029
+ files.push(file);
4030
+ var warnings = getWarnings(lyr, dataset);
4031
+ var html = '<div class="layer-item">';
4032
+ html += rowHTML('name', file);
4033
+ if (warnings) {
4034
+ // html += rowHTML('problems', warnings, 'layer-problems');
4035
+ html += rowHTML('', warnings, 'layer-problems');
4036
+ }
4037
+ html += '</div>';
4038
+ list.appendChild(El('div').html(html).firstChild());
4039
+ });
4040
+
4041
+ }
4042
+
4043
+ function renderLayerList() {
3647
4044
  var list = el.findChild('.layer-list');
3648
4045
  var uniqIds = {};
3649
4046
  var pinnableCount = 0;
@@ -3690,7 +4087,6 @@
3690
4087
  cache.cleanup();
3691
4088
 
3692
4089
  function renderLayer(lyr, dataset, opts) {
3693
- var warnings = getWarnings(lyr, dataset);
3694
4090
  var classes = 'layer-item';
3695
4091
  var entry, html;
3696
4092
 
@@ -3700,13 +4096,7 @@
3700
4096
 
3701
4097
  html = '<!-- ' + lyr.menu_id + '--><div class="' + classes + '">';
3702
4098
  html += rowHTML('name', '<span class="layer-name colored-text dot-underline">' + formatLayerNameForDisplay(lyr.name) + '</span>', 'row1');
3703
- if (opts.show_source) {
3704
- html += rowHTML('source file', describeSrc(lyr, dataset) || 'n/a');
3705
- }
3706
4099
  html += rowHTML('contents', describeLyr(lyr));
3707
- if (warnings) {
3708
- html += rowHTML('problems', warnings, 'layer-problems');
3709
- }
3710
4100
  html += '<img class="close-btn" draggable="false" src="images/close.png">';
3711
4101
  if (opts.pinnable) {
3712
4102
  html += '<img class="pin-btn unpinned" draggable="false" src="images/eye.png">';
@@ -3838,6 +4228,8 @@
3838
4228
  var file = internal.getLayerSourceFile(lyr, dataset);
3839
4229
  var missing = [];
3840
4230
  var msg;
4231
+ // show missing file warning for first layer in dataset
4232
+ // (assuming it represents the content of the original file)
3841
4233
  if (utils$1.endsWith(file, '.shp') && lyr == dataset.layers[0]) {
3842
4234
  if (!lyr.data) {
3843
4235
  missing.push('.dbf');
@@ -4320,7 +4712,8 @@
4320
4712
 
4321
4713
  // Only render edit mode button/menu if this option is present
4322
4714
  if (gui.options.inspectorControl) {
4323
- btn = gui.buttons.addButton('#pointer-icon').addClass('menu-btn');
4715
+ // use z-index so the menu is over other buttons
4716
+ btn = gui.buttons.addButton('#pointer-icon').addClass('menu-btn pointer-btn'),
4324
4717
  menu = El('div').addClass('nav-sub-menu').appendTo(btn.node());
4325
4718
 
4326
4719
  btn.on('mouseleave', function() {
@@ -4956,6 +5349,27 @@
4956
5349
  targ.prototype.__super__ = f;
4957
5350
  }
4958
5351
 
5352
+ function promisify(asyncFn) {
5353
+ return function() {
5354
+ var args = toArray(arguments);
5355
+ return new Promise((resolve, reject) => {
5356
+ var cb = function(err, data) {
5357
+ if (err) reject(err);
5358
+ else resolve(data);
5359
+ };
5360
+ args.push(cb);
5361
+ asyncFn.apply(this, args);
5362
+ });
5363
+ };
5364
+ }
5365
+
5366
+ function runAsync(fn, arg) {
5367
+ return new Promise((resolve, reject) => {
5368
+ fn(arg, function(err, data) {
5369
+ return err ? reject(err) : resolve(data);
5370
+ });
5371
+ });
5372
+ }
4959
5373
 
4960
5374
  // Call @iter on each member of an array (similar to Array#reduce(iter))
4961
5375
  // iter: function(memo, item, callback)
@@ -5973,7 +6387,7 @@
5973
6387
  return hits;
5974
6388
  }
5975
6389
 
5976
- function vertexTest(x, y) {
6390
+ function vertexTest(x, y) {
5977
6391
  var maxDist = getZoomAdjustedHitBuffer(25, 2),
5978
6392
  cands = findHitCandidates(x, y, maxDist);
5979
6393
  sortByDistance(x, y, cands, displayLayer.arcs);
@@ -10309,7 +10723,7 @@
10309
10723
  }
10310
10724
 
10311
10725
  if (!internal.isWebMercator(crs)) {
10312
- gui.map.setDisplayCRS(internal.getCRS('webmercator'));
10726
+ gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
10313
10727
  }
10314
10728
  var bbox = getLonLatBounds();
10315
10729
  if (!checkBounds(bbox)) {
@@ -10427,7 +10841,7 @@
10427
10841
  this.setDisplayCRS = function(crs) {
10428
10842
  // TODO: update bounds of frame layer, if there is a frame layer
10429
10843
  var oldCRS = this.getDisplayCRS();
10430
- var newCRS = utils$1.isString(crs) ? internal.getCRS(crs) : crs;
10844
+ var newCRS = utils$1.isString(crs) ? internal.parseCrsString(crs) : crs;
10431
10845
  // TODO: handle case that old and new CRS are the same
10432
10846
  _dynamicCRS = newCRS;
10433
10847
  if (!_activeLyr) return; // stop here if no layers have been selected
@@ -10484,12 +10898,12 @@
10484
10898
  _activeLyr.style = getActiveStyle(_activeLyr.layer, gui.state.dark_basemap);
10485
10899
  _activeLyr.active = true;
10486
10900
 
10487
- if (e.flags.same_table) {
10901
+ if (e.flags.same_table && !e.flags.proj) {
10488
10902
  // data may have changed; if popup is open, it needs to be refreshed
10489
10903
  gui.dispatchEvent('popup-needs-refresh');
10490
10904
  } else if (_hit) {
10491
- _hit.setLayer(_activeLyr);
10492
10905
  _hit.clearSelection();
10906
+ _hit.setLayer(_activeLyr);
10493
10907
  }
10494
10908
 
10495
10909
  updateVisibleMapLayers();
@@ -10818,6 +11232,7 @@
10818
11232
  homeControl: true,
10819
11233
  zoomControl: true,
10820
11234
  inspectorControl: true,
11235
+ saveControl: true,
10821
11236
  disableNavigation: false,
10822
11237
  showMouseCoordinates: true,
10823
11238
  focus: true
@@ -10834,6 +11249,10 @@
10834
11249
  gui.undo = new Undo(gui);
10835
11250
  gui.state = {};
10836
11251
 
11252
+ if (opts.saveControl) {
11253
+ new SessionSnapshots(gui);
11254
+ }
11255
+
10837
11256
  initModeRules(gui);
10838
11257
 
10839
11258
  gui.showProgressMessage = function(msg) {