mapshaper 0.6.18 → 0.6.19

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.
@@ -1106,6 +1106,590 @@
1106
1106
 
1107
1107
  // utils.inherit(SimpleButton, EventDispatcher);
1108
1108
 
1109
+ function showPopupAlert(msg, title) {
1110
+ var self = {}, html = '';
1111
+ var _cancel, _close;
1112
+ var warningRxp = /^Warning: /;
1113
+ var el = El('div').appendTo('body').addClass('error-wrapper');
1114
+ var infoBox = El('div').appendTo(el).addClass('error-box info-box selectable');
1115
+ if (!title && warningRxp.test(msg)) {
1116
+ title = 'Warning';
1117
+ msg = msg.replace(warningRxp, '');
1118
+ }
1119
+ if (title) {
1120
+ html += `<div class="error-title">${title}</div>`;
1121
+ }
1122
+ html += `<p class="error-message">${msg}</p>`;
1123
+ El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
1124
+ if (_cancel) _cancel();
1125
+ self.close();
1126
+ });
1127
+ El('div').appendTo(infoBox).addClass('error-content').html(html);
1128
+
1129
+ self.onCancel = function(cb) {
1130
+ _cancel = cb;
1131
+ return self;
1132
+ };
1133
+
1134
+ self.onClose = function(cb) {
1135
+ _close = cb;
1136
+ return self;
1137
+ };
1138
+
1139
+ self.button = function(label, cb) {
1140
+ El('div')
1141
+ .addClass("btn dialog-btn alert-btn")
1142
+ .appendTo(infoBox)
1143
+ .html(label)
1144
+ .on('click', function() {
1145
+ self.close();
1146
+ cb();
1147
+ });
1148
+ return self;
1149
+ };
1150
+
1151
+ self.close = function() {
1152
+ if (el) el.remove();
1153
+ if (_close) _close();
1154
+ el = _cancel = _close = null;
1155
+ };
1156
+ return self;
1157
+ }
1158
+
1159
+ function AlertControl(gui) {
1160
+ var openAlert; // error popup
1161
+ var openPopup; // any popup
1162
+
1163
+ gui.addMode('alert', function() {}, closePopup);
1164
+
1165
+ gui.alert = function(str, title) {
1166
+ closePopup();
1167
+ openAlert = openPopup = showPopupAlert(str, title);
1168
+ // alert.button('close', gui.clearMode);
1169
+ openAlert.onClose(gui.clearMode);
1170
+ gui.enterMode('alert');
1171
+ };
1172
+
1173
+ gui.message = function(str, title) {
1174
+ if (openPopup) return; // don't stomp on another popup
1175
+ openPopup = showPopupAlert(str, title);
1176
+ openPopup.onClose(function() {openPopup = null;});
1177
+ };
1178
+
1179
+ function closePopup() {
1180
+ if (openPopup) openPopup.close();
1181
+ openPopup = openAlert = null;
1182
+ }
1183
+ }
1184
+
1185
+ function saveZipFile(zipfileName, files, done) {
1186
+ internal.zipAsync(files, function(err, buf) {
1187
+ if (err) {
1188
+ done(errorMessage(err));
1189
+ } else {
1190
+ saveBlobToLocalFile(zipfileName, new Blob([buf]), done);
1191
+ }
1192
+ });
1193
+
1194
+ function errorMessage(err) {
1195
+ var str = "Error creating Zip file";
1196
+ if (err.message) {
1197
+ str += ": " + err.message;
1198
+ }
1199
+ return str;
1200
+ }
1201
+ }
1202
+
1203
+ function saveFilesToServer(paths, data, done) {
1204
+ var i = -1;
1205
+ next();
1206
+ function next(err) {
1207
+ i++;
1208
+ if (err) return done(err);
1209
+ if (i >= data.length) return done();
1210
+ saveBlobToServer(paths[i], new Blob([data[i]]), next);
1211
+ }
1212
+ }
1213
+
1214
+ function saveBlobToServer(path, blob, done) {
1215
+ var q = '?file=' + encodeURIComponent(path);
1216
+ var url = window.location.origin + '/save' + q;
1217
+ window.fetch(url, {
1218
+ method: 'POST',
1219
+ credentials: 'include',
1220
+ body: blob
1221
+ }).then(function(resp) {
1222
+ if (resp.status == 400) {
1223
+ return resp.text();
1224
+ }
1225
+ }).then(function(err) {
1226
+ done(err);
1227
+ }).catch(function(resp) {
1228
+ done('connection to server was lost');
1229
+ });
1230
+ }
1231
+
1232
+ async function saveBlobToLocalFile(filename, blob, done) {
1233
+ done = done || function() {};
1234
+ if (window.showSaveFilePicker) {
1235
+ saveBlobToSelectedFile(filename, blob, done);
1236
+ } else {
1237
+ saveBlobToDownloadsFolder(filename, blob, done);
1238
+ }
1239
+ }
1240
+
1241
+ function showSaveDialog(filename, blob, done) {
1242
+ showPopupAlert(`Save ${filename} to:`)
1243
+ .button('selected folder', function() {
1244
+ saveBlobToSelectedFile(filename, blob, done);
1245
+ })
1246
+ .button('downloads', function() {
1247
+ saveBlobToDownloadsFolder(filename, blob, done);
1248
+ })
1249
+ .onCancel(done);
1250
+ }
1251
+
1252
+ async function saveBlobToSelectedFile(filename, blob, done) {
1253
+ // see: https://developer.chrome.com/articles/file-system-access/
1254
+ // note: saving multiple files to a directory using showDirectoryPicker()
1255
+ // does not work well (in Chrome). User is prompted for permission each time,
1256
+ // and some directories (like Downloads and Documents) are blocked.
1257
+ //
1258
+ var options = getSaveFileOptions(filename);
1259
+ var handle;
1260
+ try {
1261
+ handle = await window.showSaveFilePicker(options);
1262
+ var writable = await handle.createWritable();
1263
+ await writable.write(blob);
1264
+ await writable.close();
1265
+ } catch(e) {
1266
+ if (e.name == 'SecurityError') {
1267
+ // assuming this is a timeout error, with message like:
1268
+ // "Must be handling a user gesture to show a file picker."
1269
+ showSaveDialog(filename, blob, done);
1270
+ } else if (e.name == 'AbortError') {
1271
+ // fired if user clicks a cancel button (normal, no error message)
1272
+ // BUT: this kind of erro rmay also get fired when saving to a protected folder
1273
+ // (should show error message)
1274
+ done();
1275
+ } else {
1276
+ console.error(e.name, e.message, e);
1277
+ done('Save failed for an unknown reason');
1278
+ }
1279
+ return;
1280
+ }
1281
+
1282
+ done();
1283
+ }
1284
+
1285
+ function getSaveFileOptions(filename) {
1286
+ // see: https://wicg.github.io/file-system-access/#api-filepickeroptions
1287
+ var type = internal.guessInputFileType(filename);
1288
+ var ext = internal.getFileExtension(filename).toLowerCase();
1289
+ var accept = {};
1290
+ if (ext == 'kml') {
1291
+ accept['application/vnd.google-earth.kml+xml'] = ['.kml'];
1292
+ } else if (ext == 'svg') {
1293
+ accept['image/svg+xml'] = ['.svg'];
1294
+ } else if (ext == 'zip') {
1295
+ accept['application/zip'] == ['.zip'];
1296
+ } else if (type == 'text') {
1297
+ accept['text/csv'] = ['.csv', '.tsv', '.tab', '.txt'];
1298
+ } else if (type == 'json') {
1299
+ accept['application/json'] = ['.json', '.geojson', '.topojson'];
1300
+ } else {
1301
+ accept['application/octet-stream'] = ['.' + ext];
1302
+ }
1303
+ return {
1304
+ suggestedName: filename,
1305
+ // If startIn is given, Chrome will always start there
1306
+ // Default is to start in the previously selected dir (better)
1307
+ // // startIn: 'downloads', // or: desktop, documents, [file handle], [directory handle]
1308
+ types: [{
1309
+ description: 'Files',
1310
+ accept: accept
1311
+ }]
1312
+ };
1313
+ }
1314
+
1315
+
1316
+ function saveBlobToDownloadsFolder(filename, blob, done) {
1317
+ var anchor, blobUrl;
1318
+ try {
1319
+ blobUrl = URL.createObjectURL(blob);
1320
+ } catch(e) {
1321
+ done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
1322
+ return;
1323
+ }
1324
+ anchor = El('a').attr('href', '#').appendTo('body').node();
1325
+ anchor.href = blobUrl;
1326
+ anchor.download = filename;
1327
+ var clickEvent = document.createEvent("MouseEvent");
1328
+ clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
1329
+ false, false, false, 0, null);
1330
+ anchor.dispatchEvent(clickEvent);
1331
+ setTimeout(function() {
1332
+ // Revoke blob url to release memory; timeout needed in firefox
1333
+ URL.revokeObjectURL(blobUrl);
1334
+ anchor.parentNode.removeChild(anchor);
1335
+ done();
1336
+ }, 400);
1337
+ }
1338
+
1339
+ var idb = require('idb-keyval');
1340
+ // https://github.com/jakearchibald/idb
1341
+ // https://github.com/jakearchibald/idb-keyval
1342
+ var sessionId = getUniqId('session');
1343
+ var snapshotCount = 0;
1344
+
1345
+ window.addEventListener('beforeunload', async function() {
1346
+ // delete snapshot data
1347
+ // This is not ideal, because the data gets deleted even if the user
1348
+ // cancels the page close... but there's no apparent good alternative
1349
+ await finalCleanup();
1350
+ });
1351
+
1352
+ function getUniqId(prefix) {
1353
+ return prefix + '_' + (Math.random() + 1).toString(36).substring(2,8);
1354
+ }
1355
+
1356
+ function isSnapshotId(str) {
1357
+ return /^session_/.test(str);
1358
+ }
1359
+
1360
+ function SessionSnapshots(gui) {
1361
+ if (!gui.options.saveControl) return;
1362
+ var _menuOpen = false;
1363
+ var _menuTimeout;
1364
+ var btn = gui.buttons.addButton('#ribbon-icon').addClass('menu-btn save-btn');
1365
+ var menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
1366
+
1367
+ initialCleanup();
1368
+
1369
+ btn.on('mouseenter', function() {
1370
+ btn.addClass('hover');
1371
+ clearTimeout(_menuTimeout); // prevent timed closing
1372
+ if (!_menuOpen) {
1373
+ openMenu();
1374
+ }
1375
+ });
1376
+
1377
+ btn.on('mouseleave', function() {
1378
+ if (!_menuOpen) {
1379
+ btn.removeClass('hover');
1380
+ } else {
1381
+ closeMenu(200);
1382
+ }
1383
+ });
1384
+
1385
+ async function renderMenu() {
1386
+ var snapshots = await fetchSnapshotList();
1387
+
1388
+ menu.empty();
1389
+ addMenuLink({
1390
+ slug: 'stash',
1391
+ // label: 'save data snapshot',
1392
+ label: 'take a snapshot',
1393
+ action: saveSnapshot
1394
+ });
1395
+
1396
+ // var available = await getAvailableStorage();
1397
+ // if (available) {
1398
+ // El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu);
1399
+ // }
1400
+
1401
+ // if (snapshots.length > 0) {
1402
+ // El('div').addClass('save-menu-entry').text('snapshots').appendTo(menu);
1403
+ // }
1404
+
1405
+ snapshots.forEach(function(item, i) {
1406
+ var id = i + 1;
1407
+ var line = El('div').appendTo(menu).addClass('save-menu-item');
1408
+ El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
1409
+ // show snapshot size
1410
+ // El('span').appendTo(line).html(` <span class="save-item-size">${item.display_size}</span>`);
1411
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1412
+ await restoreSnapshotById(item.id, gui);
1413
+ closeMenu(100);
1414
+ }).text('restore');
1415
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1416
+ var buf = await idb.get(item.id);
1417
+ saveBlobToLocalFile('mapshaper_snapshot.msx', new Blob([buf]));
1418
+ }).text('export');
1419
+ El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
1420
+ await removeSnapshotById(item.id);
1421
+ closeMenu(300);
1422
+ renderMenu();
1423
+ }).text('remove');
1424
+ });
1425
+
1426
+ // if (snapshots.length >= 1) {
1427
+ // addMenuLink({
1428
+ // slug: 'clear',
1429
+ // label: 'remove all',
1430
+ // action: clearData
1431
+ // });
1432
+ // }
1433
+ }
1434
+
1435
+ function addMenuLink(item) {
1436
+ var line = El('div').appendTo(menu);
1437
+ var link = El('div').addClass('save-menu-link save-menu-entry').attr('data-name', item.slug).text(item.label).appendTo(line);
1438
+ link.on('click', async function(e) {
1439
+ await item.action(gui);
1440
+ e.stopPropagation();
1441
+ });
1442
+ }
1443
+
1444
+ function openMenu() {
1445
+ clearTimeout(_menuTimeout);
1446
+ if (!_menuOpen) {
1447
+ btn.addClass('open');
1448
+ _menuOpen = true;
1449
+ renderMenu();
1450
+ }
1451
+ }
1452
+
1453
+ function closeMenu(delay) {
1454
+ if (!_menuOpen) return;
1455
+ clearTimeout(_menuTimeout);
1456
+ _menuTimeout = setTimeout(function() {
1457
+ _menuOpen = false;
1458
+ btn.removeClass('open');
1459
+ btn.removeClass('hover');
1460
+ }, delay || 0);
1461
+ }
1462
+
1463
+ async function saveSnapshot(gui) {
1464
+ var buf = captureSnapshot(gui);
1465
+ var entryId = String(++snapshotCount).padStart(3, '0');
1466
+ var snapshotId = sessionId + '_' + entryId; // e.g. session_d89fw_001
1467
+ var entry = {
1468
+ created: Date.now(),
1469
+ session: sessionId,
1470
+ id: snapshotId,
1471
+ name: snapshotCount + '.',
1472
+ number: snapshotCount,
1473
+ size: buf.length,
1474
+ display_size: formatSize(buf.length)
1475
+ };
1476
+
1477
+ await idb.set(entry.id, buf);
1478
+ await addToIndex(entry);
1479
+ renderMenu();
1480
+ }
1481
+ }
1482
+
1483
+ function formatSize(bytes) {
1484
+ var kb = Math.round(bytes / 1000);
1485
+ var mb = (bytes / 1e6).toFixed(1);
1486
+ if (kb < 990) {
1487
+ return kb + 'kB';
1488
+ }
1489
+ return mb + 'MB';
1490
+ }
1491
+
1492
+ async function fetchSnapshotList() {
1493
+ await removeMissingSnapshots();
1494
+ var index = await fetchIndex();
1495
+ var snapshots = index.snapshots;
1496
+ snapshots = snapshots.filter(function(o) {return o.session == sessionId;});
1497
+ return snapshots.sort(function(a, b) {b.created > a.created;});
1498
+ }
1499
+
1500
+ async function removeSnapshotById(id, gui) {
1501
+ await idb.del(id);
1502
+ return updateIndex(function(index) {
1503
+ index.snapshots = index.snapshots.filter(function(snap) {
1504
+ return snap.id != id;
1505
+ });
1506
+ });
1507
+ }
1508
+
1509
+ async function restoreSnapshotById(id, gui) {
1510
+ var buf = await idb.get(id);
1511
+ if (!buf) {
1512
+ console.log('Snapshot not available:', id);
1513
+ return;
1514
+ }
1515
+ var data = internal.unpackSession(buf);
1516
+ buf = null;
1517
+ gui.model.clear();
1518
+ importDatasets(data.datasets, gui);
1519
+ gui.clearMode();
1520
+ }
1521
+
1522
+ // Add datasets to the current project
1523
+ // TODO: figure out if interface data should be imported (e.g. should
1524
+ // visibility flag of imported layers be imported)
1525
+ function importSessionData(buf, gui) {
1526
+ if (buf instanceof ArrayBuffer) {
1527
+ buf = new Uint8Array(buf);
1528
+ }
1529
+ var data = internal.unpackSession(buf);
1530
+ importDatasets(data.datasets, gui);
1531
+ }
1532
+
1533
+ function importDatasets(datasets, gui) {
1534
+ gui.model.addDatasets(datasets);
1535
+ var target = findTargetLayer(datasets);
1536
+ gui.model.setDefaultTarget(target.layers, target.dataset);
1537
+ gui.model.updated({select: true, arc_count: true}); // arc_count to refresh display shapes
1538
+ }
1539
+
1540
+ function captureSnapshot(gui) {
1541
+ var datasets = gui.model.getDatasets();
1542
+ var lyr = gui.model.getActiveLayer().layer;
1543
+ lyr.active = true;
1544
+ var obj = internal.exportDatasetsToPack(datasets);
1545
+ delete lyr.active;
1546
+ obj.gui = getGuiState(gui);
1547
+ return internal.pack(obj);
1548
+ }
1549
+
1550
+ // TODO: capture gui state information to allow restoring more of the UI
1551
+ function getGuiState(gui) {
1552
+ return null;
1553
+ }
1554
+
1555
+ async function fetchIndex() {
1556
+ var index = await idb.get('msx_index');
1557
+ return index || {snapshots: []};
1558
+ }
1559
+
1560
+ async function updateIndex(action) {
1561
+ return idb.update('msx_index', function(index) {
1562
+ if (!index || !Array.isArray(index.snapshots)) {
1563
+ index = {snapshots: []};
1564
+ }
1565
+ action(index);
1566
+ return index;
1567
+ });
1568
+ }
1569
+
1570
+ async function addToIndex(obj) {
1571
+ updateSessionData();
1572
+ return updateIndex(function(index) {
1573
+ index.snapshots.push(obj);
1574
+ });
1575
+ }
1576
+
1577
+ async function removeMissingSnapshots() {
1578
+ var keys = await idb.keys();
1579
+ return updateIndex(function(index) {
1580
+ index.snapshots = index.snapshots.filter(function(snap) {
1581
+ return keys.includes(snap.id);
1582
+ });
1583
+ });
1584
+ }
1585
+
1586
+ async function initialCleanup() {
1587
+ // (Safari workaround) remove any lingering data from past sessions
1588
+ if (getSessionData().length === 0) {
1589
+ await idb.clear();
1590
+ }
1591
+ // remove any snapshots that are not indexed
1592
+ var keys = await idb.keys();
1593
+ var indexedIds = (await fetchIndex()).snapshots.map(function(snap) {return snap.id;});
1594
+ keys.forEach(function(key) {
1595
+ if (isSnapshotId(key) && !indexedIds.includes(key)) {
1596
+ idb.del(key);
1597
+ }
1598
+ });
1599
+ // remove old indexed snapshots
1600
+ return updateIndex(function(index) {
1601
+ index.snapshots = index.snapshots.filter(function(snap) {
1602
+ var msPerDay = 1000 * 60 * 60 * 24;
1603
+ var daysOld = (Date.now() - snap.created) / msPerDay;
1604
+ if (daysOld > 1) {
1605
+ if (keys.includes(snap.id)) idb.del(snap.id);
1606
+ return false;
1607
+ }
1608
+ return true;
1609
+ });
1610
+ return index;
1611
+ });
1612
+ }
1613
+
1614
+ async function getAvailableStorage() {
1615
+ var bytes;
1616
+ try {
1617
+ var estimate = await navigator.storage.estimate();
1618
+ bytes = (estimate.quota - estimate.usage);
1619
+ } catch(e) {
1620
+ return null;
1621
+ }
1622
+ var str = (bytes / 1e6).toFixed(1) + 'MB';
1623
+ if (str.length > 7) {
1624
+ str = (bytes / 1e9).toFixed(1) + 'GB';
1625
+ }
1626
+ if (str.length > 7) {
1627
+ str = (bytes / 1e12).toFixed(1) + 'TB';
1628
+ }
1629
+ if (parseFloat(str) >= 10) {
1630
+ str = str.replace(/\../, '');
1631
+ }
1632
+ return str;
1633
+ }
1634
+
1635
+ function findTargetLayer(datasets) {
1636
+ var target;
1637
+ datasets.forEach(function(dataset) {
1638
+ var lyr = dataset.layers.find(function(lyr) { return !!lyr.active; });
1639
+ if (lyr) target = {dataset: dataset, layers: [lyr]};
1640
+ });
1641
+ if (!target) {
1642
+ target = {dataset: datasets[0], layers: [datasets[0].layers[0]]};
1643
+ }
1644
+ return target;
1645
+ }
1646
+
1647
+ // Clean up snapshot data (called just before browser tab is closed)
1648
+ async function finalCleanup() {
1649
+ // When called on 'beforeunload', idb.clear() seems to complete
1650
+ // before tab is unloaded in Chrome and Firefox, but not in Safari.
1651
+ // Calling idb.del(key) to selectively delete data for the current session
1652
+ // does not seem to complete in any browser.
1653
+ // So we wait until the last open session is ending at this URL, and delete
1654
+ // data for all recently open sessions.
1655
+ //
1656
+ var sessions = getSessionData().filter(function(item) {
1657
+ // remove current session
1658
+ var daysOld = (Date.now() - item.timestamp) / (1000 * 60 * 60 * 24);
1659
+ if (item.session == sessionId) return false;
1660
+ // also remove any lingering old sessions (ordinarily this shouldn't be needed)
1661
+ if (daysOld > 1) return false;
1662
+ return true;
1663
+ });
1664
+ setSessionData(sessions);
1665
+ if (sessions.length === 0) {
1666
+ await idb.clear();
1667
+ }
1668
+ }
1669
+
1670
+ function updateSessionData() {
1671
+ // make sure the current session is added to the list of open sessions
1672
+ var sessions = getSessionData();
1673
+ if (sessions.find(o => o.session == sessionId)) return;
1674
+ var entry = {
1675
+ session: sessionId,
1676
+ timestamp: Date.now()
1677
+ };
1678
+ setSessionData(sessions.concat([entry]));
1679
+ }
1680
+
1681
+ function getSessionData() {
1682
+ try {
1683
+ var data = JSON.parse(window.localStorage.getItem('session_data'));
1684
+ return data || [];
1685
+ } catch(e) {}
1686
+ return [];
1687
+ }
1688
+
1689
+ function setSessionData(arr) {
1690
+ window.localStorage.setItem('session_data', JSON.stringify(arr));
1691
+ }
1692
+
1109
1693
  // @cb function(<FileList>)
