mapshaper 0.6.65 → 0.6.67

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);
@@ -4272,6 +4306,7 @@
4272
4306
 
4273
4307
  if (opts.pinnable) classes += ' pinnable';
4274
4308
  if (map.isActiveLayer(lyr)) classes += ' active';
4309
+ if (lyr.hidden) classes += ' invisible';
4275
4310
  if (lyr.pinned) classes += ' pinned';
4276
4311
 
4277
4312
  html = '<!-- ' + lyr.menu_id + '--><div class="' + classes + '">';
@@ -4350,10 +4385,24 @@
4350
4385
  // init pin button
4351
4386
  GUI.onClick(entry.findChild('img.black-eye'), function(e) {
4352
4387
  var target = findLayerById(id);
4353
- var pinned = target.layer.pinned;
4388
+ var lyr = target.layer;
4389
+ var active = map.isActiveLayer(lyr);
4390
+ var hidden = false; // active && lyr.hidden || false;
4391
+ var pinned = false;
4392
+ var unpinned = false;
4354
4393
  e.stopPropagation();
4355
- map.setLayerPinning(target, !pinned);
4356
- entry.classed('pinned', !pinned);
4394
+ if (active) {
4395
+ hidden = !lyr.hidden;
4396
+ pinned = !hidden && lyr.unpinned;
4397
+ unpinned = lyr.pinned && hidden;
4398
+ } else {
4399
+ pinned = !lyr.pinned;
4400
+ }
4401
+ lyr.hidden = hidden;
4402
+ lyr.unpinned = unpinned;
4403
+ map.setLayerPinning(target, pinned);
4404
+ entry.classed('pinned', pinned);
4405
+ entry.classed('invisible', hidden);
4357
4406
  updatePinAllButton();
4358
4407
  map.redraw();
4359
4408
  });
@@ -4379,12 +4428,16 @@
4379
4428
  GUI.onClick(entry, function() {
4380
4429
  var target = findLayerById(id);
4381
4430
  // don't select if user is typing or dragging
4382
- if (!GUI.textIsSelected() && !dragging) {
4383
- gui.clearMode();
4384
- if (!map.isActiveLayer(target.layer)) {
4385
- model.selectLayer(target.layer, target.dataset);
4386
- }
4431
+ if (GUI.textIsSelected() || dragging) return;
4432
+ // undo any temporary hiding when layer is selected
4433
+ target.layer.hidden = false;
4434
+ if (!map.isActiveLayer(target.layer)) {
4435
+ model.selectLayer(target.layer, target.dataset);
4387
4436
  }
4437
+ // close menu after a delay
4438
+ setTimeout(function() {
4439
+ gui.clearMode();
4440
+ }, 230);
4388
4441
  });
4389
4442
  }
4390
4443
 
@@ -4656,6 +4709,19 @@
4656
4709
  addHistoryState(stashedUndo, redo);
4657
4710
  });
4658
4711
 
4712
+ gui.on('rectangle_dragend', function(e) {
4713
+ var target = e.data.target;
4714
+ var points1 = e.points;
4715
+ var points2 = e.ids.map(id => getVertexCoords(target, id));
4716
+ var undo = function() {
4717
+ setRectangleCoords(target, e.ids, points1);
4718
+ };
4719
+ var redo = function() {
4720
+ setRectangleCoords(target, e.ids, points2);
4721
+ };
4722
+ addHistoryState(undo, redo);
4723
+ });
4724
+
4659
4725
  gui.on('vertex_dragend', function(e) {
4660
4726
  var target = e.data.target;
4661
4727
  var startPoint = e.points[0]; // in data coords
@@ -4701,6 +4767,9 @@
4701
4767
  }
4702
4768
 
4703
4769
  this.undo = function() {
4770
+ // firing even if history is empty
4771
+ // (because this event may trigger a new history state)
4772
+ gui.dispatchEvent('undo_redo_pre');
4704
4773
  var item = getHistoryItem();
4705
4774
  if (item) {
4706
4775
  offset++;
@@ -4710,6 +4779,7 @@
4710
4779
  };
4711
4780
 
4712
4781
  this.redo = function() {
4782
+ gui.dispatchEvent('undo_redo_pre');
4713
4783
  if (offset <= 0) return;
4714
4784
  offset--;
4715
4785
  var item = getHistoryItem();
@@ -4783,6 +4853,30 @@
4783
4853
  });
4784
4854
  }
4785
4855
 
4856
+ function ToggleButton(el) {
4857
+ var btn = El(el),
4858
+ self = new EventDispatcher(),
4859
+ on = false;
4860
+
4861
+ btn.on('click', function(e) {
4862
+ on = !on;
4863
+ btn.classed('active', on);
4864
+ self.dispatchEvent('click', {on: on, active: on});
4865
+ });
4866
+
4867
+ self.turnOff = function() {
4868
+ on = false;
4869
+ btn.removeClass('active');
4870
+ };
4871
+
4872
+ self.turnOn = function() {
4873
+ on = true;
4874
+ btn.addClass('active');
4875
+ };
4876
+
4877
+ return self;
4878
+ }
4879
+
4786
4880
  function ModeSwitcher() {
4787
4881
  var self = this;
4788
4882
  var mode = null;
@@ -4872,11 +4966,10 @@
4872
4966
  standard: ['info', 'selection', 'data', 'box'],
4873
4967
  polygons: ['info', 'selection', 'data', 'box', 'vertices'],
4874
4968
  rectangles: ['info', 'selection', 'data', 'box', 'rectangles', 'vertices'],
4875
- lines: ['info', 'selection', 'data', 'box', 'vertices'],
4969
+ lines: ['info', 'selection', 'data', 'box', 'vertices' /*, 'draw-lines'*/],
4876
4970
  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']
4971
+ labels: ['info', 'selection', 'data', 'box', 'labels', 'location', 'add-points'],
4972
+ points: ['info', 'selection', 'data', 'box', 'location', 'add-points']
4880
4973
  };
4881
4974
 
4882
4975
  var prompts = {
@@ -4895,6 +4988,7 @@
4895
4988
  vertices: 'edit vertices',
4896
4989
  selection: 'select features',
4897
4990
  'add-points': 'add points',
4991
+ 'draw-lines': 'draw lines',
4898
4992
  rectangles: 'drag-to-resize',
4899
4993
  off: 'turn off'
4900
4994
  };
@@ -5138,7 +5232,9 @@
5138
5232
  };
5139
5233
 
5140
5234
  self.selectLayer = function(lyr, dataset) {
5141
- if (self.getActiveLayer().layer == lyr) return;
5235
+ if (self.getActiveLayer().layer == lyr) {
5236
+ return;
5237
+ }
5142
5238
  self.setDefaultTarget([lyr], dataset);
5143
5239
  self.updated({select: true});
5144
5240
  gui.session.setTargetLayer(lyr);
@@ -6844,7 +6940,6 @@
6844
6940
  return internal.svg.getTransform(p, scale);
6845
6941
  }
6846
6942
 
6847
-
6848
6943
  function repositionSymbols(elements, layer, ext) {
6849
6944
  var el, idx, shp, p, displayOn, inView, displayBounds;
6850
6945
  for (var i=0, n=elements.length; i<n; i++) {
@@ -6868,7 +6963,7 @@
6868
6963
  }
6869
6964
  }
6870
6965
 
