mapshaper 0.6.15 → 0.6.16

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.
@@ -215,197 +215,6 @@
215
215
 
216
216
  }
217
217
 
218
- var GUI = {};
219
-
220
- GUI.isActiveInstance = function(gui) {
221
- return gui == GUI.__active;
222
- };
223
-
224
- GUI.getPixelRatio = function() {
225
- var deviceRatio = window.devicePixelRatio || window.webkitDevicePixelRatio || 1;
226
- return deviceRatio > 1 ? 2 : 1;
227
- };
228
-
229
- GUI.browserIsSupported = function() {
230
- return typeof ArrayBuffer != 'undefined' &&
231
- typeof Blob != 'undefined' && typeof File != 'undefined';
232
- };
233
-
234
- GUI.exportIsSupported = function() {
235
- return typeof URL != 'undefined' && URL.createObjectURL &&
236
- typeof document.createElement("a").download != "undefined" ||
237
- !!window.navigator.msSaveBlob;
238
- };
239
-
240
- // TODO: make this relative to a single GUI instance
241
- GUI.canSaveToServer = function() {
242
- return !!(mapshaper.manifest && mapshaper.manifest.allow_saving) && typeof fetch == 'function';
243
- };
244
-
245
- GUI.getUrlVars = function() {
246
- var q = window.location.search.substring(1);
247
- return q.split('&').reduce(function(memo, chunk) {
248
- var pair = chunk.split('=');
249
- var key = decodeURIComponent(pair[0]);
250
- memo[key] = parseVal(pair[1]);
251
- return memo;
252
- }, {});
253
-
254
- function parseVal(val) {
255
- var str = val ? decodeURIComponent(val) : 'true';
256
- if (str == 'true' || str == 'false') return JSON.parse(str);
257
- return str;
258
- }
259
- };
260
-
261
- // Assumes that URL path ends with a filename
262
- GUI.getUrlFilename = function(url) {
263
- var path = /\/\/([^#?]+)/.exec(url);
264
- var file = path ? path[1].split('/').pop() : '';
265
- return file;
266
- };
267
-
268
- GUI.formatMessageArgs = function(args) {
269
- // .replace(/^\[[^\]]+\] ?/, ''); // remove cli annotation (if present)
270
- return internal.formatLogArgs(args);
271
- };
272
-
273
- GUI.handleDirectEvent = function(cb) {
274
- return function(e) {
275
- if (e.target == this) cb();
276
- };
277
- };
278
-
279
- GUI.getInputElement = function() {
280
- var el = document.activeElement;
281
- return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null;
282
- };
283
-
284
- GUI.textIsSelected = function() {
285
- return !!GUI.getInputElement();
286
- };
287
-
288
- GUI.selectElement = function(el) {
289
- var range = document.createRange(),
290
- sel = window.getSelection();
291
- range.selectNodeContents(el);
292
- sel.removeAllRanges();
293
- sel.addRange(range);
294
- };
295
-
296
- GUI.blurActiveElement = function() {
297
- var el = GUI.getInputElement();
298
- if (el) el.blur();
299
- };
300
-
301
- // Filter out delayed click events, e.g. so users can highlight and copy text
302
- GUI.onClick = function(el, cb) {
303
- var time;
304
- el.on('mousedown', function() {
305
- time = +new Date();
306
- });
307
- el.on('mouseup', function(e) {
308
- if (+new Date() - time < 300) cb(e);
309
- });
310
- };
311
-
312
- // tests if filename is a type that can be used
313
- GUI.isReadableFileType = function(filename) {
314
- return !!internal.guessInputFileType(filename) || internal.couldBeDsvFile(filename) ||
315
- internal.isZipFile(filename);
316
- };
317
-
318
- GUI.parseFreeformOptions = function(raw, cmd) {
319
- var str = raw.trim(),
320
- parsed;
321
- if (!str) {
322
- return {};
323
- }
324
- if (!/^-/.test(str)) {
325
- str = '-' + cmd + ' ' + str;
326
- }
327
- parsed = internal.parseCommands(str);
328
- if (!parsed.length || parsed[0].name != cmd) {
329
- stop$1("Unable to parse command line options");
330
- }
331
- return parsed[0].options;
332
- };
333
-
334
- // Convert an options object to a command line options string
335
- // (used by gui-import-control.js)
336
- // TODO: handle options with irregular string <-> object conversion
337
- GUI.formatCommandOptions = function(o) {
338
- var arr = [];
339
- Object.keys(o).forEach(function(key) {
340
- var name = key.replace(/_/g, '-');
341
- var val = o[key];
342
- var str;
343
- // TODO: quote values that contain spaces
344
- if (Array.isArray(val)) {
345
- str = name + '=' + val.join(',');
346
- } else if (val === true) {
347
- str = name;
348
- } else if (val === false) {
349
- return;
350
- } else {
351
- str = name + '=' + val;
352
- }
353
- arr.push(str);
354
- });
355
- return arr.join(' ');
356
- };
357
-
358
- // @file: Zip file
359
- // @cb: function(err, <files>)
360
- //
361
- GUI.readZipFile = function(file, cb) {
362
- var zip = window.zip; // Assume zip.js is loaded and zip is defined globally
363
- var _files = [];
364
- zip.createReader(new zip.BlobReader(file), importZipContent, onError);
365
-
366
- function onError(err) {
367
- cb(err);
368
- }
369
-
370
- function onDone() {
371
- cb(null, _files);
372
- }
373
-
374
- function importZipContent(reader) {
375
- var _entries;
376
- reader.getEntries(readEntries);
377
-
378
- function readEntries(entries) {
379
- _entries = entries || [];
380
- readNext();
381
- }
382
-
383
- function readNext() {
384
- if (_entries.length > 0) {
385
- readEntry(_entries.pop());
386
- } else {
387
- reader.close();
388
- onDone();
389
- }
390
- }
391
-
392
- function readEntry(entry) {
393
- var filename = entry.filename,
394
- isValid = !entry.directory && GUI.isReadableFileType(filename) &&
395
- !/^__MACOSX/.test(filename); // ignore "resource-fork" files
396
- if (isValid) {
397
- entry.getData(new zip.BlobWriter(), function(file) {
398
- file.name = filename; // Give the Blob a name, like a File object
399
- _files.push(file);
400
- readNext();
401
- });
402
- } else {
403
- readNext();
404
- }
405
- }
406
- }
407
- };
408
-
409
218
  function Handler(type, target, callback, listener, priority) {
410
219
  this.type = type;
411
220
  this.callback = callback;
@@ -969,6 +778,146 @@
969
778
  return this;
970
779
  };
971
780
 
781
+ var GUI = {};
782
+
783
+ GUI.isActiveInstance = function(gui) {
784
+ return gui == GUI.__active;
785
+ };
786
+
787
+ GUI.getPixelRatio = function() {
788
+ var deviceRatio = window.devicePixelRatio || window.webkitDevicePixelRatio || 1;
789
+ return deviceRatio > 1 ? 2 : 1;
790
+ };
791
+
792
+ GUI.browserIsSupported = function() {
793
+ return typeof ArrayBuffer != 'undefined' &&
794
+ typeof Blob != 'undefined' && typeof File != 'undefined';
795
+ };
796
+
797
+ GUI.exportIsSupported = function() {
798
+ return typeof URL != 'undefined' && URL.createObjectURL &&
799
+ typeof document.createElement("a").download != "undefined" ||
800
+ !!window.navigator.msSaveBlob;
801
+ };
802
+
803
+ // TODO: make this relative to a single GUI instance
804
+ GUI.canSaveToServer = function() {
805
+ return !!(mapshaper.manifest && mapshaper.manifest.allow_saving) && typeof fetch == 'function';
806
+ };
807
+
808
+ GUI.getUrlVars = function() {
809
+ var q = window.location.search.substring(1);
810
+ return q.split('&').reduce(function(memo, chunk) {
811
+ var pair = chunk.split('=');
812
+ var key = decodeURIComponent(pair[0]);
813
+ memo[key] = parseVal(pair[1]);
814
+ return memo;
815
+ }, {});
816
+
817
+ function parseVal(val) {
818
+ var str = val ? decodeURIComponent(val) : 'true';
819
+ if (str == 'true' || str == 'false') return JSON.parse(str);
820
+ return str;
821
+ }
822
+ };
823
+
824
+ // Assumes that URL path ends with a filename
825
+ GUI.getUrlFilename = function(url) {
826
+ var path = /\/\/([^#?]+)/.exec(url);
827
+ var file = path ? path[1].split('/').pop() : '';
828
+ return file;
829
+ };
830
+
831
+ GUI.formatMessageArgs = function(args) {
832
+ // .replace(/^\[[^\]]+\] ?/, ''); // remove cli annotation (if present)
833
+ return internal.formatLogArgs(args);
834
+ };
835
+
836
+ GUI.handleDirectEvent = function(cb) {
837
+ return function(e) {
838
+ if (e.target == this) cb();
839
+ };
840
+ };
841
+
842
+ GUI.getInputElement = function() {
843
+ var el = document.activeElement;
844
+ return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null;
845
+ };
846
+
847
+ GUI.textIsSelected = function() {
848
+ return !!GUI.getInputElement();
849
+ };
850
+
851
+ GUI.selectElement = function(el) {
852
+ var range = document.createRange(),
853
+ sel = window.getSelection();
854
+ range.selectNodeContents(el);
855
+ sel.removeAllRanges();
856
+ sel.addRange(range);
857
+ };
858
+
859
+ GUI.blurActiveElement = function() {
860
+ var el = GUI.getInputElement();
861
+ if (el) el.blur();
862
+ };
863
+
864
+ // Filter out delayed click events, e.g. so users can highlight and copy text
865
+ GUI.onClick = function(el, cb) {
866
+ var time;
867
+ el.on('mousedown', function() {
868
+ time = +new Date();
869
+ });
870
+ el.on('mouseup', function(e) {
871
+ if (+new Date() - time < 300) cb(e);
872
+ });
873
+ };
874
+
875
+ // tests if filename is a type that can be used
876
+ // GUI.isReadableFileType = function(filename) {
877
+ // return !!internal.guessInputFileType(filename) || internal.couldBeDsvFile(filename) ||
878
+ // internal.isZipFile(filename);
879
+ // };
880
+
881
+ GUI.parseFreeformOptions = function(raw, cmd) {
882
+ var str = raw.trim(),
883
+ parsed;
884
+ if (!str) {
885
+ return {};
886
+ }
887
+ if (!/^-/.test(str)) {
888
+ str = '-' + cmd + ' ' + str;
889
+ }
890
+ parsed = internal.parseCommands(str);
891
+ if (!parsed.length || parsed[0].name != cmd) {
892
+ stop$1("Unable to parse command line options");
893
+ }
894
+ return parsed[0].options;
895
+ };
896
+
897
+ // Convert an options object to a command line options string
898
+ // (used by gui-import-control.js)
899
+ // TODO: handle options with irregular string <-> object conversion
900
+ GUI.formatCommandOptions = function(o) {
901
+ var arr = [];
902
+ Object.keys(o).forEach(function(key) {
903
+ var name = key.replace(/_/g, '-');
904
+ var val = o[key];
905
+ var str;
906
+ // TODO: quote values that contain spaces
907
+ if (Array.isArray(val)) {
908
+ str = name + '=' + val.join(',');
909
+ } else if (val === true) {
910
+ str = name;
911
+ } else if (val === false) {
912
+ return;
913
+ } else {
914
+ str = name + '=' + val;
915
+ }
916
+ arr.push(str);
917
+ });
918
+ return arr.join(' ');
919
+ };
920
+
972
921
  // TODO: switch all ClickText to ClickText2
973
922
 
974
923
  // @ref Reference to an element containing a text node
@@ -1320,6 +1269,7 @@
1320
1269
  await importFiles(files);
1321
1270
  }
1322
1271
  } catch(e) {
1272
+ console.log(e);
1323
1273
  gui.alert(e.message, 'Import error');
1324
1274
  }
1325
1275
  if (gui.getMode() == 'import') {
@@ -1373,7 +1323,7 @@
1373
1323
  var index = {};
1374
1324
  queuedFiles = queuedFiles.concat(files).reduce(function(memo, f) {
1375
1325
  // filter out unreadable types and dupes
1376
- if (GUI.isReadableFileType(f.name) && f.name in index === false) {
1326
+ if (internal.looksLikeContentFile(f.name) && f.name in index === false) {
1377
1327
  index[f.name] = true;
1378
1328
  memo.push(f);
1379
1329
  }
@@ -1389,16 +1339,17 @@
1389
1339
  }
1390
1340
 
1391
1341
  async function receiveFiles(files) {
1342
+ var names = getFileNames(files);
1392
1343
  var expanded = [];
1393
1344
  try {
1394
1345
  expanded = await expandFiles(files);
1395
1346
  } catch(e) {
1347
+ console.log(e);
1396
1348
  gui.alert(e.message, 'Import error');
1397
1349
  return;
1398
1350
  }
1399
1351
  addFilesToQueue(expanded);
1400
1352
  if (queuedFiles.length === 0) {
1401
- var names = getFileNames(files);
1402
1353
  var msg = `Unable to import data from: ${names.join(', ')}`;
1403
1354
  gui.alert(msg, 'Import error');
1404
1355
  return;
@@ -1418,24 +1369,29 @@
1418
1369
  }
1419
1370
 
1420
1371
  async function expandFiles(files) {
1421
- var files2 = [], expanded;
1372
+ var expanded = [], tmp;
1373
+ await wait(35); // pause a beat so status message can display
1422
1374
  for (var f of files) {
1423
- if (internal.isZipFile(f.name)) {
1424
- expanded = await readZipFile(f);
1375
+ var data = await readFileData(f);
1376
+ if (internal.isGzipFile(f.name)) {
1377
+ tmp = await readGzipFile(data);
1378
+ } else if (internal.isZipFile(f.name)) {
1379
+ tmp = await readZipFile(data);
1425
1380
  } else if (internal.isKmzFile(f.name)) {
1426
- expanded = await readKmzFile(f);
1381
+ tmp = await readKmzFile(data);
1427
1382
  } else {
1428
- expanded = [f];
1383
+ tmp = [data];
1429
1384
  }
1430
- files2 = files2.concat(expanded);
1385
+ expanded = expanded.concat(tmp);
1431
1386
  }
1432
- return files2;
1387
+ files.length = 0; // clear source array for gc (works?)
1388
+ return expanded;
1433
1389
  }
1434
1390
 
1435
- async function importFiles(files) {
1436
- var fileData = await readFiles(files);
1391
+ async function importFiles(fileData) {
1437
1392
  var importOpts = readImportOpts();
1438
1393
  var groups = groupFilesForImport(fileData, importOpts);
1394
+ fileData = null;
1439
1395
  for (var group of groups) {
1440
1396
  if (group.size > 4e7) {
1441
1397
  gui.showProgressMessage('Importing');
@@ -1485,7 +1441,6 @@
1485
1441
  });
1486
1442
  }
1487
1443
 
1488
-
1489
1444
  function isShapefilePart(name) {
1490
1445
  return /\.(shp|shx|dbf|prj|cpg)$/i.test(name);
1491
1446
  }
@@ -1505,13 +1460,7 @@
1505
1460
 
1506
1461
  // @file a File object
1507
1462
  async function readContentFileAsync(file, cb) {
1508
- var name = file.name,
1509
- reader = new FileReader(),
1510
- useBinary = internal.isSupportedBinaryInputType(name) ||
1511
- internal.isZipFile(name) ||
1512
- internal.guessInputFileType(name) == 'json' ||
1513
- internal.guessInputFileType(name) == 'text';
1514
-
1463
+ var reader = new FileReader();
1515
1464
  reader.addEventListener('loadend', function(e) {
1516
1465
  if (!reader.result) {
1517
1466
  cb(new Error());
@@ -1519,10 +1468,9 @@
1519
1468
  cb(null, reader.result);
1520
1469
  }
1521
1470
  });
1522
- if (useBinary) {
1471
+ if (internal.isImportableAsBinary(file.name)) {
1523
1472
  reader.readAsArrayBuffer(file);
1524
1473
  } else {
1525
- // TODO: consider using "encoding" option, to support CSV files in other encodings than utf8
1526
1474
  reader.readAsText(file, 'UTF-8');
1527
1475
  }
1528
1476
  }
@@ -1541,7 +1489,8 @@
1541
1489
  item.url = '/data/' + name;
1542
1490
  item.url = item.url.replace('/../', '/~/'); // kludge to allow accessing one parent
1543
1491
  }
1544
- return GUI.isReadableFileType(item.basename) ? item : null;
1492
+ // return GUI.isReadableFileType(item.basename) ? item : null;
1493
+ return internal.looksLikeImportableFile(item.basename) ? item : null;
1545
1494
  });
1546
1495
  return items.filter(Boolean);
1547
1496
  }
@@ -1593,21 +1542,46 @@
1593
1542
  return files;
1594
1543
  }
1595
1544
 
1596
- async function readZipFile(file) {
1597
- var files = [];
1545
+ async function readGzipFile(file) {
1546
+ var name = file.name.replace(/\.gz$/, '');
1598
1547
  await wait(35); // pause a beat so status message can display
1599
- try {
1600
- files = await runAsync(GUI.readZipFile, file);
1601
- // don't try to import .txt files from zip files
1602
- // (these would be parsed as dsv and throw errows)
1603
- files = files.filter(function(f) {
1604
- return !/\.txt$/i.test(f.name);
1605
- });
1606
- } catch(e) {
1607
- console.error(e);
1608
- throw Error(`Unable to unzip ${file.name}`);
1609
- }
1610
- return files;
1548
+ return [{
1549
+ name: name,
1550
+ content: internal.gunzipSync(file.content, name)
1551
+ }];
1552
+ }
1553
+
1554
+ async function readZipFile(file) {
1555
+ var index = internal.unzipSync(file.content);
1556
+ return Object.keys(index).reduce(function(memo, filename) {
1557
+ if (!/\.txt$/i.test(filename)) {
1558
+ memo.push({
1559
+ name: filename,
1560
+ content: index[filename]
1561
+ });
1562
+ return memo;
1563
+ }
1564
+ }, []);
1565
+ }
1566
+
1567
+ function fileSize(data) {
1568
+ return data.content.byteLength || data.content.length; // ArrayBuffer or string
1569
+ }
1570
+
1571
+ function fileType(data) {
1572
+ return internal.guessInputType(data.name, data.content);
1573
+ }
1574
+
1575
+ function key(basename, type) {
1576
+ return basename + '.' + type;
1577
+ }
1578
+
1579
+ function fileBase(data) {
1580
+ return internal.getFileBase(data.name).toLowerCase();
1581
+ }
1582
+
1583
+ function fileKey(data) {
1584
+ return key(fileBase(data), fileType(data));
1611
1585
  }
1612
1586
 
1613
1587
  async function readFileData(file) {
@@ -1615,10 +1589,7 @@
1615
1589
  var content = await runAsync(readContentFileAsync, file);
1616
1590
  return {
1617
1591
  content: content,
1618
- size: content.byteLength || content.length, // ArrayBuffer or string
1619
- name: file.name,
1620
- basename: internal.getFileBase(file.name).toLowerCase(),
1621
- type: internal.guessInputType(file.name, content)
1592
+ name: file.name
1622
1593
  };
1623
1594
  } catch (e) {
1624
1595
  console.error(e);
@@ -1626,37 +1597,28 @@
1626
1597
  }
1627
1598
  }
1628
1599
 
1629
- async function readFiles(files) {
1630
- var data = [], d;
1631
- for (var file of files) {
1632
- d = await readFileData(file);
1633
- if (d) data.push(d);
1634
- }
1635
- return data;
1636
- }
1637
-
1638
1600
  function groupFilesForImport(data, importOpts) {
1639
1601
  var names = importOpts.name ? [importOpts.name] : null;
1640
1602
  if (initialImport && opts.name) { // name from mapshaper-gui --name option
1641
1603
  names = opts.name.split(',');
1642
1604
  }
1643
1605
 
1644
- function key(basename, type) {
1645
- return basename + '.' + type;
1646
- }
1647
1606
  function hasShp(basename) {
1648
1607
  var shpKey = key(basename, 'shp');
1649
- return data.some(d => key(d.basename, d.type) == shpKey);
1608
+ return data.some(d => fileKey(d) == shpKey);
1650
1609
  }
1610
+
1651
1611
  data.forEach(d => {
1652
- if (d.type == 'shp' || !isShapefilePart(d.name)) {
1653
- d.group = key(d.basename, d.type);
1612
+ var basename = fileBase(d);
1613
+ var type = fileType(d);
1614
+ if (type == 'shp' || !isShapefilePart(d.name)) {
1615
+ d.group = key(basename, type);
1654
1616
  d.filename = d.name;
1655
- } else if (hasShp(d.basename)) {
1656
- d.group = key(d.basename, 'shp');
1657
- } else if (d.type == 'dbf') {
1617
+ } else if (hasShp(basename)) {
1618
+ d.group = key(basename, 'shp');
1619
+ } else if (type == 'dbf') {
1658
1620
  d.filename = d.name;
1659
- d.group = key(d.basename, 'dbf');
1621
+ d.group = key(basename, 'dbf');
1660
1622
  } else {
1661
1623
  // shapefile part without a .shp file
1662
1624
  d.group = null;
@@ -1673,8 +1635,8 @@
1673
1635
  groups.push(g);
1674
1636
  index[d.group] = g;
1675
1637
  }
1676
- g.size = (g.size || 0) + d.size; // accumulate size
1677
- g[d.type] = {
1638
+ g.size = (g.size || 0) + fileSize(d); // accumulate size
1639
+ g[fileType(d)] = {
1678
1640
  filename: d.name,
1679
1641
  content: d.content
1680
1642
  };
@@ -1792,42 +1754,97 @@
1792
1754
 
1793
1755
  utils$1.inherit(Slider, EventDispatcher);
1794
1756
 
1795
- function saveZipFile(zipfileName, files, done) {
1796
- var zip = window.zip; // assumes zip library is loaded globally
1797
- var toAdd = files;
1798
- var zipWriter;
1799
- try {
1800
- zip.createWriter(new zip.BlobWriter("application/zip"), function(writer) {
1801
- zipWriter = writer;
1802
- nextFile();
1803
- }, zipError);
1804
- } catch(e) {
1805
- done("This browser doesn't support Zip file creation.");
1806
- }
1757
+ function showPopupAlert(msg, title) {
1758
+ var self = {}, html = '';
1759
+ var _cancel, _close;
1760
+ var warningRxp = /^Warning: /;
1761
+ var el = El('div').appendTo('body').addClass('error-wrapper');
1762
+ var infoBox = El('div').appendTo(el).addClass('error-box info-box selectable');
1763
+ if (!title && warningRxp.test(msg)) {
1764
+ title = 'Warning';
1765
+ msg = msg.replace(warningRxp, '');
1766
+ }
1767
+ if (title) {
1768
+ html += `<div class="error-title">${title}</div>`;
1769
+ }
1770
+ html += `<p class="error-message">${msg}</p>`;
1771
+ El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
1772
+ if (_cancel) _cancel();
1773
+ self.close();
1774
+ });
1775
+ El('div').appendTo(infoBox).addClass('error-content').html(html);
1807
1776
 
1808
- function zipError(err) {
1809
- var str = "Error creating Zip file";
1810
- var msg = '';
1811
- // error events thrown by Zip library seem to be missing a message
1812
- if (err && err.message) {
1813
- msg = err.message;
1814
- }
1815
- if (msg) {
1816
- str += ": " + msg;
1817
- }
1818
- done(str);
1819
- }
1777
+ self.onCancel = function(cb) {
1778
+ _cancel = cb;
1779
+ return self;
1780
+ };
1820
1781
 
1821
- function nextFile() {
1822
- if (toAdd.length === 0) {
1823
- zipWriter.close(function(blob) {
1824
- saveBlobToDownloadFolder(zipfileName, blob, done);
1782
+ self.onClose = function(cb) {
1783
+ _close = cb;
1784
+ return self;
1785
+ };
1786
+
1787
+ self.button = function(label, cb) {
1788
+ El('div')
1789
+ .addClass("btn dialog-btn alert-btn")
1790
+ .appendTo(infoBox)
1791
+ .html(label)
1792
+ .on('click', function() {
1793
+ self.close();
1794
+ cb();
1825
1795
  });
1796
+ return self;
1797
+ };
1798
+
1799
+ self.close = function() {
1800
+ if (el) el.remove();
1801
+ if (_close) _close();
1802
+ el = _cancel = _close = null;
1803
+ };
1804
+ return self;
1805
+ }
1806
+
1807
+ function AlertControl(gui) {
1808
+ var openAlert; // error popup
1809
+ var openPopup; // any popup
1810
+
1811
+ gui.addMode('alert', function() {}, closePopup);
1812
+
1813
+ gui.alert = function(str, title) {
1814
+ closePopup();
1815
+ openAlert = openPopup = showPopupAlert(str, title);
1816
+ // alert.button('close', gui.clearMode);
1817
+ openAlert.onClose(gui.clearMode);
1818
+ gui.enterMode('alert');
1819
+ };
1820
+
1821
+ gui.message = function(str, title) {
1822
+ if (openPopup) return; // don't stomp on another popup
1823
+ openPopup = showPopupAlert(str, title);
1824
+ openPopup.onClose(function() {openPopup = null;});
1825
+ };
1826
+
1827
+ function closePopup() {
1828
+ if (openPopup) openPopup.close();
1829
+ openPopup = openAlert = null;
1830
+ }
1831
+ }
1832
+
1833
+ function saveZipFile(zipfileName, files, done) {
1834
+ internal.zipAsync(files, function(err, buf) {
1835
+ if (err) {
1836
+ done(errorMessage(err));
1826
1837
  } else {
1827
- var obj = toAdd.pop(),
1828
- blob = new Blob([obj.content]);
1829
- zipWriter.add(obj.filename, new zip.BlobReader(blob), nextFile);
1838
+ saveBlobToLocalFile(zipfileName, new Blob([buf]), done);
1830
1839
  }
1840
+ });
1841
+
1842
+ function errorMessage(err) {
1843
+ var str = "Error creating Zip file";
1844
+ if (err.message) {
1845
+ str += ": " + err.message;
1846
+ }
1847
+ return str;
1831
1848
  }
1832
1849
  }
1833
1850
 
@@ -1860,12 +1877,91 @@
1860
1877
  });
1861
1878
  }
1862
1879
 
1863
- function saveBlobToDownloadFolder(filename, blob, done) {
1864
- var anchor, blobUrl;
1865
- if (window.navigator.msSaveBlob) {
1866
- window.navigator.msSaveBlob(blob, filename);
1867
- return done();
1880
+ async function saveBlobToLocalFile(filename, blob, done) {
1881
+ if (window.showSaveFilePicker) {
1882
+ saveBlobToSelectedFile(filename, blob, done);
1883
+ } else {
1884
+ saveBlobToDownloadsFolder(filename, blob, done);
1885
+ }
1886
+ }
1887
+
1888
+ function showSaveDialog(filename, blob, done) {
1889
+ showPopupAlert(`Save ${filename} to:`)
1890
+ .button('selected folder', function() {
1891
+ saveBlobToSelectedFile(filename, blob, done);
1892
+ })
1893
+ .button('downloads', function() {
1894
+ saveBlobToDownloadsFolder(filename, blob, done);
1895
+ })
1896
+ .onCancel(done);
1897
+ }
1898
+
1899
+ async function saveBlobToSelectedFile(filename, blob, done) {
1900
+ // see: https://developer.chrome.com/articles/file-system-access/
1901
+ // note: saving multiple files to a directory using showDirectoryPicker()
1902
+ // does not work well (in Chrome). User is prompted for permission each time,
1903
+ // and some directories (like Downloads and Documents) are blocked.
1904
+ //
1905
+ var options = getSaveFileOptions(filename);
1906
+ var handle;
1907
+ try {
1908
+ handle = await window.showSaveFilePicker(options);
1909
+ var writable = await handle.createWritable();
1910
+ await writable.write(blob);
1911
+ await writable.close();
1912
+ } catch(e) {
1913
+ if (e.name == 'SecurityError') {
1914
+ // assuming this is a timeout error, with message like:
1915
+ // "Must be handling a user gesture to show a file picker."
1916
+ showSaveDialog(filename, blob, done);
1917
+ } else if (e.name == 'AbortError') {
1918
+ // fired if user clicks a cancel button (normal, no error message)
1919
+ // BUT: this kind of erro rmay also get fired when saving to a protected folder
1920
+ // (should show error message)
1921
+ done();
1922
+ } else {
1923
+ console.error(e.name, e.message, e);
1924
+ done('Save failed for an unknown reason');
1925
+ }
1926
+ return;
1927
+ }
1928
+
1929
+ done();
1930
+ }
1931
+
1932
+ function getSaveFileOptions(filename) {
1933
+ // see: https://wicg.github.io/file-system-access/#api-filepickeroptions
1934
+ var type = internal.guessInputFileType(filename);
1935
+ var ext = internal.getFileExtension(filename).toLowerCase();
1936
+ var accept = {};
1937
+ if (ext == 'kml') {
1938
+ accept['application/vnd.google-earth.kml+xml'] = ['.kml'];
1939
+ } else if (ext == 'svg') {
1940
+ accept['image/svg+xml'] = ['.svg'];
1941
+ } else if (ext == 'zip') {
1942
+ accept['application/zip'] == ['.zip'];
1943
+ } else if (type == 'text') {
1944
+ accept['text/csv'] = ['.csv', '.tsv', '.tab', '.txt'];
1945
+ } else if (type == 'json') {
1946
+ accept['application/json'] = ['.json', '.geojson', '.topojson'];
1947
+ } else {
1948
+ accept['application/octet-stream'] = ['.' + ext];
1868
1949
  }
1950
+ return {
1951
+ suggestedName: filename,
1952
+ // If startIn is given, Chrome will always start there
1953
+ // Default is to start in the previously selected dir (better)
1954
+ // // startIn: 'downloads', // or: desktop, documents, [file handle], [directory handle]
1955
+ types: [{
1956
+ description: 'Files',
1957
+ accept: accept
1958
+ }]
1959
+ };
1960
+ }
1961
+
1962
+
1963
+ function saveBlobToDownloadsFolder(filename, blob, done) {
1964
+ var anchor, blobUrl;
1869
1965
  try {
1870
1966
  blobUrl = URL.createObjectURL(blob);
1871
1967
  } catch(e) {
@@ -1901,6 +1997,8 @@
1901
1997
  }
1902
1998
 
1903
1999
  function message() {
2000
+ var msg = GUI.formatMessageArgs(arguments);
2001
+ gui.message(msg);
1904
2002
  internal.logArgs(arguments);
1905
2003
  }
1906
2004
 
@@ -1932,7 +2030,7 @@
1932
2030
  }
1933
2031
  });
1934
2032
  } else if (files.length == 1) {
1935
- saveBlobToDownloadFolder(files[0].filename, new Blob([files[0].content]), done);
2033
+ saveBlobToLocalFile(files[0].filename, new Blob([files[0].content]), done);
1936
2034
  } else {
1937
2035
  filename = internal.getCommonFileBase(utils$1.pluck(files, 'filename')) || "output";
1938
2036
  saveZipFile(filename + ".zip", files, done);
@@ -3010,34 +3108,6 @@
3010
3108
 
3011
3109
  }
3012
3110
 
3013
- function AlertControl(gui) {
3014
- var el;
3015
- gui.addMode('alert', function() {}, turnOff);
3016
-
3017
- gui.alert = function(str, title) {
3018
- var infoBox, html = '';
3019
- if (!el) {
3020
- el = El('div').appendTo('body').addClass('error-wrapper');
3021
- infoBox = El('div').appendTo(el).addClass('error-box info-box selectable');
3022
- El('div').appendTo(infoBox).addClass('error-content');
3023
- El('div').addClass("btn dialog-btn").appendTo(infoBox).html('close').on('click', gui.clearMode);
3024
- }
3025
- if (title) {
3026
- html += `<div class="error-title">${title}</div>`;
3027
- }
3028
- html += `<p class="error-message">${str}</p>`;
3029
- el.findChild('.error-content').html(html);
3030
- gui.enterMode('alert');
3031
- };
3032
-
3033
- function turnOff() {
3034
- if (el) {
3035
- el.remove();
3036
- el = null;
3037
- }
3038
- }
3039
- }
3040
-
3041
3111
  function RepairControl(gui) {
3042
3112
  var map = gui.map,
3043
3113
  model = gui.model,
@@ -3221,6 +3291,7 @@
3221
3291
 
3222
3292
  function turnOn() {
3223
3293
  layersArr = initLayerMenu();
3294
+ // initZipOption();
3224
3295
  initFormatMenu();
3225
3296
  menu.show();
3226
3297
  }
@@ -3269,6 +3340,9 @@
3269
3340
  if (/format=/.test(freeform) === false) {
3270
3341
  freeform += ' format=' + getSelectedFormat();
3271
3342
  }
3343
+ if (getZipOption()) {
3344
+ freeform += ' zip';
3345
+ }
3272
3346
  return freeform.trim();
3273
3347
  }
3274
3348
 
@@ -3413,10 +3487,19 @@
3413
3487
  menu.findChild('.export-formats input[value="' + getDefaultExportFormat() + '"]').node().checked = true;
3414
3488
  }
3415
3489
 
3490
+ function initZipOption() {
3491
+ var html = `<label><input type="checkbox">Save to .zip file</label>`;
3492
+ menu.findChild('.export-zip-option').html(html);
3493
+ }
3494
+
3416
3495
  function getSelectedFormat() {
3417
3496
  return menu.findChild('.export-formats input:checked').node().value;
3418
3497
  }
3419
3498
 
3499
+ function getZipOption() {
3500
+ return !!menu.findChild('.export-zip-option input:checked');
3501
+ }
3502
+
3420
3503
  function getTargetLayerIds() {
3421
3504
  return layersArr.reduce(function(memo, o, i) {
3422
3505
  if (o.checkbox.checked) memo.push(o.checkbox.value);