1110
1694
  function DropControl(gui, el, cb) {
1111
1695
  var area = El(el);
@@ -1391,18 +1975,24 @@
1391
1975
  async function importFiles(fileData) {
1392
1976
  var importOpts = readImportOpts();
1393
1977
  var groups = groupFilesForImport(fileData, importOpts);
1978
+ var optStr = GUI.formatCommandOptions(importOpts);
1394
1979
  fileData = null;
1395
1980
  for (var group of groups) {
1396
1981
  if (group.size > 4e7) {
1397
1982
  gui.showProgressMessage('Importing');
1398
1983
  await wait(35);
1399
1984
  }
1400
- importDataset(group, importOpts);
1985
+ if (group[internal.PACKAGE_EXT]) {
1986
+ importSessionData(group[internal.PACKAGE_EXT].content, gui);
1987
+ } else {
1988
+ importDataset(group, importOpts);
1989
+ }
1990
+ importCount++;
1991
+ gui.session.fileImported(group.filename, optStr);
1401
1992
  }
1402
1993
  }
1403
1994
 
1404
1995
  function importDataset(group, importOpts) {
1405
- var optStr = GUI.formatCommandOptions(importOpts);
1406
1996
  var dataset = internal.importContent(group, importOpts);
1407
1997
  if (datasetIsEmpty(dataset)) return;
1408
1998
  if (group.layername) {
@@ -1411,8 +2001,6 @@
1411
2001
  // save import options for use by repair control, etc.
1412
2002
  dataset.info.import_options = importOpts;
1413
2003
  model.addDataset(dataset);
1414
- importCount++;
1415
- gui.session.fileImported(group.filename, optStr);
1416
2004
  }
1417
2005
 
1418
2006
  function addEmptyLayer() {
@@ -1756,235 +2344,6 @@
1756
2344
 
1757
2345
  utils$1.inherit(Slider, EventDispatcher);
1758
2346
 
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;
1832
- }
1833
- }
1834
-
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
- }
1842
- });
1843
-
1844
- function errorMessage(err) {
1845
- var str = "Error creating Zip file";
1846
- if (err.message) {
1847
- str += ": " + err.message;
1848
- }
1849
- return str;
1850
- }
1851
- }
1852
-
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);
1861
- }
1862
- }
1863
-
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
- }
1881
-
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
- }
1889
-
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
- }
1900
-
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');
1927
- }
1928
- return;
1929
- }
1930
-
1931
- done();
1932
- }
1933
-
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];
1951
- }
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
- }]
1961
- };
1962
- }
1963
-
1964
-
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;
1972
- }
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
- }
1987
-
1988
2347
  // replace default error, stop and message functions
