mapshaper 0.5.88 → 0.5.92

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.
@@ -233,6 +233,30 @@
233
233
  return parsed[0].options;
234
234
  };
235
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
+
236
260
  // @file: Zip file
237
261
  // @cb: function(err, <files>)
238
262
  //
@@ -1107,20 +1131,19 @@
1107
1131
 
1108
1132
  function ImportControl(gui, opts) {
1109
1133
  var model = gui.model;
1134
+ var initialImport = true;
1110
1135
  var importCount = 0;
1111
1136
  var importTotal = 0;
1112
1137
  var overQuickView = false;
1113
- var useQuickView = opts.quick_view; // may be set by mapshaper-gui
1114
1138
  var queuedFiles = [];
1115
1139
  var manifestFiles = opts.files || [];
1116
- var cachedFiles = {};
1117
1140
  var catalog;
1118
1141
 
1119
1142
  if (opts.catalog) {
1120
1143
  catalog = new CatalogControl(gui, opts.catalog, downloadFiles);
1121
1144
  }
1122
1145
 
1123
- new SimpleButton('#import-buttons .submit-btn').on('click', onSubmit);
1146
+ new SimpleButton('#import-buttons .submit-btn').on('click', importQueuedFiles);
1124
1147
  new SimpleButton('#import-buttons .cancel-btn').on('click', gui.clearMode);
1125
1148
  new DropControl(gui, 'body', receiveFiles);
1126
1149
  new FileChooser('#file-selection-btn', receiveFiles);
@@ -1128,7 +1151,7 @@
1128
1151
  new FileChooser('#add-file-btn', receiveFiles);
1129
1152
  initDropArea('#import-quick-drop', true);
1130
1153
  initDropArea('#import-drop');
1131
- gui.keyboard.onMenuSubmit(El('#import-options'), onSubmit);
1154
+ gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
1132
1155
 
1133
1156
  gui.addMode('import', turnOn, turnOff);
1134
1157
  gui.enterMode('import');
@@ -1140,6 +1163,10 @@
1140
1163
  }
1141
1164
  });
1142
1165
 
1166
+ function useQuickView() {
1167
+ return initialImport && (opts.quick_view || overQuickView);
1168
+ }
1169
+
1143
1170
  function initDropArea(el, isQuick) {
1144
1171
  var area = El(el)
1145
1172
  .on('dragleave', onout)
@@ -1157,15 +1184,19 @@
1157
1184
  }
1158
1185
  }
1159
1186
 
1160
- function findMatchingShp(filename) {
1161
- // use case-insensitive matching
1162
- var base = internal.getPathBase(filename).toLowerCase();
1163
- return model.getDatasets().filter(function(d) {
1164
- var fname = d.info.input_files && d.info.input_files[0] || "";
1165
- var ext = internal.getFileExtension(fname).toLowerCase();
1166
- var base2 = internal.getPathBase(fname).toLowerCase();
1167
- return base == base2 && ext == 'shp';
1168
- });
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();
1169
1200
  }
1170
1201
 
1171
1202
  function turnOn() {
@@ -1186,13 +1217,8 @@
1186
1217
  importCount = 0;
1187
1218
  }
1188
1219
  gui.clearProgressMessage();
1189
- useQuickView = false; // unset 'quick view' mode, if on
1190
- close();
1191
- }
1192
-
1193
- function close() {
1220
+ initialImport = false; // unset 'quick view' mode, if on
1194
1221
  clearQueuedFiles();
1195
- cachedFiles = {};
1196
1222
  }
1197
1223
 
1198
1224
  function onImportComplete() {
@@ -1206,6 +1232,7 @@
1206
1232
  }
1207
1233
  }
1208
1234
  model.updated({select: true});
1235
+
1209
1236
  }
1210
1237
 
1211
1238
  function clearQueuedFiles() {
@@ -1226,34 +1253,6 @@
1226
1253
  }, []);
1227
1254
  }
1228
1255
 
1229
- // When a Shapefile component is at the head of the queue, move the entire
1230
- // Shapefile to the front of the queue, sorted in reverse alphabetical order,
1231
- // (a kludge), so .shp is read before .dbf and .prj
1232
- // (If a .dbf file is imported before a .shp, it becomes a separate dataset)
1233
- // TODO: import Shapefile parts without relying on this kludge
1234
- function sortQueue(queue) {
1235
- var nextFile = queue[0];
1236
- var basename, parts;
1237
- if (!isShapefilePart(nextFile.name)) {
1238
- return queue;
1239
- }
1240
- basename = internal.getFileBase(nextFile.name).toLowerCase();
1241
- parts = [];
1242
- queue = queue.filter(function(file) {
1243
- if (internal.getFileBase(file.name).toLowerCase() == basename) {
1244
- parts.push(file);
1245
- return false;
1246
- }
1247
- return true;
1248
- });
1249
- parts.sort(function(a, b) {
1250
- // Sorting on LC filename so Shapefiles with mixed-case
1251
- // extensions are sorted correctly
1252
- return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1;
1253
- });
1254
- return parts.concat(queue);
1255
- }
1256
-
1257
1256
  function showQueuedFiles() {
1258
1257
  var list = gui.container.findChild('.dropped-file-list').empty();
1259
1258
  queuedFiles.forEach(function(f) {
@@ -1261,16 +1260,14 @@
1261
1260
  });
1262
1261
  }
1263
1262
 
