mapshaper 0.5.85 → 0.5.89

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.
@@ -182,6 +182,10 @@
182
182
  return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null;
183
183
  };
184
184
 
185
+ GUI.textIsSelected = function() {
186
+ return !!GUI.getInputElement();
187
+ };
188
+
185
189
  GUI.selectElement = function(el) {
186
190
  var range = document.createRange(),
187
191
  sel = window.getSelection();
@@ -229,6 +233,30 @@
229
233
  return parsed[0].options;
230
234
  };
231
235
 
236
+ // Convert an options object to a command line options string
237
+ // (used by gui-import-control.js)
238
+ // TODO: handle options with irregular string <-> object conversion
239
+ GUI.formatCommandOptions = function(o) {
240
+ var arr = [];
241
+ Object.keys(o).forEach(function(key) {
242
+ var name = key.replace(/_/g, '-');
243
+ var val = o[key];
244
+ var str;
245
+ // TODO: quote values that contain spaces
246
+ if (Array.isArray(val)) {
247
+ str = name + '=' + val.join(',');
248
+ } else if (val === true) {
249
+ str = name;
250
+ } else if (val === false) {
251
+ return;
252
+ } else {
253
+ str = name + '=' + val;
254
+ }
255
+ arr.push(str);
256
+ });
257
+ return arr.join(' ');
258
+ };
259
+
232
260
  // @file: Zip file
233
261
  // @cb: function(err, <files>)
234
262
  //
@@ -1027,6 +1055,10 @@
1027
1055
  var types = Array.from(e.clipboardData.types || []).join(',');
1028
1056
  var items = Array.from(e.clipboardData.items || []);
1029
1057
  var files;
1058
+ if (GUI.textIsSelected()) {
1059
+ // user is probably pasting text into an editable text field
1060
+ return;
1061
+ }
1030
1062
  block(e);
1031
1063
  // Browser compatibility (tested on MacOS only):
1032
1064
  // Chrome and Safari: full support
@@ -1099,20 +1131,19 @@
1099
1131
 
1100
1132
  function ImportControl(gui, opts) {
1101
1133
  var model = gui.model;
1134
+ var initialImport = true;
1102
1135
  var importCount = 0;
1103
1136
  var importTotal = 0;
1104
1137
  var overQuickView = false;
1105
- var useQuickView = opts.quick_view; // may be set by mapshaper-gui
1106
1138
  var queuedFiles = [];
1107
1139
  var manifestFiles = opts.files || [];
1108
- var cachedFiles = {};
1109
1140
  var catalog;
1110
1141
 
1111
1142
  if (opts.catalog) {
1112
1143
  catalog = new CatalogControl(gui, opts.catalog, downloadFiles);
1113
1144
  }
1114
1145
 
1115
- new SimpleButton('#import-buttons .submit-btn').on('click', onSubmit);
1146
+ new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles);
1116
1147
  new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode);
1117
1148
  new DropControl(gui, 'body', receiveFiles);
1118
1149
  new FileChooser('#file-selection-btn', receiveFiles);
@@ -1120,7 +1151,7 @@
1120
1151
  new FileChooser('#add-file-btn', receiveFiles);
1121
1152
  initDropArea('#import-quick-drop', true);
1122
1153
  initDropArea('#import-drop');
1123
- gui.keyboard.onMenuSubmit(El('#import-options'), onSubmit);
1154
+ gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
1124
1155
 
1125
1156
  gui.addMode('import', turnOn, turnOff);
1126
1157
  gui.enterMode('import');
@@ -1132,6 +1163,10 @@
1132
1163
  }
1133
1164
  });
1134
1165
 
1166
+ function useQuickView() {
1167
+ return initialImport && (opts.quick_view || overQuickView);
1168
+ }
1169
+
1135
1170
  function initDropArea(el, isQuick) {
1136
1171
  var area = El(el)
1137
1172
  .on('dragleave', onout)
@@ -1149,15 +1184,19 @@
1149
1184
  }
1150
1185
  }
1151
1186
 
1152
- function findMatchingShp(filename) {
1153
- // use case-insensitive matching
1154
- var base = internal.getPathBase(filename).toLowerCase();
1155
- return model.getDatasets().filter(function(d) {
1156
- var fname = d.info.input_files && d.info.input_files[0] || "";
1157
- var ext = internal.getFileExtension(fname).toLowerCase();
1158
- var base2 = internal.getPathBase(fname).toLowerCase();
1159
- return base == base2 && ext == 'shp';
1160
- });
1187
+ async function importQueuedFiles() {
1188
+ gui.container.removeClass('queued-files');
1189
+ gui.container.removeClass('splash-screen');
1190
+ var files = queuedFiles;
1191
+ try {
1192
+ if (files.length > 0) {
1193
+ queuedFiles = [];
1194
+ await importFiles(files);
1195
+ }
1196
+ } catch(e) {
1197
+ console.error(e);
1198
+ }
1199
+ gui.clearMode();
1161
1200
  }
1162
1201
 
1163
1202
  function turnOn() {
@@ -1178,13 +1217,8 @@
1178
1217
  importCount = 0;
1179
1218
  }
1180
1219
  gui.clearProgressMessage();
1181
- useQuickView = false; // unset 'quick view' mode, if on
1182
- close();
1183
- }
1184
-
1185
- function close() {
1220
+ initialImport = false; // unset 'quick view' mode, if on
1186
1221
  clearQueuedFiles();
1187
- cachedFiles = {};
1188
1222
  }
1189
1223
 
1190
1224
  function onImportComplete() {
@@ -1218,34 +1252,6 @@
1218
1252
  }, []);
1219
1253
  }
1220
1254
 