1989
2348
  function setLoggingForGUI(gui) {
1990
2349
  function stop() {
@@ -2488,7 +2847,7 @@
2488
2847
  return copy;
2489
2848
  }
2490
2849
 
2491
- var wgs84 = internal.getCRS('wgs84');
2850
+ var wgs84 = internal.parseCrsString('wgs84');
2492
2851
  var toWGS84 = internal.getProjTransform2(src, wgs84);
2493
2852
  var fromWGS84 = internal.getProjTransform2(wgs84, dest);
2494
2853
 
@@ -2597,7 +2956,7 @@
2597
2956
  function setDisplayProjection(gui, cmd) {
2598
2957
  var arg = cmd.replace(/^projd[ ]*/, '');
2599
2958
  if (arg) {
2600
- gui.map.setDisplayCRS(internal.getCRS(arg));
2959
+ gui.map.setDisplayCRS(internal.parseCrsString(arg));
2601
2960
  } else {
2602
2961
  gui.map.setDisplayCRS(null);
2603
2962
  }
@@ -4320,7 +4679,8 @@
4320
4679
 
4321
4680
  // Only render edit mode button/menu if this option is present
4322
4681
  if (gui.options.inspectorControl) {
4323
- btn = gui.buttons.addButton('#pointer-icon').addClass('menu-btn');
4682
+ // use z-index so the menu is over other buttons
4683
+ btn = gui.buttons.addButton('#pointer-icon').addClass('menu-btn pointer-btn'),
4324
4684
  menu = El('div').addClass('nav-sub-menu').appendTo(btn.node());
4325
4685
 
4326
4686
  btn.on('mouseleave', function() {
@@ -5973,7 +6333,7 @@
5973
6333
  return hits;
5974
6334
  }
5975
6335
 
5976
- function vertexTest(x, y) {
6336
+ function vertexTest(x, y) {
5977
6337
  var maxDist = getZoomAdjustedHitBuffer(25, 2),
5978
6338
  cands = findHitCandidates(x, y, maxDist);
5979
6339
  sortByDistance(x, y, cands, displayLayer.arcs);
@@ -10309,7 +10669,7 @@
10309
10669
  }
10310
10670
 
10311
10671
  if (!internal.isWebMercator(crs)) {
10312
- gui.map.setDisplayCRS(internal.getCRS('webmercator'));
10672
+ gui.map.setDisplayCRS(internal.parseCrsString('webmercator'));
10313
10673
  }
10314
10674
  var bbox = getLonLatBounds();
10315
10675
  if (!checkBounds(bbox)) {
@@ -10427,7 +10787,7 @@
10427
10787
  this.setDisplayCRS = function(crs) {
10428
10788
  // TODO: update bounds of frame layer, if there is a frame layer
10429
10789
  var oldCRS = this.getDisplayCRS();
10430
- var newCRS = utils$1.isString(crs) ? internal.getCRS(crs) : crs;
10790
+ var newCRS = utils$1.isString(crs) ? internal.parseCrsString(crs) : crs;
10431
10791
  // TODO: handle case that old and new CRS are the same
10432
10792
  _dynamicCRS = newCRS;
10433
10793
  if (!_activeLyr) return; // stop here if no layers have been selected
@@ -10484,12 +10844,12 @@
10484
10844
  _activeLyr.style = getActiveStyle(_activeLyr.layer, gui.state.dark_basemap);
10485
10845
  _activeLyr.active = true;
10486
10846
 
10487
- if (e.flags.same_table) {
10847
+ if (e.flags.same_table && !e.flags.proj) {
10488
10848
  // data may have changed; if popup is open, it needs to be refreshed
10489
10849
  gui.dispatchEvent('popup-needs-refresh');
10490
10850
  } else if (_hit) {
10491
- _hit.setLayer(_activeLyr);
10492
10851
  _hit.clearSelection();
10852
+ _hit.setLayer(_activeLyr);
10493
10853
  }
10494
10854
 
10495
10855
  updateVisibleMapLayers();
@@ -10818,6 +11178,7 @@
10818
11178
  homeControl: true,
10819
11179
  zoomControl: true,
10820
11180
  inspectorControl: true,
11181
+ saveControl: true,
10821
11182
  disableNavigation: false,
10822
11183
  showMouseCoordinates: true,
10823
11184
  focus: true
@@ -10830,6 +11191,7 @@
10830
11191
  gui.buttons = new SidebarButtons(gui);
10831
11192
  gui.map = new MshpMap(gui);
10832
11193
  gui.interaction = new InteractionMode(gui);
11194
+ gui.sessionSave = new SessionSnapshots(gui);
10833
11195
  gui.session = new SessionHistory(gui);
10834
11196
  gui.undo = new Undo(gui);
10835
11197
  gui.state = {};