1264
- function receiveFiles(files) {
1265
- var prevSize = queuedFiles.length;
1266
- useQuickView = useQuickView || overQuickView;
1267
- files = handleZipFiles(utils.toArray(files));
1268
- addFilesToQueue(files);
1263
+ async function receiveFiles(files) {
1264
+ // TODO: show importing message here?
1265
+ var expanded = await expandFiles(files);
1266
+ addFilesToQueue(expanded);
1269
1267
  if (queuedFiles.length === 0) return;
1270
1268
  gui.enterMode('import');
1271
-
1272
- if (useQuickView) {
1273
- onSubmit();
1269
+ if (useQuickView()) {
1270
+ importQueuedFiles();
1274
1271
  } else {
1275
1272
  gui.container.addClass('queued-files');
1276
1273
  El('#path-import-options').classed('hidden', !filesMayContainPaths(queuedFiles));
@@ -1278,25 +1275,51 @@
1278
1275
  }
1279
1276
  }
1280
1277
 
1281
- function filesMayContainPaths(files) {
1282
- return utils.some(files, function(f) {
1283
- var type = internal.guessInputFileType(f.name);
1284
- return type == 'shp' || type == 'json' || internal.isZipFile(f.name);
1285
- });
1278
+ async function expandFiles(files) {
1279
+ var files2 = [], expanded;
1280
+ for (var f of files) {
1281
+ if (internal.isZipFile(f.name)) {
1282
+ expanded = await readZipFile(f);
1283
+ files2 = files2.concat(expanded);
1284
+ } else {
1285
+ files2.push(f);
1286
+ }
1287
+ }
1288
+ return files2;
1286
1289
  }
1287
1290
 
1288
- function onSubmit() {
1289
- gui.container.removeClass('queued-files');
1290
- gui.container.removeClass('splash-screen');
1291
- procNextQueuedFile();
1291
+ async function importFiles(files) {
1292
+ var fileData = await readFiles(files);
1293
+ var importOpts = readImportOpts();
1294
+ var groups = groupFilesForImport(fileData, importOpts);
1295
+ for (var group of groups) {
1296
+ if (group.size > 4e7) {
1297
+ gui.showProgressMessage('Importing');
1298
+ await wait(35);
1299
+ }
1300
+ importDataset(group, importOpts);
1301
+ }
1292
1302
  }
1293
1303
 
1294
- function addDataset(dataset) {
1295
- if (!datasetIsEmpty(dataset)) {
1296
- model.addDataset(dataset);
1297
- importCount++;
1304
+ function importDataset(group, importOpts) {
1305
+ var optStr = GUI.formatCommandOptions(importOpts);
1306
+ var dataset = internal.importContent(group, importOpts);
1307
+ if (datasetIsEmpty(dataset)) return;
1308
+ if (group.layername) {
1309
+ dataset.layers.forEach(lyr => lyr.name = group.layername);
1298
1310
  }
1299
- procNextQueuedFile();
1311
+ // save import options for use by repair control, etc.
1312
+ dataset.info.import_options = importOpts;
1313
+ model.addDataset(dataset);
1314
+ importCount++;
1315
+ gui.session.fileImported(group.filename, optStr);
1316
+ }
1317
+
1318
+ function filesMayContainPaths(files) {
1319
+ return utils.some(files, function(f) {
1320
+ var type = internal.guessInputFileType(f.name);
1321
+ return type == 'shp' || type == 'json' || internal.isZipFile(f.name);
1322
+ });
1300
1323
  }
1301
1324
 
1302
1325
  function datasetIsEmpty(dataset) {
@@ -1305,41 +1328,27 @@
1305
1328
  });
1306
1329
  }
1307
1330
 
1308
- function procNextQueuedFile() {
1309
- if (queuedFiles.length === 0) {
1310
- gui.clearMode();
1311
- } else {
1312
- queuedFiles = sortQueue(queuedFiles);
1313
- readFile(queuedFiles.shift());
1314
- }
1315
- }
1316
1331
 
1317
1332
  // TODO: support .cpg
1318
1333
  function isShapefilePart(name) {
1319
1334
  return /\.(shp|shx|dbf|prj)$/i.test(name);
1320
1335
  }
1321
1336
 
1322
-
1323
1337
  function readImportOpts() {
1324
- if (useQuickView) return {};
1325
- var freeform = El('#import-options .advanced-options').node().value,
1326
- opts = GUI.parseFreeformOptions(freeform, 'i');
1327
- opts.no_repair = !El("#repair-intersections-opt").node().checked;
1328
- opts.snap = !!El("#snap-points-opt").node().checked;
1329
- return opts;
1330
- }
1331
-
1332
- // for CLI output
1333
- function readImportOptsAsString() {
1334
- if (useQuickView) return '';
1335
- var freeform = El('#import-options .advanced-options').node().value;
1336
- var opts = readImportOpts();
1337
- if (opts.snap) freeform = 'snap ' + freeform;
1338
- return freeform.trim();
1338
+ var importOpts;
1339
+ if (useQuickView()) {
1340
+ importOpts = {}; // default opts using quickview
1341
+ } else {
1342
+ var freeform = El('#import-options .advanced-options').node().value;
1343
+ importOpts = GUI.parseFreeformOptions(freeform, 'i');
1344
+ importOpts.no_repair = !El("#repair-intersections-opt").node().checked;
1345
+ importOpts.snap = !!El("#snap-points-opt").node().checked;
1346
+ }
1347
+ return importOpts;
1339
1348
  }
1340
1349
 
1341
1350
  // @file a File object
1342
- function readFile(file) {
1351
+ async function readContentFileAsync(file, cb) {
1343
1352
  var name = file.name,
1344
1353
  reader = new FileReader(),
1345
1354
  useBinary = internal.isSupportedBinaryInputType(name) ||
@@ -1349,9 +1358,9 @@
1349
1358
 
1350
1359
  reader.addEventListener('loadend', function(e) {
1351
1360
  if (!reader.result) {
1352
- handleImportError("Web browser was unable to load the file.", name);
1361
+ cb(new Error());
1353
1362
  } else {
1354
- importFileContent(name, reader.result);
1363
+ cb(null, reader.result);
1355
1364
  }
1356
1365
  });
1357
1366
  if (useBinary) {
@@ -1362,91 +1371,6 @@
1362
1371
  }
1363
1372
  }
1364
1373
 
1365
- function importFileContent(fileName, content) {
1366
- var fileType = internal.guessInputType(fileName, content),
1367
- importOpts = readImportOpts(),
1368
- matches = findMatchingShp(fileName),
1369
- dataset, lyr;
1370
-
1371
- // Add dbf data to a previously imported .shp file with a matching name
1372
- // (.shp should have been queued before .dbf)
1373
- if (fileType == 'dbf' && matches.length > 0) {
1374
- // find an imported .shp layer that is missing attribute data
1375
- // (if multiple matches, try to use the most recently imported one)
1376
- dataset = matches.reduce(function(memo, d) {
1377
- if (!d.layers[0].data) {
1378
- memo = d;
1379
- }
1380
- return memo;
1381
- }, null);
1382
- if (dataset) {
1383
- lyr = dataset.layers[0];
1384
- lyr.data = new internal.ShapefileTable(content, importOpts.encoding);
1385
- if (lyr.shapes && lyr.data.size() != lyr.shapes.length) {
1386
- stop("Different number of records in .shp and .dbf files");
1387
- }
1388
- if (!lyr.geometry_type) {
1389
- // kludge: trigger display of table cells if .shp has null geometry
1390
- // TODO: test case if lyr is not the current active layer
1391
- model.updated({});
1392
- }
1393
- procNextQueuedFile();
1394
- return;
1395
- }
1396
- }
1397
-
1398
- if (fileType == 'shx') {
1399
- // save .shx for use when importing .shp
1400
- // (queue should be sorted so that .shx is processed before .shp)
1401
- cachedFiles[fileName.toLowerCase()] = {filename: fileName, content: content};
1402
- procNextQueuedFile();
1403
- return;
1404
- }
1405
-
1406
- // Add .prj file to previously imported .shp file
1407
- if (fileType == 'prj') {
1408
- matches.forEach(function(d) {
1409
- if (!d.info.prj) {
1410
- d.info.prj = content;
1411
- }
1412
- });
1413
- procNextQueuedFile();
1414
- return;
1415
- }
1416
-
1417
- importNewDataset(fileType, fileName, content, importOpts);
1418
- }
1419
-
1420
- function importNewDataset(fileType, fileName, content, importOpts) {
1421
- var size = content.byteLength || content.length, // ArrayBuffer or string
1422
- delay = 0;
1423
-
1424
- // show importing message if file is large
1425
- if (size > 4e7) {
1426
- gui.showProgressMessage('Importing');
1427
- delay = 35;
1428
- }
1429
- setTimeout(function() {
1430
- var dataset;
1431
- var input = {};
1432
- try {
1433
- input[fileType] = {filename: fileName, content: content};
1434
- if (fileType == 'shp') {
1435
- // shx file should already be cached, if it was added together with the shp
1436
- input.shx = cachedFiles[fileName.replace(/shp$/i, 'shx').toLowerCase()] || null;
1437
- }
1438
- dataset = internal.importContent(input, importOpts);
1439
- // save import options for use by repair control, etc.
1440
- dataset.info.import_options = importOpts;
1441
- gui.session.fileImported(fileName, readImportOptsAsString());
1442
- addDataset(dataset);
1443
-
1444
- } catch(e) {
1445
- handleImportError(e, fileName);
1446
- }
1447
- }, delay);
1448
- }
1449
-
1450
1374
  function handleImportError(e, fileName) {
1451
1375
  var msg = utils.isString(e) ? e : e.message;
1452
1376
  if (fileName) {
@@ -1457,34 +1381,6 @@
1457
1381
  console.error(e);
1458
1382
  }
1459
1383
 
1460
- function handleZipFiles(files) {
1461
- return files.filter(function(file) {
1462
- var isZip = internal.isZipFile(file.name);
1463
- if (isZip) {
1464
- importZipFile(file);
1465
- }
1466
- return !isZip;
1467
- });
1468
- }
1469
-
1470
- function importZipFile(file) {
1471
- // gui.showProgressMessage('Importing');
1472
- setTimeout(function() {
1473
- GUI.readZipFile(file, function(err, files) {
1474
- if (err) {
1475
- handleImportError(err, file.name);
1476
- } else {
1477
- // don't try to import .txt files from zip files
1478
- // (these would be parsed as dsv and throw errows)
1479
- files = files.filter(function(f) {
1480
- return !/\.txt$/i.test(f.name);
1481
- });
1482
- receiveFiles(files);
1483
- }
1484
- });
1485
- }, 35);
1486
- }
1487
-
1488
1384
  function prepFilesForDownload(names) {
1489
1385
  var items = names.map(function(name) {
1490
1386
  var isUrl = /:\/\//.test(name);
@@ -1530,37 +1426,107 @@
1530
1426
  });
1531
1427
  }
1532
1428
 
1533
- function downloadNextFile_v1(memo, item, next) {
1534
- var req = new XMLHttpRequest();
1535
- var blob;
1536
- req.responseType = 'blob';
1537
- req.addEventListener('load', function(e) {
1538
- if (req.status == 200) {
1539
- blob = req.response;
1540
- }
1541
- });
1542
- req.addEventListener('progress', function(e) {
1543
- if (!e.lengthComputable) return;
1544
- var pct = e.loaded / e.total;
1545
- if (catalog) catalog.progress(pct);
1429
+ function wait(ms) {
1430
+ return new Promise(resolve => setTimeout(resolve, ms));
1431
+ }
1432
+
1433
+ function runAsync(fn, arg) {
1434
+ return new Promise((resolve, reject) => {
1435
+ fn(arg, function(err, data) {
1436
+ return err ? reject(err) : resolve(data);
1437
+ });
1546
1438
  });
1547
- req.addEventListener('loadend', function() {
1548
- var err;
1549
- if (req.status == 404) {
1550
- err = "Not&nbsp;found:&nbsp;" + item.name;
1551
- } else if (!blob) {
1552
- // Errors like DNS lookup failure, no CORS headers, no network connection
1553
- // all are status 0 - it seems impossible to show a more specific message
1554
- // actual reason is displayed on the console
1555
- err = "Error&nbsp;loading&nbsp;" + item.name + ". Possible causes include: wrong URL, no network connection, server not configured for cross-domain sharing (CORS).";
1439
+ }
1440
+
1441
+ async function readZipFile(file) {
1442
+ var files;
1443
+ await wait(35); // pause a beat so status message can display
1444
+ try {
1445
+ files = await runAsync(GUI.readZipFile, file);
1446
+ // don't try to import .txt files from zip files
1447
+ // (these would be parsed as dsv and throw errows)
1448
+ files = files.filter(function(f) {
1449
+ return !/\.txt$/i.test(f.name);
1450
+ });
1451
+ } catch(e) {
1452
+ handleImportError(e, file.name);
1453
+ files = [];
1454
+ }
1455
+ return files;
1456
+ }
1457
+
1458
+ async function readFileData(file) {
1459
+ try {
1460
+ var content = await runAsync(readContentFileAsync, file);
1461
+ return {
1462
+ content: content,
1463
+ size: content.byteLength || content.length, // ArrayBuffer or string
1464
+ name: file.name,
1465
+ basename: internal.getFileBase(file.name).toLowerCase(),
1466
+ type: internal.guessInputType(file.name, content)
1467
+ };
1468
+ } catch (e) {
1469
+ handleImportError("Web browser was unable to load the file.", file.name);
1470
+ }
1471
+ return null;
1472
+ }
1473
+
1474
+ async function readFiles(files) {
1475
+ var data = [], d;
1476
+ for (var file of files) {
1477
+ d = await readFileData(file);
1478
+ if (d) data.push(d);
1479
+ }
1480
+ return data;
1481
+ }
1482
+
1483
+ function groupFilesForImport(data, importOpts) {
1484
+ var names = importOpts.name ? [importOpts.name] : null;
1485
+ if (initialImport && opts.name) { // name from mapshaper-gui --name option
1486
+ names = opts.name.split(',');
1487
+ }
1488
+
1489
+ function key(basename, type) {
1490
+ return basename + '.' + type;
1491
+ }
1492
+ function hasShp(basename) {
1493
+ var shpKey = key(basename, 'shp');
1494
+ return data.some(d => key(d.basename, d.type) == shpKey);
1495
+ }
1496
+ data.forEach(d => {
1497
+ if (d.type == 'shp' || !isShapefilePart(d.name)) {
1498
+ d.group = key(d.basename, d.type);
1499
+ d.filename = d.name;
1500
+ } else if (hasShp(d.basename)) {
1501
+ d.group = key(d.basename, 'shp');
1502
+ } else if (d.type == 'dbf') {
1503
+ d.filename = d.name;
1504
+ d.group = key(d.basename, 'dbf');
1556
1505
  } else {
1557
- blob.name = item.basename;
1558
- memo.push(blob);
1506
+ // shapefile part without a .shp file
1507
+ d.group = null;
1559
1508
  }
1560
- next(err, memo);
1561
1509
  });
1562
- req.open('GET', item.url);
1563
- req.send();
1510
+ var index = {};
1511
+ var groups = [];
1512
+ data.forEach(d => {
1513
+ if (!d.group) return;
1514
+ var g = index[d.group];
1515
+ if (!g) {
1516
+ g = {};
1517
+ g.layername = names ? names[groups.length] || names[names.length - 1] : null;
1518
+ groups.push(g);
1519
+ index[d.group] = g;
1520
+ }
1521
+ g.size = (g.size || 0) + d.size; // accumulate size
1522
+ g[d.type] = {
1523
+ filename: d.name,
1524
+ content: d.content
1525
+ };
1526
+ // kludge: stash import name for session history
1527
+ if (d.filename) g.filename = d.filename;
1528
+ });
1529
+ return groups;
1564
1530
  }
1565
1531
  }
1566
1532
 
@@ -2222,6 +2188,13 @@
2222
2188
  // expose this function, so other components can run commands (e.g. box tool)
2223
2189
  this.runMapshaperCommands = runMapshaperCommands;
2224
2190
 
2191
+ this.runInitialCommands = function(str) {
2192
+ str = str.trim();
2193
+ if (!str) return;
2194
+ turnOn();
2195
+ submit(str);
2196
+ };
2197
+
2225
2198
  consoleMessage(PROMPT);
2226
2199
  gui.keyboard.on('keydown', onKeyDown);
2227
2200
  window.addEventListener('beforeunload', saveHistory); // save history if console is open on refresh
@@ -3584,7 +3557,7 @@
3584
3557
 
3585
3558
  // import { cloneShape } from '../paths/mapshaper-shape-utils';
3586
3559
  // import { copyRecord } from '../datatable/mapshaper-data-utils';
3587
- var snapVerticesToPoint$1 = internal.snapVerticesToPoint;
3560
+ var snapVerticesToPoint = internal.snapVerticesToPoint;
3588
3561
  var cloneShape = internal.cloneShape;
3589
3562
  var copyRecord = internal.copyRecord;
3590
3563
 
@@ -3693,7 +3666,7 @@
3693
3666
  var arcs = target.dataset.arcs;
3694
3667
  var p = internal.getVertexCoords(ids[0], arcs);
3695
3668
  return function() {
3696
- snapVerticesToPoint$1(ids, p, arcs, true);
3669
+ snapVerticesToPoint(ids, p, arcs, true);
3697
3670
  };
3698
3671
  };
3699
3672
 
@@ -4133,13 +4106,38 @@
4133
4106
  return self;
4134
4107
  }
4135
4108
 
4136
- function getShapeHitTest(displayLayer, ext) {
4109
+ function absArcId(arcId) {
4110
+ return arcId >= 0 ? arcId : ~arcId;
4111
+ }
4112
+
4113
+ function calcArcBounds(xx, yy, start, len) {
4114
+ var i = start | 0,
4115
+ n = isNaN(len) ? xx.length - i : len + i,
4116
+ x, y, xmin, ymin, xmax, ymax;
4117
+ if (n > 0) {
4118
+ xmin = xmax = xx[i];
4119
+ ymin = ymax = yy[i];
4120
+ }
4121
+ for (i++; i<n; i++) {
4122
+ x = xx[i];
4123
+ y = yy[i];
4124
+ if (x < xmin) xmin = x;
4125
+ if (x > xmax) xmax = x;
4126
+ if (y < ymin) ymin = y;
4127
+ if (y > ymax) ymax = y;
4128
+ }
4129
+ return [xmin, ymin, xmax, ymax];
4130
+ }
4131
+
4132
+ function getShapeHitTest(displayLayer, ext, interactionMode) {
4137
4133
  var geoType = displayLayer.layer.geometry_type;
4138
4134
  var test;
4139
4135
  if (geoType == 'point' && displayLayer.style.type == 'styled') {
4140
4136
  test = getGraduatedCircleTest(getRadiusFunction(displayLayer.style));
4141
4137
  } else if (geoType == 'point') {
4142
4138
  test = pointTest;
4139
+ } else if (interactionMode == 'vertices') {
4140
+ test = vertexTest;
4143
4141
  } else if (geoType == 'polyline') {
4144
4142
  test = polylineTest;
4145
4143
  } else if (geoType == 'polygon') {
@@ -4165,14 +4163,14 @@
4165
4163
  }
4166
4164
 
4167
4165
  function polygonTest(x, y) {
4168
- var maxDist = getZoomAdjustedHitBuffer(5, 1),
4166
+ var maxDist = getZoomAdjustedHitBuffer(10, 1),
4169
4167
  cands = findHitCandidates(x, y, maxDist),
4170
4168
  hits = [],
4171
4169
  cand, hitId;
4172
4170
  for (var i=0; i<cands.length; i++) {
4173
4171
  cand = cands[i];
4174
4172
  if (geom.testPointInPolygon(x, y, cand.shape, displayLayer.arcs)) {
4175
- hits.push(cand.id);
4173
+ hits.push(cand);
4176
4174
  }
4177
4175
  }
4178
4176
  if (cands.length > 0 && hits.length === 0) {
@@ -4180,7 +4178,9 @@
4180
4178
  sortByDistance(x, y, cands, displayLayer.arcs);
4181
4179
  hits = pickNearestCandidates(cands, 0, maxDist);
4182
4180
  }
4183
- return hits;
4181
+ return {
4182
+ ids: utils.pluck(hits, 'id')
4183
+ };
4184
4184
  }
4185
4185
 
4186
4186
  function pickNearestCandidates(sorted, bufDist, maxDist) {
@@ -4195,22 +4195,41 @@
4195
4195
  } else if (cand.dist - minDist > bufDist) {
4196
4196
  break;
4197
4197
  }
4198
- hits.push(cand.id);
4198
+ hits.push(cand);
4199
4199
  }
4200
4200
  return hits;
4201
4201
  }
4202
4202
 
4203
+ function vertexTest(x, y) {
4204
+ var maxDist = getZoomAdjustedHitBuffer(15, 2),
4205
+ bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines
4206
+ cands = findHitCandidates(x, y, maxDist);
4207
+ sortByDistance(x, y, cands, displayLayer.arcs);
4208
+ cands = pickNearestCandidates(cands, bufDist, maxDist);
4209
+ var arcs = cands.map(function(cand) { return absArcId(cand.info.arcId); });
4210
+ return {
4211
+ arcs: utils.uniq(arcs),
4212
+ ids: utils.pluck(cands, 'id')
4213
+ };
4214
+ }
4215
+
4203
4216
  function polylineTest(x, y) {
4204
4217
  var maxDist = getZoomAdjustedHitBuffer(15, 2),
4205
4218
  bufDist = getZoomAdjustedHitBuffer(0.05), // tiny threshold for hitting almost-identical lines
4206
4219
  cands = findHitCandidates(x, y, maxDist);
4207
4220
  sortByDistance(x, y, cands, displayLayer.arcs);
4208
- return pickNearestCandidates(cands, bufDist, maxDist);
4221
+ cands = pickNearestCandidates(cands, bufDist, maxDist);
4222
+ return {
4223
+ ids: utils.pluck(cands, 'id')
4224
+ };
4209
4225
  }
4210
4226
 
4211
4227
  function sortByDistance(x, y, cands, arcs) {
4228
+ var cand;
4212
4229
  for (var i=0; i<cands.length; i++) {
4213
- cands[i].dist = geom.getPointToShapeDistance(x, y, cands[i].shape, arcs);
4230
+ cand = cands[i];
4231
+ cand.info = geom.getPointToShapeInfo(x, y, cands[i].shape, arcs);
4232
+ cand.dist = cand.info.distance;
4214
4233
  }
4215
4234
  utils.sortOn(cands, 'dist');
4216
4235
  }
@@ -4238,7 +4257,9 @@
4238
4257
  }
4239
4258
  });
4240
4259
  // console.log(hitThreshold, bullseye);
4241
- return utils.uniq(hits); // multipoint features can register multiple hits
4260
+ return {
4261
+ ids: utils.uniq(hits) // multipoint features can register multiple hits
4262
+ };
4242
4263
  }
4243
4264
 
4244
4265
  function getRadiusFunction(style) {
@@ -4292,7 +4313,9 @@
4292
4313
  hits.push(id);
4293
4314
  }
4294
4315
  });
4295
- return hits;
4316
+ return {
4317
+ ids: hits
4318
+ };
4296
4319
  };
4297
4320
  }
4298
4321
 
@@ -4429,20 +4452,18 @@
4429
4452
 
4430
4453
  }