1221
- // When a Shapefile component is at the head of the queue, move the entire
1222
- // Shapefile to the front of the queue, sorted in reverse alphabetical order,
1223
- // (a kludge), so .shp is read before .dbf and .prj
1224
- // (If a .dbf file is imported before a .shp, it becomes a separate dataset)
1225
- // TODO: import Shapefile parts without relying on this kludge
1226
- function sortQueue(queue) {
1227
- var nextFile = queue[0];
1228
- var basename, parts;
1229
- if (!isShapefilePart(nextFile.name)) {
1230
- return queue;
1231
- }
1232
- basename = internal.getFileBase(nextFile.name).toLowerCase();
1233
- parts = [];
1234
- queue = queue.filter(function(file) {
1235
- if (internal.getFileBase(file.name).toLowerCase() == basename) {
1236
- parts.push(file);
1237
- return false;
1238
- }
1239
- return true;
1240
- });
1241
- parts.sort(function(a, b) {
1242
- // Sorting on LC filename so Shapefiles with mixed-case
1243
- // extensions are sorted correctly
1244
- return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1;
1245
- });
1246
- return parts.concat(queue);
1247
- }
1248
-
1249
1255
  function showQueuedFiles() {
1250
1256
  var list = gui.container.findChild('.dropped-file-list').empty();
1251
1257
  queuedFiles.forEach(function(f) {
@@ -1253,16 +1259,14 @@
1253
1259
  });
1254
1260
  }
1255
1261
 
