mapshaper 0.6.65 → 0.6.66

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.
@@ -1714,6 +1714,47 @@
1714
1714
  }
1715
1715
  }
1716
1716
 
1717
+ function openAddLayerPopup(gui) {
1718
+ var popup = showPopupAlert('', 'Add empty layer');
1719
+ var el = popup.container();
1720
+ el.addClass('option-menu');
1721
+ var html = `<div><input type="text" class="layer-name text-input" placeholder="layer name"></div>
1722
+ <div style="margin: 2px 0 4px;">
1723
+ Type: &nbsp;
1724
+ <label><input type="radio" name="geomtype" checked value="point" class="radio">point</label> &nbsp;
1725
+ <label><input type="radio" name="geomtype" value="polygon" class="radio">polygon</label> &nbsp;
1726
+ <label><input type="radio" name="geomtype" value="polyline" class="radio">line</label>
1727
+ </div>
1728
+ <div tabindex="0" class="btn dialog-btn">Create</div></span>`;
1729
+ el.html(html);
1730
+ var name = el.findChild('.layer-name');
1731
+ name.node().focus();
1732
+ var btn = el.findChild('.btn').on('click', function() {
1733
+ var nameStr = name.node().value.trim();
1734
+ var type = el.findChild('input:checked').node().value;
1735
+ addLayer(gui, nameStr, type);
1736
+ popup.close();
1737
+ });
1738
+ }
1739
+
1740
+ function addLayer(gui, name, type) {
1741
+ var targ = gui.model.getActiveLayer();
1742
+ var crsInfo = targ && internal.getDatasetCrsInfo(targ.dataset);
1743
+ var dataset = {
1744
+ layers: [{
1745
+ name: name || undefined,
1746
+ geometry_type: type,
1747
+ shapes: []
1748
+ }],
1749
+ info: {}
1750
+ };
1751
+ if (crsInfo) {
1752
+ internal.setDatasetCrsInfo(dataset, crsInfo);
1753
+ }
1754
+ gui.model.addDataset(dataset);
1755
+ gui.model.updated({select: true});
1756
+ }
1757
+
1717
1758
  // @cb function(<FileList>)
1718
1759
  function DropControl(gui, el, cb) {
1719
1760
  var area = El(el);
@@ -1832,7 +1873,10 @@
1832
1873
  new FileChooser('#file-selection-btn', receiveFiles);
1833
1874
  new FileChooser('#import-buttons .add-btn', receiveFiles);
1834
1875
  new FileChooser('#add-file-btn', receiveFiles);
1835
- // new SimpleButton('#add-empty-btn').on('click', addEmptyLayer);
1876
+ new SimpleButton('#add-empty-btn').on('click', function() {
1877
+ gui.clearMode(); // close import dialog
1878
+ openAddLayerPopup(gui);
1879
+ });
1836
1880
  initDropArea('#import-quick-drop', true);
1837
1881
  initDropArea('#import-drop');
1838
1882
  gui.keyboard.onMenuSubmit(El('#import-options'), importQueuedFiles);
@@ -2040,18 +2084,7 @@
2040
2084
  return true;
2041
2085
  }
2042
2086
 
2043
- function addEmptyLayer() {
2044
- var dataset = {
2045
- layers: [{
2046
- name: 'New layer',
2047
- geometry_type: 'point',
2048
- shapes: []
2049
- }],
2050
- info: {}
2051
- };
2052
- model.addDataset(dataset);
2053
- gui.clearMode();
2054
- }
2087
+
2055
2088
 