6871
- function renderSymbols(lyr, ext, type) {
6966
+ function renderSymbols(lyr, ext) {
6872
6967
  var records = lyr.data.getRecords();
6873
6968
  var symbols = lyr.shapes.map(function(shp, i) {
6874
6969
  var d = records[i];
@@ -6976,6 +7071,8 @@
6976
7071
  var interactionMode;
6977
7072
  var targetLayer;
6978
7073
  var hitTest;
7074
+ var pinnedOn; // used in multi-edit mode (selection) for toggling pinning behavior
7075
+
6979
7076
  // event priority is higher than navigation, so stopping propagation disables
6980
7077
  // pan navigation
6981
7078
  var priority = 2;
@@ -7034,6 +7131,7 @@
7034
7131
  updateSelectionState(null); // no hit data, no event
7035
7132
  active = false;
7036
7133
  hitTest = null;
7134
+ pinnedOn = false;
7037
7135
  }
7038
7136
  }
7039
7137
 
@@ -7070,6 +7168,13 @@
7070
7168
  updateSelectionState({ids: ids});
7071
7169
  };
7072
7170
 
7171
+ self.setPinning = function(val) {
7172
+ if (pinnedOn != val) {
7173
+ pinnedOn = val;
7174
+ triggerHitEvent('change');
7175
+ }
7176
+ };
7177
+
7073
7178
  self.setTransientIds = function(ids) {
7074
7179
  // turnOn('selection');
7075
7180
  transientIds = ids || [];
@@ -7139,7 +7244,7 @@
7139
7244
  // make sure popup is unpinned and turned off when switching editing modes
7140
7245
  // (some modes do not support pinning)
7141
7246
  gui.on('interaction_mode_change', function(e) {
7142
- updateSelectionState(null);
7247
+ self.clearSelection();
7143
7248
  // if (e.mode == 'off' || e.mode == 'box') {
7144
7249
  if (gui.interaction.modeUsesSelection(e.mode)) {
7145
7250
  turnOn(e.mode);
@@ -7148,7 +7253,11 @@
7148
7253
  }
7149
7254
  });
7150
7255
 
7151
- gui.on('box_drag_start', function() {
7256
+ gui.on('undo_redo_pre', function() {
7257
+ self.clearSelection();
7258
+ });
7259
+
7260
+ gui.on('shift_drag_start', function() {
7152
7261
  self.clearHover();
7153
7262
  });
7154
7263
 
@@ -7300,6 +7409,9 @@
7300
7409
  if (transientIds.length) {
7301
7410
  eventData.ids = utils$1.uniq(transientIds.concat(eventData.ids || []));
7302
7411
  }
7412
+ if (pinnedOn) {
7413
+ eventData.pinned = true;
7414
+ }
7303
7415
  self.dispatchEvent(type, eventData);
7304
7416
  }
7305
7417
 
@@ -7801,13 +7913,19 @@
7801
7913
 
7802
7914
  function HighlightBox(gui, optsArg) {
7803
7915
  var el = El('div').addClass('zoom-box').appendTo('body'),
7804
- opts = Object.assign({handles: false, persistent: false}, optsArg),
7916
+ opts = Object.assign({
7917
+ name: 'box',
7918
+ handles: false,
7919
+ persistent: false,
7920
+ draggable: false // does dragging the map draw a box
7921
+ }, optsArg),
7805
7922
  box = new EventDispatcher(),
7806
7923
  stroke = 2,
7807
7924
  activeHandle = null,
7808
7925
  prevXY = null,
7809
7926
  boxCoords = null,
7810
7927
  _on = false,
7928
+ _visible = false,
7811
7929
  handles;
7812
7930
 
7813
7931
  if (opts.classname) {
@@ -7817,19 +7935,20 @@
7817
7935
  el.hide();
7818
7936
 
7819
7937
  gui.on('map_rendered', function() {
7820
- if (!_on) return;
7938
+ if (!_on || !_visible) return;
7821
7939
  redraw();
7822
7940
  });
7823
7941
 
7824
- gui.on('box_drag', function(e) {
7942
+ gui.on('shift_drag', function(e) {
7825
7943
  if (!_on) return;
7944
+ if (!opts.draggable) return;
7826
7945
  boxCoords = getBoxCoords(e.data);
7827
7946
  redraw();
7828
7947
  box.dispatchEvent('drag');
7829
7948
  });
7830
7949
 
7831
- gui.on('box_drag_end', function(e) {
7832
- if (!_on) return;
7950
+ gui.on('shift_drag_end', function(e) {
7951
+ if (!_on || !_visible || !opts.draggable) return;
7833
7952
  boxCoords = getBoxCoords(e.data);
7834
7953
  var pix = coordsToPix(boxCoords, gui.map.getExtent());
7835
7954
  box.dispatchEvent('dragend', {map_bbox: pix});
@@ -7851,21 +7970,30 @@
7851
7970
  });
7852
7971
 
7853
7972
  document.addEventListener('mousemove', function(e) {
7854
- if (!_on || !activeHandle || !prevXY || !boxCoords) return;
7973
+ if (!_on || !activeHandle || !prevXY || !boxCoords || !_visible) return;
7855
7974
  var xy = {x: e.pageX, y: e.pageY};
7856
7975
  var scale = gui.map.getExtent().getPixelSize();
7857
7976
  var dx = (xy.x - prevXY.x) * scale;
7858
7977
  var dy = -(xy.y - prevXY.y) * scale;
7859
- if (activeHandle.col == 'left') {
7978
+ var shifting = activeHandle.col == 'center' && activeHandle.row == 'center';
7979
+ var centered = gui.keyboard.shiftIsPressed() && !shifting;
7980
+ if (activeHandle.col == 'left' || shifting) {
7860
7981
  boxCoords[0] += dx;
7861
- } else if (activeHandle.col == 'right') {
7982
+ if (centered) boxCoords[2] -= dx;
7983
+ }
7984
+ if (activeHandle.col == 'right' || shifting) {
7862
7985
  boxCoords[2] += dx;
7986
+ if (centered) boxCoords[0] -= dx;
7863
7987
  }
7864
- if (activeHandle.row == 'top') {
7988
+ if (activeHandle.row == 'top' || shifting) {
7865
7989
  boxCoords[3] += dy;
7866
- } else if (activeHandle.row == 'bottom') {
7990
+ if (centered) boxCoords[1] -= dy;
7991
+ }
7992
+ if (activeHandle.row == 'bottom' || shifting) {
7867
7993
  boxCoords[1] += dy;
7994
+ if (centered) boxCoords[3] -= dy;
7868
7995
  }
7996
+
7869
7997
  prevXY = xy;
7870
7998
  redraw();
7871
7999
  box.dispatchEvent('handle_drag');
@@ -7907,9 +8035,11 @@
7907
8035
  box.hide = function() {
7908
8036
  el.hide();
7909
8037
  boxCoords = null;
8038
+ _visible = false;
7910
8039
  };
7911
8040
 
7912
8041
  box.show = function(x1, y1, x2, y2) {
8042
+ _visible = true;
7913
8043
  var w = Math.abs(x1 - x2),
7914
8044
  h = Math.abs(y1 - y2),
7915
8045
  props = {
@@ -7974,7 +8104,7 @@
7974
8104
  function initHandles(el) {
7975
8105
  var handles = [];
7976
8106
  for (var i=0; i<9; i++) {
7977
- if (i == 4) continue;
8107
+ // if (i == 4) continue; // skip middle handle
7978
8108
  var c = Math.floor(i / 3);
7979
8109
  var r = i % 3;
7980
8110
  handles.push({
@@ -8020,13 +8150,14 @@
8020
8150
  function MapNav(gui, ext, mouse) {
8021
8151
  var wheel = new MouseWheel(mouse),
8022
8152
  zoomTween = new Tween(Tween.sineInOut),
8023
- zoomBox = new HighlightBox(gui), // .addClass('zooming'),
8024
- boxDrag = false,
8153
+ zoomBox = new HighlightBox(gui, {draggable: true, name: 'zoom-box'}), // .addClass('zooming'),
8154
+ shiftDrag = false,
8025
8155
  zoomScaleMultiplier = 1,
8026
8156
  inBtn, outBtn,
8027
8157
  dragStartEvt,
8028
8158
  _fx, _fy; // zoom foci, [0,1]
8029
8159
 
8160
+ // Was used in old frame view... remove?
8030
8161
  this.setZoomFactor = function(k) {
8031
8162
  zoomScaleMultiplier = k || 1;
8032
8163
  };
@@ -8071,18 +8202,18 @@
8071
8202
  if (disabled()) return;
8072
8203
  if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
8073
8204
  // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
8074
- boxDrag = !!e.shiftKey;
8075
- if (boxDrag) {
8205
+ shiftDrag = !!e.shiftKey;
8206
+ if (shiftDrag) {
8076
8207
  if (useBoxZoom()) zoomBox.turnOn();
8077
8208
  dragStartEvt = e;
8078
- gui.dispatchEvent('box_drag_start');
8209
+ gui.dispatchEvent('shift_drag_start');
8079
8210
  }
8080
8211
  });
8081
8212
 
8082
8213
  mouse.on('drag', function(e) {
8083
8214
  if (disabled()) return;
8084
- if (boxDrag) {
8085
- gui.dispatchEvent('box_drag', getBoxData(e));
8215
+ if (shiftDrag) {
8216
+ gui.dispatchEvent('shift_drag', getBoxData(e));
8086
8217
  } else {
8087
8218
  ext.pan(e.dx, e.dy);
8088
8219
  }
@@ -8091,9 +8222,9 @@
8091
8222
  mouse.on('dragend', function(e) {
8092
8223
  var bbox;
8093
8224
  if (disabled()) return;
8094
- if (boxDrag) {
8095
- boxDrag = false;
8096
- gui.dispatchEvent('box_drag_end', getBoxData(e));
8225
+ if (shiftDrag) {
8226
+ shiftDrag = false;
8227
+ gui.dispatchEvent('shift_drag_end', getBoxData(e));
8097
8228
  zoomBox.turnOff();
8098
8229
  }
8099
8230
  });
@@ -8111,7 +8242,9 @@
8111
8242
  });
8112
8243
 
8113
8244
  function useBoxZoom() {
8114
- return gui.getMode() != 'selection_tool' && gui.getMode() != 'box_tool';
8245
+ var mode = gui.getMode();
8246
+ var disabled = ['selection_tool', 'box_tool', 'rectangle_tool'].includes(mode);
8247
+ return !disabled;
8115
8248
  }
8116
8249
 
8117
8250
  function getBoxData(e) {
@@ -8167,7 +8300,7 @@
8167
8300
 
8168
8301
  function SelectionTool(gui, ext, hit) {
8169
8302
  var popup = gui.container.findChild('.selection-tool-options');
8170
- var box = new HighlightBox(gui);
8303
+ var box = new HighlightBox(gui, {draggable: true});
8171
8304
  var coords = popup.findChild('.box-coords').hide();
8172
8305
  var _on = false;
8173
8306
 
@@ -8211,6 +8344,7 @@
8211
8344
  }
8212
8345
 
8213
8346
  function turnOff() {
8347
+ dataBtn.turnOff();
8214
8348
  box.turnOff();
8215
8349
  reset();
8216
8350
  _on = false;
@@ -8221,10 +8355,17 @@
8221
8355
  }
8222
8356
 
8223
8357
  function reset() {
8224
- popup.hide();
8358
+ hidePopup();
8359
+ setPinning(false);
8225
8360
  hit.clearSelection();
8226
8361
  }
8227
8362
 
8363
+ function setPinning(on) {
8364
+ hit.setPinning(on);
8365
+ if (on) dataBtn.turnOn();
8366
+ else dataBtn.turnOff();
8367
+ }
8368
+
8228
8369
  function getIdsOpt() {
8229
8370
  return hit.getSelectionIds().join(',');
8230
8371
  }
@@ -8239,11 +8380,15 @@
8239
8380
  popup.show();
8240
8381
  updateCoords();
8241
8382
  } else {
8242
- popup.hide();
8243
- hideCoords();
8383
+ hidePopup();
8244
8384
  }
8245
8385
  });
8246
8386
 
8387
+ function hidePopup() {
8388
+ popup.hide();
8389
+ hideCoords();
8390
+ }
8391
+
8247
8392
  new SimpleButton(popup.findChild('.delete-btn')).on('click', function() {
8248
8393
  var cmd = '-filter invert ids=' + getIdsOpt();
8249
8394
  runCommand(cmd);
@@ -8259,6 +8404,10 @@
8259
8404
  runCommand(cmd);
8260
8405
  });
8261
8406
 
8407
+ var dataBtn = new ToggleButton(popup.findChild('.data-btn')).on('click', function(e) {
8408
+ setPinning(e.on);
8409
+ });
8410
+
8262
8411
  new SimpleButton(popup.findChild('.duplicate-btn')).on('click', function() {
8263
8412
  var cmd = '-filter + name=selection ids=' + getIdsOpt();
8264
8413
  runCommand(cmd);
@@ -8296,19 +8445,19 @@
8296
8445
  hideCoords();
8297
8446
  return;
8298
8447
  }
8299
- El(coordsBtn.node()).addClass('selected-btn');
8448
+ El(coordsBtn.node()).addClass('active');
8300
8449
  coords.text(bbox.join(','));
8301
8450
  coords.show();
8302
8451
  GUI.selectElement(coords.node());
8303
8452
  }
8304
8453
 
8305
8454
  function hideCoords() {
8306
- El(coordsBtn.node()).removeClass('selected-btn');
8455
+ El(coordsBtn.node()).removeClass('active');
8307
8456
  coords.hide();
8308
8457
  }
8309
8458
 
8310
8459
  function runCommand(cmd, turnOff) {
8311
- popup.hide();
8460
+ hidePopup();
8312
8461
  gui.quiet(true);
8313
8462
  if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) {
8314
8463
  gui.quiet(false);
@@ -8318,6 +8467,55 @@
8318
8467
  }
8319
8468
  }
8320
8469
 
8470
+ function openAddFieldPopup(gui, ids, lyr) {
8471
+ var popup = showPopupAlert('', 'Add field');
8472
+ var el = popup.container();
8473
+ el.addClass('option-menu');
8474
+ var html = `<div><input type="text" class="field-name text-input" placeholder="field name"></div>
8475
+ <div><input type="text" class="field-value text-input" placeholder="value"><div>
8476
+ <div tabindex="0" class="btn dialog-btn">Apply</div> <span class="inline-checkbox"><input type="checkbox" class="all" />assign value to all records</span>`;
8477
+ el.html(html);
8478
+
8479
+ var name = el.findChild('.field-name');
8480
+ name.node().focus();
8481
+ var val = el.findChild('.field-value');
8482
+ var box = el.findChild('.all');
8483
+ var btn = el.findChild('.btn').on('click', function() {
8484
+ var table = internal.getLayerDataTable(lyr); // creates new table if missing
8485
+ var all = box.node().checked;
8486
+ var nameStr = name.node().value.trim();
8487
+ if (!nameStr) return;
8488
+ if (table.fieldExists(nameStr)) {
8489
+ name.node().value = '';
8490
+ return;
8491
+ }
8492
+ var valStr = val.node().value.trim();
8493
+ var value = internal.parseUnknownType(valStr);
8494
+ // table.addField(nameStr, function(d) {
8495
+ // // parse each time to avoid multiple references to objects
8496
+ // return (all || d == rec) ? parseUnknownType(valStr) : null;
8497
+ // });
8498
+
8499
+ var cmdStr = `-each "d['${nameStr}'] = `;
8500
+ if (!all) {
8501
+ cmdStr += ids.length == 1 ?
8502
+ `this.id != ${ids[0]}` :
8503
+ `!${JSON.stringify(ids)}.includes(this.id)`;
8504
+ cmdStr += ' ? null : ';
8505
+ }
8506
+ valStr = JSON.stringify(JSON.stringify(value)); // add escapes to strings
8507
+ cmdStr = valStr.replace('"', cmdStr);
8508
+
8509
+ gui.console.runMapshaperCommands(cmdStr, function(err) {
8510
+ if (!err) {
8511
+ popup.close();
8512
+ } else {
8513
+ console.error(err);
8514
+ }
8515
+ });
8516
+ });
8517
+ }
8518
+
8321
8519
  // toNext, toPrev: trigger functions for switching between multiple records
8322
8520
  function Popup(gui, toNext, toPrev) {
8323
8521
  var self = new EventDispatcher();
@@ -8331,7 +8529,6 @@
8331
8529
  var navInfo = El('span').addClass('popup-nav-info').appendTo(nav);
8332
8530
  var nextLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('▶');
8333
8531
  var refresh = null;
8334
- var currIds = [];
8335
8532
 
8336
8533
  el.addClass('rollover'); // used as a sentinel for the hover function
8337
8534
 
@@ -8342,16 +8539,20 @@
8342
8539
  });
8343
8540
 
8344
8541
  self.show = function(id, ids, lyr, pinned) {
8345
- var editable = pinned && gui.interaction.getMode() == 'data';
8542
+ var singleEdit = pinned && gui.interaction.getMode() == 'data';
8543
+ var multiEdit = pinned && gui.interaction.getMode() == 'selection';
8346
8544
  var maxHeight = parent.node().clientHeight - 36;
8347
- currIds = [id];
8545
+ var recIds = multiEdit ? ids : [id];
8546
+
8348
8547
  // stash a function for refreshing the current popup when data changes
8349
8548
  // while the popup is being displayed (e.g. while dragging a label)
8350
8549
  refresh = function() {
8351
- render(content, id, lyr, editable);
8550
+ render(content, recIds, lyr, singleEdit || multiEdit);
8352
8551
  };
8353
8552
  refresh();
8354
- if (ids && ids.length > 1) {
8553
+ if (multiEdit) {
8554
+ showRecords(ids.length);
8555
+ } else if (ids && ids.length > 1 && !multiEdit) {
8355
8556
  showNav(id, ids, pinned);
8356
8557
  } else {
8357
8558
  tab.hide();
@@ -8365,7 +8566,6 @@
8365
8566
  self.hide = function() {
8366
8567
  if (!isOpen()) return;
8367
8568
  refresh = null;
8368
- currIds = [];
8369
8569
  // make sure any pending edits are made before re-rendering popup
8370
8570
  GUI.blurActiveElement(); // this should be more selective -- could cause a glitch if typing in console
8371
8571
  content.empty();
@@ -8379,6 +8579,13 @@
8379
8579
  return el.visible();
8380
8580
  }
8381
8581
 
8582
+ function showRecords(n) {
8583
+ navInfo.text(n);
8584
+ nextLink.css('display','none');
8585
+ prevLink.css('display','none');
8586
+ tab.show();
8587
+ }
8588
+
8382
8589
  function showNav(id, ids, pinned) {
8383
8590
  var num = ids.indexOf(id) + 1;
8384
8591
  navInfo.text(' ' + num + ' / ' + ids.length + ' ');
@@ -8387,28 +8594,12 @@
8387
8594
  tab.show();
8388
8595
  }
8389
8596
 
8390
- function render(el, recId, lyr, editable) {
8391
- var recIds = [recId]; // TODO: support multiple ids
8597
+ function render(el, recIds, lyr, editable) {
8392
8598
  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
8599
+ var tableEl = table ? renderTable(recIds, table, editable) : null;
8397
8600
  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
-
8601
+ if (tableEl) {
8602
+ tableEl.appendTo(el);
8412
8603
  tableEl.on('copy', function(e) {
8413
8604
  // remove leading or trailing tabs that sometimes get copied when
8414
8605
  // selecting from a table
@@ -8419,7 +8610,6 @@
8419
8610
  e.preventDefault(); // don't copy original string with tabs
8420
8611
  }
8421
8612
  });
8422
-
8423
8613
  } else {
8424
8614
  // Some individual features can have undefined values for some or all of
8425
8615
  // their data properties (properties are set to undefined when an input JSON file
@@ -8433,72 +8623,65 @@
8433
8623
  var line = El('div').appendTo(el);
8434
8624
  El('span').addClass('add-field-btn').appendTo(line).on('click', async function(e) {
8435
8625
  // show "add field" dialog
8436
- renderAddFieldPopup(recIds, lyr);
8626
+ openAddFieldPopup(gui, recIds, lyr);
8437
8627
  }).text('+ add field');
8438
8628
  }
8439
8629
  }
8440
8630
 
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;
8631
+ function renderTable(recIds, table, editable) {
8632
+ var tableEl = El('table').addClass('selectable');
8633
+ var rows = 0;
8634
+ var rec;
8635
+ if (recIds.length == 1) {
8636
+ rec = editable ?
8637
+ table.getReadOnlyRecordAt(recIds[0]) :
8638
+ table.getRecordAt(recIds[0]);
8639
+ } else {
8640
+ rec = getMultiRecord(recIds, table);
8641
+ }
8642
+ utils$1.forEachProperty(rec, function(v, k) {
8643
+ // missing GeoJSON fields are set to undefined on import; skip these
8644
+ if (v === undefined) return;
8645
+ var rowEl = renderRow(k, v, recIds, table, editable);
8646
+ if (rowEl) {
8647
+ rowEl.appendTo(tableEl);
8648
+ rows++;
8462
8649
  }
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);
8650
+ });
8651
+ return rows > 0 ? tableEl : null;
8652
+ }
8653
+
8654
+ function getMultiRecord(recIds, table) {
8655
+ var fields = table.getFields();
8656
+ var rec = {};
8657
+ recIds.forEach(function(id) {
8658
+ var d = table.getRecordAt(id) || {};
8659
+ var k, v;
8660
+ for (var i=0; i<fields.length; i++) {
8661
+ k = fields[i];
8662
+ v = d[k];
8663
+ if (k in rec === false) {
8664
+ rec[k] = v;
8665
+ } else if (rec[k] !== v) {
8666
+ rec[k] = null;
8485
8667
  }
8486
- });
8668
+ }
8487
8669
  });
8670
+ return rec;
8488
8671
  }
8489
8672
 
8490
- function renderRow(table, rec, key, type, editable) {
8491
- var val = rec[key];
8673
+
8674
+ function renderRow(key, val, recIds, table, editable) {
8675
+ var type = getFieldType(val, key, table);
8492
8676
  var str = formatInspectorValue(val, type);
8493
8677
  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);
8678
+ var rowEl = El('tr').html(rowHtml);
8679
+ var cellEl = rowEl.findChild('.value');
8680
+ setFieldClass(cellEl, val, type);
8499
8681
  if (editable) {
8500
- editItem(cell, rec, key, type);
8682
+ editItem(cellEl, key, val, recIds, table, type);
8501
8683
  }
8684
+ return rowEl;
8502
8685
  }
8503
8686
 
8504
8687
  function setFieldClass(el, val, type) {
@@ -8511,10 +8694,10 @@
8511
8694
  el.classed('empty', isEmpty);
8512
8695
  }
8513
8696
 
8514
- function editItem(el, rec, key, type) {
8697
+ function editItem(el, key, val, recIds, table, type) {
8515
8698
  var input = new ClickText2(el),
8516
- strval = formatInspectorValue(rec[key], type),
8517
- parser = getInputParser(type);
8699
+ strval = formatInspectorValue(val, type),
8700
+ parser = internal.getInputParser(type);
8518
8701
  el.parent().addClass('editable-cell');
8519
8702
  el.addClass('colored-text dot-underline');
8520
8703
  input.on('change', function(e) {
@@ -8526,17 +8709,27 @@
8526
8709
  } else if (strval != strval2) {
8527
8710
  // field content has changed
8528
8711
  strval = strval2;
8529
- gui.dispatchEvent('data_preupdate', {ids: currIds}); // for undo/redo
8530
- rec[key] = val2;
8531
- gui.dispatchEvent('data_postupdate', {ids: currIds});
8712
+ gui.dispatchEvent('data_preupdate', {ids: recIds}); // for undo/redo
8713
+ // rec[key] = val2;
8714
+ updateRecords(recIds, key, val2, table);
8715
+ gui.dispatchEvent('data_postupdate', {ids: recIds});
8532
8716
  input.value(strval);
8533
8717
  setFieldClass(el, val2, type);
8534
- self.dispatchEvent('data_updated', {field: key, value: val2, ids: currIds});
8718
+ self.dispatchEvent('data_updated', {field: key, value: val2, ids: recIds});
8535
8719
  }
8536
8720
  });
8537
8721
  }
8538
8722
  }
8539
8723
 
8724
+ function updateRecords(ids, f, v, table) {
8725
+ var records = table.getRecords();
8726
+ ids.forEach(function(id) {
8727
+ var d = records[id] || {};
8728
+ d[f] = v;
8729
+ records[id] = d;
8730
+ });
8731
+ }
8732
+
8540
8733
  function formatInspectorValue(val, type) {
8541
8734
  var str;
8542
8735
  if (type == 'date') {
@@ -8549,60 +8742,10 @@
8549
8742
  return str;
8550
8743
  }
8551
8744
 
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
8745
 
8603
8746
  function getFieldType(val, key, table) {
8604
8747
  // if a field has a null value, look at entire column to identify type
8605
- return internal.getValueType(val) || internal.getColumnType(key, table.getRecords());
8748
+ return internal.getValueType(val) || internal.getColumnType(key, table.getRecords()) ;
8606
8749
  }
8607
8750
 
8608
8751
  function InspectionControl2(gui, hit) {
@@ -8627,16 +8770,20 @@
8627
8770
  });
8628
8771
 
8629
8772
  hit.on('change', function(e) {
8630
- var ids;
8631
8773
  if (!inspecting()) return;
8632
- ids = e.mode == 'selection' ? null : e.ids;
8633
- inspect(e.id, e.pinned, ids);
8774
+ var ids;
8775
+ if (e.mode == 'selection') {
8776
+ ids = e.pinned && e.ids || [];
8777
+ } else {
8778
+ ids = e.ids || [];
8779
+ }
8780
+ inspect(e.id, ids, e.pinned);
8634
8781
  });
8635
8782
 
8636
8783
  // id: Id of a feature in the active layer, or -1
8637
- function inspect(id, pin, ids) {
8784
+ function inspect(id, ids, pin) {
8638
8785
  var target = hit.getHitTarget();
8639
- if (id > -1 && inspecting() && target && target.layer) {
8786
+ if ((id > -1 || ids && ids.length > 0) && inspecting() && target && target.layer) {
8640
8787
  _popup.show(id, ids, target.layer, pin);
8641
8788
  } else {
8642
8789
  _popup.hide();
@@ -8935,6 +9082,9 @@
8935
9082
  hit.clearVertexOverlay();
8936
9083
  }
8937
9084
 
9085
+ // return data on the nearest vertex (or identical vertices) to the pointer
9086
+ // (if within a distance threshold)
9087
+ //
8938
9088
  function findDraggableVertices(e) {
8939
9089
  var target = hit.getHitTarget();
8940
9090
  var shp = target.layer.shapes[e.id];
@@ -9097,35 +9247,6 @@
9097
9247
  // }
9098
9248
  }
9099
9249
 
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
9250
  var darkStroke = "#334",
9130
9251
  lightStroke = "#b7d9ea",
9131
9252
  violet = "#cc6acc",
@@ -9452,13 +9573,145 @@
9452
9573
  return internal.findPropertiesBySymbolGeom(fields, lyr.geometry_type);
9453
9574
  }
9454
9575
 
9576
+ function initPointDrawing(gui, ext, mouse, hit) {
9577
+
9578
+ gui.on('interaction_mode_change', function(e) {
9579
+ gui.container.findChild('.map-layers').classed('add-points', e.mode === 'add-points');
9580
+ });
9581
+
9582
+ function active() {
9583
+ return gui.interaction.getMode() == 'add-points';
9584
+ }
9585
+
9586
+ mouse.on('click', function(e) {
9587
+ if (!active()) return;
9588
+ addPoint(e.x, e.y);
9589
+ gui.dispatchEvent('map-needs-refresh');
9590
+ });
9591
+
9592
+ // x, y: pixel coordinates
9593
+ function addPoint(x, y) {
9594
+ var p = ext.translatePixelCoords(x, y);
9595
+ var target = hit.getHitTarget();
9596
+ var lyr = target.layer;
9597
+ var fid = lyr.shapes.length;
9598
+ var d = lyr.data ? getEmptyDataRecord(lyr.data) : null;
9599
+ if (d) {
9600
+ // this seems to work even for projected layers -- the data tables
9601
+ // of projected and original data seem to be shared.
9602
+ lyr.data.getRecords()[fid] = d;
9603
+ // TODO: handle SVG symbol layer
9604
+ if (internal.layerHasLabels(lyr)) {
9605
+ d['label-text'] = 'TBD'; // without text, new labels will be invisible
9606
+ } else if (layerHasCanvasDisplayStyle(lyr)) {
9607
+ d.r = 3; // show a black circle if layer is styled
9608
+ }
9609
+ }
9610
+ lyr.shapes[fid] = [p];
9611
+ updatePointCoords(target, fid);
9612
+ }
9613
+
9614
+ function getEmptyDataRecord(table) {
9615
+ return table.getFields().reduce(function(memo, name) {
9616
+ memo[name] = null;
9617
+ return memo;
9618
+ }, {});
9619
+ }
9620
+ }
9621
+
9622
+ function initLineDrawing(gui, ext, mouse, hit) {
9623
+ var _on = false;
9624
+ var _coords;
9625
+ var _lastClick;
9626
+
9627
+ gui.on('interaction_mode_change', function(e) {
9628
+ _on = e.mode === 'draw-lines';
9629
+ gui.container.findChild('.map-layers').classed('draw-lines', _on);
9630
+ });
9631
+
9632
+ function active() {
9633
+ return _on;
9634
+ }
9635
+
9636
+ function extending() {
9637
+ return active() && !!_coords;
9638
+ }
9639
+
9640
+ function startPath(e) {
9641
+
9642
+ }
9643
+
9644
+ function extendPath(e) {
9645
+
9646
+ }
9647
+
9648
+ function finishPath() {
9649
+ _coords = null;
9650
+ }
9651
+
9652
+ gui.keyboard.on('keydown', function(evt) {
9653
+ if (!active()) return;
9654
+ if (evt.keyCode == 27) { // esc
9655
+ finishPath();
9656
+ }
9657
+ });
9658
+
9659
+ mouse.on('click', function(e) {
9660
+ if (!active()) return;
9661
+ // console.log('[click]', e)
9662
+ // addPoint(e.x, e.y);
9663
+ // gui.dispatchEvent('map-needs-refresh');
9664
+ _lastClick = e;
9665
+ });
9666
+
9667
+ // note: second click event is fired before this
9668
+ mouse.on('dblclick', function(e) {
9669
+ if (!active()) return;
9670
+ // block navigation
9671
+ e.stopPropagation();
9672
+ finishPath();
9673
+
9674
+ }, null, 3); // hit detection is priority 2
9675
+
9676
+ mouse.on('hover', function(e) {
9677
+ if (!active()) return;
9678
+ });
9679
+
9680
+ // x, y: pixel coordinates
9681
+ function addPoint(x, y) {
9682
+ var p = ext.translatePixelCoords(x, y);
9683
+ var target = hit.getHitTarget();
9684
+ var lyr = target.layer;
9685
+ var fid = lyr.shapes.length;
9686
+ if (lyr.data) {
9687
+ // this seems to work even for projected layers -- the data tables
9688
+ // of projected and original data seem to be shared.
9689
+ lyr.data.getRecords()[fid] = getEmptyDataRecord(lyr.data);
9690
+ }
9691
+ lyr.shapes[fid] = [p];
9692
+ updatePointCoords(target, fid);
9693
+ }
9694
+
9695
+ function getEmptyDataRecord(table) {
9696
+ return table.getFields().reduce(function(memo, name) {
9697
+ memo[name] = null;
9698
+ return memo;
9699
+ }, {});
9700
+ }
9701
+ }
9702
+
9703
+ function initDrawing(gui, ext, mouse, hit) {
9704
+ initPointDrawing(gui, ext, mouse, hit);
9705
+ initLineDrawing(gui, ext, mouse, hit);
9706
+ }
9707
+
9455
9708
  function MapExtent(_position) {
9456
9709
  var _scale = 1,
9457
9710
  _cx, _cy, // center in geographic units
9458
9711
  _fullBounds, // full (zoomed-out) content bounds, including any padding
9459
9712
  _strictBounds, // full extent must fit inside, if set
9460
9713
  _self = this,
9461
- _frame;
9714
+ _frame; // optional frame data (bbox, width, height)
9462
9715
 
9463
9716
  _position.on('resize', function(e) {
9464
9717
  if (ready()) {
@@ -9576,16 +9829,16 @@
9576
9829
  return this.getTransform().transform(x, y);
9577
9830
  };
9578
9831
 
9579
- this.setFrame = function(frame) {
9832
+ this.setFrameData = function(frame) {
9580
9833
  _frame = frame || null;
9581
9834
  };
9582
9835
 
9583
- this.getFrame = function() {
9836
+ this.getFrameData = function() {
9584
9837
  return _frame || null;
9585
9838
  };
9586
9839
 
9587
9840
  this.getSymbolScale = function() {
9588
- if (!_frame) return 0;
9841
+ if (!_frame) return 1;
9589
9842
  var bounds = new Bounds(_frame.bbox);
9590
9843
  var bounds2 = bounds.clone().transform(this.getTransform());
9591
9844
  return bounds2.width() / _frame.width;
@@ -10198,12 +10451,12 @@
10198
10451
  // (style.opacity < 1 ? '~' + style.opacity : '') +
10199
10452
  (style.fillPattern ? '~' + style.fillPattern : '');
10200
10453
  }
10201
-
10202
10454
  return _self;
10203
10455
  }
10204
10456
 
10205
10457
  function getScaledLineScale(ext) {
10206
- return ext.getSymbolScale() || getLineScale(ext);
10458
+ var previewScale = ext.getSymbolScale();
10459
+ return previewScale == 1 ? getLineScale(ext) : previewScale;
10207
10460
  }
10208
10461
 
10209
10462
  // Vary line width according to zoom ratio.
@@ -10381,7 +10634,8 @@
10381
10634
  if (pixRatio > 1) {
10382
10635
  // bump up thin lines on retina, but not to more than 1px
10383
10636
  // (tests on Chrome showed much faster rendering of 1px lines)
10384
- strokeWidth = strokeWidth < 1 ? 1 : strokeWidth * pixRatio;
10637
+ // strokeWidth = strokeWidth < 1 ? 1 : strokeWidth * pixRatio;
10638
+ strokeWidth = strokeWidth * pixRatio;
10385
10639
  }
10386
10640
  ctx.lineCap = style.lineCap || 'round';
10387
10641
  ctx.lineJoin = style.lineJoin || 'round';
@@ -10431,7 +10685,7 @@
10431
10685
 
10432
10686
  function getSvgFurnitureTransform(ext) {
10433
10687
  var scale = ext.getSymbolScale();
10434
- var frame = ext.getFrame();
10688
+ var frame = ext.getFrameData();
10435
10689
  var p = ext.translateCoords(frame.bbox[0], frame.bbox[3]);
10436
10690
  return internal.svg.getTransform(p, scale);
10437
10691
  }
@@ -10442,7 +10696,7 @@
10442
10696
  }
10443
10697
 
10444
10698
  function renderFurniture(lyr, ext) {
10445
- var frame = ext.getFrame(); // frame should be set if we're rendering a furniture layer
10699
+ var frame = ext.getFrameData(); // frame should be set if we're rendering a furniture layer
10446
10700
  var obj = internal.getEmptyLayerForSVG(lyr, {});
10447
10701
  if (!frame) {
10448
10702
  stop$1('Missing map frame data');
@@ -10479,7 +10733,7 @@
10479
10733
  target.svg_id = id;
10480
10734
  resize(ext);
10481
10735
  if (type == 'label' || type == 'symbol') {
10482
- html = renderSymbols(target.layer, ext, type);
10736
+ html = renderSymbols(target.layer, ext);
10483
10737
  } else if (type == 'furniture') {
10484
10738
  html = renderFurniture(target.layer, ext);
10485
10739
  }
@@ -10493,13 +10747,17 @@
10493
10747
  };
10494
10748
 
10495
10749
  function reposition(target, type, ext) {
10496
- var container = el.findChild('.' + target.svg_id).node();
10750
+ var container = el.findChild('.' + target.svg_id);
10751
+ if (!container || !container.node()) {
10752
+ console.error('[reposition] missing SVG container');
10753
+ return;
10754
+ }
10497
10755
  var elements;
10498
10756
  if (type == 'symbol') {
10499
- elements = El.findAll('.mapshaper-svg-symbol', container);
10757
+ elements = El.findAll('.mapshaper-svg-symbol', container.node());
10500
10758
  repositionSymbols(elements, target.layer, ext);
10501
10759
  } else if (type == 'furniture') {
10502
- repositionFurniture(container, target.layer, ext);
10760
+ repositionFurniture(container.node(), target.layer, ext);
10503
10761
  } else {
10504
10762
  // container.getElementsByTagName('text')
10505
10763
  error('Unsupported symbol type:', type);
@@ -10514,7 +10772,7 @@
10514
10772
  return el;
10515
10773
  }
10516
10774
 
10517
- function LayerStack(gui, container, ext, mouse) {
10775
+ function LayerRenderer(gui, container, ext, mouse) {
10518
10776
  var el = El(container),
10519
10777
  _mainCanv = new DisplayCanvas().appendTo(el),
10520
10778
  _overlayCanv = new DisplayCanvas().appendTo(el),
@@ -10534,14 +10792,12 @@
10534
10792
  }
10535
10793
  layers.forEach(function(lyr) {
10536
10794
  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
10795
  if (isSvgLayer && !needSvgRedraw) {
10542
10796
  _svg.reposition(lyr, 'symbol');
10543
10797
  } else if (isSvgLayer) {
10544
10798
  _svg.drawLayer(lyr, 'symbol');
10799
+ } else {
10800
+ drawCanvasLayer(lyr, _mainCanv);
10545
10801
  }
10546
10802
  });
10547
10803
  };
@@ -10606,7 +10862,7 @@
10606
10862
  //
10607
10863
  function BoxTool(gui, ext, nav) {
10608
10864
  var self = new EventDispatcher();
10609
- var box = new HighlightBox(gui, {persistent: true, handles: true});
10865
+ var box = new HighlightBox(gui, {name: 'box-tool', persistent: true, handles: true, draggable: true});
10610
10866
  var popup = gui.container.findChild('.box-tool-options');
10611
10867
  var coords = popup.findChild('.box-coords');
10612
10868
  var _on = false;
@@ -10639,7 +10895,8 @@
10639
10895
  });
10640
10896
 
10641
10897
  new SimpleButton(popup.findChild('.rect-btn')).on('click', function() {
10642
- runCommand('-rectangle + bbox=' + box.getDataCoords().join(','));
10898
+ var cmd = '-rectangle + bbox=' + box.getDataCoords().join(',');
10899
+ runCommand(cmd);
10643
10900
  });
10644
10901
 
10645
10902
  gui.addMode('box_tool', turnOn, turnOff);
@@ -10652,7 +10909,7 @@
10652
10909
  }
10653
10910
  });
10654
10911
 
10655
- gui.on('box_drag_start', function() {
10912
+ gui.on('shift_drag_start', function() {
10656
10913
  // box.classed('zooming', inZoomMode());
10657
10914
  hideCoords();
10658
10915
  });
@@ -10718,7 +10975,7 @@
10718
10975
  }
10719
10976
 
10720
10977
  function RectangleControl(gui, hit) {
10721
- var box = new HighlightBox(gui, {persistent: true, handles: true, classname: 'rectangles'});
10978
+ var box = new HighlightBox(gui, {name: 'rectangle-tool', persistent: true, handles: true, classname: 'rectangles', draggable: false});
10722
10979
  var _on = false;
10723
10980
  var dragInfo;
10724
10981
 
@@ -10734,7 +10991,7 @@
10734
10991
 
10735
10992
  hit.on('change', function(e) {
10736
10993
  if (!_on) return;
10737
- // TODO: handle multiple hits (see gui-inspection-control2)
10994
+ // TODO: handle multiple hits (see gui-inspection-control)
10738
10995
  var id = e.id;
10739
10996
  if (e.id > -1 && e.pinned) {
10740
10997
  var target = hit.getHitTarget();
@@ -10755,8 +11012,7 @@
10755
11012
  gui.container.findChild('.map-layers').classed('dragging', true);
10756
11013
 
10757
11014
  } else if (dragInfo) {
10758
- // TODO: handle this event: add undo/redo states
10759
- gui.dispatchEvent('rectangle_dragend', dragInfo);
11015
+ gui.dispatchEvent('rectangle_dragend', dragInfo); // save undo state
10760
11016
  gui.container.findChild('.map-layers').classed('dragging', false);
10761
11017
  reset();
10762
11018
  } else {
@@ -11296,7 +11552,7 @@
11296
11552
  _hit,
11297
11553
  _basemap,
11298
11554
  _intersectionLyr, _activeLyr, _overlayLyr,
11299
- _stack, _dynamicCRS;
11555
+ _renderer, _dynamicCRS;
11300
11556
 
11301
11557
  _basemap = new Basemap(gui, _ext);
11302
11558
 
@@ -11391,13 +11647,12 @@
11391
11647
  clearAllDisplayArcs();
11392
11648
 
11393
11649
  // 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
11650
+ getContentLayers().forEach(function(lyr) {
11651
+ projectDisplayLayer(lyr, newCRS);
11652
+ });
11653
+
11654
+ // kludge to make sure all layers have styles
11655
+ updateLayerStyles(getContentLayers());
11401
11656
 
11402
11657
  // Update map extent (also triggers redraw)
11403
11658
  projectMapExtent(_ext, oldCRS, this.getDisplayCRS(), calcFullBounds());
@@ -11450,19 +11705,12 @@
11450
11705
  updateVisibleMapLayers();
11451
11706
  fullBounds = calcFullBounds();
11452
11707
 
11453
- if (!prevLyr || prevLyr.tabular || _activeLyr.tabular || isFrameView()) {
11708
+ if (!prevLyr || prevLyr.tabular || _activeLyr.tabular) {
11454
11709
  needReset = true;
11455
11710
  } else {
11456
11711
  needReset = mapNeedsReset(fullBounds, _ext.getFullBounds(), _ext.getBounds(), e.flags);
11457
11712
  }
11458
11713
 
11459
- if (isFrameView()) {
11460
- _nav.setZoomFactor(0.05); // slow zooming way down to allow fine-tuning frame placement // 0.03
11461
- _ext.setFrame(calcFullBounds()); // TODO: remove redundancy with drawLayers()
11462
- needReset = true; // snap to frame extent
11463
- } else {
11464
- _nav.setZoomFactor(1);
11465
- }
11466
11714
  _ext.setFullBounds(fullBounds, getStrictBounds()); // update 'home' button extent
11467
11715
 
11468
11716
  if (needReset) {
@@ -11475,7 +11723,7 @@
11475
11723
  // Initialization just before displaying the map for the first time
11476
11724
  function initMap() {
11477
11725
  _ext.resize();
11478
- _stack = new LayerStack(gui, el, _ext, _mouse);
11726
+ _renderer = new LayerRenderer(gui, el, _ext, _mouse);
11479
11727
  gui.buttons.show();
11480
11728
 
11481
11729
  if (opts.inspectorControl) {
@@ -11485,16 +11733,13 @@
11485
11733
  new BoxTool(gui, _ext, _nav),
11486
11734
  new RectangleControl(gui, _hit),
11487
11735
  initInteractiveEditing(gui, _ext, _hit);
11488
- // initDrawing(gui, _ext, _mouse, _hit);
11736
+ initDrawing(gui, _ext, _mouse, _hit);
11489
11737
  _hit.on('change', updateOverlayLayer);
11490
11738
  }
11491
11739
 
11492
11740
  _ext.on('change', function(e) {
11493
11741
  if (_basemap) _basemap.refresh(); // keep basemap synced up (if enabled)
11494
11742
  if (e.reset) return; // don't need to redraw map here if extent has been reset
11495
- if (isFrameView()) {
11496
- updateFrameExtent();
11497
- }
11498
11743
  drawLayers('nav');
11499
11744
  });
11500
11745
 
@@ -11516,7 +11761,6 @@
11516
11761
 
11517
11762
  // 'hover' avoids redrawing all svg symbols when only highlight needs to refresh
11518
11763
  drawLayers('hover');
11519
- // drawLayers();
11520
11764
  }
11521
11765
 
11522
11766
  function getDisplayOptions() {
@@ -11525,21 +11769,6 @@
11525
11769
  };
11526
11770
  }
11527
11771
 
11528
- // Update map frame after user navigates the map in frame edit mode
11529
- function updateFrameExtent() {
11530
- var frameLyr = internal.findFrameLayer(model);
11531
- var rec = frameLyr.data.getRecordAt(0);
11532
- var viewBounds = _ext.getBounds();
11533
- var w = viewBounds.width() * rec.width / _ext.width();
11534
- var h = w * rec.height / rec.width;
11535
- var cx = viewBounds.centerX();
11536
- var cy = viewBounds.centerY();
11537
- rec.bbox = [cx - w/2, cy - h/2, cx + w/2, cy + h/2];
11538
- _ext.setFrame(getFrameData());
11539
- _ext.setFullBounds(new Bounds(rec.bbox));
11540
- _ext.reset();
11541
- }
11542
-
11543
11772
  function getStrictBounds() {
11544
11773
  if (internal.isWebMercator(map.getDisplayCRS())) {
11545
11774
  return getMapboxBounds();
@@ -11551,12 +11780,9 @@
11551
11780
  _ext.setFullBounds(calcFullBounds(), getStrictBounds());
11552
11781
  }
11553
11782
 
11554
- function calcFullBounds() {
11555
- if (isPreviewView()) {
11556
- return internal.getFrameLayerBounds(internal.findFrameLayer(model));
11557
- }
11783
+ function getContentLayerBounds() {
11558
11784
  var b = new Bounds();
11559
- var layers = getDrawableContentLayers();
11785
+ var layers = getContentLayers();
11560
11786
  layers.forEach(function(lyr) {
11561
11787
  b.mergeBounds(lyr.bounds);
11562
11788
  });
@@ -11565,6 +11791,11 @@
11565
11791
  // assign bounds to empty layers, to prevent rendering errors downstream
11566
11792
  b.setBounds(0,0,0,0);
11567
11793
  }
11794
+ return b;
11795
+ }
11796
+
11797
+ function calcFullBounds() {
11798
+ var b = getContentLayerBounds();
11568
11799
 
11569
11800
  // add margin
11570
11801
  // use larger margin for small sizes
@@ -11590,33 +11821,25 @@
11590
11821
  return isActiveLayer(lyr) || lyr.pinned;
11591
11822
  }
11592
11823
 
11593
- function isFrameLayer(lyr) {
11594
- return !!(lyr && lyr == internal.findFrameLayer(model));
11595
- }
11596
-
11597
11824
  function isTableView() {
11598
11825
  return !isPreviewView() && !!_activeLyr.tabular;
11599
11826
  }
11600
11827
 
11601
- // Preview view: a special view centering the map 'frame'
11602
- // (disabling this pending interface rethink)
11603
- function isPreviewView() {
11604
- return false;
11605
- // var frameLyr = internal.findFrameLayer(model);
11606
- // return !!frameLyr; // && isVisibleLayer(frameLyr)
11828
+ function findFrameLayer() {
11829
+ return getVisibleMapLayers().find(function(lyr) {
11830
+ return internal.isFrameLayer(lyr.layer, lyr.arcs);
11831
+ });
11607
11832
  }
11608
11833
 
11609
- // Frame view means frame layer is visible and active (selected)
11610
- // (disabling pending rethink)
11611
- function isFrameView() {
11612
- return false;
11613
- // var frameLyr = internal.findFrameLayer(model);
11614
- // return isActiveLayer(frameLyr) && isVisibleLayer(frameLyr);
11834
+ // Preview view: symbols are scaled based on display size of frame layer
11835
+ function isPreviewView() {
11836
+ var data = getFrameData();
11837
+ return !!data;
11615
11838
  }
11616
11839
 
11617
11840
  function getFrameData() {
11618
- var frameLyr = internal.findFrameLayer(model);
11619
- return frameLyr && internal.getFurnitureLayerData(frameLyr) || null;
11841
+ var lyr = findFrameLayer();
11842
+ return lyr && internal.getFrameLayerData(lyr.layer, lyr.arcs) || null;
11620
11843
  }
11621
11844
 
11622
11845
  function clearAllDisplayArcs() {
@@ -11648,7 +11871,7 @@
11648
11871
  });
11649
11872
  }
11650
11873
 
11651
- function getDrawableContentLayers() {
11874
+ function getContentLayers() {
11652
11875
  var layers = getVisibleMapLayers();
11653
11876
  if (isTableView()) return findActiveLayer(layers);
11654
11877
  return layers.filter(function(o) {
@@ -11656,6 +11879,13 @@
11656
11879
  });
11657
11880
  }
11658
11881
 
11882
+ function getDrawableContentLayers() {
11883
+ return getContentLayers().filter(function(lyr) {
11884
+ if (isActiveLayer(lyr.layer) && lyr.layer.hidden) return false;
11885
+ return true;
11886
+ });
11887
+ }
11888
+
11659
11889
  function getDrawableFurnitureLayers(layers) {
11660
11890
  if (!isPreviewView()) return [];
11661
11891
  return getVisibleMapLayers().filter(function(o) {
@@ -11706,7 +11936,7 @@
11706
11936
  var layersMayHaveChanged = action != 'nav'; // !action;
11707
11937
  var fullBounds;
11708
11938
  var contentLayers = getDrawableContentLayers();
11709
- var furnitureLayers = getDrawableFurnitureLayers();
11939
+ // var furnitureLayers = getDrawableFurnitureLayers();
11710
11940
  if (!(_ext.width() > 0 && _ext.height() > 0)) {
11711
11941
  // TODO: track down source of these errors
11712
11942
  console.error("Collapsed map container, unable to draw.");
@@ -11714,7 +11944,7 @@
11714
11944
  }
11715
11945
  if (layersMayHaveChanged) {
11716
11946
  // kludge to handle layer visibility toggling
11717
- _ext.setFrame(isPreviewView() ? getFrameData() : null);
11947
+ _ext.setFrameData(isPreviewView() ? getFrameData() : null);
11718
11948
  updateFullBounds();
11719
11949
  updateLayerStyles(contentLayers);
11720
11950
  updateLayerStackOrder(model.getLayers());// update menu_order property of all layers
@@ -11725,11 +11955,11 @@
11725
11955
  }
11726
11956
  // RENDERING
11727
11957
  // draw main content layers
11728
- _stack.drawMainLayers(contentLayers, action);
11958
+ _renderer.drawMainLayers(contentLayers, action);
11729
11959
  // draw hover & selection overlay
11730
- _stack.drawOverlayLayer(_overlayLyr, action);
11960
+ _renderer.drawOverlayLayer(_overlayLyr, action);
11731
11961
  // draw furniture
11732
- _stack.drawFurnitureLayers(furnitureLayers, action);
11962
+ // _renderer.drawFurnitureLayers(furnitureLayers, action);
11733
11963
  gui.dispatchEvent('map_rendered');
11734
11964
  }
11735
11965
  }