1256
- function receiveFiles(files) {
1257
- var prevSize = queuedFiles.length;
1258
- useQuickView = useQuickView || overQuickView;
1259
- files = handleZipFiles(utils.toArray(files));
1260
- addFilesToQueue(files);
1262
+ async function receiveFiles(files) {
1263
+ // TODO: show importing message here?
1264
+ var expanded = await expandFiles(files);
1265
+ addFilesToQueue(expanded);
1261
1266
  if (queuedFiles.length === 0) return;
1262
1267
  gui.enterMode('import');
1263
-
1264
- if (useQuickView) {
1265
- onSubmit();
1268
+ if (useQuickView()) {
1269
+ importQueuedFiles();
1266
1270
  } else {
1267
1271
  gui.container.addClass('queued-files');
1268
1272
  El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
@@ -1270,25 +1274,51 @@
1270
1274
  }
1271
1275
  }
1272
1276
 
1273
- function filesMayContainPaths(files) {
1274
- return utils.some(files, function(f) {
1275
- var type = internal.guessInputFileType(f.name);
1276
- return type == 'shp' || type == 'json' || internal.isZipFile(f.name);
1277
- });
1277
+ async function expandFiles(files) {
1278
+ var files2 = [], expanded;
1279
+ for (var f of files) {
1280
+ if (internal.isZipFile(f.name)) {
1281
+ expanded = await readZipFile(f);
1282
+ files2 = files2.concat(expanded);
1283
+ } else {
1284
+ files2.push(f);
1285
+ }
1286
+ }
1287
+ return files2;
1278
1288
  }
1279
1289
 
1280
- function onSubmit() {
1281
- gui.container.removeClass('queued-files');
1282
- gui.container.removeClass('splash-screen');
1283
- procNextQueuedFile();
1290
+ async function importFiles(files) {
1291
+ var fileData = await readFiles(files);
1292
+ var importOpts = readImportOpts();
1293
+ var groups = groupFilesForImport(fileData, importOpts);
1294
+ for (var group of groups) {
1295
+ if (group.size > 4e7) {
1296
+ gui.showProgressMessage('Importing');
1297
+ await wait(35);
1298
+ }
1299
+ importDataset(group, importOpts);
1300
+ }
1284
1301
  }
1285
1302
 
1286
- function addDataset(dataset) {
1287
- if (!datasetIsEmpty(dataset)) {
1288
- model.addDataset(dataset);
1289
- importCount++;
1303
+ function importDataset(group, importOpts) {
1304
+ var optStr = GUI.formatCommandOptions(importOpts);
1305
+ var dataset = internal.importContent(group, importOpts);
1306
+ if (datasetIsEmpty(dataset)) return;
1307
+ if (group.layername) {
1308
+ dataset.layers.forEach(lyr => lyr.name = group.layername);
1290
1309
  }
1291
- procNextQueuedFile();
1310
+ // save import options for use by repair control, etc.
1311
+ dataset.info.import_options = importOpts;
1312
+ model.addDataset(dataset);
1313
+ importCount++;
1314
+ gui.session.fileImported(group.filename, optStr);
1315
+ }
1316
+
1317
+ function filesMayContainPaths(files) {
1318
+ return utils.some(files, function(f) {
1319
+ var type = internal.guessInputFileType(f.name);
1320
+ return type == 'shp' || type == 'json' || internal.isZipFile(f.name);
1321
+ });
1292
1322
  }
1293
1323
 
1294
1324
  function datasetIsEmpty(dataset) {
@@ -1297,41 +1327,27 @@
1297
1327
  });
1298
1328
  }
1299
1329
 
1300
- function procNextQueuedFile() {
1301
- if (queuedFiles.length === 0) {
1302
- gui.clearMode();
1303
- } else {
1304
- queuedFiles = sortQueue(queuedFiles);
1305
- readFile(queuedFiles.shift());
1306
- }
1307
- }
1308
1330
 
1309
1331
  // TODO: support .cpg
1310
1332
  function isShapefilePart(name) {
1311
1333
  return /\.(shp|shx|dbf|prj)$/i.test(name);
1312
1334
  }
1313
1335
 
1314
-
1315
1336
  function readImportOpts() {
1316
- if (useQuickView) return {};
1317
- var freeform = El('#import-options .advanced-options').node().value,
1318
- opts = GUI.parseFreeformOptions(freeform, 'i');
1319
- opts.no_repair = !El("#repair-intersections-opt").node().checked;
1320
- opts.snap = !!El("#snap-points-opt").node().checked;
1321
- return opts;
1322
- }
1323
-
1324
- // for CLI output
1325
- function readImportOptsAsString() {
1326
- if (useQuickView) return '';
1327
- var freeform = El('#import-options .advanced-options').node().value;
1328
- var opts = readImportOpts();
1329
- if (opts.snap) freeform = 'snap ' + freeform;
1330
- return freeform.trim();
1337
+ var importOpts;
1338
+ if (useQuickView()) {
1339
+ importOpts = {}; // default opts using quickview
1340
+ } else {
1341
+ var freeform = El('#import-options .advanced-options').node().value;
1342
+ importOpts = GUI.parseFreeformOptions(freeform, 'i');
1343
+ importOpts.no_repair = !El("#repair-intersections-opt").node().checked;
1344
+ importOpts.snap = !!El("#snap-points-opt").node().checked;
1345
+ }
1346
+ return importOpts;
1331
1347
  }
1332
1348
 
1333
1349
  // @file a File object
1334
- function readFile(file) {
1350
+ async function readContentFileAsync(file, cb) {
1335
1351
  var name = file.name,
1336
1352
  reader = new FileReader(),
1337
1353
  useBinary = internal.isSupportedBinaryInputType(name) ||
@@ -1341,9 +1357,9 @@
1341
1357
 
1342
1358
  reader.addEventListener('loadend', function(e) {
1343
1359
  if (!reader.result) {
1344
- handleImportError("Web browser was unable to load the file.", name);
1360
+ cb(new Error());
1345
1361
  } else {
1346
- importFileContent(name, reader.result);
1362
+ cb(null, reader.result);
1347
1363
  }
1348
1364
  });
1349
1365
  if (useBinary) {
@@ -1354,91 +1370,6 @@
1354
1370
  }
1355
1371
  }
1356
1372
 
1357
- function importFileContent(fileName, content) {
1358
- var fileType = internal.guessInputType(fileName, content),
1359
- importOpts = readImportOpts(),
1360
- matches = findMatchingShp(fileName),
1361
- dataset, lyr;
1362
-
1363
- // Add dbf data to a previously imported .shp file with a matching name
1364
- // (.shp should have been queued before .dbf)
1365
- if (fileType == 'dbf' && matches.length > 0) {
1366
- // find an imported .shp layer that is missing attribute data
1367
- // (if multiple matches, try to use the most recently imported one)
1368
- dataset = matches.reduce(function(memo, d) {
1369
- if (!d.layers[0].data) {
1370
- memo = d;
1371
- }
1372
- return memo;
1373
- }, null);
1374
- if (dataset) {
1375
- lyr = dataset.layers[0];
1376
- lyr.data = new internal.ShapefileTable(content, importOpts.encoding);
1377
- if (lyr.shapes && lyr.data.size() != lyr.shapes.length) {
1378
- stop("Different number of records in .shp and .dbf files");
1379
- }
1380
- if (!lyr.geometry_type) {
1381
- // kludge: trigger display of table cells if .shp has null geometry
1382
- // TODO: test case if lyr is not the current active layer
1383
- model.updated({});
1384
- }
1385
- procNextQueuedFile();
1386
- return;
1387
- }
1388
- }
1389
-
1390
- if (fileType == 'shx') {
1391
- // save .shx for use when importing .shp
1392
- // (queue should be sorted so that .shx is processed before .shp)
1393
- cachedFiles[fileName.toLowerCase()] = {filename: fileName, content: content};
1394
- procNextQueuedFile();
1395
- return;
1396
- }
1397
-
1398
- // Add .prj file to previously imported .shp file
1399
- if (fileType == 'prj') {
1400
- matches.forEach(function(d) {
1401
- if (!d.info.prj) {
1402
- d.info.prj = content;
1403
- }
1404
- });
1405
- procNextQueuedFile();
1406
- return;
1407
- }
1408
-
1409
- importNewDataset(fileType, fileName, content, importOpts);
1410
- }
1411
-
1412
- function importNewDataset(fileType, fileName, content, importOpts) {
1413
- var size = content.byteLength || content.length, // ArrayBuffer or string
1414
- delay = 0;
1415
-
1416
- // show importing message if file is large
1417
- if (size > 4e7) {
1418
- gui.showProgressMessage('Importing');
1419
- delay = 35;
1420
- }
1421
- setTimeout(function() {
1422
- var dataset;
1423
- var input = {};
1424
- try {
1425
- input[fileType] = {filename: fileName, content: content};
1426
- if (fileType == 'shp') {
1427
- // shx file should already be cached, if it was added together with the shp
1428
- input.shx = cachedFiles[fileName.replace(/shp$/i, 'shx').toLowerCase()] || null;
1429
- }
1430
- dataset = internal.importContent(input, importOpts);
1431
- // save import options for use by repair control, etc.
1432
- dataset.info.import_options = importOpts;
1433
- gui.session.fileImported(fileName, readImportOptsAsString());
1434
- addDataset(dataset);
1435
-
1436
- } catch(e) {
1437
- handleImportError(e, fileName);
1438
- }
1439
- }, delay);
1440
- }
1441
-
1442
1373
  function handleImportError(e, fileName) {
1443
1374
  var msg = utils.isString(e) ? e : e.message;
1444
1375
  if (fileName) {
@@ -1449,34 +1380,6 @@
1449
1380
  console.error(e);
1450
1381
  }
1451
1382
 
1452
- function handleZipFiles(files) {
1453
- return files.filter(function(file) {
1454
- var isZip = internal.isZipFile(file.name);
1455
- if (isZip) {
1456
- importZipFile(file);
1457
- }
1458
- return !isZip;
1459
- });
1460
- }
1461
-
1462
- function importZipFile(file) {
1463
- // gui.showProgressMessage('Importing');
1464
- setTimeout(function() {
1465
- GUI.readZipFile(file, function(err, files) {
1466
- if (err) {
1467
- handleImportError(err, file.name);
1468
- } else {
1469
- // don't try to import .txt files from zip files
1470
- // (these would be parsed as dsv and throw errows)
1471
- files = files.filter(function(f) {
1472
- return !/\.txt$/i.test(f.name);
1473
- });
1474
- receiveFiles(files);
1475
- }
1476
- });
1477
- }, 35);
1478
- }
1479
-
1480
1383
  function prepFilesForDownload(names) {
1481
1384
  var items = names.map(function(name) {
1482
1385
  var isUrl = /:\/\//.test(name);
@@ -1522,37 +1425,107 @@
1522
1425
  });
1523
1426
  }
1524
1427
 
1525
- function downloadNextFile_v1(memo, item, next) {
1526
- var req = new XMLHttpRequest();
1527
- var blob;
1528
- req.responseType = 'blob';
1529
- req.addEventListener('load', function(e) {
1530
- if (req.status == 200) {
1531
- blob = req.response;
1532
- }
1533
- });
1534
- req.addEventListener('progress', function(e) {
1535
- if (!e.lengthComputable) return;
1536
- var pct = e.loaded / e.total;
1537
- if (catalog) catalog.progress(pct);
1428
+ function wait(ms) {
1429
+ return new Promise(resolve => setTimeout(resolve, ms));
1430
+ }
1431
+
1432
+ function runAsync(fn, arg) {
1433
+ return new Promise((resolve, reject) => {
1434
+ fn(arg, function(err, data) {
1435
+ return err ? reject(err) : resolve(data);
1436
+ });
1538
1437
  });
1539
- req.addEventListener('loadend', function() {
1540
- var err;
1541
- if (req.status == 404) {
1542
- err = "Not&nbsp;found:&nbsp;" + item.name;
1543
- } else if (!blob) {
1544
- // Errors like DNS lookup failure, no CORS headers, no network connection
1545
- // all are status 0 - it seems impossible to show a more specific message
1546
- // actual reason is displayed on the console
1547
- err = "Error&nbsp;loading&nbsp;" + item.name + ". Possible causes include: wrong URL, no network connection, server not configured for cross-domain sharing (CORS).";
1438
+ }
1439
+
1440
+ async function readZipFile(file) {
1441
+ var files;
1442
+ await wait(35); // pause a beat so status message can display
1443
+ try {
1444
+ files = await runAsync(GUI.readZipFile, file);
1445
+ // don't try to import .txt files from zip files
1446
+ // (these would be parsed as dsv and throw errows)
1447
+ files = files.filter(function(f) {
1448
+ return !/\.txt$/i.test(f.name);
1449
+ });
1450
+ } catch(e) {
1451
+ handleImportError(e, file.name);
1452
+ files = [];
1453
+ }
1454
+ return files;
1455
+ }
1456
+
1457
+ async function readFileData(file) {
1458
+ try {
1459
+ var content = await runAsync(readContentFileAsync, file);
1460
+ return {
1461
+ content: content,
1462
+ size: content.byteLength || content.length, // ArrayBuffer or string
1463
+ name: file.name,
1464
+ basename: internal.getFileBase(file.name).toLowerCase(),
1465
+ type: internal.guessInputType(file.name, content)
1466
+ };
1467
+ } catch (e) {
1468
+ handleImportError("Web browser was unable to load the file.", file.name);
1469
+ }
1470
+ return null;
1471
+ }
1472
+
1473
+ async function readFiles(files) {
1474
+ var data = [], d;
1475
+ for (var file of files) {
1476
+ d = await readFileData(file);
1477
+ if (d) data.push(d);
1478
+ }
1479
+ return data;
1480
+ }
1481
+
1482
+ function groupFilesForImport(data, importOpts) {
1483
+ var names = importOpts.name ? [importOpts.name] : null;
1484
+ if (initialImport && opts.name) { // name from mapshaper-gui --name option
1485
+ names = opts.name.split(',');
1486
+ }
1487
+
1488
+ function key(basename, type) {
1489
+ return basename + '.' + type;
1490
+ }
1491
+ function hasShp(basename) {
1492
+ var shpKey = key(basename, 'shp');
1493
+ return data.some(d => key(d.basename, d.type) == shpKey);
1494
+ }
1495
+ data.forEach(d => {
1496
+ if (d.type == 'shp' || !isShapefilePart(d.name)) {
1497
+ d.group = key(d.basename, d.type);
1498
+ d.filename = d.name;
1499
+ } else if (hasShp(d.basename)) {
1500
+ d.group = key(d.basename, 'shp');
1501
+ } else if (d.type == 'dbf') {
1502
+ d.filename = d.name;
1503
+ d.group = key(d.basename, 'dbf');
1548
1504
  } else {
1549
- blob.name = item.basename;
1550
- memo.push(blob);
1505
+ // shapefile part without a .shp file
1506
+ d.group = null;
1551
1507
  }
1552
- next(err, memo);
1553
1508
  });
1554
- req.open('GET', item.url);
1555
- req.send();
1509
+ var index = {};
1510
+ var groups = [];
1511
+ data.forEach(d => {
1512
+ if (!d.group) return;
1513
+ var g = index[d.group];
1514
+ if (!g) {
1515
+ g = {};
1516
+ g.layername = names ? names[groups.length] || names[names.length - 1] : null;
1517
+ groups.push(g);
1518
+ index[d.group] = g;
1519
+ }
1520
+ g.size = (g.size || 0) + d.size; // accumulate size
1521
+ g[d.type] = {
1522
+ filename: d.name,
1523
+ content: d.content
1524
+ };
1525
+ // kludge: stash import name for session history
1526
+ if (d.filename) g.filename = d.filename;
1527
+ });
1528
+ return groups;
1556
1529
  }
1557
1530
  }
1558
1531
 
@@ -3395,7 +3368,7 @@
3395
3368
  GUI.onClick(entry, function() {
3396
3369
  var target = findLayerById(id);
3397
3370
  // don't select if user is typing or dragging
3398
- if (!GUI.getInputElement() && !dragging) {
3371
+ if (!GUI.textIsSelected() && !dragging) {
3399
3372
  gui.clearMode();
3400
3373
  if (!map.isActiveLayer(target.layer)) {
3401
3374
  model.selectLayer(target.layer, target.dataset);
@@ -3574,6 +3547,153 @@
3574
3547
  }
3575
3548
  }
3576
3549
 
3550
+ // import { cloneShape } from '../paths/mapshaper-shape-utils';
3551
+ // import { copyRecord } from '../datatable/mapshaper-data-utils';
3552
+ var snapVerticesToPoint$1 = internal.snapVerticesToPoint;
3553
+ var cloneShape = internal.cloneShape;
3554
+ var copyRecord = internal.copyRecord;
3555
+
3556
+ function Undo(gui) {
3557
+ var history, offset, stashedUndo;
3558
+ reset();
3559
+
3560
+ function reset() {
3561
+ history = [];
3562
+ stashedUndo = null;
3563
+ offset = 0;
3564
+ }
3565
+
3566
+
3567
+ function isUndoEvt(e) {
3568
+ return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
3569
+ }
3570
+
3571
+ function isRedoEvt(e) {
3572
+ return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y');
3573
+ }
3574
+
3575
+ gui.keyboard.on('keydown', function(evt) {
3576
+ var e = evt.originalEvent,
3577
+ kc = e.keyCode;
3578
+ if (isUndoEvt(e)) {
3579
+ this.undo();
3580
+ e.stopPropagation();
3581
+ e.preventDefault();
3582
+ }
3583
+ if (isRedoEvt(e)) {
3584
+ this.redo();
3585
+ e.stopPropagation();
3586
+ e.preventDefault();
3587
+ }
3588
+
3589
+ }, this, 10);
3590
+
3591
+ // undo/redo point/symbol dragging
3592
+ //
3593
+ gui.on('symbol_dragstart', function(e) {
3594
+ stashedUndo = this.makePointSetter(e.FID);
3595
+ }, this);
3596
+
3597
+ gui.on('symbol_dragend', function(e) {
3598
+ var redo = this.makePointSetter(e.FID);
3599
+ this.addHistoryState(stashedUndo, redo);
3600
+ }, this);
3601
+
3602
+ // undo/redo label dragging
3603
+ //
3604
+ gui.on('label_dragstart', function(e) {
3605
+ stashedUndo = this.makeDataSetter(e.FID);
3606
+ }, this);
3607
+
3608
+ gui.on('label_dragend', function(e) {
3609
+ var redo = this.makeDataSetter(e.FID);
3610
+ this.addHistoryState(stashedUndo, redo);
3611
+ }, this);
3612
+
3613
+ // undo/redo data editing
3614
+ // TODO: consider setting selected feature to the undo/redo target feature
3615
+ //
3616
+ gui.on('data_preupdate', function(e) {
3617
+ stashedUndo = this.makeDataSetter(e.FID);
3618
+ }, this);
3619
+
3620
+ gui.on('data_postupdate', function(e) {
3621
+ var redo = this.makeDataSetter(e.FID);
3622
+ this.addHistoryState(stashedUndo, redo);
3623
+ }, this);
3624
+
3625
+ // undo/redo vertex dragging
3626
+ gui.on('vertex_dragstart', function(e) {
3627
+ stashedUndo = this.makeVertexSetter(e.FID, e.vertex_ids);
3628
+ }, this);
3629
+
3630
+ gui.on('vertex_dragend', function(e) {
3631
+ var redo = this.makeVertexSetter(e.FID, e.vertex_ids);
3632
+ this.addHistoryState(stashedUndo, redo);
3633
+ }, this);
3634
+
3635
+ this.clear = function() {
3636
+ reset();
3637
+ };
3638
+
3639
+ this.makePointSetter = function(i) {
3640
+ var target = gui.model.getActiveLayer();
3641
+ var shp = cloneShape(target.layer.shapes[i]);
3642
+ return function() {
3643
+ target.layer.shapes[i] = shp;
3644
+ };
3645
+ };
3646
+
3647
+ this.makeDataSetter = function(id) {
3648
+ var target = gui.model.getActiveLayer();
3649
+ var rec = copyRecord(target.layer.data.getRecordAt(id));
3650
+ return function() {
3651
+ target.layer.data.getRecords()[id] = rec;
3652
+ gui.dispatchEvent('popup-needs-refresh');
3653
+ };
3654
+ };
3655
+
3656
+ this.makeVertexSetter = function(fid, ids) {
3657
+ var target = gui.model.getActiveLayer();
3658
+ var arcs = target.dataset.arcs;
3659
+ var p = internal.getVertexCoords(ids[0], arcs);
3660
+ return function() {
3661
+ snapVerticesToPoint$1(ids, p, arcs, true);
3662
+ };
3663
+ };
3664
+
3665
+ this.addHistoryState = function(undo, redo) {
3666
+ if (offset > 0) {
3667
+ history.splice(-offset);
3668
+ offset = 0;
3669
+ }
3670
+ history.push({undo, redo});
3671
+ };
3672
+
3673
+ this.undo = function() {
3674
+ var item = getHistoryItem();
3675
+ if (item) {
3676
+ offset++;
3677
+ item.undo();
3678
+ gui.dispatchEvent('map-needs-refresh');
3679
+ }
3680
+ };
3681
+
3682
+ this.redo = function() {
3683
+ if (offset <= 0) return;
3684
+ offset--;
3685
+ var item = getHistoryItem();
3686
+ item.redo();
3687
+ gui.dispatchEvent('map-needs-refresh');
3688
+ };
3689
+
3690
+ function getHistoryItem() {
3691
+ var item = history[history.length - offset - 1];
3692
+ return item || null;
3693
+ }
3694
+
3695
+ }
3696
+
3577
3697
  function SidebarButtons(gui) {
3578
3698
  var root = gui.container.findChild('.mshp-main-map');
3579
3699
  var buttons = El('div').addClass('nav-buttons').appendTo(root).hide();
@@ -3705,6 +3825,7 @@
3705
3825
 
3706
3826
  var menus = {
3707
3827
  standard: ['info', 'selection', 'data', 'box'],
3828
+ polygons: ['info', 'selection', 'data', 'box', 'vertices'],
3708
3829
  lines: ['info', 'selection', 'data', 'box', 'vertices'],
3709
3830
  table: ['info', 'selection', 'data'],
3710
3831
  labels: ['info', 'selection', 'data', 'box', 'labels', 'location'],
@@ -3816,6 +3937,9 @@
3816
3937
  if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polyline') {
3817
3938
  return menus.lines;
3818
3939
  }
3940
+ if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polygon') {
3941
+ return menus.polygons;
3942
+ }
3819
3943
  return menus.standard;
3820
3944
  }
3821
3945
 
@@ -3888,7 +4012,9 @@
3888
4012
  }
3889
4013
 
3890
4014
  function onModeChange() {
3891
- gui.dispatchEvent('interaction_mode_change', {mode: getInteractionMode()});
4015
+ var mode = getInteractionMode();
4016
+ gui.state.interaction_mode = mode;
4017
+ gui.dispatchEvent('interaction_mode_change', {mode: mode});
3892
4018
  }
3893
4019
 
3894
4020
  // Update button highlight and selected menu item highlight (if any)
@@ -4319,7 +4445,7 @@
4319
4445
  }
4320
4446
 
4321
4447
  // ignore keypress if no feature is selected or user is editing text
4322
- if (pinnedId() == -1 || GUI.getInputElement()) return;
4448
+ if (pinnedId() == -1 || GUI.textIsSelected()) return;
4323
4449
 
4324
4450
  if (e.keyCode == 37 || e.keyCode == 39) {
4325
4451
  // L/R arrow keys
@@ -4477,6 +4603,7 @@
4477
4603
 
4478
4604
  // Hits are re-detected on 'hover' (if hit detection is active)
4479
4605
  mouse.on('hover', function(e) {
4606
+ handlePointerEvent(e);
4480
4607
  if (storedData.pinned || !hitTest || !active) return;
4481
4608
  if (e.hover && isOverMap(e)) {
4482
4609
  // mouse is hovering directly over map area -- update hit detection
@@ -5521,7 +5648,9 @@
5521
5648
  } else {
5522
5649
  // field content has changed
5523
5650
  strval = strval2;
5651
+ gui.dispatchEvent('data_preupdate', {FID: currId}); // for undo/redo
5524
5652
  rec[key] = val2;
5653
+ gui.dispatchEvent('data_postupdate', {FID: currId});
5525
5654
  input.value(strval);
5526
5655
  setFieldClass(el, val2, type);
5527
5656
  self.dispatchEvent('update', {field: key, value: val2, id: currId});
@@ -5737,6 +5866,8 @@
5737
5866
  rec[key] = isString ? String(newVal) : newVal;
5738
5867
  }
5739
5868
 
5869
+ var snapVerticesToPoint = internal.snapVerticesToPoint;
5870
+
5740
5871
  function getDisplayCoordsById(id, layer, ext) {
5741
5872
  var coords = getPointCoordsById(id, layer);
5742
5873
  return ext.translateCoords(coords[0], coords[1]);
@@ -5757,7 +5888,7 @@
5757
5888
  }
5758
5889
 
5759
5890
 
5760
- function SymbolDragging2(gui, ext, hit) {
5891
+ function InteractiveEditor(gui, ext, hit) {
5761
5892
  // var targetTextNode; // text node currently being dragged
5762
5893
  var dragging = false;
5763
5894
  var activeRecord;
@@ -5819,6 +5950,7 @@
5819
5950
  if (e.mode != 'labels') {
5820
5951
  stopDragging();
5821
5952
  }
5953
+ gui.undo.clear(); // TODO: put this elsewhere?
5822
5954
  });
5823
5955
 
5824
5956
  // down event on svg
@@ -5830,12 +5962,17 @@
5830
5962
  // 3: on other text -> stop dragging, select new text
5831
5963
 
5832
5964
  hit.on('dragstart', function(e) {
5833
- if (labelEditingEnabled()) {
5834
- onLabelDragStart(e);
5965
+ if (e.id >= 0 === false) return;
5966
+ if (labelEditingEnabled() && onLabelDragStart(e)) {
5967
+ triggerGlobalEvent('label_dragstart', e);
5968
+ startDragging();
5835
5969
  } else if (locationEditingEnabled()) {
5836
- onLocationDragStart(e);
5970
+ triggerGlobalEvent('symbol_dragstart', e);
5971
+ startDragging();
5837
5972
  } else if (vertexEditingEnabled()) {
5838
5973
  onVertexDragStart(e);
5974
+ triggerGlobalEvent('vertex_dragstart', e);
5975
+ startDragging();
5839
5976
  }
5840
5977
  });
5841
5978
 
@@ -5851,70 +5988,77 @@
5851
5988
 
5852
5989
  hit.on('dragend', function(e) {
5853
5990
  if (locationEditingEnabled()) {
5854
- onLocationDragEnd(e);
5991
+ triggerGlobalEvent('symbol_dragend', e);
5855
5992
  stopDragging();
5856
5993
  } else if (labelEditingEnabled()) {
5994
+ triggerGlobalEvent('label_dragend', e);
5857
5995
  stopDragging();
5858
5996
  } else if (vertexEditingEnabled()) {
5859
- onVertexDragEnd(e);
5997
+ // kludge to get dataset to recalculate internal bounding boxes
5998
+ hit.getHitTarget().arcs.transformPoints(function() {});
5999
+ triggerGlobalEvent('vertex_dragend', e);
5860
6000
  stopDragging();
5861
6001
  }
5862
6002
  });
5863
6003
 
5864
6004
  hit.on('click', function(e) {
5865
6005
  if (labelEditingEnabled()) {
6006
+ var target = hit.getHitTarget();
5866
6007
  onLabelClick(e);
5867
6008
  }
5868
6009
  });
5869
6010
 
5870
- function onLocationDragStart(e) {
5871
- if (e.id >= 0) {
5872
- dragging = true;
5873
- triggerGlobalEvent('symbol_dragstart', e);
6011
+ // TODO: highlight hit vertex in path edit mode
6012
+ if (false) hit.on('hover', function(e) {
6013
+ if (vertexEditingEnabled() && !dragging) {
6014
+ onVertexHover(e);
5874
6015
  }
6016
+ }, null, 100);
6017
+
6018
+ function onVertexHover(e) {
6019
+ // hovering in vertex edit mode: find vertex insertion point
6020
+ var target = hit.getHitTarget();
6021
+ var shp = target.layer.shapes[e.id];
6022
+ var p = ext.translatePixelCoords(e.x, e.y);
6023
+ var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize());
5875
6024
  }
5876
6025
 
5877
- function onVertexDragStart(e) {
5878
- if (e.id >= 0) {
5879
- dragging = true;
5880
- }
6026
+
6027
+ function getVertexEventData(e) {
6028
+ return {
6029
+ FID: activeId,
6030
+ vertexIds: activeVertexIds
6031
+ };
5881
6032
  }
5882
6033
 
5883
6034
  function onLocationDrag(e) {
5884
6035
  var lyr = hit.getHitTarget().layer;
5885
- var p = getPointCoordsById(e.id, hit.getHitTarget().layer);
6036
+ var p = getPointCoordsById(e.id, lyr);
5886
6037
  if (!p) return;
5887
6038
  var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
5888
6039
  p[0] += diff[0];
5889
6040
  p[1] += diff[1];
5890
- self.dispatchEvent('location_change'); // signal map to redraw
6041
+ triggerRedraw();
5891
6042
  triggerGlobalEvent('symbol_drag', e);
5892
6043
  }
5893
6044
 
5894
- function onVertexDrag(e) {
6045
+ function onVertexDragStart(e) {
5895
6046
  var target = hit.getHitTarget();
6047
+ var shp = target.layer.shapes[e.id];
5896
6048
  var p = ext.translatePixelCoords(e.x, e.y);
5897
- if (!activeVertexIds) {
5898
- activeVertexIds = internal.findNearestVertices(p, target.layer.shapes[e.id], target.arcs);
5899
- }
6049
+ activeVertexIds = internal.findNearestVertices(p, shp, target.arcs);
6050
+ activeId = e.id;
6051
+ }
6052
+
6053
+ function onVertexDrag(e) {
6054
+ var target = hit.getHitTarget();
5900
6055
  if (!activeVertexIds) return; // ignore error condition
6056
+ var p = ext.translatePixelCoords(e.x, e.y);
5901
6057
  if (gui.keyboard.shiftIsPressed()) {
5902
6058
  internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs);
5903
6059
  }
5904
- activeVertexIds.forEach(function(idx) {
5905
- internal.setVertexCoords(p[0], p[1], idx, target.arcs);
5906
- });
5907
- self.dispatchEvent('location_change'); // signal map to redraw
5908
- }
5909
-
5910
- function onLocationDragEnd(e) {
5911
- triggerGlobalEvent('symbol_dragend', e);
5912
- }
5913
-
5914
- function onVertexDragEnd(e) {
5915
- // kludge to get dataset to recalculate internal bounding boxes
5916
- hit.getHitTarget().arcs.transformPoints(function() {});
5917
- activeVertexIds = null;
6060
+ snapVerticesToPoint(activeVertexIds, p, target.arcs);
6061
+ triggerRedraw();
5918
6062
  }
5919
6063
 
5920
6064
  function onLabelClick(e) {
@@ -5927,11 +6071,19 @@
5927
6071
  }
5928
6072
  }
5929
6073
 
6074
+ function triggerRedraw() {
6075
+ gui.dispatchEvent('map-needs-refresh');
6076
+ }
6077
+
5930
6078
  function triggerGlobalEvent(type, e) {
5931
- if (e.id >= 0) {
5932
- // fire event to signal external editor that symbol coords have changed
5933
- gui.dispatchEvent(type, {FID: e.id, layer_name: hit.getHitTarget().layer.name});
5934
- }
6079
+ if (e.id >= 0 === false) return;
6080
+ var o = {
6081
+ FID: e.id,
6082
+ layer_name: hit.getHitTarget().layer.name,
6083
+ vertex_ids: activeVertexIds
6084
+ };
6085
+ // fire event to signal external editor that symbol coords have changed
6086
+ gui.dispatchEvent(type, o);
5935
6087
  }
5936
6088
 
5937
6089
  function getLabelRecordById(id) {
@@ -5953,11 +6105,11 @@
5953
6105
  function onLabelDragStart(e) {
5954
6106
  var textNode = getTextTarget3(e);
5955
6107
  var table = hit.getTargetDataTable();
5956
- if (!textNode || !table) return;
6108
+ if (!textNode || !table) return false;
5957
6109
  activeId = e.id;
5958
6110
  activeRecord = getLabelRecordById(activeId);
5959
- dragging = true;
5960
6111
  downEvt = e;
6112
+ return true;
5961
6113
  }
5962
6114
 
5963
6115
  function onLabelDrag(e) {
@@ -6006,88 +6158,17 @@
6006
6158
  }
6007
6159
  return el.tagName == 'text' ? el : null;
6008
6160
  }
6161
+ }
6009
6162
 
6010
- // svg.addEventListener('mousedown', function(e) {
6011
- // var textTarget = getTextTarget(e);
6012
- // downEvt = e;
6013
- // if (!textTarget) {
6014
- // stopEditing();
6015
- // } else if (!editing) {
6016
- // // nop
6017
- // } else if (textTarget == targetTextNode) {
6018
- // startDragging();
6019
- // } else {
6020
- // startDragging();
6021
- // editTextNode(textTarget);
6022
- // }
6023
- // });
6024
-
6025
- // up event on svg
6026
- // a: currently dragging text
6027
- // -> stop dragging
6028
- // b: clicked on a text feature
6029
- // -> start editing it
6030
-
6031
-
6032
- // svg.addEventListener('mouseup', function(e) {
6033
- // var textTarget = getTextTarget(e);
6034
- // var isClick = isClickEvent(e, downEvt);
6035
- // if (isClick && textTarget && textTarget == targetTextNode &&
6036
- // activeRecord && isMultilineLabel(targetTextNode)) {
6037
- // toggleTextAlign(targetTextNode, activeRecord);
6038
- // updateSymbol();
6039
- // }
6040
- // if (dragging) {
6041
- // stopDragging();
6042
- // } else if (isClick && textTarget) {
6043
- // editTextNode(textTarget);
6044
- // }
6045
- // });
6046
-
6047
- // block dbl-click navigation when editing
6048
- // mouse.on('dblclick', function(e) {
6049
- // if (editing) e.stopPropagation();
6050
- // }, null, eventPriority);
6051
-
6052
- // mouse.on('dragstart', function(e) {
6053
- // onLabelDrag(e);
6054
- // }, null, eventPriority);
6055
-
6056
- // mouse.on('drag', function(e) {
6057
- // var scale = ext.getSymbolScale() || 1;
6058
- // onLabelDrag(e);
6059
- // if (!dragging || !activeRecord) return;
6060
- // applyDelta(activeRecord, 'dx', e.dx / scale);
6061
- // applyDelta(activeRecord, 'dy', e.dy / scale);
6062
- // if (!isMultilineLabel(targetTextNode)) {
6063
- // // update anchor position of single-line labels based on label position
6064
- // // relative to anchor point, for better placement when eventual display font is
6065
- // // different from mapshaper's font.
6066
- // updateTextAnchor(targetTextNode, activeRecord);
6067
- // }
6068
- // // updateSymbol(targetTextNode, activeRecord);
6069
- // targetTextNode = updateSymbol2(targetTextNode, activeRecord, activeId);
6070
- // }, null, eventPriority);
6071
-
6072
- // mouse.on('dragend', function(e) {
6073
- // onLabelDrag(e);
6074
- // stopDragging();
6075
- // }, null, eventPriority);
6076
-
6077
-
6078
- // function onLabelDrag(e) {
6079
- // if (dragging) {
6080
- // e.stopPropagation();
6081
- // }
6082
- // }
6163
+ function startDragging() {
6164
+ dragging = true;
6083
6165
  }
6084
6166
 
6085
6167
  function stopDragging() {
6086
6168
  dragging = false;
6087
6169
  activeId = -1;
6088
6170
  activeRecord = null;
6089
- // targetTextNode = null;
6090
- // svg.removeAttribute('class');
6171
+ activeVertexIds = null;
6091
6172
  }
6092
6173
 
6093
6174
  function isClickEvent(up, down) {
@@ -6098,16 +6179,6 @@
6098
6179
  return dist <= 4 && elapsed < 300;
6099
6180
  }
6100
6181
 
6101
-
6102
- // function deselectText(el) {
6103
- // el.removeAttribute('class');
6104
- // }
6105
-
6106
- // function selectText(el) {
6107
- // el.setAttribute('class', 'selected');
6108
- // }
6109
-
6110
-
6111
6182
  }
6112
6183
 
6113
6184
  var darkStroke = "#334",
@@ -6151,7 +6222,7 @@
6151
6222
  dotSize: 2.5
6152
6223
  }, polyline: {
6153
6224
  strokeColor: black,
6154
- strokeWidth: 2.5
6225
+ strokeWidth: 2.5,
6155
6226
  }
6156
6227
  },
6157
6228
  unselectedHoverStyles = {
@@ -6465,6 +6536,7 @@
6465
6536
 
6466
6537
  this.maxScale = maxScale;
6467
6538
 
6539
+ // Display scale, e.g. meters per pixel or degrees per pixel
6468
6540
  this.getPixelSize = function() {
6469
6541
  return 1 / this.getTransform().mx;
6470
6542
  };
@@ -6656,7 +6728,10 @@
6656
6728
  } else {
6657
6729
  arcs = getArcsForRendering(obj, ext);
6658
6730
  filter = getShapeFilter(arcs, ext);
6659
- canv.drawPathShapes(layer.shapes, arcs, style, filter);
6731
+ canv.drawStyledPaths(layer.shapes, arcs, style, filter);
6732
+ if (style.vertices) {
6733
+ canv.drawVertices(layer.shapes, arcs, style, filter);
6734
+ }
6660
6735
  }
6661
6736
  }
6662
6737
 
@@ -6730,7 +6805,7 @@
6730
6805
 
6731
6806
  /*
6732
6807
  // Original function, not optimized
6733
- _self.drawPathShapes = function(shapes, arcs, style) {
6808
+ _self.drawStyledPaths = function(shapes, arcs, style) {
6734
6809
  var startPath = getPathStart(_ext),
6735
6810
  drawPath = getShapePencil(arcs, _ext),
6736
6811
  styler = style.styler || null;
@@ -6743,8 +6818,28 @@
6743
6818
  };
6744
6819
  */
6745
6820
 
6821
+ _self.drawVertices = function(shapes, arcs, style, filter) {
6822
+ var iter = new internal.ShapeIter(arcs);
6823
+ var t = getScaledTransform(_ext);
6824
+ var radius = (style.strokeWidth * 0.9 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext);
6825
+ var color = style.strokeColor || 'black';
6826
+ var shp;
6827
+ _ctx.beginPath();
6828
+ _ctx.fillStyle = color;
6829
+ for (var i=0; i<shapes.length; i++) {
6830
+ shp = shapes[i];
6831
+ if (!shp || filter && !filter(shp)) continue;
6832
+ iter.init(shp);
6833
+ while (iter.hasNext()) {
6834
+ drawCircle(iter.x * t.mx + t.bx, iter.y * t.my + t.by, radius, _ctx);
6835
+ }
6836
+ }
6837
+ _ctx.fill();
6838
+ _ctx.closePath();
6839
+ };
6840
+
6746
6841
  // Optimized to draw paths in same-style batches (faster Canvas drawing)
6747
- _self.drawPathShapes = function(shapes, arcs, style, filter) {
6842
+ _self.drawStyledPaths = function(shapes, arcs, style, filter) {
6748
6843
  var styleIndex = {};
6749
6844
  var batchSize = 1500;
6750
6845
  var startPath = getPathStart(_ext, getScaledLineScale(_ext));
@@ -6872,7 +6967,7 @@
6872
6967
  }
6873
6968
  }
6874
6969
 
6875
- // TODO: consider using drawPathShapes(), which draws paths in batches
6970
+ // TODO: consider using drawStyledPaths(), which draws paths in batches
6876
6971
  // for faster Canvas rendering. Downside: changes stacking order, which
6877
6972
  // is bad if circles are graduated.
6878
6973
  _self.drawPoints = function(shapes, style) {
@@ -7812,6 +7907,10 @@
7812
7907
  _mouse.disable();
7813
7908
  });
7814
7909
 
7910
+ gui.on('map-needs-refresh', function() {
7911
+ drawLayers();
7912
+ });
7913
+
7815
7914
  model.on('update', onUpdate);
7816
7915
 
7817
7916
  // Update display of segment intersections
@@ -7970,13 +8069,7 @@
7970
8069
  });
7971
8070
  }
7972
8071
 
7973
- if (true) { // TODO: add option to disable?
7974
- _editor = new SymbolDragging2(gui, _ext, _hit);
7975
- _editor.on('location_change', function(e) {
7976
- // TODO: look into optimizing, so only changed symbol is redrawn
7977
- drawLayers();
7978
- });
7979
- }
8072
+ _editor = new InteractiveEditor(gui, _ext, _hit);
7980
8073
 
7981
8074
  _ext.on('change', function(e) {
7982
8075
  if (e.reset) return; // don't need to redraw map here if extent has been reset
@@ -7996,6 +8089,10 @@
7996
8089
  function updateOverlayLayer(e) {
7997
8090
  var style = getOverlayStyle(_activeLyr.layer, e);
7998
8091
  if (style) {
8092
+ // kludge to show vertices when editing path shapes
8093
+ if (gui.state.interaction_mode == 'vertices') {
8094
+ style.vertices = true;
8095
+ }
7999
8096
  _overlayLyr = utils.defaults({
8000
8097
  layer: filterLayerByIds(_activeLyr.layer, style.ids),
8001
8098
  style: style
@@ -8235,6 +8332,8 @@
8235
8332
  gui.map = new MshpMap(gui);
8236
8333
  gui.interaction = new InteractionMode(gui);
8237
8334
  gui.session = new SessionHistory(gui);
8335
+ gui.undo = new Undo(gui);
8336
+ gui.state = {};
8238
8337
 
8239
8338
  gui.showProgressMessage = function(msg) {
8240
8339
  if (!gui.progressMessage) {
@@ -8318,6 +8417,7 @@
8318
8417
  opts.display_all = vars['display-all'] || vars.a || !!manifest.display_all;
8319
8418
  opts.quick_view = vars['quick-view'] || vars.q || !!manifest.quick_view;
8320
8419
  opts.target = vars.target || manifest.target || null;
8420
+ opts.name = vars.name || manifest.name || null;
8321
8421
  return opts;
8322
8422
  }
8323
8423