2056
2089
  function filesMayContainPaths(files) {
2057
2090
  return utils$1.some(files, function(f) {
@@ -2565,6 +2598,7 @@
2565
2598
  }
2566
2599
  }
2567
2600
 
2601
+ // coords: [x, y] point in data CRS (not display CRS)
2568
2602
  function setPointCoords(lyr, fid, coords) {
2569
2603
  lyr.source.layer.shapes[fid] = coords;
2570
2604
  if (isProjectedLayer(lyr)) {
@@ -2602,7 +2636,7 @@
2602
2636
  return !!(lyr.source && lyr.invertPoint);
2603
2637
  }
2604
2638
 
2605
- // Update data coordinates by projecting display coordinates
2639
+ // Update source data coordinates by projecting display coordinates
2606
2640
  function updatePointCoords(lyr, fid) {
2607
2641
  if (!isProjectedLayer(lyr)) return;
2608
2642
  var displayShp = lyr.layer.shapes[fid];
@@ -3875,7 +3909,7 @@
3875
3909
  async function exportMenuSelection(targets) {
3876
3910
  var opts = getExportOpts();
3877
3911
  // note: command line "target" option gets ignored
3878
- var files = await internal.exportTargetLayers(targets, opts);
3912
+ var files = await internal.exportTargetLayers(model, targets, opts);
3879
3913
  gui.session.layersExported(getTargetLayerIds(), getExportOptsAsString());
3880
3914
  if (files.length == 1 && checkboxOn(clipboardCheckbox)) {
3881
3915
  await saveFileContentToClipboard(files[0].content);
@@ -4656,6 +4690,19 @@
4656
4690
  addHistoryState(stashedUndo, redo);
4657
4691
  });
4658
4692
 
4693
+ gui.on('rectangle_dragend', function(e) {
4694
+ var target = e.data.target;
4695
+ var points1 = e.points;
4696
+ var points2 = e.ids.map(id => getVertexCoords(target, id));
4697
+ var undo = function() {
4698
+ setRectangleCoords(target, e.ids, points1);
4699
+ };
4700
+ var redo = function() {
4701
+ setRectangleCoords(target, e.ids, points2);
4702
+ };
4703
+ addHistoryState(undo, redo);
4704
+ });
4705
+
4659
4706
  gui.on('vertex_dragend', function(e) {
4660
4707
  var target = e.data.target;
4661
4708
  var startPoint = e.points[0]; // in data coords
@@ -4701,6 +4748,9 @@
4701
4748
  }
4702
4749
 
4703
4750
  this.undo = function() {
4751
+ // firing even if history is empty
4752
+ // (because this event may trigger a new history state)
4753
+ gui.dispatchEvent('undo_redo_pre');
4704
4754
  var item = getHistoryItem();
4705
4755
  if (item) {
4706
4756
  offset++;
@@ -4710,6 +4760,7 @@
4710
4760
  };
4711
4761
 
4712
4762
  this.redo = function() {
4763
+ gui.dispatchEvent('undo_redo_pre');
4713
4764
  if (offset <= 0) return;
4714
4765
  offset--;
4715
4766
  var item = getHistoryItem();
@@ -4783,6 +4834,30 @@
4783
4834
  });
4784
4835
  }
4785
4836
 
4837
+ function ToggleButton(el) {
4838
+ var btn = El(el),
4839
+ self = new EventDispatcher(),
4840
+ on = false;
4841
+
4842
+ btn.on('click', function(e) {
4843
+ on = !on;
4844
+ btn.classed('active', on);
4845
+ self.dispatchEvent('click', {on: on, active: on});
4846
+ });
4847
+
4848
+ self.turnOff = function() {
4849
+ on = false;
4850
+ btn.removeClass('active');
4851
+ };
4852
+
4853
+ self.turnOn = function() {
4854
+ on = true;
4855
+ btn.addClass('active');
4856
+ };
4857
+
4858
+ return self;
4859
+ }
4860
+
4786
4861
  function ModeSwitcher() {
4787
4862
  var self = this;
4788
4863
  var mode = null;
@@ -4872,11 +4947,10 @@
4872
4947
  standard: ['info', 'selection', 'data', 'box'],
4873
4948
  polygons: ['info', 'selection', 'data', 'box', 'vertices'],
4874
4949
  rectangles: ['info', 'selection', 'data', 'box', 'rectangles', 'vertices'],
4875
- lines: ['info', 'selection', 'data', 'box', 'vertices'],
4950
+ lines: ['info', 'selection', 'data', 'box', 'vertices' /*, 'draw-lines'*/],
4876
4951
  table: ['info', 'selection', 'data'],
4877
- labels: ['info', 'selection', 'data', 'box', 'labels', 'location'],
4878
- // points: ['info', 'selection', 'data', 'box', 'location', 'add-points']
4879
- points: ['info', 'selection', 'data', 'box', 'location']
4952
+ labels: ['info', 'selection', 'data', 'box', 'labels', 'location', 'add-points'],
4953
+ points: ['info', 'selection', 'data', 'box', 'location', 'add-points']
4880
4954
  };
4881
4955
 
4882
4956
  var prompts = {
@@ -4895,6 +4969,7 @@
4895
4969
  vertices: 'edit vertices',
4896
4970
  selection: 'select features',
4897
4971
  'add-points': 'add points',
4972
+ 'draw-lines': 'draw lines',
4898
4973
  rectangles: 'drag-to-resize',
4899
4974
  off: 'turn off'
4900
4975
  };
@@ -5138,7 +5213,9 @@
5138
5213
  };
5139
5214
 
5140
5215
  self.selectLayer = function(lyr, dataset) {
5141
- if (self.getActiveLayer().layer == lyr) return;
5216
+ if (self.getActiveLayer().layer == lyr) {
5217
+ return;
5218
+ }
5142
5219
  self.setDefaultTarget([lyr], dataset);
5143
5220
  self.updated({select: true});
5144
5221
  gui.session.setTargetLayer(lyr);
@@ -6844,7 +6921,6 @@
6844
6921
  return internal.svg.getTransform(p, scale);
6845
6922
  }
6846
6923
 
6847
-
6848
6924
  function repositionSymbols(elements, layer, ext) {
6849
6925
  var el, idx, shp, p, displayOn, inView, displayBounds;
6850
6926
  for (var i=0, n=elements.length; i<n; i++) {
@@ -6868,7 +6944,7 @@
6868
6944
  }
6869
6945
  }
6870
6946
 
6871
- function renderSymbols(lyr, ext, type) {
6947
+ function renderSymbols(lyr, ext) {
6872
6948
  var records = lyr.data.getRecords();
6873
6949
  var symbols = lyr.shapes.map(function(shp, i) {
6874
6950
  var d = records[i];
@@ -6976,6 +7052,8 @@
6976
7052
  var interactionMode;
6977
7053
  var targetLayer;
6978
7054
  var hitTest;
7055
+ var pinnedOn; // used in multi-edit mode (selection) for toggling pinning behavior
7056
+
6979
7057
  // event priority is higher than navigation, so stopping propagation disables
6980
7058
  // pan navigation
6981
7059
  var priority = 2;
@@ -7034,6 +7112,7 @@
7034
7112
  updateSelectionState(null); // no hit data, no event
7035
7113
  active = false;
7036
7114
  hitTest = null;
7115
+ pinnedOn = false;
7037
7116
  }
7038
7117
  }
7039
7118
 
@@ -7070,6 +7149,13 @@
7070
7149
  updateSelectionState({ids: ids});
7071
7150
  };
7072
7151
 
7152
+ self.setPinning = function(val) {
7153
+ if (pinnedOn != val) {
7154
+ pinnedOn = val;
7155
+ triggerHitEvent('change');
7156
+ }
7157
+ };
7158
+
7073
7159
  self.setTransientIds = function(ids) {
7074
7160
  // turnOn('selection');
7075
7161
  transientIds = ids || [];
@@ -7139,7 +7225,7 @@
7139
7225
  // make sure popup is unpinned and turned off when switching editing modes
7140
7226
  // (some modes do not support pinning)
7141
7227
  gui.on('interaction_mode_change', function(e) {
7142
- updateSelectionState(null);
7228
+ self.clearSelection();
7143
7229
  // if (e.mode == 'off' || e.mode == 'box') {
7144
7230
  if (gui.interaction.modeUsesSelection(e.mode)) {
7145
7231
  turnOn(e.mode);
@@ -7148,6 +7234,10 @@
7148
7234
  }
7149
7235
  });
7150
7236
 
7237
+ gui.on('undo_redo_pre', function() {
7238
+ self.clearSelection();
7239
+ });
7240
+
7151
7241
  gui.on('box_drag_start', function() {
7152
7242
  self.clearHover();
7153
7243
  });
@@ -7300,6 +7390,9 @@
7300
7390
  if (transientIds.length) {
7301
7391
  eventData.ids = utils$1.uniq(transientIds.concat(eventData.ids || []));
7302
7392
  }
7393
+ if (pinnedOn) {
7394
+ eventData.pinned = true;
7395
+ }
7303
7396
  self.dispatchEvent(type, eventData);
7304
7397
  }
7305
7398
 
@@ -7856,14 +7949,17 @@
7856
7949
  var scale = gui.map.getExtent().getPixelSize();
7857
7950
  var dx = (xy.x - prevXY.x) * scale;
7858
7951
  var dy = -(xy.y - prevXY.y) * scale;
7859
- if (activeHandle.col == 'left') {
7952
+ var center = activeHandle.col == 'center' && activeHandle.row == 'center';
7953
+ if (activeHandle.col == 'left' || center) {
7860
7954
  boxCoords[0] += dx;
7861
- } else if (activeHandle.col == 'right') {
7955
+ }
7956
+ if (activeHandle.col == 'right' || center) {
7862
7957
  boxCoords[2] += dx;
7863
7958
  }
7864
- if (activeHandle.row == 'top') {
7959
+ if (activeHandle.row == 'top' || center) {
7865
7960
  boxCoords[3] += dy;
7866
- } else if (activeHandle.row == 'bottom') {
7961
+ }
7962
+ if (activeHandle.row == 'bottom' || center) {
7867
7963
  boxCoords[1] += dy;
7868
7964
  }
7869
7965
  prevXY = xy;
@@ -7974,7 +8070,7 @@
7974
8070
  function initHandles(el) {
7975
8071
  var handles = [];
7976
8072
  for (var i=0; i<9; i++) {
7977
- if (i == 4) continue;
8073
+ // if (i == 4) continue; // skip middle handle
7978
8074
  var c = Math.floor(i / 3);
7979
8075
  var r = i % 3;
7980
8076
  handles.push({
@@ -8211,6 +8307,7 @@
8211
8307
  }
8212
8308
 
8213
8309
  function turnOff() {
8310
+ dataBtn.turnOff();
8214
8311
  box.turnOff();
8215
8312
  reset();
8216
8313
  _on = false;
@@ -8221,10 +8318,17 @@
8221
8318
  }
8222
8319
 
8223
8320
  function reset() {
8224
- popup.hide();
8321
+ hidePopup();
8322
+ setPinning(false);
8225
8323
  hit.clearSelection();
8226
8324
  }
8227
8325
 
8326
+ function setPinning(on) {
8327
+ hit.setPinning(on);
8328
+ if (on) dataBtn.turnOn();
8329
+ else dataBtn.turnOff();
8330
+ }
8331
+
8228
8332
  function getIdsOpt() {
8229
8333
  return hit.getSelectionIds().join(',');
8230
8334
  }
@@ -8239,11 +8343,15 @@
8239
8343
  popup.show();
8240
8344
  updateCoords();
8241
8345
  } else {
8242
- popup.hide();
8243
- hideCoords();
8346
+ hidePopup();
8244
8347
  }
8245
8348
  });
8246
8349
 
8350
+ function hidePopup() {
8351
+ popup.hide();
8352
+ hideCoords();
8353
+ }
8354
+
8247
8355
  new SimpleButton(popup.findChild('.delete-btn')).on('click', function() {
8248
8356
  var cmd = '-filter invert ids=' + getIdsOpt();
8249
8357
  runCommand(cmd);
@@ -8259,6 +8367,10 @@
8259
8367
  runCommand(cmd);
8260
8368
  });
8261
8369
 
8370
+ var dataBtn = new ToggleButton(popup.findChild('.data-btn')).on('click', function(e) {
8371
+ setPinning(e.on);
8372
+ });
8373
+
8262
8374
  new SimpleButton(popup.findChild('.duplicate-btn')).on('click', function() {
8263
8375
  var cmd = '-filter + name=selection ids=' + getIdsOpt();
8264
8376
  runCommand(cmd);
@@ -8296,19 +8408,19 @@
8296
8408
  hideCoords();
8297
8409
  return;
8298
8410
  }
8299
- El(coordsBtn.node()).addClass('selected-btn');
8411
+ El(coordsBtn.node()).addClass('active');
8300
8412
  coords.text(bbox.join(','));
8301
8413
  coords.show();
8302
8414
  GUI.selectElement(coords.node());
8303
8415
  }
8304
8416
 
8305
8417
  function hideCoords() {
8306
- El(coordsBtn.node()).removeClass('selected-btn');
8418
+ El(coordsBtn.node()).removeClass('active');
8307
8419
  coords.hide();
8308
8420
  }
8309
8421
 
8310
8422
  function runCommand(cmd, turnOff) {
8311
- popup.hide();
8423
+ hidePopup();
8312
8424
  gui.quiet(true);
8313
8425
  if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) {
8314
8426
  gui.quiet(false);
@@ -8318,6 +8430,55 @@
8318
8430
  }
8319
8431
  }
8320
8432
 
8433
+ function openAddFieldPopup(gui, ids, lyr) {
8434
+ var popup = showPopupAlert('', 'Add field');
8435
+ var el = popup.container();
8436
+ el.addClass('option-menu');
8437
+ var html = `<div><input type="text" class="field-name text-input" placeholder="field name"></div>
8438
+ <div><input type="text" class="field-value text-input" placeholder="value"><div>
8439
+ <div tabindex="0" class="btn dialog-btn">Apply</div> <span class="inline-checkbox"><input type="checkbox" class="all" />assign value to all records</span>`;
8440
+ el.html(html);
8441
+
8442
+ var name = el.findChild('.field-name');
8443
+ name.node().focus();
8444
+ var val = el.findChild('.field-value');
8445
+ var box = el.findChild('.all');
8446
+ var btn = el.findChild('.btn').on('click', function() {
8447
+ var table = internal.getLayerDataTable(lyr); // creates new table if missing
8448
+ var all = box.node().checked;
8449
+ var nameStr = name.node().value.trim();
8450
+ if (!nameStr) return;
8451
+ if (table.fieldExists(nameStr)) {
8452
+ name.node().value = '';
8453
+ return;
8454
+ }
8455
+ var valStr = val.node().value.trim();
8456
+ var value = internal.parseUnknownType(valStr);
8457
+ // table.addField(nameStr, function(d) {
8458
+ // // parse each time to avoid multiple references to objects
8459
+ // return (all || d == rec) ? parseUnknownType(valStr) : null;
8460
+ // });
8461
+
8462
+ var cmdStr = `-each "d['${nameStr}'] = `;
8463
+ if (!all) {
8464
+ cmdStr += ids.length == 1 ?
8465
+ `this.id != ${ids[0]}` :
8466
+ `!${JSON.stringify(ids)}.includes(this.id)`;
8467
+ cmdStr += ' ? null : ';
8468
+ }
8469
+ valStr = JSON.stringify(JSON.stringify(value)); // add escapes to strings
8470
+ cmdStr = valStr.replace('"', cmdStr);
8471
+
8472
+ gui.console.runMapshaperCommands(cmdStr, function(err) {
8473
+ if (!err) {
8474
+ popup.close();
8475
+ } else {
8476
+ console.error(err);
8477
+ }
8478
+ });
8479
+ });
8480
+ }
8481
+
8321
8482
  // toNext, toPrev: trigger functions for switching between multiple records
8322
8483
  function Popup(gui, toNext, toPrev) {
8323
8484
  var self = new EventDispatcher();
@@ -8331,7 +8492,6 @@
8331
8492
  var navInfo = El('span').addClass('popup-nav-info').appendTo(nav);
8332
8493
  var nextLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('▶');
8333
8494
  var refresh = null;
8334
- var currIds = [];
8335
8495
 
8336
8496
  el.addClass('rollover'); // used as a sentinel for the hover function
8337
8497
 
@@ -8342,16 +8502,20 @@
8342
8502
  });
8343
8503
 
8344
8504
  self.show = function(id, ids, lyr, pinned) {
8345
- var editable = pinned && gui.interaction.getMode() == 'data';
8505
+ var singleEdit = pinned && gui.interaction.getMode() == 'data';
8506
+ var multiEdit = pinned && gui.interaction.getMode() == 'selection';
8346
8507
  var maxHeight = parent.node().clientHeight - 36;
8347
- currIds = [id];
8508
+ var recIds = multiEdit ? ids : [id];
8509
+
8348
8510
  // stash a function for refreshing the current popup when data changes
8349
8511
  // while the popup is being displayed (e.g. while dragging a label)
8350
8512
  refresh = function() {
8351
- render(content, id, lyr, editable);
8513
+ render(content, recIds, lyr, singleEdit || multiEdit);
8352
8514
  };
8353
8515
  refresh();
8354
- if (ids && ids.length > 1) {
8516
+ if (multiEdit) {
8517
+ showRecords(ids.length);
8518
+ } else if (ids && ids.length > 1 && !multiEdit) {
8355
8519
  showNav(id, ids, pinned);
8356
8520
  } else {
8357
8521
  tab.hide();
@@ -8365,7 +8529,6 @@
8365
8529
  self.hide = function() {
8366
8530
  if (!isOpen()) return;
8367
8531
  refresh = null;
8368
- currIds = [];
8369
8532
  // make sure any pending edits are made before re-rendering popup
8370
8533
  GUI.blurActiveElement(); // this should be more selective -- could cause a glitch if typing in console
8371
8534
  content.empty();
@@ -8379,6 +8542,13 @@
8379
8542
  return el.visible();
8380
8543
  }
8381
8544
 
8545
+ function showRecords(n) {
8546
+ navInfo.text(n);
8547
+ nextLink.css('display','none');
8548
+ prevLink.css('display','none');
8549
+ tab.show();
8550
+ }
8551
+
8382
8552
  function showNav(id, ids, pinned) {
8383
8553
  var num = ids.indexOf(id) + 1;
8384
8554
  navInfo.text(' ' + num + ' / ' + ids.length + ' ');
@@ -8387,28 +8557,12 @@
8387
8557
  tab.show();
8388
8558
  }
8389
8559
 
8390
- function render(el, recId, lyr, editable) {
8391
- var recIds = [recId]; // TODO: support multiple ids
8560
+ function render(el, recIds, lyr, editable) {
8392
8561
  var table = lyr.data; // table can be null (e.g. if layer has no attribute data)
8393
- var rec = table && (editable ? table.getRecordAt(recId) : table.getReadOnlyRecordAt(recId)) || {};
8394
- var tableEl = El('table').addClass('selectable'),
8395
- rows = 0;
8396
- // self.hide(); // clean up if panel is already open
8562
+ var tableEl = table ? renderTable(recIds, table, editable) : null;
8397
8563
  el.empty(); // clean up if panel is already open
8398
- utils$1.forEachProperty(rec, function(v, k) {
8399
- var type;
8400
- // missing GeoJSON fields are set to undefined on import; skip these
8401
- if (v !== undefined) {
8402
- type = getFieldType(v, k, table);
8403
- renderRow(tableEl, rec, k, type, editable);
8404
- rows++;
8405
- }
8406
- });
8407
-
8408
- tableEl.appendTo(el);
8409
- if (rows > 0) {
8410
- // tableEl.appendTo(el);
8411
-
8564
+ if (tableEl) {
8565
+ tableEl.appendTo(el);
8412
8566
  tableEl.on('copy', function(e) {
8413
8567
  // remove leading or trailing tabs that sometimes get copied when
8414
8568
  // selecting from a table
@@ -8419,7 +8573,6 @@
8419
8573
  e.preventDefault(); // don't copy original string with tabs
8420
8574
  }
8421
8575
  });
8422
-
8423
8576
  } else {
8424
8577
  // Some individual features can have undefined values for some or all of
8425
8578
  // their data properties (properties are set to undefined when an input JSON file
@@ -8433,72 +8586,65 @@
8433
8586
  var line = El('div').appendTo(el);
8434
8587
  El('span').addClass('add-field-btn').appendTo(line).on('click', async function(e) {
8435
8588
  // show "add field" dialog
8436
- renderAddFieldPopup(recIds, lyr);
8589
+ openAddFieldPopup(gui, recIds, lyr);
8437
8590
  }).text('+ add field');
8438
8591
  }
8439
8592
  }
8440
8593
 
8441
- function renderAddFieldPopup(ids, lyr) {
8442
- var popup = showPopupAlert('', 'Add field');
8443
- var el = popup.container();
8444
- el.addClass('option-menu');
8445
- var html = `<div><input type="text" class="field-name text-input" placeholder="field name"></div>
8446
- <div><input type="text" class="field-value text-input" placeholder="value"><div>
8447
- <div tabindex="0" class="btn dialog-btn">Apply</div> <span class="inline-checkbox"><input type="checkbox" class="all" />assign value to all records</span>`;
8448
- el.html(html);
8449
-
8450
- var name = el.findChild('.field-name');
8451
- name.node().focus();
8452
- var val = el.findChild('.field-value');
8453
- var box = el.findChild('.all');
8454
- var btn = el.findChild('.btn').on('click', function() {
8455
- var table = internal.getLayerDataTable(lyr); // creates new table if missing
8456
- var all = box.node().checked;
8457
- var nameStr = name.node().value.trim();
8458
- if (!nameStr) return;
8459
- if (table.fieldExists(nameStr)) {
8460
- name.node().value = '';
8461
- return;
8594
+ function renderTable(recIds, table, editable) {
8595
+ var tableEl = El('table').addClass('selectable');
8596
+ var rows = 0;
8597
+ var rec;
8598
+ if (recIds.length == 1) {
8599
+ rec = editable ?
8600
+ table.getReadOnlyRecordAt(recIds[0]) :
8601
+ table.getRecordAt(recIds[0]);
8602
+ } else {
8603
+ rec = getMultiRecord(recIds, table);
8604
+ }
8605
+ utils$1.forEachProperty(rec, function(v, k) {
8606
+ // missing GeoJSON fields are set to undefined on import; skip these
8607
+ if (v === undefined) return;
8608
+ var rowEl = renderRow(k, v, recIds, table, editable);
8609
+ if (rowEl) {
8610
+ rowEl.appendTo(tableEl);
8611
+ rows++;
8462
8612
  }
8463
- var valStr = val.node().value.trim();
8464
- var value = parseUnknownType(valStr);
8465
- // table.addField(nameStr, function(d) {
8466
- // // parse each time to avoid multiple references to objects
8467
- // return (all || d == rec) ? parseUnknownType(valStr) : null;
8468
- // });
8469
-
8470
- var cmdStr = `-each "d['${nameStr}'] = `;
8471
- if (!all) {
8472
- cmdStr += ids.length == 1 ?
8473
- `this.id != ${ids[0]}` :
8474
- `!${JSON.stringify(ids)}.includes(this.id)`;
8475
- cmdStr += ' ? null : ';
8476
- }
8477
- valStr = JSON.stringify(JSON.stringify(value)); // add escapes to strings
8478
- cmdStr = valStr.replace('"', cmdStr);
8479
-
8480
- gui.console.runMapshaperCommands(cmdStr, function(err) {
8481
- if (!err) {
8482
- popup.close();
8483
- } else {
8484
- console.error(err);
8613
+ });
8614
+ return rows > 0 ? tableEl : null;
8615
+ }
8616
+
8617
+ function getMultiRecord(recIds, table) {
8618
+ var fields = table.getFields();
8619
+ var rec = {};
8620
+ recIds.forEach(function(id) {
8621
+ var d = table.getRecordAt(id) || {};
8622
+ var k, v;
8623
+ for (var i=0; i<fields.length; i++) {
8624
+ k = fields[i];
8625
+ v = d[k];
8626
+ if (k in rec === false) {
8627
+ rec[k] = v;
8628
+ } else if (rec[k] !== v) {
8629
+ rec[k] = null;
8485
8630
  }
8486
- });
8631
+ }
8487
8632
  });
8633
+ return rec;
8488
8634
  }
8489
8635
 
8490
- function renderRow(table, rec, key, type, editable) {
8491
- var val = rec[key];
8636
+
8637
+ function renderRow(key, val, recIds, table, editable) {
8638
+ var type = getFieldType(val, key, table);
8492
8639
  var str = formatInspectorValue(val, type);
8493
8640
  var rowHtml = `<td class="field-name">${key}</td><td><span class="value">${utils$1.htmlEscape(str)}</span> </td>`;
8494
- var cell = El('tr')
8495
- .appendTo(table)
8496
- .html(rowHtml)
8497
- .findChild('.value');
8498
- setFieldClass(cell, val, type);
8641
+ var rowEl = El('tr').html(rowHtml);
8642
+ var cellEl = rowEl.findChild('.value');
8643
+ setFieldClass(cellEl, val, type);
8499
8644
  if (editable) {
8500
- editItem(cell, rec, key, type);
8645
+ editItem(cellEl, key, val, recIds, table, type);
8501
8646
  }
8647
+ return rowEl;
8502
8648
  }
8503
8649
 
8504
8650
  function setFieldClass(el, val, type) {
@@ -8511,10 +8657,10 @@
8511
8657
  el.classed('empty', isEmpty);
8512
8658
  }
8513
8659
 
8514
- function editItem(el, rec, key, type) {
8660
+ function editItem(el, key, val, recIds, table, type) {
8515
8661
  var input = new ClickText2(el),
8516
- strval = formatInspectorValue(rec[key], type),
8517
- parser = getInputParser(type);
8662
+ strval = formatInspectorValue(val, type),
8663
+ parser = internal.getInputParser(type);
8518
8664
  el.parent().addClass('editable-cell');
8519
8665
  el.addClass('colored-text dot-underline');
8520
8666
  input.on('change', function(e) {
@@ -8526,17 +8672,27 @@
8526
8672
  } else if (strval != strval2) {
8527
8673
  // field content has changed
8528
8674
  strval = strval2;
8529
- gui.dispatchEvent('data_preupdate', {ids: currIds}); // for undo/redo
8530
- rec[key] = val2;
8531
- gui.dispatchEvent('data_postupdate', {ids: currIds});
8675
+ gui.dispatchEvent('data_preupdate', {ids: recIds}); // for undo/redo
8676
+ // rec[key] = val2;
8677
+ updateRecords(recIds, key, val2, table);
8678
+ gui.dispatchEvent('data_postupdate', {ids: recIds});
8532
8679
  input.value(strval);
8533
8680
  setFieldClass(el, val2, type);
8534
- self.dispatchEvent('data_updated', {field: key, value: val2, ids: currIds});
8681
+ self.dispatchEvent('data_updated', {field: key, value: val2, ids: recIds});
8535
8682
  }
8536
8683
  });
8537
8684
  }
8538
8685
  }
8539
8686
 
8687
+ function updateRecords(ids, f, v, table) {
8688
+ var records = table.getRecords();
8689
+ ids.forEach(function(id) {
8690
+ var d = records[id] || {};
8691
+ d[f] = v;
8692
+ records[id] = d;
8693
+ });
8694
+ }
8695
+
8540
8696
  function formatInspectorValue(val, type) {
8541
8697
  var str;
8542
8698
  if (type == 'date') {
@@ -8549,60 +8705,10 @@
8549
8705
  return str;
8550
8706
  }
8551
8707
 
8552
- var inputParsers = {
8553
- date: function(raw) {
8554
- var d = new Date(raw);
8555
- return isNaN(+d) ? null : d;
8556
- },
8557
- string: function(raw) {
8558
- return raw;
8559
- },
8560
- number: function(raw) {
8561
- var val = Number(raw);
8562
- if (raw == 'NaN') {
8563
- val = NaN;
8564
- } else if (isNaN(val)) {
8565
- val = null;
8566
- }
8567
- return val;
8568
- },
8569
- object: function(raw) {
8570
- var val = null;
8571
- try {
8572
- val = JSON.parse(raw);
8573
- } catch(e) {}
8574
- return val;
8575
- },
8576
- boolean: function(raw) {
8577
- var val = null;
8578
- if (raw == 'true') {
8579
- val = true;
8580
- } else if (raw == 'false') {
8581
- val = false;
8582
- }
8583
- return val;
8584
- },
8585
- multiple: function(raw) {
8586
- var val = Number(raw);
8587
- return isNaN(val) ? raw : val;
8588
- }
8589
- };
8590
-
8591
- function parseUnknownType(str) {
8592
- var val = inputParsers.number(str);
8593
- if (val !== null) return val;
8594
- val = inputParsers.object(str);
8595
- if (val !== null) return val;
8596
- return str;
8597
- }
8598
-
8599
- function getInputParser(type) {
8600
- return inputParsers[type || 'multiple'];
8601
- }
8602
8708
 
8603
8709
  function getFieldType(val, key, table) {
8604
8710
  // if a field has a null value, look at entire column to identify type
8605
- return internal.getValueType(val) || internal.getColumnType(key, table.getRecords());
8711
+ return internal.getValueType(val) || internal.getColumnType(key, table.getRecords()) ;
8606
8712
  }
8607
8713
 
8608
8714
  function InspectionControl2(gui, hit) {
@@ -8627,16 +8733,20 @@
8627
8733
  });
8628
8734
 
8629
8735
  hit.on('change', function(e) {
8630
- var ids;
8631
8736
  if (!inspecting()) return;
8632
- ids = e.mode == 'selection' ? null : e.ids;
8633
- inspect(e.id, e.pinned, ids);
8737
+ var ids;
8738
+ if (e.mode == 'selection') {
8739
+ ids = e.pinned && e.ids || [];
8740
+ } else {
8741
+ ids = e.ids || [];
8742
+ }
8743
+ inspect(e.id, ids, e.pinned);
8634
8744
  });
8635
8745
 
8636
8746
  // id: Id of a feature in the active layer, or -1
8637
- function inspect(id, pin, ids) {
8747
+ function inspect(id, ids, pin) {
8638
8748
  var target = hit.getHitTarget();
8639
- if (id > -1 && inspecting() && target && target.layer) {
8749
+ if ((id > -1 || ids && ids.length > 0) && inspecting() && target && target.layer) {
8640
8750
  _popup.show(id, ids, target.layer, pin);
8641
8751
  } else {
8642
8752
  _popup.hide();
@@ -8935,6 +9045,9 @@
8935
9045
  hit.clearVertexOverlay();
8936
9046
  }
8937
9047
 
9048
+ // return data on the nearest vertex (or identical vertices) to the pointer
9049
+ // (if within a distance threshold)
9050
+ //
8938
9051
  function findDraggableVertices(e) {
8939
9052
  var target = hit.getHitTarget();
8940
9053
  var shp = target.layer.shapes[e.id];
@@ -9097,35 +9210,6 @@
9097
9210
  // }
9098
9211
  }
9099
9212
 
9100
- function initPointDrawing(gui, ext, hit) {
9101
- function active(e) {
9102
- return e.id > -1 && gui.interaction.getMode() == 'add-points';
9103
- }
9104
- }
9105
-
9106
- function Pencil(gui, mouse, hit) {
9107
- var self = this;
9108
- var _on = false;
9109
-
9110
- self.turnOn = function() {
9111
- _on = true;
9112
- };
9113
-
9114
- self.turnOff = function() {
9115
- _on = false;
9116
- };
9117
-
9118
-
9119
-
9120
-
9121
-
9122
-
9123
- }
9124
-
9125
- function initDrawing(gui, ext, mouse, hit) {
9126
- initPointDrawing(gui, new Pencil(gui, mouse, hit));
9127
- }
9128
-
9129
9213
  var darkStroke = "#334",
9130
9214
  lightStroke = "#b7d9ea",
9131
9215
  violet = "#cc6acc",
@@ -9452,6 +9536,59 @@
9452
9536
  return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
9453
9537
  }
9454
9538
 
9539
+ function initPointDrawing(gui, ext, mouse, hit) {
9540
+
9541
+ gui.on('interaction_mode_change', function(e) {
9542
+ gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
9543
+ });
9544
+
9545
+ function active() {
9546
+ return gui.interaction.getMode() == 'add-points';
9547
+ }
9548
+
9549
+ mouse.on('click', function(e) {
9550
+ if (!active()) return;
9551
+ addPoint(e.x, e.y);
9552
+ gui.dispatchEvent('map-needs-refresh');
9553
+ });
9554
+
9555
+ // x, y: pixel coordinates
9556
+ function addPoint(x, y) {
9557
+ var p = ext.translatePixelCoords(x, y);
9558
+ var target = hit.getHitTarget();
9559
+ var lyr = target.layer;
9560
+ var fid = lyr.shapes.length;
9561
+ var d = lyr.data ? getEmptyDataRecord(lyr.data) : null;
9562
+ if (d) {
9563
+ // this seems to work even for projected layers -- the data tables
9564
+ // of projected and original data seem to be shared.
9565
+ lyr.data.getRecords()[fid] = d;
9566
+ // TODO: handle SVG symbol layer
9567
+ if (internal.layerHasLabels(lyr)) {
9568
+ d['label-text'] = 'TBD'; // without text, new labels will be invisible
9569
+ } else if (layerHasCanvasDisplayStyle(lyr)) {
9570
+ d.r = 3; // show a black circle if layer is styled
9571
+ }
9572
+ }
9573
+ lyr.shapes[fid] = [p];
9574
+ updatePointCoords(target, fid);
9575
+ }
9576
+
9577
+ function getEmptyDataRecord(table) {
9578
+ return table.getFields().reduce(function(memo, name) {
9579
+ memo[name] = null;
9580
+ return memo;
9581
+ }, {});
9582
+ }
9583
+ }
9584
+
9585
+ // import { initLineDrawing } from './gui-draw-lines';
9586
+
9587
+ function initDrawing(gui, ext, mouse, hit) {
9588
+ initPointDrawing(gui, ext, mouse, hit);
9589
+ // initLineDrawing(gui, ext, mouse, hit);
9590
+ }
9591
+
9455
9592
  function MapExtent(_position) {
9456
9593
  var _scale = 1,
9457
9594
  _cx, _cy, // center in geographic units
@@ -10479,7 +10616,7 @@
10479
10616
  target.svg_id = id;
10480
10617
  resize(ext);
10481
10618
  if (type == 'label' || type == 'symbol') {
10482
- html = renderSymbols(target.layer, ext, type);
10619
+ html = renderSymbols(target.layer, ext);
10483
10620
  } else if (type == 'furniture') {
10484
10621
  html = renderFurniture(target.layer, ext);
10485
10622
  }
@@ -10493,13 +10630,17 @@
10493
10630
  };
10494
10631
 
10495
10632
  function reposition(target, type, ext) {
10496
- var container = el.findChild('.' + target.svg_id).node();
10633
+ var container = el.findChild('.' + target.svg_id);
10634
+ if (!container || !container.node()) {
10635
+ console.error('[reposition] missing SVG container');
10636
+ return;
10637
+ }
10497
10638
  var elements;
10498
10639
  if (type == 'symbol') {
10499
- elements = El.findAll('.mapshaper-svg-symbol', container);
10640
+ elements = El.findAll('.mapshaper-svg-symbol', container.node());
10500
10641
  repositionSymbols(elements, target.layer, ext);
10501
10642
  } else if (type == 'furniture') {
10502
- repositionFurniture(container, target.layer, ext);
10643
+ repositionFurniture(container.node(), target.layer, ext);
10503
10644
  } else {
10504
10645
  // container.getElementsByTagName('text')
10505
10646
  error('Unsupported symbol type:', type);
@@ -10514,7 +10655,7 @@
10514
10655
  return el;
10515
10656
  }
10516
10657
 
10517
- function LayerStack(gui, container, ext, mouse) {
10658
+ function LayerRenderer(gui, container, ext, mouse) {
10518
10659
  var el = El(container),
10519
10660
  _mainCanv = new DisplayCanvas().appendTo(el),
10520
10661
  _overlayCanv = new DisplayCanvas().appendTo(el),
@@ -10534,14 +10675,12 @@
10534
10675
  }
10535
10676
  layers.forEach(function(lyr) {
10536
10677
  var isSvgLayer = internal.layerHasSvgSymbols(lyr.layer) || internal.layerHasLabels(lyr.layer);
10537
- //if (!svgType || svgType == 'label') { // svg labels may have canvas dots
10538
- if (!isSvgLayer) { // svg labels may have canvas dots
10539
- drawCanvasLayer(lyr, _mainCanv);
10540
- }
10541
10678
  if (isSvgLayer && !needSvgRedraw) {
10542
10679
  _svg.reposition(lyr, 'symbol');
10543
10680
  } else if (isSvgLayer) {
10544
10681
  _svg.drawLayer(lyr, 'symbol');
10682
+ } else {
10683
+ drawCanvasLayer(lyr, _mainCanv);
10545
10684
  }
10546
10685
  });
10547
10686
  };
@@ -10639,7 +10778,8 @@
10639
10778
  });
10640
10779
 
10641
10780
  new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
10642
- runCommand('-rectangle + bbox=' + box.getDataCoords().join(','));
10781
+ var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
10782
+ runCommand(cmd);
10643
10783
  });
10644
10784
 
10645
10785
  gui.addMode('box_tool', turnOn, turnOff);
@@ -10734,7 +10874,7 @@
10734
10874
 
10735
10875
  hit.on('change', function(e) {
10736
10876
  if (!_on) return;
10737
- // TODO: handle multiple hits (see gui-inspection-control2)
10877
+ // TODO: handle multiple hits (see gui-inspection-control)
10738
10878
  var id = e.id;
10739
10879
  if (e.id > -1 && e.pinned) {
10740
10880
  var target = hit.getHitTarget();
@@ -10755,8 +10895,7 @@
10755
10895
  gui.container.findChild('.map-layers').classed('dragging', true);
10756
10896
 
10757
10897
  } else if (dragInfo) {
10758
- // TODO: handle this event: add undo/redo states
10759
- gui.dispatchEvent('rectangle_dragend', dragInfo);
10898
+ gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
10760
10899
  gui.container.findChild('.map-layers').classed('dragging', false);
10761
10900
  reset();
10762
10901
  } else {
@@ -11296,7 +11435,7 @@
11296
11435
  _hit,
11297
11436
  _basemap,
11298
11437
  _intersectionLyr, _activeLyr, _overlayLyr,
11299
- _stack, _dynamicCRS;
11438
+ _renderer, _dynamicCRS;
11300
11439
 
11301
11440
  _basemap = new Basemap(gui, _ext);
11302
11441
 
@@ -11391,13 +11530,12 @@
11391
11530
  clearAllDisplayArcs();
11392
11531
 
11393
11532
  // Reproject all visible map layers
11394
- if (_activeLyr) projectDisplayLayer(_activeLyr, newCRS);
11395
- if (_intersectionLyr) projectDisplayLayer(_intersectionLyr, newCRS);
11396
- if (_overlayLyr) {
11397
- projectDisplayLayer(_overlayLyr, newCRS);
11398
- }
11399
- updateVisibleMapLayers(); // any other display layers will be projected as they are regenerated
11400
- updateLayerStyles(getDrawableContentLayers()); // kludge to make sure all layers have styles
11533
+ getDrawableContentLayers().forEach(function(lyr) {
11534
+ projectDisplayLayer(lyr, newCRS);
11535
+ });
11536
+
11537
+ // kludge to make sure all layers have styles
11538
+ updateLayerStyles(getDrawableContentLayers());
11401
11539
 
11402
11540
  // Update map extent (also triggers redraw)
11403
11541
  projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), calcFullBounds());
@@ -11475,7 +11613,7 @@
11475
11613
  // Initialization just before displaying the map for the first time
11476
11614
  function initMap() {
11477
11615
  _ext.resize();
11478
- _stack = new LayerStack(gui, el, _ext, _mouse);
11616
+ _renderer = new LayerRenderer(gui, el, _ext, _mouse);
11479
11617
  gui.buttons.show();
11480
11618
 
11481
11619
  if (opts.inspectorControl) {
@@ -11485,7 +11623,7 @@
11485
11623
  new BoxTool(gui, _ext, _nav),
11486
11624
  new RectangleControl(gui, _hit),
11487
11625
  initInteractiveEditing(gui, _ext, _hit);
11488
- // initDrawing(gui, _ext, _mouse, _hit);
11626
+ initDrawing(gui, _ext, _mouse, _hit);
11489
11627
  _hit.on('change', updateOverlayLayer);
11490
11628
  }
11491
11629
 
@@ -11516,7 +11654,6 @@
11516
11654
 
11517
11655
  // 'hover' avoids redrawing all svg symbols when only highlight needs to refresh
11518
11656
  drawLayers('hover');
11519
- // drawLayers();
11520
11657
  }
11521
11658
 
11522
11659
  function getDisplayOptions() {
@@ -11551,10 +11688,7 @@
11551
11688
  _ext.setFullBounds(calcFullBounds(), getStrictBounds());
11552
11689
  }
11553
11690
 
11554
- function calcFullBounds() {
11555
- if (isPreviewView()) {
11556
- return internal.getFrameLayerBounds(internal.findFrameLayer(model));
11557
- }
11691
+ function getVisibleContentBounds() {
11558
11692
  var b = new Bounds();
11559
11693
  var layers = getDrawableContentLayers();
11560
11694
  layers.forEach(function(lyr) {
@@ -11565,6 +11699,14 @@
11565
11699
  // assign bounds to empty layers, to prevent rendering errors downstream
11566
11700
  b.setBounds(0,0,0,0);
11567
11701
  }
11702
+ return b;
11703
+ }
11704
+
11705
+ function calcFullBounds() {
11706
+ if (isPreviewView()) {
11707
+ return internal.getFrameLayerBounds(internal.findFrameLayer(model));
11708
+ }
11709
+ var b = getVisibleContentBounds();
11568
11710
 
11569
11711
  // add margin
11570
11712
  // use larger margin for small sizes
@@ -11590,10 +11732,6 @@
11590
11732
  return isActiveLayer(lyr) || lyr.pinned;
11591
11733
  }
11592
11734
 
11593
- function isFrameLayer(lyr) {
11594
- return !!(lyr && lyr == internal.findFrameLayer(model));
11595
- }
11596
-
11597
11735
  function isTableView() {
11598
11736
  return !isPreviewView() && !!_activeLyr.tabular;
11599
11737
  }
@@ -11725,11 +11863,11 @@
11725
11863
  }
11726
11864
  // RENDERING
11727
11865
  // draw main content layers
11728
- _stack.drawMainLayers(contentLayers, action);
11866
+ _renderer.drawMainLayers(contentLayers, action);
11729
11867
  // draw hover & selection overlay
11730
- _stack.drawOverlayLayer(_overlayLyr, action);
11868
+ _renderer.drawOverlayLayer(_overlayLyr, action);
11731
11869
  // draw furniture
11732
- _stack.drawFurnitureLayers(furnitureLayers, action);
11870
+ // _renderer.drawFurnitureLayers(furnitureLayers, action);
11733
11871
  gui.dispatchEvent('map_rendered');
11734
11872
  }
11735
11873
  }