4431
4454
 
4432
- function getPointerHitTest(mapLayer, ext) {
4455
+ function getPointerHitTest(mapLayer, ext, interactionMode) {
4433
4456
  var shapeTest, svgTest, targetLayer;
4434
4457
  if (!mapLayer || !internal.layerHasGeometry(mapLayer.layer)) {
4435
- return null;
4458
+ return function() {return {ids: []};};
4436
4459
  }
4437
- shapeTest = getShapeHitTest(mapLayer, ext);
4460
+ shapeTest = getShapeHitTest(mapLayer, ext, interactionMode);
4438
4461
  svgTest = getSvgHitTest(mapLayer);
4439
4462
 
4440
4463
  // e: pointer event
4441
4464
  return function(e) {
4442
4465
  var p = ext.translatePixelCoords(e.x, e.y);
4443
- var data = {
4444
- ids: shapeTest(p[0], p[1]) || []
4445
- };
4466
+ var data = shapeTest(p[0], p[1]) || {ids:[]};
4446
4467
  var svgData = svgTest(e); // null or a data object
4447
4468
  if (svgData) { // mouse is over an SVG symbol
4448
4469
  utils.extend(data, svgData);
@@ -4502,22 +4523,25 @@
4502
4523
  }, !!'capture'); // preempt the layer control's arrow key handler
4503
4524
 
4504
4525
  self.setLayer = function(mapLayer) {
4505
- hitTest = getPointerHitTest(mapLayer, ext);
4506
- if (!hitTest) {
4507
- hitTest = function() {return {ids: []};};
4508
- }
4509
4526
  targetLayer = mapLayer;
4527
+ updateHitTest();
4510
4528
  };
4511
4529
 
4530
+ function updateHitTest() {
4531
+ hitTest = getPointerHitTest(targetLayer, ext, interactionMode);
4532
+ }
4533
+
4512
4534
  function turnOn(mode) {
4513
4535
  interactionMode = mode;
4514
4536
  active = true;
4537
+ updateHitTest();
4515
4538
  }
4516
4539
 
4517
4540
  function turnOff() {
4518
4541
  if (active) {
4519
4542
  updateSelectionState(null); // no hit data, no event
4520
4543
  active = false;
4544
+ hitTest = null;
4521
4545
  }
4522
4546
  }
4523
4547
 
@@ -4561,6 +4585,20 @@
4561
4585
  }
4562
4586
  };
4563
4587
 
4588
+ self.setHoverVertex = function(p) {
4589
+ var p2 = storedData.hit_coordinates;
4590
+ if (!active || !p) return;
4591
+ if (p2 && p2[0] == p[0] && p2[1] == p[1]) return;
4592
+ storedData.hit_coordinates = p;
4593
+ triggerHitEvent('change');
4594
+ };
4595
+
4596
+ self.clearVertexOverlay = function() {
4597
+ if (!storedData.hit_coordinates) return;
4598
+ delete storedData.hit_coordinates;
4599
+ triggerHitEvent('change');
4600
+ };
4601
+
4564
4602
  self.clearSelection = function() {
4565
4603
  updateSelectionState(null);
4566
4604
  };
@@ -5901,50 +5939,108 @@
5901
5939
  rec[key] = isString ? String(newVal) : newVal;
5902
5940
  }
5903
5941
 
5904
- var snapVerticesToPoint = internal.snapVerticesToPoint;
5905
-
5906
- function getDisplayCoordsById(id, layer, ext) {
5907
- var coords = getPointCoordsById(id, layer);
5908
- return ext.translateCoords(coords[0], coords[1]);
5909
- }
5942
+ function initLabelDragging(gui, ext, hit) {
5943
+ var downEvt;
5944
+ var activeId;
5945
+ var activeRecord;
5910
5946
 
5911
- function getPointCoordsById(id, layer) {
5912
- var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
5913
- if (!coords || coords.length != 1) {
5914
- return null;
5947
+ function active(e) {
5948
+ return e.id > -1 && gui.interaction.getMode() == 'labels';
5915
5949
  }
5916
- return coords[0];
5917
- }
5918
5950
 
5919
- function translateDeltaDisplayCoords(dx, dy, ext) {
5920
- var a = ext.translatePixelCoords(0, 0);
5921
- var b = ext.translatePixelCoords(dx, dy);
5922
- return [b[0] - a[0], b[1] - a[1]];
5923
- }
5951
+ hit.on('dragstart', function(e) {
5952
+ if (!active(e)) return;
5953
+ var textNode = getTextTarget3(e);
5954
+ var table = hit.getTargetDataTable();
5955
+ if (!textNode || !table) return false;
5956
+ activeId = e.id;
5957
+ activeRecord = getLabelRecordById(activeId);
5958
+ downEvt = e;
5959
+ gui.dispatchEvent('label_dragstart', {FID: activeId});
5960
+ });
5924
5961
 
5962
+ hit.on('drag', function(e) {
5963
+ if (!active(e)) return;
5964
+ if (e.id != activeId) {
5965
+ error("Mismatched hit ids:", e.id, activeId);
5966
+ }
5967
+ var scale = ext.getSymbolScale() || 1;
5968
+ var textNode;
5969
+ applyDelta(activeRecord, 'dx', e.dx / scale);
5970
+ applyDelta(activeRecord, 'dy', e.dy / scale);
5971
+ textNode = getTextTarget3(e);
5972
+ if (!isMultilineLabel(textNode)) {
5973
+ // update anchor position of single-line labels based on label position
5974
+ // relative to anchor point, for better placement when eventual display font is
5975
+ // different from mapshaper's font.
5976
+ autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext));
5977
+ }
5978
+ // updateSymbol(targetTextNode, activeRecord);
5979
+ updateSymbol2(textNode, activeRecord, activeId);
5980
+ });
5925
5981
 
5926
- function InteractiveEditor(gui, ext, hit) {
5927
- // var targetTextNode; // text node currently being dragged
5928
- var dragging = false;
5929
- var activeRecord;
5930
- var activeId = -1;
5931
- var self = new EventDispatcher();
5932
- var activeVertexIds = null; // for vertex dragging
5982
+ hit.on('dragend', function(e) {
5983
+ if (!active(e)) return;
5984
+ gui.dispatchEvent('label_dragend', {FID: e.id});
5985
+ activeId = -1;
5986
+ activeRecord = null;
5987
+ downEvt = null;
5988
+ });
5933
5989
 
5934
- initDragging();
5990
+ function getDisplayCoordsById(id, layer, ext) {
5991
+ var coords = getPointCoordsById(id, layer);
5992
+ return ext.translateCoords(coords[0], coords[1]);
5993
+ }
5935
5994
 
5936
- return self;
5995
+ function getPointCoordsById(id, layer) {
5996
+ var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
5997
+ if (!coords || coords.length != 1) {
5998
+ return null;
5999
+ }
6000
+ return coords[0];
6001
+ }
5937
6002
 
5938
- function labelEditingEnabled() {
5939
- return gui.interaction && gui.interaction.getMode() == 'labels' ? true : false;
6003
+ function getTextTarget3(e) {
6004
+ if (e.id > -1 === false || !e.container) return null;
6005
+ return getSymbolNodeById(e.id, e.container);
5940
6006
  }
5941
6007
 
5942
- function locationEditingEnabled() {
5943
- return gui.interaction && gui.interaction.getMode() == 'location' ? true : false;
6008
+ function getSymbolNodeById(id, parent) {
6009
+ // TODO: optimize selector
6010
+ var sel = '[data-id="' + id + '"]';
6011
+ return parent.querySelector(sel);
6012
+ }
6013
+
6014
+ function getTextTarget2(e) {
6015
+ var el = e && e.targetSymbol || null;
6016
+ if (el && el.tagName == 'tspan') {
6017
+ el = el.parentNode;
6018
+ }
6019
+ return el && el.tagName == 'text' ? el : null;
5944
6020
  }
5945
6021
 
5946
- function vertexEditingEnabled() {
5947
- return gui.interaction && gui.interaction.getMode() == 'vertices' ? true : false;
6022
+ function getTextTarget(e) {
6023
+ var el = e.target;
6024
+ if (el.tagName == 'tspan') {
6025
+ el = el.parentNode;
6026
+ }
6027
+ return el.tagName == 'text' ? el : null;
6028
+ }
6029
+
6030
+ function getLabelRecordById(id) {
6031
+ var table = hit.getTargetDataTable();
6032
+ if (id >= 0 === false || !table) return null;
6033
+ // add dx and dy properties, if not available
6034
+ if (!table.fieldExists('dx')) {
6035
+ table.addField('dx', 0);
6036
+ }
6037
+ if (!table.fieldExists('dy')) {
6038
+ table.addField('dy', 0);
6039
+ }
6040
+ if (!table.fieldExists('text-anchor')) {
6041
+ table.addField('text-anchor', '');
6042
+ }
6043
+ return table.getRecordAt(id);
5948
6044
  }
5949
6045
 
5950
6046
  // update symbol by setting attributes
@@ -5971,249 +6067,167 @@
5971
6067
  gui.dispatchEvent('popup-needs-refresh');
5972
6068
  return node2;
5973
6069
  }
6070
+ }
5974
6071
 
5975
- function initDragging() {
5976
- var downEvt;
5977
- var eventPriority = 1;
5978
-
5979
- // inspector and label editing aren't fully synced - stop editing if inspector opens
5980
- // gui.on('inspector_on', function() {
5981
- // stopEditing();
5982
- // });
5983
-
5984
- gui.on('interaction_mode_change', function(e) {
5985
- if (e.mode != 'labels') {
5986
- stopDragging();
5987
- }
5988
- gui.undo.clear(); // TODO: put this elsewhere?
5989
- });
5990
-
5991
- // down event on svg
5992
- // a: off text
5993
- // -> stop editing
5994
- // b: on text
5995
- // 1: not editing -> nop
5996
- // 2: on selected text -> start dragging
5997
- // 3: on other text -> stop dragging, select new text
5998
-
5999
- hit.on('dragstart', function(e) {
6000
- if (e.id >= 0 === false) return;
6001
- if (labelEditingEnabled() && onLabelDragStart(e)) {
6002
- triggerGlobalEvent('label_dragstart', e);
6003
- startDragging();
6004
- } else if (locationEditingEnabled()) {
6005
- triggerGlobalEvent('symbol_dragstart', e);
6006
- startDragging();
6007
- } else if (vertexEditingEnabled()) {
6008
- onVertexDragStart(e);
6009
- triggerGlobalEvent('vertex_dragstart', e);
6010
- startDragging();
6011
- }
6012
- });
6013
-
6014
- hit.on('drag', function(e) {
6015
- if (labelEditingEnabled()) {
6016
- onLabelDrag(e);
6017
- } else if (locationEditingEnabled()) {
6018
- onLocationDrag(e);
6019
- } else if (vertexEditingEnabled()) {
6020
- onVertexDrag(e);
6021
- }
6022
- });
6023
-
6024
- hit.on('dragend', function(e) {
6025
- if (locationEditingEnabled()) {
6026
- triggerGlobalEvent('symbol_dragend', e);
6027
- stopDragging();
6028
- } else if (labelEditingEnabled()) {
6029
- triggerGlobalEvent('label_dragend', e);
6030
- stopDragging();
6031
- } else if (vertexEditingEnabled()) {
6032
- // kludge to get dataset to recalculate internal bounding boxes
6033
- hit.getHitTarget().arcs.transformPoints(function() {});
6034
- triggerGlobalEvent('vertex_dragend', e);
6035
- stopDragging();
6036
- }
6037
- });
6038
-
6039
- hit.on('click', function(e) {
6040
- if (labelEditingEnabled()) {
6041
- var target = hit.getHitTarget();
6042
- onLabelClick(e);
6043
- }
6044
- });
6072
+ function initPointDragging(gui, ext, hit) {
6045
6073
 
6046
- // TODO: highlight hit vertex in path edit mode
6047
- if (false) hit.on('hover', function(e) {
6048
- if (vertexEditingEnabled() && !dragging) {
6049
- onVertexHover(e);
6050
- }
6051
- }, null, 100);
6074
+ function active(e) {
6075
+ return e.id > -1 && gui.interaction.getMode() == 'location';
6076
+ }
6052
6077
 
6053
- function onVertexHover(e) {
6054
- // hovering in vertex edit mode: find vertex insertion point
6055
- var target = hit.getHitTarget();
6056
- var shp = target.layer.shapes[e.id];
6057
- var p = ext.translatePixelCoords(e.x, e.y);
6058
- var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize());
6059
- }
6078
+ hit.on('dragstart', function(e) {
6079
+ if (!active(e)) return;
6080
+ gui.dispatchEvent('symbol_dragstart', {FID: e.id});
6081
+ });
6060
6082
 
6083
+ hit.on('drag', function(e) {
6084
+ if (!active(e)) return;
6085
+ var lyr = hit.getHitTarget().layer;
6086
+ var p = getPointCoordsById(e.id, lyr);
6087
+ if (!p) return;
6088
+ var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
6089
+ p[0] += diff[0];
6090
+ p[1] += diff[1];
6091
+ gui.dispatchEvent('map-needs-refresh');
6092
+ gui.dispatchEvent('symbol_drag', {FID: e.id});
6093
+ });
6061
6094
 
6062
- function getVertexEventData(e) {
6063
- return {
6064
- FID: activeId,
6065
- vertexIds: activeVertexIds
6066
- };
6067
- }
6095
+ hit.on('dragend', function(e) {
6096
+ if (!active(e)) return;
6097
+ gui.dispatchEvent('symbol_dragend', {FID: e.id});
6098
+ });
6068
6099
 
6069
- function onLocationDrag(e) {
6070
- var lyr = hit.getHitTarget().layer;
6071
- var p = getPointCoordsById(e.id, lyr);
6072
- if (!p) return;
6073
- var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
6074
- p[0] += diff[0];
6075
- p[1] += diff[1];
6076
- triggerRedraw();
6077
- triggerGlobalEvent('symbol_drag', e);
6078
- }
6100
+ function translateDeltaDisplayCoords(dx, dy, ext) {
6101
+ var a = ext.translatePixelCoords(0, 0);
6102
+ var b = ext.translatePixelCoords(dx, dy);
6103
+ return [b[0] - a[0], b[1] - a[1]];
6104
+ }
6079
6105
 
6080
- function onVertexDragStart(e) {
6081
- var target = hit.getHitTarget();
6082
- var shp = target.layer.shapes[e.id];
6083
- var p = ext.translatePixelCoords(e.x, e.y);
6084
- activeVertexIds = internal.findNearestVertices(p, shp, target.arcs);
6085
- activeId = e.id;
6106
+ function getPointCoordsById(id, layer) {
6107
+ var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
6108
+ if (!coords || coords.length != 1) {
6109
+ return null;
6086
6110
  }
6111
+ return coords[0];
6112
+ }
6113
+ }
6087
6114
 
6088
- function onVertexDrag(e) {
6089
- var target = hit.getHitTarget();
6090
- if (!activeVertexIds) return; // ignore error condition
6091
- var p = ext.translatePixelCoords(e.x, e.y);
6092
- if (gui.keyboard.shiftIsPressed()) {
6093
- internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs);
6094
- }
6095
- snapVerticesToPoint(activeVertexIds, p, target.arcs);
6096
- triggerRedraw();
6097
- }
6115
+ function initVertexDragging(gui, ext, hit) {
6116
+ var activeShapeId = -1;
6117
+ var draggedVertexIds = null;
6118
+ var selectedVertexIds = null;
6098
6119
 
6099
- function onLabelClick(e) {
6100
- var textNode = getTextTarget3(e);
6101
- var rec = getLabelRecordById(e.id);
6102
- if (textNode && rec && isMultilineLabel(textNode)) {
6103
- toggleTextAlign(textNode, rec);
6104
- updateSymbol2(textNode, rec, e.id);
6105
- // e.stopPropagation(); // prevent pin/unpin on popup
6106
- }
6107
- }
6120
+ function active(e) {
6121
+ return e.id > -1 && gui.interaction.getMode() == 'vertices';
6122
+ }
6108
6123
 
6109
- function triggerRedraw() {
6110
- gui.dispatchEvent('map-needs-refresh');
6111
- }
6124
+ function fire(type) {
6125
+ gui.dispatchEvent(type, {
6126
+ FID: activeShapeId,
6127
+ vertex_ids: draggedVertexIds
6128
+ });
6129
+ }
6112
6130
 
6113
- function triggerGlobalEvent(type, e) {
6114
- if (e.id >= 0 === false) return;
6115
- var o = {
6116
- FID: e.id,
6117
- layer_name: hit.getHitTarget().layer.name,
6118
- vertex_ids: activeVertexIds
6119
- };
6120
- // fire event to signal external editor that symbol coords have changed
6121
- gui.dispatchEvent(type, o);
6122
- }
6131
+ function setHoverVertex(id) {
6132
+ var target = hit.getHitTarget();
6133
+ hit.setHoverVertex(target.arcs.getVertex2(id));
6134
+ }
6123
6135
 
6124
- function getLabelRecordById(id) {
6125
- var table = hit.getTargetDataTable();
6126
- if (id >= 0 === false || !table) return null;
6127
- // add dx and dy properties, if not available
6128
- if (!table.fieldExists('dx')) {
6129
- table.addField('dx', 0);
6130
- }
6131
- if (!table.fieldExists('dy')) {
6132
- table.addField('dy', 0);
6133
- }
6134
- if (!table.fieldExists('text-anchor')) {
6135
- table.addField('text-anchor', '');
6136
- }
6137
- return table.getRecordAt(id);
6138
- }
6136
+ function clearHoverVertex() {
6137
+ hit.clearVertexOverlay();
6138
+ // gui.state.vertex_overlay = null;
6139
+ }
6139
6140
 
6140
- function onLabelDragStart(e) {
6141
- var textNode = getTextTarget3(e);
6142
- var table = hit.getTargetDataTable();
6143
- if (!textNode || !table) return false;
6144
- activeId = e.id;
6145
- activeRecord = getLabelRecordById(activeId);
6146
- downEvt = e;
6147
- return true;
6141
+ function findDraggableVertices(e) {
6142
+ var target = hit.getHitTarget();
6143
+ var shp = target.layer.shapes[e.id];
6144
+ var p = ext.translatePixelCoords(e.x, e.y);
6145
+ var nearestIds = internal.findNearestVertices(p, shp, target.arcs);
6146
+ var p2 = target.arcs.getVertex2(nearestIds[0]);
6147
+ var dist = geom.distance2D(p[0], p[1], p2[0], p2[1]);
6148
+ var pixelDist = dist / ext.getPixelSize();
6149
+ if (pixelDist > 5) {
6150
+ draggedVertexIds = null;
6151
+ return null;
6148
6152
  }
6153
+ return nearestIds;
6154
+ }
6149
6155
 
6150
- function onLabelDrag(e) {
6151
- var scale = ext.getSymbolScale() || 1;
6152
- var textNode;
6153
- if (!dragging) return;
6154
- if (e.id != activeId) {
6155
- error("Mismatched hit ids:", e.id, activeId);
6156
- }
6157
- applyDelta(activeRecord, 'dx', e.dx / scale);
6158
- applyDelta(activeRecord, 'dy', e.dy / scale);
6159
- textNode = getTextTarget3(e);
6160
- if (!isMultilineLabel(textNode)) {
6161
- // update anchor position of single-line labels based on label position
6162
- // relative to anchor point, for better placement when eventual display font is
6163
- // different from mapshaper's font.
6164
- autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext));
6165
- }
6166
- // updateSymbol(targetTextNode, activeRecord);
6167
- updateSymbol2(textNode, activeRecord, activeId);
6168
- }
6156
+ hit.on('dragstart', function(e) {
6157
+ if (!active(e)) return;
6158
+ draggedVertexIds = findDraggableVertices(e);
6159
+ if (!draggedVertexIds) return;
6160
+ setHoverVertex(draggedVertexIds[0]);
6161
+ activeShapeId = e.id;
6162
+ fire('vertex_dragstart');
6163
+ });
6169
6164
 
6170
- function getSymbolNodeById(id, parent) {
6171
- // TODO: optimize selector
6172
- var sel = '[data-id="' + id + '"]';
6173
- return parent.querySelector(sel);
6165
+ hit.on('drag', function(e) {
6166
+ if (!active(e) || !draggedVertexIds) return;
6167
+ var target = hit.getHitTarget();
6168
+ var p = ext.translatePixelCoords(e.x, e.y);
6169
+ if (gui.keyboard.shiftIsPressed()) {
6170
+ internal.snapPointToArcEndpoint(p, draggedVertexIds, target.arcs);
6174
6171
  }
6172
+ internal.snapVerticesToPoint(draggedVertexIds, p, target.arcs);
6173
+ setHoverVertex(draggedVertexIds[0]);
6174
+ // redrawing the whole map updates the data layer as well as the overlay layer
6175
+ // gui.dispatchEvent('map-needs-refresh');
6176
+ });
6175
6177
 
6176
- function getTextTarget3(e) {
6177
- if (e.id > -1 === false || !e.container) return null;
6178
- return getSymbolNodeById(e.id, e.container);
6179
- }
6178
+ hit.on('dragend', function(e) {
6179
+ if (!active(e) || !draggedVertexIds) return;
6180
+ // kludge to get dataset to recalculate internal bounding boxes
6181
+ hit.getHitTarget().arcs.transformPoints(function() {});
6182
+ clearHoverVertex();
6183
+ fire('vertex_dragend');
6184
+ draggedVertexIds = null;
6185
+ activeShapeId = -1;
6186
+ // redraw data layer
6187
+ gui.dispatchEvent('map-needs-refresh');
6188
+ });
6180
6189
 
6181
- function getTextTarget2(e) {
6182
- var el = e && e.targetSymbol || null;
6183
- if (el && el.tagName == 'tspan') {
6184
- el = el.parentNode;
6185
- }
6186
- return el && el.tagName == 'text' ? el : null;
6187
- }
6190
+ // select clicked vertices
6191
+ hit.on('click', function(e) {
6192
+ if (!active(e)) return;
6193
+ var vertices = findDraggableVertices(e); // same selection criteria as for dragging
6194
+ // TODO
6195
+ });
6188
6196
 
6189
- function getTextTarget(e) {
6190
- var el = e.target;
6191
- if (el.tagName == 'tspan') {
6192
- el = el.parentNode;
6193
- }
6194
- return el.tagName == 'text' ? el : null;
6197
+ // highlight hit vertex in path edit mode
6198
+ hit.on('hover', function(e) {
6199
+ if (!active(e) || draggedVertexIds) return; // no hover effect while dragging
6200
+ var vertexIds = findDraggableVertices(e);
6201
+ if (vertexIds) {
6202
+ setHoverVertex(vertexIds[0]);
6203
+ return;
6195
6204
  }
6196
- }
6205
+ var target = hit.getHitTarget();
6206
+ var shp = target.layer.shapes[e.id];
6207
+ var p = ext.translatePixelCoords(e.x, e.y);
6208
+ var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize());
6209
+ console.log('*', o, p);
6210
+ clearHoverVertex();
6211
+ }, null, 100);
6197
6212
 
6198
- function startDragging() {
6199
- dragging = true;
6200
- }
6213
+ }
6201
6214
 
6202
- function stopDragging() {
6203
- dragging = false;
6204
- activeId = -1;
6205
- activeRecord = null;
6206
- activeVertexIds = null;
6207
- }
6215
+ function initInteractiveEditing(gui, ext, hit) {
6216
+ initLabelDragging(gui, ext, hit);
6217
+ initPointDragging(gui, ext, hit);
6218
+ initVertexDragging(gui, ext, hit);
6208
6219
 
6209
- function isClickEvent(up, down) {
6210
- var elapsed = Math.abs(down.timeStamp - up.timeStamp);
6211
- var dx = up.screenX - down.screenX;
6212
- var dy = up.screenY - down.screenY;
6213
- var dist = Math.sqrt(dx * dx + dy * dy);
6214
- return dist <= 4 && elapsed < 300;
6215
- }
6220
+ gui.on('interaction_mode_change', function(e) {
6221
+ gui.undo.clear(); // TODO: put this elsewhere?
6222
+ });
6216
6223
 
6224
+ // function isClickEvent(up, down) {
6225
+ // var elapsed = Math.abs(down.timeStamp - up.timeStamp);
6226
+ // var dx = up.screenX - down.screenX;
6227
+ // var dy = up.screenY - down.screenY;
6228
+ // var dist = Math.sqrt(dx * dx + dy * dy);
6229
+ // return dist <= 4 && elapsed < 300;
6230
+ // }
6217
6231
  }
6218
6232
 
6219
6233
  var darkStroke = "#334",
@@ -6384,8 +6398,14 @@
6384
6398
  var geomType = lyr.geometry_type;
6385
6399
  var topId = o.id; // pinned id (if pinned) or hover id
6386
6400
  var topIdx = -1;
6387
- var styler = function(o, i) {
6388
- utils.extend(o, i === topIdx ? topStyle: baseStyle);
6401
+ var styler = function(style, i) {
6402
+ utils.extend(style, i === topIdx ? topStyle: baseStyle);
6403
+ // kludge to show vertices when editing path shapes
6404
+ if (o.mode == 'vertices') {
6405
+ style.vertices = true;
6406
+ style.vertex_overlay = o.hit_coordinates || null;
6407
+ style.fillColor = null;
6408
+ }
6389
6409
  };
6390
6410
  var baseStyle = getDefaultStyle(lyr, selectionStyles[geomType]);
6391
6411
  var topStyle;
@@ -6856,19 +6876,25 @@
6856
6876
  _self.drawVertices = function(shapes, arcs, style, filter) {
6857
6877
  var iter = new internal.ShapeIter(arcs);
6858
6878
  var t = getScaledTransform(_ext);
6859
- var radius = (style.strokeWidth * 0.9 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext);
6879
+ var radius = (style.strokeWidth > 2 ? style.strokeWidth * 0.9 : 2) * GUI.getPixelRatio() * getScaledLineScale(_ext);
6860
6880
  var color = style.strokeColor || 'black';
6861
- var shp;
6881
+ var radius2 = radius * 2;
6862
6882
  _ctx.beginPath();
6863
6883
  _ctx.fillStyle = color;
6864
6884
  for (var i=0; i<shapes.length; i++) {
6865
- shp = shapes[i];
6885
+ var shp = shapes[i];
6866
6886
  if (!shp || filter && !filter(shp)) continue;
6867
- iter.init(shp);
6868
- while (iter.hasNext()) {
6869
- drawCircle(iter.x * t.mx + t.bx, iter.y * t.my + t.by, radius, _ctx);
6887
+ for (var j=0; j<shp.length; j++) {
6888
+ iter.init(shp[j]);
6889
+ while (iter.hasNext()) {
6890
+ drawCircle(iter.x * t.mx + t.bx, iter.y * t.my + t.by, radius, _ctx);
6891
+ }
6870
6892
  }
6871
6893
  }
6894
+ if (style.vertex_overlay) {
6895
+ var p = style.vertex_overlay;
6896
+ drawCircle(p[0] * t.mx + t.bx, p[1] * t.my + t.by, radius2, _ctx);
6897
+ }
6872
6898
  _ctx.fill();
6873
6899
  _ctx.closePath();
6874
6900
  };
@@ -7921,7 +7947,7 @@
7921
7947
  _visibleLayers = [], // cached visible map layers
7922
7948
  _fullBounds = null,
7923
7949
  _intersectionLyr, _activeLyr, _overlayLyr,
7924
- _inspector, _stack, _editor,
7950
+ _inspector, _stack,
7925
7951
  _dynamicCRS;
7926
7952
 
7927
7953
  if (gui.options.showMouseCoordinates) {
@@ -8104,7 +8130,9 @@
8104
8130
  });
8105
8131
  }
8106
8132
 
8107
- _editor = new InteractiveEditor(gui, _ext, _hit);
8133
+ if (gui.interaction) {
8134
+ initInteractiveEditing(gui, _ext, _hit);
8135
+ }
8108
8136
 
8109
8137
  _ext.on('change', function(e) {
8110
8138
  if (e.reset) return; // don't need to redraw map here if extent has been reset
@@ -8124,10 +8152,6 @@
8124
8152
  function updateOverlayLayer(e) {
8125
8153
  var style = getOverlayStyle(_activeLyr.layer, e);
8126
8154
  if (style) {
8127
- // kludge to show vertices when editing path shapes
8128
- if (gui.state.interaction_mode == 'vertices') {
8129
- style.vertices = true;
8130
- }
8131
8155
  _overlayLyr = utils.defaults({
8132
8156
  layer: filterLayerByIds(_activeLyr.layer, style.ids),
8133
8157
  style: style
@@ -8431,14 +8455,14 @@
8431
8455
  startEditing();
8432
8456
  });
8433
8457
 
8434
- function getImportOpts() {
8458
+ function getManifest() {
8459
+ return window.mapshaper.manifest || {}; // kludge -- bin/mapshaper-gui sets this
8460
+ }
8461
+
8462
+ function getImportOpts(manifest) {
8435
8463
  var vars = GUI.getUrlVars();
8436
8464
  var opts = {};
8437
- var manifest = window.mapshaper.manifest || {}; // kludge -- bin/mapshaper-gui sets this
8438
- if (Array.isArray(manifest)) {
8439
- // old-style manifest: an array of filenames
8440
- opts.files = manifest;
8441
- } else if (manifest.files) {
8465
+ if (manifest.files) {
8442
8466
  opts.files = manifest.files.concat();
8443
8467
  } else {
8444
8468
  opts.files = [];
@@ -8452,14 +8476,24 @@
8452
8476
  opts.display_all = vars['display-all'] || vars.a || !!manifest.display_all;
8453
8477
  opts.quick_view = vars['quick-view'] || vars.q || !!manifest.quick_view;
8454
8478
  opts.target = vars.target || manifest.target || null;
8479
+ opts.name = vars.name || manifest.name || null;
8455
8480
  return opts;
8456
8481
  }
8457
8482
 
8483
+ function getInitialConsoleCommands() {
8484
+ return getManifest().commands || '';
8485
+ }
8486
+
8458
8487
  var startEditing = function() {
8459
8488
  var dataLoaded = false,
8460
- importOpts = getImportOpts(),
8489
+ manifest = getManifest(),
8490
+ importOpts = getImportOpts(manifest),
8461
8491
  gui = new GuiInstance('body');
8462
8492
 
8493
+ if (manifest.blurb) {
8494
+ El('#splash-screen-blurb').text(manifest.blurb);
8495
+ }
8496
+
8463
8497
  new AlertControl(gui);
8464
8498
  new RepairControl(gui);
8465
8499
  new SimplifyControl(gui);
@@ -8496,6 +8530,8 @@
8496
8530
  gui.map.setLayerPinning(o, true);
8497
8531
  });
8498
8532
  }
8533
+ gui.console.runInitialCommands(getInitialConsoleCommands());
8534
+
8499
8535
  });
8500
8536
  };
8501
8537