mapshaper 0.5.87 → 0.5.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -182,6 +182,10 @@
182
182
  return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null;
183
183
  };
184
184
 
185
+ GUI.textIsSelected = function() {
186
+ return !!GUI.getInputElement();
187
+ };
188
+
185
189
  GUI.selectElement = function(el) {
186
190
  var range = document.createRange(),
187
191
  sel = window.getSelection();
@@ -1027,6 +1031,10 @@
1027
1031
  var types = Array.from(e.clipboardData.types || []).join(',');
1028
1032
  var items = Array.from(e.clipboardData.items || []);
1029
1033
  var files;
1034
+ if (GUI.textIsSelected()) {
1035
+ // user is probably pasting text into an editable text field
1036
+ return;
1037
+ }
1030
1038
  block(e);
1031
1039
  // Browser compatibility (tested on MacOS only):
1032
1040
  // Chrome and Safari: full support
@@ -3395,7 +3403,7 @@
3395
3403
  GUI.onClick(entry, function() {
3396
3404
  var target = findLayerById(id);
3397
3405
  // don't select if user is typing or dragging
3398
- if (!GUI.getInputElement() && !dragging) {
3406
+ if (!GUI.textIsSelected() && !dragging) {
3399
3407
  gui.clearMode();
3400
3408
  if (!map.isActiveLayer(target.layer)) {
3401
3409
  model.selectLayer(target.layer, target.dataset);
@@ -3574,637 +3582,135 @@
3574
3582
  }
3575
3583
  }
3576
3584
 
3577
- function getSymbolNodeId(node) {
3578
- return parseInt(node.getAttribute('data-id'));
3579
- }
3580
-
3581
- function getSvgSymbolTransform(xy, ext) {
3582
- var scale = ext.getSymbolScale();
3583
- var p = ext.translateCoords(xy[0], xy[1]);
3584
- return internal.svg.getTransform(p, scale);
3585
- }
3585
+ // import { cloneShape } from '../paths/mapshaper-shape-utils';
3586
+ // import { copyRecord } from '../datatable/mapshaper-data-utils';
3587
+ var snapVerticesToPoint$1 = internal.snapVerticesToPoint;
3588
+ var cloneShape = internal.cloneShape;
3589
+ var copyRecord = internal.copyRecord;
3586
3590
 
3591
+ function Undo(gui) {
3592
+ var history, offset, stashedUndo;
3593
+ reset();
3587
3594
 
3588
- function repositionSymbols(elements, layer, ext) {
3589
- var el, idx, shp, p, displayOn, inView, displayBounds;
3590
- for (var i=0, n=elements.length; i<n; i++) {
3591
- el = elements[i];
3592
- idx = getSymbolNodeId(el);
3593
- shp = layer.shapes[idx];
3594
- if (!shp) continue;
3595
- p = shp[0];
3596
- // OPTIMIZATION: only display symbols that are in view
3597
- // quick-and-dirty hit-test: expand the extent rectangle by a percentage.
3598
- // very large symbols will disappear before they're completely out of view
3599
- displayBounds = ext.getBounds(1.15);
3600
- displayOn = !el.hasAttribute('display') || el.getAttribute('display') == 'block';
3601
- inView = displayBounds.containsPoint(p[0], p[1]);
3602
- if (inView) {
3603
- if (!displayOn) el.setAttribute('display', 'block');
3604
- el.setAttribute('transform', getSvgSymbolTransform(p, ext));
3605
- } else {
3606
- if (displayOn) el.setAttribute('display', 'none');
3607
- }
3595
+ function reset() {
3596
+ history = [];
3597
+ stashedUndo = null;
3598
+ offset = 0;
3608
3599
  }
3609
- }
3610
-
3611
- function renderSymbols(lyr, ext, type) {
3612
- var records = lyr.data.getRecords();
3613
- var symbols = lyr.shapes.map(function(shp, i) {
3614
- var d = records[i];
3615
- var obj = type == 'label' ? internal.svg.importStyledLabel(d) :
3616
- internal.svg.importSymbol(d['svg-symbol']);
3617
- if (!obj || !shp) return null;
3618
- obj.properties.transform = getSvgSymbolTransform(shp[0], ext);
3619
- obj.properties['data-id'] = i;
3620
- return obj;
3621
- });
3622
- var obj = internal.getEmptyLayerForSVG(lyr, {});
3623
- obj.children = symbols;
3624
- return internal.svg.stringify(obj);
3625
- }
3626
-
3627
- function isMultilineLabel(textNode) {
3628
- return textNode.childNodes.length > 1;
3629
- }
3630
3600
 
3631
- function toggleTextAlign(textNode, rec) {
3632
- var curr = rec['text-anchor'] || 'middle';
3633
- var value = curr == 'middle' && 'start' || curr == 'start' && 'end' || 'middle';
3634
- updateTextAnchor(value, textNode, rec);
3635
- }
3636
3601
 
3637
- // Set an attribute on a <text> node and any child <tspan> elements
3638
- // (mapshaper's svg labels require tspans to have the same x and dx values
3639
- // as the enclosing text node)
3640
- function setMultilineAttribute(textNode, name, value) {
3641
- var n = textNode.childNodes.length;
3642
- var i = -1;
3643
- var child;
3644
- textNode.setAttribute(name, value);
3645
- while (++i < n) {
3646
- child = textNode.childNodes[i];
3647
- if (child.tagName == 'tspan') {
3648
- child.setAttribute(name, value);
3649
- }
3602
+ function isUndoEvt(e) {
3603
+ return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
3650
3604
  }
3651
- }
3652
3605
 
3653
- function findSvgRoot(el) {
3654
- while (el && el.tagName != 'html' && el.tagName != 'body') {
3655
- if (el.tagName == 'svg') return el;
3656
- el = el.parentNode;
3606
+ function isRedoEvt(e) {
3607
+ return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y');
3657
3608
  }
3658
- return null;
3659
- }
3660
-
3661
- // p: pixel coordinates of label anchor
3662
- function autoUpdateTextAnchor(textNode, rec, p) {
3663
- var svg = findSvgRoot(textNode);
3664
- var rect = textNode.getBoundingClientRect();
3665
- var labelCenterX = rect.left - svg.getBoundingClientRect().left + rect.width / 2;
3666
- var xpct = (labelCenterX - p[0]) / rect.width; // offset of label center from anchor center
3667
- var value = xpct < -0.25 && 'end' || xpct > 0.25 && 'start' || 'middle';
3668
- updateTextAnchor(value, textNode, rec);
3669
- }
3670
3609
 
3671
- // @value: optional position to set; if missing, auto-set
3672
- function updateTextAnchor(value, textNode, rec) {
3673
- var rect = textNode.getBoundingClientRect();
3674
- var width = rect.width;
3675
- var curr = rec['text-anchor'] || 'middle';
3676
- var xshift = 0;
3610
+ gui.keyboard.on('keydown', function(evt) {
3611
+ var e = evt.originalEvent,
3612
+ kc = e.keyCode;
3613
+ if (isUndoEvt(e)) {
3614
+ this.undo();
3615
+ e.stopPropagation();
3616
+ e.preventDefault();
3617
+ }
3618
+ if (isRedoEvt(e)) {
3619
+ this.redo();
3620
+ e.stopPropagation();
3621
+ e.preventDefault();
3622
+ }
3677
3623
 
3678
- // console.log("anchor() curr:", curr, "xpct:", xpct, "left:", rect.left, "anchorX:", anchorX, "targ:", targ, "dx:", xshift)
3679
- if (curr == 'middle' && value == 'end' || curr == 'start' && value == 'middle') {
3680
- xshift = width / 2;
3681
- } else if (curr == 'middle' && value == 'start' || curr == 'end' && value == 'middle') {
3682
- xshift = -width / 2;
3683
- } else if (curr == 'start' && value == 'end') {
3684
- xshift = width;
3685
- } else if (curr == 'end' && value == 'start') {
3686
- xshift = -width;
3687
- }
3688
- if (xshift) {
3689
- rec['text-anchor'] = value;
3690
- applyDelta(rec, 'dx', Math.round(xshift));
3691
- }
3692
- }
3624
+ }, this, 10);
3693
3625
 
3694
- // handle either numeric strings or numbers in fields
3695
- function applyDelta(rec, key, delta) {
3696
- var currVal = rec[key];
3697
- var isString = utils.isString(currVal);
3698
- var newVal = (+currVal + delta) || 0;
3699
- rec[key] = isString ? String(newVal) : newVal;
3700
- }
3626
+ // undo/redo point/symbol dragging
3627
+ //
3628
+ gui.on('symbol_dragstart', function(e) {
3629
+ stashedUndo = this.makePointSetter(e.FID);
3630
+ }, this);
3701
3631
 
3702
- function getDisplayCoordsById(id, layer, ext) {
3703
- var coords = getPointCoordsById(id, layer);
3704
- return ext.translateCoords(coords[0], coords[1]);
3705
- }
3632
+ gui.on('symbol_dragend', function(e) {
3633
+ var redo = this.makePointSetter(e.FID);
3634
+ this.addHistoryState(stashedUndo, redo);
3635
+ }, this);
3706
3636
 
3707
- function snapVerticesToPoint(ids, p, arcs, final) {
3708
- ids.forEach(function(idx) {
3709
- internal.setVertexCoords(p[0], p[1], idx, arcs);
3710
- });
3711
- if (final) {
3712
- // kludge to get dataset to recalculate internal bounding boxes
3713
- arcs.transformPoints(function() {});
3714
- }
3715
- }
3637
+ // undo/redo label dragging
3638
+ //
3639
+ gui.on('label_dragstart', function(e) {
3640
+ stashedUndo = this.makeDataSetter(e.FID);
3641
+ }, this);
3716
3642
 
3717
- function getPointCoordsById(id, layer) {
3718
- var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
3719
- if (!coords || coords.length != 1) {
3720
- return null;
3721
- }
3722
- return coords[0];
3723
- }
3643
+ gui.on('label_dragend', function(e) {
3644
+ var redo = this.makeDataSetter(e.FID);
3645
+ this.addHistoryState(stashedUndo, redo);
3646
+ }, this);
3724
3647
 
3725
- function translateDeltaDisplayCoords(dx, dy, ext) {
3726
- var a = ext.translatePixelCoords(0, 0);
3727
- var b = ext.translatePixelCoords(dx, dy);
3728
- return [b[0] - a[0], b[1] - a[1]];
3729
- }
3648
+ // undo/redo data editing
3649
+ // TODO: consider setting selected feature to the undo/redo target feature
3650
+ //
3651
+ gui.on('data_preupdate', function(e) {
3652
+ stashedUndo = this.makeDataSetter(e.FID);
3653
+ }, this);
3730
3654
 
3655
+ gui.on('data_postupdate', function(e) {
3656
+ var redo = this.makeDataSetter(e.FID);
3657
+ this.addHistoryState(stashedUndo, redo);
3658
+ }, this);
3731
3659
 
3732
- function SymbolDragging2(gui, ext, hit) {
3733
- // var targetTextNode; // text node currently being dragged
3734
- var dragging = false;
3735
- var activeRecord;
3736
- var activeId = -1;
3737
- var self = new EventDispatcher();
3738
- var activeVertexIds = null; // for vertex dragging
3660
+ // undo/redo vertex dragging
3661
+ gui.on('vertex_dragstart', function(e) {
3662
+ stashedUndo = this.makeVertexSetter(e.FID, e.vertex_ids);
3663
+ }, this);
3739
3664
 
3740
- initDragging();
3665
+ gui.on('vertex_dragend', function(e) {
3666
+ var redo = this.makeVertexSetter(e.FID, e.vertex_ids);
3667
+ this.addHistoryState(stashedUndo, redo);
3668
+ }, this);
3741
3669
 
3742
- return self;
3670
+ this.clear = function() {
3671
+ reset();
3672
+ };
3743
3673
 
3744
- function labelEditingEnabled() {
3745
- return gui.interaction && gui.interaction.getMode() == 'labels' ? true : false;
3746
- }
3674
+ this.makePointSetter = function(i) {
3675
+ var target = gui.model.getActiveLayer();
3676
+ var shp = cloneShape(target.layer.shapes[i]);
3677
+ return function() {
3678
+ target.layer.shapes[i] = shp;
3679
+ };
3680
+ };
3747
3681
 
3748
- function locationEditingEnabled() {
3749
- return gui.interaction && gui.interaction.getMode() == 'location' ? true : false;
3750
- }
3682
+ this.makeDataSetter = function(id) {
3683
+ var target = gui.model.getActiveLayer();
3684
+ var rec = copyRecord(target.layer.data.getRecordAt(id));
3685
+ return function() {
3686
+ target.layer.data.getRecords()[id] = rec;
3687
+ gui.dispatchEvent('popup-needs-refresh');
3688
+ };
3689
+ };
3751
3690
 
3752
- function vertexEditingEnabled() {
3753
- return gui.interaction && gui.interaction.getMode() == 'vertices' ? true : false;
3754
- }
3691
+ this.makeVertexSetter = function(fid, ids) {
3692
+ var target = gui.model.getActiveLayer();
3693
+ var arcs = target.dataset.arcs;
3694
+ var p = internal.getVertexCoords(ids[0], arcs);
3695
+ return function() {
3696
+ snapVerticesToPoint$1(ids, p, arcs, true);
3697
+ };
3698
+ };
3755
3699
 
3756
- // update symbol by setting attributes
3757
- function updateSymbol(node, d) {
3758
- var a = d['text-anchor'];
3759
- if (a) node.setAttribute('text-anchor', a);
3760
- setMultilineAttribute(node, 'dx', d.dx || 0);
3761
- node.setAttribute('y', d.dy || 0);
3762
- }
3763
-
3764
- // update symbol by re-rendering it
3765
- function updateSymbol2(node, d, id) {
3766
- var o = internal.svg.importStyledLabel(d); // TODO: symbol support
3767
- var activeLayer = hit.getHitTarget().layer;
3768
- var xy = activeLayer.shapes[id][0];
3769
- var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3770
- var node2;
3771
- o.properties.transform = getSvgSymbolTransform(xy, ext);
3772
- o.properties['data-id'] = id;
3773
- // o.properties['class'] = 'selected';
3774
- g.innerHTML = internal.svg.stringify(o);
3775
- node2 = g.firstChild;
3776
- node.parentNode.replaceChild(node2, node);
3777
- gui.dispatchEvent('popup-needs-refresh');
3778
- return node2;
3779
- }
3780
-
3781
- function initDragging() {
3782
- var downEvt;
3783
- var eventPriority = 1;
3784
-
3785
- // inspector and label editing aren't fully synced - stop editing if inspector opens
3786
- // gui.on('inspector_on', function() {
3787
- // stopEditing();
3788
- // });
3789
-
3790
- gui.on('interaction_mode_change', function(e) {
3791
- if (e.mode != 'labels') {
3792
- stopDragging();
3793
- }
3794
- gui.undo.clear(); // TODO: put this elsewhere?
3795
- });
3796
-
3797
- // down event on svg
3798
- // a: off text
3799
- // -> stop editing
3800
- // b: on text
3801
- // 1: not editing -> nop
3802
- // 2: on selected text -> start dragging
3803
- // 3: on other text -> stop dragging, select new text
3804
-
3805
- hit.on('dragstart', function(e) {
3806
- if (e.id >= 0 === false) return;
3807
- if (labelEditingEnabled() && onLabelDragStart(e)) {
3808
- triggerGlobalEvent('label_dragstart', e);
3809
- startDragging();
3810
- } else if (locationEditingEnabled()) {
3811
- triggerGlobalEvent('symbol_dragstart', e);
3812
- startDragging();
3813
- } else if (vertexEditingEnabled()) {
3814
- onVertexDragStart(e);
3815
- triggerGlobalEvent('vertex_dragstart', e);
3816
- startDragging();
3817
- }
3818
- });
3819
-
3820
- hit.on('drag', function(e) {
3821
- if (labelEditingEnabled()) {
3822
- onLabelDrag(e);
3823
- } else if (locationEditingEnabled()) {
3824
- onLocationDrag(e);
3825
- } else if (vertexEditingEnabled()) {
3826
- onVertexDrag(e);
3827
- }
3828
- });
3829
-
3830
- hit.on('dragend', function(e) {
3831
- if (locationEditingEnabled()) {
3832
- triggerGlobalEvent('symbol_dragend', e);
3833
- stopDragging();
3834
- } else if (labelEditingEnabled()) {
3835
- triggerGlobalEvent('label_dragend', e);
3836
- stopDragging();
3837
- } else if (vertexEditingEnabled()) {
3838
- // kludge to get dataset to recalculate internal bounding boxes
3839
- hit.getHitTarget().arcs.transformPoints(function() {});
3840
- triggerGlobalEvent('vertex_dragend', e);
3841
- stopDragging();
3842
- }
3843
- });
3844
-
3845
- hit.on('click', function(e) {
3846
- if (labelEditingEnabled()) {
3847
- onLabelClick(e);
3848
- }
3849
- });
3850
-
3851
- function getVertexEventData(e) {
3852
- return {
3853
- FID: activeId,
3854
- vertexIds: activeVertexIds
3855
- };
3856
- }
3857
-
3858
- function onLocationDrag(e) {
3859
- var lyr = hit.getHitTarget().layer;
3860
- var p = getPointCoordsById(e.id, lyr);
3861
- if (!p) return;
3862
- var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
3863
- p[0] += diff[0];
3864
- p[1] += diff[1];
3865
- self.dispatchEvent('location_change'); // signal map to redraw
3866
- triggerGlobalEvent('symbol_drag', e);
3867
- }
3868
-
3869
- function onVertexDragStart(e) {
3870
- var target = hit.getHitTarget();
3871
- var p = ext.translatePixelCoords(e.x, e.y);
3872
- activeVertexIds = internal.findNearestVertices(p, target.layer.shapes[e.id], target.arcs);
3873
- activeId = e.id;
3874
- }
3875
-
3876
- function onVertexDrag(e) {
3877
- var target = hit.getHitTarget();
3878
- if (!activeVertexIds) return; // ignore error condition
3879
- var p = ext.translatePixelCoords(e.x, e.y);
3880
- if (gui.keyboard.shiftIsPressed()) {
3881
- internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs);
3882
- }
3883
- snapVerticesToPoint(activeVertexIds, p, target.arcs);
3884
- self.dispatchEvent('location_change'); // signal map to redraw
3885
- }
3886
-
3887
- function onLabelClick(e) {
3888
- var textNode = getTextTarget3(e);
3889
- var rec = getLabelRecordById(e.id);
3890
- if (textNode && rec && isMultilineLabel(textNode)) {
3891
- toggleTextAlign(textNode, rec);
3892
- updateSymbol2(textNode, rec, e.id);
3893
- // e.stopPropagation(); // prevent pin/unpin on popup
3894
- }
3895
- }
3896
-
3897
- function triggerGlobalEvent(type, e) {
3898
- if (e.id >= 0 === false) return;
3899
- var o = {
3900
- FID: e.id,
3901
- layer_name: hit.getHitTarget().layer.name,
3902
- vertex_ids: activeVertexIds
3903
- };
3904
- // fire event to signal external editor that symbol coords have changed
3905
- gui.dispatchEvent(type, o);
3906
- }
3907
-
3908
- function getLabelRecordById(id) {
3909
- var table = hit.getTargetDataTable();
3910
- if (id >= 0 === false || !table) return null;
3911
- // add dx and dy properties, if not available
3912
- if (!table.fieldExists('dx')) {
3913
- table.addField('dx', 0);
3914
- }
3915
- if (!table.fieldExists('dy')) {
3916
- table.addField('dy', 0);
3917
- }
3918
- if (!table.fieldExists('text-anchor')) {
3919
- table.addField('text-anchor', '');
3920
- }
3921
- return table.getRecordAt(id);
3922
- }
3923
-
3924
- function onLabelDragStart(e) {
3925
- var textNode = getTextTarget3(e);
3926
- var table = hit.getTargetDataTable();
3927
- if (!textNode || !table) return false;
3928
- activeId = e.id;
3929
- activeRecord = getLabelRecordById(activeId);
3930
- downEvt = e;
3931
- return true;
3932
- }
3933
-
3934
- function onLabelDrag(e) {
3935
- var scale = ext.getSymbolScale() || 1;
3936
- var textNode;
3937
- if (!dragging) return;
3938
- if (e.id != activeId) {
3939
- error("Mismatched hit ids:", e.id, activeId);
3940
- }
3941
- applyDelta(activeRecord, 'dx', e.dx / scale);
3942
- applyDelta(activeRecord, 'dy', e.dy / scale);
3943
- textNode = getTextTarget3(e);
3944
- if (!isMultilineLabel(textNode)) {
3945
- // update anchor position of single-line labels based on label position
3946
- // relative to anchor point, for better placement when eventual display font is
3947
- // different from mapshaper's font.
3948
- autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext));
3949
- }
3950
- // updateSymbol(targetTextNode, activeRecord);
3951
- updateSymbol2(textNode, activeRecord, activeId);
3952
- }
3953
-
3954
- function getSymbolNodeById(id, parent) {
3955
- // TODO: optimize selector
3956
- var sel = '[data-id="' + id + '"]';
3957
- return parent.querySelector(sel);
3958
- }
3959
-
3960
- function getTextTarget3(e) {
3961
- if (e.id > -1 === false || !e.container) return null;
3962
- return getSymbolNodeById(e.id, e.container);
3963
- }
3964
-
3965
- function getTextTarget2(e) {
3966
- var el = e && e.targetSymbol || null;
3967
- if (el && el.tagName == 'tspan') {
3968
- el = el.parentNode;
3969
- }
3970
- return el && el.tagName == 'text' ? el : null;
3971
- }
3972
-
3973
- function getTextTarget(e) {
3974
- var el = e.target;
3975
- if (el.tagName == 'tspan') {
3976
- el = el.parentNode;
3977
- }
3978
- return el.tagName == 'text' ? el : null;
3979
- }
3980
-
3981
- // svg.addEventListener('mousedown', function(e) {
3982
- // var textTarget = getTextTarget(e);
3983
- // downEvt = e;
3984
- // if (!textTarget) {
3985
- // stopEditing();
3986
- // } else if (!editing) {
3987
- // // nop
3988
- // } else if (textTarget == targetTextNode) {
3989
- // startDragging();
3990
- // } else {
3991
- // startDragging();
3992
- // editTextNode(textTarget);
3993
- // }
3994
- // });
3995
-
3996
- // up event on svg
3997
- // a: currently dragging text
3998
- // -> stop dragging
3999
- // b: clicked on a text feature
4000
- // -> start editing it
4001
-
4002
-
4003
- // svg.addEventListener('mouseup', function(e) {
4004
- // var textTarget = getTextTarget(e);
4005
- // var isClick = isClickEvent(e, downEvt);
4006
- // if (isClick && textTarget && textTarget == targetTextNode &&
4007
- // activeRecord && isMultilineLabel(targetTextNode)) {
4008
- // toggleTextAlign(targetTextNode, activeRecord);
4009
- // updateSymbol();
4010
- // }
4011
- // if (dragging) {
4012
- // stopDragging();
4013
- // } else if (isClick && textTarget) {
4014
- // editTextNode(textTarget);
4015
- // }
4016
- // });
4017
-
4018
- // block dbl-click navigation when editing
4019
- // mouse.on('dblclick', function(e) {
4020
- // if (editing) e.stopPropagation();
4021
- // }, null, eventPriority);
4022
-
4023
- // mouse.on('dragstart', function(e) {
4024
- // onLabelDrag(e);
4025
- // }, null, eventPriority);
4026
-
4027
- // mouse.on('drag', function(e) {
4028
- // var scale = ext.getSymbolScale() || 1;
4029
- // onLabelDrag(e);
4030
- // if (!dragging || !activeRecord) return;
4031
- // applyDelta(activeRecord, 'dx', e.dx / scale);
4032
- // applyDelta(activeRecord, 'dy', e.dy / scale);
4033
- // if (!isMultilineLabel(targetTextNode)) {
4034
- // // update anchor position of single-line labels based on label position
4035
- // // relative to anchor point, for better placement when eventual display font is
4036
- // // different from mapshaper's font.
4037
- // updateTextAnchor(targetTextNode, activeRecord);
4038
- // }
4039
- // // updateSymbol(targetTextNode, activeRecord);
4040
- // targetTextNode = updateSymbol2(targetTextNode, activeRecord, activeId);
4041
- // }, null, eventPriority);
4042
-
4043
- // mouse.on('dragend', function(e) {
4044
- // onLabelDrag(e);
4045
- // stopDragging();
4046
- // }, null, eventPriority);
4047
-
4048
-
4049
- // function onLabelDrag(e) {
4050
- // if (dragging) {
4051
- // e.stopPropagation();
4052
- // }
4053
- // }
4054
- }
4055
-
4056
- function startDragging() {
4057
- dragging = true;
4058
- }
4059
-
4060
- function stopDragging() {
4061
- dragging = false;
4062
- activeId = -1;
4063
- activeRecord = null;
4064
- activeVertexIds = null;
4065
- }
4066
-
4067
- function isClickEvent(up, down) {
4068
- var elapsed = Math.abs(down.timeStamp - up.timeStamp);
4069
- var dx = up.screenX - down.screenX;
4070
- var dy = up.screenY - down.screenY;
4071
- var dist = Math.sqrt(dx * dx + dy * dy);
4072
- return dist <= 4 && elapsed < 300;
4073
- }
4074
-
4075
- }
4076
-
4077
- // import { cloneShape } from '../paths/mapshaper-shape-utils';
4078
- // import { copyRecord } from '../datatable/mapshaper-data-utils';
4079
- var cloneShape = internal.cloneShape;
4080
- var copyRecord = internal.copyRecord;
4081
-
4082
- function Undo(gui) {
4083
- var history, offset, stashedUndo;
4084
- reset();
4085
-
4086
- function reset() {
4087
- history = [];
4088
- stashedUndo = null;
4089
- offset = 0;
4090
- }
4091
-
4092
- function refreshMap() {
4093
- gui.dispatchEvent('undo_redo');
4094
- }
4095
-
4096
- function isUndoEvt(e) {
4097
- return (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key == 'z';
4098
- }
4099
-
4100
- function isRedoEvt(e) {
4101
- return (e.ctrlKey || e.metaKey) && (e.shiftKey && e.key == 'z' || !e.shiftKey && e.key == 'y');
4102
- }
4103
-
4104
- gui.keyboard.on('keydown', function(evt) {
4105
- var e = evt.originalEvent,
4106
- kc = e.keyCode;
4107
- if (isUndoEvt(e)) {
4108
- this.undo();
4109
- e.stopPropagation();
4110
- e.preventDefault();
4111
- }
4112
- if (isRedoEvt(e)) {
4113
- this.redo();
4114
- e.stopPropagation();
4115
- e.preventDefault();
4116
- }
4117
-
4118
- }, this, 10);
4119
-
4120
- // undo/redo point/symbol dragging
4121
- //
4122
- gui.on('symbol_dragstart', function(e) {
4123
- stashedUndo = this.makePointSetter(e.FID);
4124
- }, this);
4125
-
4126
- gui.on('symbol_dragend', function(e) {
4127
- var redo = this.makePointSetter(e.FID);
4128
- this.addHistoryState(stashedUndo, redo);
4129
- }, this);
4130
-
4131
- // undo/redo label dragging
4132
- //
4133
- gui.on('label_dragstart', function(e) {
4134
- stashedUndo = this.makeDataSetter(e.FID);
4135
- }, this);
4136
-
4137
- gui.on('label_dragend', function(e) {
4138
- var redo = this.makeDataSetter(e.FID);
4139
- this.addHistoryState(stashedUndo, redo);
4140
- }, this);
4141
-
4142
- // undo/redo data editing
4143
- // TODO: consider setting selected feature to the undo/redo target feature
4144
- //
4145
- gui.on('data_preupdate', function(e) {
4146
- stashedUndo = this.makeDataSetter(e.FID);
4147
- }, this);
4148
-
4149
- gui.on('data_postupdate', function(e) {
4150
- var redo = this.makeDataSetter(e.FID);
4151
- this.addHistoryState(stashedUndo, redo);
4152
- }, this);
4153
-
4154
- // undo/redo vertex dragging
4155
- gui.on('vertex_dragstart', function(e) {
4156
- stashedUndo = this.makeVertexSetter(e.FID, e.vertex_ids);
4157
- }, this);
4158
-
4159
- gui.on('vertex_dragend', function(e) {
4160
- var redo = this.makeVertexSetter(e.FID, e.vertex_ids);
4161
- this.addHistoryState(stashedUndo, redo);
4162
- }, this);
4163
-
4164
- this.clear = function() {
4165
- reset();
4166
- };
4167
-
4168
- this.makePointSetter = function(i) {
4169
- var target = gui.model.getActiveLayer();
4170
- var shp = cloneShape(target.layer.shapes[i]);
4171
- return function() {
4172
- target.layer.shapes[i] = shp;
4173
- };
4174
- };
4175
-
4176
- this.makeDataSetter = function(id) {
4177
- var target = gui.model.getActiveLayer();
4178
- var rec = copyRecord(target.layer.data.getRecordAt(id));
4179
- return function() {
4180
- target.layer.data.getRecords()[id] = rec;
4181
- gui.dispatchEvent('popup-needs-refresh');
4182
- };
4183
- };
4184
-
4185
- this.makeVertexSetter = function(fid, ids) {
4186
- var target = gui.model.getActiveLayer();
4187
- var arcs = target.dataset.arcs;
4188
- var p = arcs.getVertex2(ids[0]);
4189
- return function() {
4190
- snapVerticesToPoint(ids, p, arcs, true);
4191
- };
4192
- };
4193
-
4194
- this.addHistoryState = function(undo, redo) {
4195
- if (offset > 0) {
4196
- history.splice(-offset);
4197
- offset = 0;
4198
- }
4199
- history.push({undo, redo});
4200
- };
3700
+ this.addHistoryState = function(undo, redo) {
3701
+ if (offset > 0) {
3702
+ history.splice(-offset);
3703
+ offset = 0;
3704
+ }
3705
+ history.push({undo, redo});
3706
+ };
4201
3707
 
4202
3708
  this.undo = function() {
4203
3709
  var item = getHistoryItem();
4204
3710
  if (item) {
4205
3711
  offset++;
4206
3712
  item.undo();
4207
- refreshMap();
3713
+ gui.dispatchEvent('map-needs-refresh');
4208
3714
  }
4209
3715
  };
4210
3716
 
@@ -4213,7 +3719,7 @@
4213
3719
  offset--;
4214
3720
  var item = getHistoryItem();
4215
3721
  item.redo();
4216
- refreshMap();
3722
+ gui.dispatchEvent('map-needs-refresh');
4217
3723
  };
4218
3724
 
4219
3725
  function getHistoryItem() {
@@ -4354,6 +3860,7 @@
4354
3860
 
4355
3861
  var menus = {
4356
3862
  standard: ['info', 'selection', 'data', 'box'],
3863
+ polygons: ['info', 'selection', 'data', 'box', 'vertices'],
4357
3864
  lines: ['info', 'selection', 'data', 'box', 'vertices'],
4358
3865
  table: ['info', 'selection', 'data'],
4359
3866
  labels: ['info', 'selection', 'data', 'box', 'labels', 'location'],
@@ -4465,6 +3972,9 @@
4465
3972
  if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polyline') {
4466
3973
  return menus.lines;
4467
3974
  }
3975
+ if (internal.layerHasPaths(o.layer) && o.layer.geometry_type == 'polygon') {
3976
+ return menus.polygons;
3977
+ }
4468
3978
  return menus.standard;
4469
3979
  }
4470
3980
 
@@ -4811,6 +4321,56 @@
4811
4321
  }
4812
4322
  }
4813
4323
 
4324
+ function getSymbolNodeId(node) {
4325
+ return parseInt(node.getAttribute('data-id'));
4326
+ }
4327
+
4328
+ function getSvgSymbolTransform(xy, ext) {
4329
+ var scale = ext.getSymbolScale();
4330
+ var p = ext.translateCoords(xy[0], xy[1]);
4331
+ return internal.svg.getTransform(p, scale);
4332
+ }
4333
+
4334
+
4335
+ function repositionSymbols(elements, layer, ext) {
4336
+ var el, idx, shp, p, displayOn, inView, displayBounds;
4337
+ for (var i=0, n=elements.length; i<n; i++) {
4338
+ el = elements[i];
4339
+ idx = getSymbolNodeId(el);
4340
+ shp = layer.shapes[idx];
4341
+ if (!shp) continue;
4342
+ p = shp[0];
4343
+ // OPTIMIZATION: only display symbols that are in view
4344
+ // quick-and-dirty hit-test: expand the extent rectangle by a percentage.
4345
+ // very large symbols will disappear before they're completely out of view
4346
+ displayBounds = ext.getBounds(1.15);
4347
+ displayOn = !el.hasAttribute('display') || el.getAttribute('display') == 'block';
4348
+ inView = displayBounds.containsPoint(p[0], p[1]);
4349
+ if (inView) {
4350
+ if (!displayOn) el.setAttribute('display', 'block');
4351
+ el.setAttribute('transform', getSvgSymbolTransform(p, ext));
4352
+ } else {
4353
+ if (displayOn) el.setAttribute('display', 'none');
4354
+ }
4355
+ }
4356
+ }
4357
+
4358
+ function renderSymbols(lyr, ext, type) {
4359
+ var records = lyr.data.getRecords();
4360
+ var symbols = lyr.shapes.map(function(shp, i) {
4361
+ var d = records[i];
4362
+ var obj = type == 'label' ? internal.svg.importStyledLabel(d) :
4363
+ internal.svg.importSymbol(d['svg-symbol']);
4364
+ if (!obj || !shp) return null;
4365
+ obj.properties.transform = getSvgSymbolTransform(shp[0], ext);
4366
+ obj.properties['data-id'] = i;
4367
+ return obj;
4368
+ });
4369
+ var obj = internal.getEmptyLayerForSVG(lyr, {});
4370
+ obj.children = symbols;
4371
+ return internal.svg.stringify(obj);
4372
+ }
4373
+
4814
4374
  function getSvgHitTest(displayLayer) {
4815
4375
 
4816
4376
  return function(pointerEvent) {
@@ -4920,7 +4480,7 @@
4920
4480
  }
4921
4481
 
4922
4482
  // ignore keypress if no feature is selected or user is editing text
4923
- if (pinnedId() == -1 || GUI.getInputElement()) return;
4483
+ if (pinnedId() == -1 || GUI.textIsSelected()) return;
4924
4484
 
4925
4485
  if (e.keyCode == 37 || e.keyCode == 39) {
4926
4486
  // L/R arrow keys
@@ -5078,6 +4638,7 @@
5078
4638
 
5079
4639
  // Hits are re-detected on 'hover' (if hit detection is active)
5080
4640
  mouse.on('hover', function(e) {
4641
+ handlePointerEvent(e);
5081
4642
  if (storedData.pinned || !hitTest || !active) return;
5082
4643
  if (e.hover && isOverMap(e)) {
5083
4644
  // mouse is hovering directly over map area -- update hit detection
@@ -5728,541 +5289,931 @@
5728
5289
  });
5729
5290
  }
5730
5291
 
5731
- gui.on('map_reset', function() {
5732
- ext.home();
5292
+ gui.on('map_reset', function() {
5293
+ ext.home();
5294
+ });
5295
+
5296
+ zoomTween.on('change', function(e) {
5297
+ ext.zoomToExtent(e.value, _fx, _fy);
5298
+ });
5299
+
5300
+ mouse.on('dblclick', function(e) {
5301
+ if (disabled()) return;
5302
+ zoomByPct(getZoomInPct(), e.x / ext.width(), e.y / ext.height());
5303
+ });
5304
+
5305
+ mouse.on('dragstart', function(e) {
5306
+ if (disabled()) return;
5307
+ if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
5308
+ // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
5309
+ boxDrag = !!e.shiftKey;
5310
+ if (boxDrag) {
5311
+ dragStartEvt = e;
5312
+ gui.dispatchEvent('box_drag_start');
5313
+ }
5314
+ });
5315
+
5316
+ mouse.on('drag', function(e) {
5317
+ if (disabled()) return;
5318
+ if (boxDrag) {
5319
+ gui.dispatchEvent('box_drag', getBoxData(e));
5320
+ } else {
5321
+ ext.pan(e.dx, e.dy);
5322
+ }
5323
+ });
5324
+
5325
+ mouse.on('dragend', function(e) {
5326
+ var bbox;
5327
+ if (disabled()) return;
5328
+ if (boxDrag) {
5329
+ boxDrag = false;
5330
+ gui.dispatchEvent('box_drag_end', getBoxData(e));
5331
+ }
5332
+ });
5333
+
5334
+ wheel.on('mousewheel', function(e) {
5335
+ var tickFraction = 0.11; // 0.15; // fraction of zoom step per wheel event;
5336
+ var k = 1 + (tickFraction * e.multiplier * zoomScaleMultiplier),
5337
+ delta = e.direction > 0 ? k : 1 / k;
5338
+ if (disabled()) return;
5339
+ ext.zoomByPct(delta, e.x / ext.width(), e.y / ext.height());
5340
+ });
5341
+
5342
+ function swapElements(arr, i, j) {
5343
+ var tmp = arr[i];
5344
+ arr[i] = arr[j];
5345
+ arr[j] = tmp;
5346
+ }
5347
+
5348
+ function getBoxData(e) {
5349
+ var pageBox = [e.pageX, e.pageY, dragStartEvt.pageX, dragStartEvt.pageY];
5350
+ var mapBox = [e.x, e.y, dragStartEvt.x, dragStartEvt.y];
5351
+ var tmp;
5352
+ if (pageBox[0] > pageBox[2]) {
5353
+ swapElements(pageBox, 0, 2);
5354
+ swapElements(mapBox, 0, 2);
5355
+ }
5356
+ if (pageBox[1] > pageBox[3]) {
5357
+ swapElements(pageBox, 1, 3);
5358
+ swapElements(mapBox, 1, 3);
5359
+ }
5360
+ return {
5361
+ map_bbox: mapBox,
5362
+ page_bbox: pageBox
5363
+ };
5364
+ }
5365
+
5366
+ function disabled() {
5367
+ return !!gui.options.disableNavigation;
5368
+ }
5369
+
5370
+ function zoomIn(e) {
5371
+ if (disabled()) return;
5372
+ zoomByPct(getZoomInPct(e.time), 0.5, 0.5);
5373
+ }
5374
+
5375
+ function zoomOut(e) {
5376
+ if (disabled()) return;
5377
+ zoomByPct(1/getZoomInPct(e.time), 0.5, 0.5);
5378
+ }
5379
+
5380
+ function getZoomInPct(clickTime) {
5381
+ var minScale = 0.2,
5382
+ maxScale = 4,
5383
+ minTime = 100,
5384
+ maxTime = 800,
5385
+ time = utils.clamp(clickTime || 200, minTime, maxTime),
5386
+ k = (time - minTime) / (maxTime - minTime),
5387
+ scale = minScale + k * (maxScale - minScale);
5388
+ return 1 + scale * zoomScaleMultiplier;
5389
+ }
5390
+
5391
+ // @box Bounds with pixels from t,l corner of map area.
5392
+ function zoomToBbox(bbox) {
5393
+ var bounds = new Bounds(bbox),
5394
+ pct = Math.max(bounds.width() / ext.width(), bounds.height() / ext.height()),
5395
+ fx = bounds.centerX() / ext.width() * (1 + pct) - pct / 2,
5396
+ fy = bounds.centerY() / ext.height() * (1 + pct) - pct / 2;
5397
+ zoomByPct(1 / pct, fx, fy);
5398
+ }
5399
+
5400
+ // @pct Change in scale (2 = 2x zoom)
5401
+ // @fx, @fy zoom focus, [0, 1]
5402
+ function zoomByPct(pct, fx, fy) {
5403
+ var w = ext.getBounds().width();
5404
+ _fx = fx;
5405
+ _fy = fy;
5406
+ zoomTween.start(w, w / pct, 400);
5407
+ }
5408
+ }
5409
+
5410
+ function HighlightBox() {
5411
+ var box = El('div').addClass('zoom-box').appendTo('body'),
5412
+ show = box.show.bind(box), // original show() function
5413
+ stroke = 2;
5414
+ box.hide();
5415
+ box.show = function(x1, y1, x2, y2) {
5416
+ var w = Math.abs(x1 - x2),
5417
+ h = Math.abs(y1 - y2);
5418
+ box.css({
5419
+ top: Math.min(y1, y2),
5420
+ left: Math.min(x1, x2),
5421
+ width: Math.max(w - stroke * 2, 1),
5422
+ height: Math.max(h - stroke * 2, 1)
5423
+ });
5424
+ show();
5425
+ };
5426
+ return box;
5427
+ }
5428
+
5429
+ function SelectionTool(gui, ext, hit) {
5430
+ var popup = gui.container.findChild('.selection-tool-options');
5431
+ var box = new HighlightBox();
5432
+ var _on = false;
5433
+
5434
+ gui.addMode('selection_tool', turnOn, turnOff);
5435
+
5436
+ gui.on('interaction_mode_change', function(e) {
5437
+ if (e.mode === 'selection') {
5438
+ gui.enterMode('selection_tool');
5439
+ } else if (gui.getMode() == 'selection_tool') {
5440
+ gui.clearMode();
5441
+ }
5442
+ });
5443
+
5444
+ gui.on('box_drag', function(e) {
5445
+ if (!_on) return;
5446
+ var b = e.page_bbox;
5447
+ box.show(b[0], b[1], b[2], b[3]);
5448
+ updateSelection(e.map_bbox, true);
5449
+ });
5450
+
5451
+ gui.on('box_drag_end', function(e) {
5452
+ if (!_on) return;
5453
+ box.hide();
5454
+ updateSelection(e.map_bbox);
5455
+ });
5456
+
5457
+ function updateSelection(bboxPixels, transient) {
5458
+ var bbox = bboxToCoords(bboxPixels);
5459
+ var active = gui.model.getActiveLayer();
5460
+ var ids = internal.findShapesIntersectingBBox(bbox, active.layer, active.dataset.arcs);
5461
+ if (transient) {
5462
+ hit.setTransientIds(ids);
5463
+ } else if (ids.length) {
5464
+ hit.addSelectionIds(ids);
5465
+ }
5466
+ }
5467
+
5468
+ function turnOn() {
5469
+ _on = true;
5470
+ }
5471
+
5472
+ function bboxToCoords(bbox) {
5473
+ var a = ext.translatePixelCoords(bbox[0], bbox[1]);
5474
+ var b = ext.translatePixelCoords(bbox[2], bbox[3]);
5475
+ return [a[0], b[1], b[0], a[1]];
5476
+ }
5477
+
5478
+ function turnOff() {
5479
+ reset();
5480
+ _on = false;
5481
+ if (gui.interaction.getMode() == 'selection') {
5482
+ // mode change was not initiated by interactive menu -- turn off interactivity
5483
+ gui.interaction.turnOff();
5484
+ }
5485
+ }
5486
+
5487
+ function reset() {
5488
+ popup.hide();
5489
+ hit.clearSelection();
5490
+ }
5491
+
5492
+ function getIdsOpt() {
5493
+ return hit.getSelectionIds().join(',');
5494
+ }
5495
+
5496
+ hit.on('change', function(e) {
5497
+ if (e.mode != 'selection') return;
5498
+ var ids = hit.getSelectionIds();
5499
+ if (ids.length > 0) {
5500
+ // enter this mode when we're ready to show the selection options
5501
+ // (this closes any other active mode, e.g. box_tool)
5502
+ gui.enterMode('selection_tool');
5503
+ popup.show();
5504
+ } else {
5505
+ popup.hide();
5506
+ }
5507
+ });
5508
+
5509
+ new SimpleButton(popup.findChild('.delete-btn')).on('click', function() {
5510
+ var cmd = '-filter invert ids=' + getIdsOpt();
5511
+ runCommand(cmd);
5512
+ });
5513
+
5514
+ new SimpleButton(popup.findChild('.filter-btn')).on('click', function() {
5515
+ var cmd = '-filter ids=' + getIdsOpt();
5516
+ runCommand(cmd);
5733
5517
  });
5734
5518
 
5735
- zoomTween.on('change', function(e) {
5736
- ext.zoomToExtent(e.value, _fx, _fy);
5519
+ new SimpleButton(popup.findChild('.split-btn')).on('click', function() {
5520
+ var cmd = '-split ids=' + getIdsOpt();
5521
+ runCommand(cmd);
5737
5522
  });
5738
5523
 
5739
- mouse.on('dblclick', function(e) {
5740
- if (disabled()) return;
5741
- zoomByPct(getZoomInPct(), e.x / ext.width(), e.y / ext.height());
5524
+ new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
5525
+ hit.clearSelection();
5742
5526
  });
5743
5527
 
5744
- mouse.on('dragstart', function(e) {
5745
- if (disabled()) return;
5746
- if (!internal.layerHasGeometry(gui.model.getActiveLayer().layer)) return;
5747
- // zoomDrag = !!e.metaKey || !!e.ctrlKey; // meta is command on mac, windows key on windows
5748
- boxDrag = !!e.shiftKey;
5749
- if (boxDrag) {
5750
- dragStartEvt = e;
5751
- gui.dispatchEvent('box_drag_start');
5752
- }
5528
+ function runCommand(cmd) {
5529
+ popup.hide();
5530
+ if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) {
5531
+ reset();
5532
+ });
5533
+ }
5534
+ }
5535
+
5536
+ // toNext, toPrev: trigger functions for switching between multiple records
5537
+ function Popup(gui, toNext, toPrev) {
5538
+ var self = new EventDispatcher();
5539
+ var parent = gui.container.findChild('.mshp-main-map');
5540
+ var el = El('div').addClass('popup').appendTo(parent).hide();
5541
+ var content = El('div').addClass('popup-content').appendTo(el);
5542
+ // multi-hit display and navigation
5543
+ var tab = El('div').addClass('popup-tab').appendTo(el).hide();
5544
+ var nav = El('div').addClass('popup-nav').appendTo(tab);
5545
+ var prevLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('◀');
5546
+ var navInfo = El('span').addClass('popup-nav-info').appendTo(nav);
5547
+ var nextLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('▶');
5548
+ var refresh = null;
5549
+ var currId = -1;
5550
+
5551
+ el.addClass('rollover'); // used as a sentinel for the hover function
5552
+
5553
+ nextLink.on('click', toNext);
5554
+ prevLink.on('click', toPrev);
5555
+ gui.on('popup-needs-refresh', function() {
5556
+ if (refresh) refresh();
5753
5557
  });
5754
5558
 
5755
- mouse.on('drag', function(e) {
5756
- if (disabled()) return;
5757
- if (boxDrag) {
5758
- gui.dispatchEvent('box_drag', getBoxData(e));
5559
+ self.show = function(id, ids, lyr, pinned) {
5560
+ var table = lyr.data; // table can be null (e.g. if layer has no attribute data)
5561
+ var editable = pinned && gui.interaction.getMode() == 'data';
5562
+ var maxHeight = parent.node().clientHeight - 36;
5563
+ currId = id;
5564
+ // stash a function for refreshing the current popup when data changes
5565
+ // while the popup is being displayed (e.g. while dragging a label)
5566
+ refresh = function() {
5567
+ var rec = table && (editable ? table.getRecordAt(id) : table.getReadOnlyRecordAt(id)) || {};
5568
+ render(content, rec, table, editable);
5569
+ };
5570
+ refresh();
5571
+ if (ids && ids.length > 1) {
5572
+ showNav(id, ids, pinned);
5759
5573
  } else {
5760
- ext.pan(e.dx, e.dy);
5574
+ tab.hide();
5761
5575
  }
5762
- });
5763
-
5764
- mouse.on('dragend', function(e) {
5765
- var bbox;
5766
- if (disabled()) return;
5767
- if (boxDrag) {
5768
- boxDrag = false;
5769
- gui.dispatchEvent('box_drag_end', getBoxData(e));
5576
+ el.show();
5577
+ if (content.node().clientHeight > maxHeight) {
5578
+ content.css('height:' + maxHeight + 'px');
5770
5579
  }
5771
- });
5580
+ };
5772
5581
 
5773
- wheel.on('mousewheel', function(e) {
5774
- var tickFraction = 0.11; // 0.15; // fraction of zoom step per wheel event;
5775
- var k = 1 + (tickFraction * e.multiplier * zoomScaleMultiplier),
5776
- delta = e.direction > 0 ? k : 1 / k;
5777
- if (disabled()) return;
5778
- ext.zoomByPct(delta, e.x / ext.width(), e.y / ext.height());
5779
- });
5582
+ self.hide = function() {
5583
+ if (!isOpen()) return;
5584
+ refresh = null;
5585
+ currId = -1;
5586
+ // make sure any pending edits are made before re-rendering popup
5587
+ GUI.blurActiveElement(); // this should be more selective -- could cause a glitch if typing in console
5588
+ content.empty();
5589
+ content.node().removeAttribute('style'); // remove inline height
5590
+ el.hide();
5591
+ };
5780
5592
 
5781
- function swapElements(arr, i, j) {
5782
- var tmp = arr[i];
5783
- arr[i] = arr[j];
5784
- arr[j] = tmp;
5593
+ return self;
5594
+
5595
+ function isOpen() {
5596
+ return el.visible();
5785
5597
  }
5786
5598
 
5787
- function getBoxData(e) {
5788
- var pageBox = [e.pageX, e.pageY, dragStartEvt.pageX, dragStartEvt.pageY];
5789
- var mapBox = [e.x, e.y, dragStartEvt.x, dragStartEvt.y];
5790
- var tmp;
5791
- if (pageBox[0] > pageBox[2]) {
5792
- swapElements(pageBox, 0, 2);
5793
- swapElements(mapBox, 0, 2);
5794
- }
5795
- if (pageBox[1] > pageBox[3]) {
5796
- swapElements(pageBox, 1, 3);
5797
- swapElements(mapBox, 1, 3);
5599
+ function showNav(id, ids, pinned) {
5600
+ var num = ids.indexOf(id) + 1;
5601
+ navInfo.text(' ' + num + ' / ' + ids.length + ' ');
5602
+ nextLink.css('display', pinned ? 'inline-block' : 'none');
5603
+ prevLink.css('display', pinned && ids.length > 2 ? 'inline-block' : 'none');
5604
+ tab.show();
5605
+ }
5606
+
5607
+ function render(el, rec, table, editable) {
5608
+ var tableEl = El('table').addClass('selectable'),
5609
+ rows = 0;
5610
+ // self.hide(); // clean up if panel is already open
5611
+ el.empty(); // clean up if panel is already open
5612
+ utils.forEachProperty(rec, function(v, k) {
5613
+ var type;
5614
+ // missing GeoJSON fields are set to undefined on import; skip these
5615
+ if (v !== undefined) {
5616
+ type = getFieldType(v, k, table);
5617
+ renderRow(tableEl, rec, k, type, editable);
5618
+ rows++;
5619
+ }
5620
+ });
5621
+
5622
+ if (rows > 0) {
5623
+ tableEl.appendTo(el);
5624
+
5625
+ tableEl.on('copy', function(e) {
5626
+ // remove leading or trailing tabs that sometimes get copied when
5627
+ // selecting from a table
5628
+ var pasted = window.getSelection().toString();
5629
+ var cleaned = pasted.replace(/^\t/, '').replace(/\t$/, '');
5630
+ if (pasted != cleaned && !window.clipboardData) { // ignore ie
5631
+ (e.clipboardData || e.originalEvent.clipboardData).setData("text", cleaned);
5632
+ e.preventDefault(); // don't copy original string with tabs
5633
+ }
5634
+ });
5635
+
5636
+ } else {
5637
+ // Some individual features can have undefined values for some or all of
5638
+ // their data properties (properties are set to undefined when an input JSON file
5639
+ // has inconsistent fields, or after force-merging layers with inconsistent fields).
5640
+ el.html(utils.format('<div class="note">This %s is missing attribute data.</div>',
5641
+ table && table.getFields().length > 0 ? 'feature': 'layer'));
5798
5642
  }
5799
- return {
5800
- map_bbox: mapBox,
5801
- page_bbox: pageBox
5802
- };
5803
5643
  }
5804
5644
 
5805
- function disabled() {
5806
- return !!gui.options.disableNavigation;
5645
+ function renderRow(table, rec, key, type, editable) {
5646
+ var rowHtml = '<td class="field-name">%s</td><td><span class="value">%s</span> </td>';
5647
+ var val = rec[key];
5648
+ var str = formatInspectorValue(val, type);
5649
+ var cell = El('tr')
5650
+ .appendTo(table)
5651
+ .html(utils.format(rowHtml, key, utils.htmlEscape(str)))
5652
+ .findChild('.value');
5653
+ setFieldClass(cell, val, type);
5654
+ if (editable) {
5655
+ editItem(cell, rec, key, type);
5656
+ }
5807
5657
  }
5808
5658
 
5809
- function zoomIn(e) {
5810
- if (disabled()) return;
5811
- zoomByPct(getZoomInPct(e.time), 0.5, 0.5);
5659
+ function setFieldClass(el, val, type) {
5660
+ var isNum = type ? type == 'number' : utils.isNumber(val);
5661
+ var isNully = val === undefined || val === null || val !== val;
5662
+ var isEmpty = val === '';
5663
+ el.classed('num-field', isNum);
5664
+ el.classed('object-field', type == 'object');
5665
+ el.classed('null-value', isNully);
5666
+ el.classed('empty', isEmpty);
5812
5667
  }
5813
5668
 
5814
- function zoomOut(e) {
5815
- if (disabled()) return;
5816
- zoomByPct(1/getZoomInPct(e.time), 0.5, 0.5);
5669
+ function editItem(el, rec, key, type) {
5670
+ var input = new ClickText2(el),
5671
+ strval = formatInspectorValue(rec[key], type),
5672
+ parser = getInputParser(type);
5673
+ el.parent().addClass('editable-cell');
5674
+ el.addClass('colored-text dot-underline');
5675
+ input.on('change', function(e) {
5676
+ var val2 = parser(input.value()),
5677
+ strval2 = formatInspectorValue(val2, type);
5678
+ if (strval == strval2) {
5679
+ // contents unchanged
5680
+ } else if (val2 === null && type != 'object') { // allow null objects
5681
+ // invalid value; revert to previous value
5682
+ input.value(strval);
5683
+ } else {
5684
+ // field content has changed
5685
+ strval = strval2;
5686
+ gui.dispatchEvent('data_preupdate', {FID: currId}); // for undo/redo
5687
+ rec[key] = val2;
5688
+ gui.dispatchEvent('data_postupdate', {FID: currId});
5689
+ input.value(strval);
5690
+ setFieldClass(el, val2, type);
5691
+ self.dispatchEvent('update', {field: key, value: val2, id: currId});
5692
+ }
5693
+ });
5817
5694
  }
5695
+ }
5818
5696
 
5819
- function getZoomInPct(clickTime) {
5820
- var minScale = 0.2,
5821
- maxScale = 4,
5822
- minTime = 100,
5823
- maxTime = 800,
5824
- time = utils.clamp(clickTime || 200, minTime, maxTime),
5825
- k = (time - minTime) / (maxTime - minTime),
5826
- scale = minScale + k * (maxScale - minScale);
5827
- return 1 + scale * zoomScaleMultiplier;
5697
+ function formatInspectorValue(val, type) {
5698
+ var str;
5699
+ if (type == 'date') {
5700
+ str = utils.formatDateISO(val);
5701
+ } else if (type == 'object') {
5702
+ str = val ? JSON.stringify(val) : "";
5703
+ } else {
5704
+ str = String(val);
5828
5705
  }
5706
+ return str;
5707
+ }
5829
5708
 
5830
- // @box Bounds with pixels from t,l corner of map area.
5831
- function zoomToBbox(bbox) {
5832
- var bounds = new Bounds(bbox),
5833
- pct = Math.max(bounds.width() / ext.width(), bounds.height() / ext.height()),
5834
- fx = bounds.centerX() / ext.width() * (1 + pct) - pct / 2,
5835
- fy = bounds.centerY() / ext.height() * (1 + pct) - pct / 2;
5836
- zoomByPct(1 / pct, fx, fy);
5709
+ var inputParsers = {
5710
+ date: function(raw) {
5711
+ var d = new Date(raw);
5712
+ return isNaN(+d) ? null : d;
5713
+ },
5714
+ string: function(raw) {
5715
+ return raw;
5716
+ },
5717
+ number: function(raw) {
5718
+ var val = Number(raw);
5719
+ if (raw == 'NaN') {
5720
+ val = NaN;
5721
+ } else if (isNaN(val)) {
5722
+ val = null;
5723
+ }
5724
+ return val;
5725
+ },
5726
+ object: function(raw) {
5727
+ var val = null;
5728
+ try {
5729
+ val = JSON.parse(raw);
5730
+ } catch(e) {}
5731
+ return val;
5732
+ },
5733
+ boolean: function(raw) {
5734
+ var val = null;
5735
+ if (raw == 'true') {
5736
+ val = true;
5737
+ } else if (raw == 'false') {
5738
+ val = false;
5739
+ }
5740
+ return val;
5741
+ },
5742
+ multiple: function(raw) {
5743
+ var val = Number(raw);
5744
+ return isNaN(val) ? raw : val;
5837
5745
  }
5746
+ };
5838
5747
 
5839
- // @pct Change in scale (2 = 2x zoom)
5840
- // @fx, @fy zoom focus, [0, 1]
5841
- function zoomByPct(pct, fx, fy) {
5842
- var w = ext.getBounds().width();
5843
- _fx = fx;
5844
- _fy = fy;
5845
- zoomTween.start(w, w / pct, 400);
5846
- }
5748
+ function getInputParser(type) {
5749
+ return inputParsers[type || 'multiple'];
5847
5750
  }
5848
5751
 
5849
- function HighlightBox() {
5850
- var box = El('div').addClass('zoom-box').appendTo('body'),
5851
- show = box.show.bind(box), // original show() function
5852
- stroke = 2;
5853
- box.hide();
5854
- box.show = function(x1, y1, x2, y2) {
5855
- var w = Math.abs(x1 - x2),
5856
- h = Math.abs(y1 - y2);
5857
- box.css({
5858
- top: Math.min(y1, y2),
5859
- left: Math.min(x1, x2),
5860
- width: Math.max(w - stroke * 2, 1),
5861
- height: Math.max(h - stroke * 2, 1)
5862
- });
5863
- show();
5864
- };
5865
- return box;
5752
+ function getFieldType(val, key, table) {
5753
+ // if a field has a null value, look at entire column to identify type
5754
+ return internal.getValueType(val) || internal.getColumnType(key, table.getRecords());
5866
5755
  }
5867
5756
 
5868
- function SelectionTool(gui, ext, hit) {
5869
- var popup = gui.container.findChild('.selection-tool-options');
5870
- var box = new HighlightBox();
5871
- var _on = false;
5872
-
5873
- gui.addMode('selection_tool', turnOn, turnOff);
5757
+ function InspectionControl2(gui, hit) {
5758
+ var _popup = new Popup(gui, hit.getSwitchTrigger(1), hit.getSwitchTrigger(-1));
5759
+ var _self = new EventDispatcher();
5874
5760
 
5875
5761
  gui.on('interaction_mode_change', function(e) {
5876
- if (e.mode === 'selection') {
5877
- gui.enterMode('selection_tool');
5878
- } else if (gui.getMode() == 'selection_tool') {
5879
- gui.clearMode();
5762
+ if (e.mode == 'off') {
5763
+ inspect(-1); // clear the popup
5880
5764
  }
5881
5765
  });
5882
5766
 
5883
- gui.on('box_drag', function(e) {
5884
- if (!_on) return;
5885
- var b = e.page_bbox;
5886
- box.show(b[0], b[1], b[2], b[3]);
5887
- updateSelection(e.map_bbox, true);
5767
+ _popup.on('update', function(e) {
5768
+ _self.dispatchEvent('data_change', e.data); // let map know which field has changed
5888
5769
  });
5889
5770
 
5890
- gui.on('box_drag_end', function(e) {
5891
- if (!_on) return;
5892
- box.hide();
5893
- updateSelection(e.map_bbox);
5771
+ hit.on('change', function(e) {
5772
+ var ids;
5773
+ if (!inspecting()) return;
5774
+ ids = e.mode == 'selection' ? null : e.ids;
5775
+ inspect(e.id, e.pinned, ids);
5894
5776
  });
5895
5777
 
5896
- function updateSelection(bboxPixels, transient) {
5897
- var bbox = bboxToCoords(bboxPixels);
5898
- var active = gui.model.getActiveLayer();
5899
- var ids = internal.findShapesIntersectingBBox(bbox, active.layer, active.dataset.arcs);
5900
- if (transient) {
5901
- hit.setTransientIds(ids);
5902
- } else if (ids.length) {
5903
- hit.addSelectionIds(ids);
5778
+ // id: Id of a feature in the active layer, or -1
5779
+ function inspect(id, pin, ids) {
5780
+ var target = hit.getHitTarget();
5781
+ if (id > -1 && inspecting() && target && target.layer) {
5782
+ _popup.show(id, ids, target.layer, pin);
5783
+ } else {
5784
+ _popup.hide();
5904
5785
  }
5905
5786
  }
5906
5787
 
5907
- function turnOn() {
5908
- _on = true;
5788
+ // does the attribute inspector appear on rollover
5789
+ function inspecting() {
5790
+ return gui.interaction && gui.interaction.getMode() != 'off';
5909
5791
  }
5910
5792
 
5911
- function bboxToCoords(bbox) {
5912
- var a = ext.translatePixelCoords(bbox[0], bbox[1]);
5913
- var b = ext.translatePixelCoords(bbox[2], bbox[3]);
5914
- return [a[0], b[1], b[0], a[1]];
5915
- }
5793
+ return _self;
5794
+ }
5916
5795
 
5917
- function turnOff() {
5918
- reset();
5919
- _on = false;
5920
- if (gui.interaction.getMode() == 'selection') {
5921
- // mode change was not initiated by interactive menu -- turn off interactivity
5922
- gui.interaction.turnOff();
5923
- }
5924
- }
5796
+ // Test if map should be re-framed to show updated layer
5797
+ function mapNeedsReset(newBounds, prevBounds, viewportBounds, flags) {
5798
+ var viewportPct = getIntersectionPct(newBounds, viewportBounds);
5799
+ var contentPct = getIntersectionPct(viewportBounds, newBounds);
5800
+ var boundsChanged = !prevBounds.equals(newBounds);
5801
+ var inView = newBounds.intersects(viewportBounds);
5802
+ var areaChg = newBounds.area() / prevBounds.area();
5803
+ var chgThreshold = flags.proj ? 1e3 : 1e8;
5804
+ // don't reset if layer extent hasn't changed
5805
+ if (!boundsChanged) return false;
5806
+ // reset if layer is out-of-view
5807
+ if (!inView) return true;
5808
+ // reset if content is mostly offscreen
5809
+ if (viewportPct < 0.3 && contentPct < 0.9) return true;
5810
+ // reset if content bounds have changed a lot (e.g. after projection)
5811
+ if (areaChg > chgThreshold || areaChg < 1/chgThreshold) return true;
5812
+ return false;
5813
+ }
5925
5814
 
5926
- function reset() {
5927
- popup.hide();
5928
- hit.clearSelection();
5929
- }
5815
+ // Returns proportion of bb2 occupied by bb1
5816
+ function getIntersectionPct(bb1, bb2) {
5817
+ return getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0;
5818
+ }
5930
5819
 
5931
- function getIdsOpt() {
5932
- return hit.getSelectionIds().join(',');
5820
+ function getBoundsIntersection(a, b) {
5821
+ var c = new Bounds();
5822
+ if (a.intersects(b)) {
5823
+ c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
5824
+ Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
5933
5825
  }
5826
+ return c;
5827
+ }
5934
5828
 
5935
- hit.on('change', function(e) {
5936
- if (e.mode != 'selection') return;
5937
- var ids = hit.getSelectionIds();
5938
- if (ids.length > 0) {
5939
- // enter this mode when we're ready to show the selection options
5940
- // (this closes any other active mode, e.g. box_tool)
5941
- gui.enterMode('selection_tool');
5942
- popup.show();
5943
- } else {
5944
- popup.hide();
5829
+ function isMultilineLabel(textNode) {
5830
+ return textNode.childNodes.length > 1;
5831
+ }
5832
+
5833
+ function toggleTextAlign(textNode, rec) {
5834
+ var curr = rec['text-anchor'] || 'middle';
5835
+ var value = curr == 'middle' && 'start' || curr == 'start' && 'end' || 'middle';
5836
+ updateTextAnchor(value, textNode, rec);
5837
+ }
5838
+
5839
+ // Set an attribute on a <text> node and any child <tspan> elements
5840
+ // (mapshaper's svg labels require tspans to have the same x and dx values
5841
+ // as the enclosing text node)
5842
+ function setMultilineAttribute(textNode, name, value) {
5843
+ var n = textNode.childNodes.length;
5844
+ var i = -1;
5845
+ var child;
5846
+ textNode.setAttribute(name, value);
5847
+ while (++i < n) {
5848
+ child = textNode.childNodes[i];
5849
+ if (child.tagName == 'tspan') {
5850
+ child.setAttribute(name, value);
5945
5851
  }
5946
- });
5852
+ }
5853
+ }
5947
5854
 
5948
- new SimpleButton(popup.findChild('.delete-btn')).on('click', function() {
5949
- var cmd = '-filter invert ids=' + getIdsOpt();
5950
- runCommand(cmd);
5951
- });
5855
+ function findSvgRoot(el) {
5856
+ while (el && el.tagName != 'html' && el.tagName != 'body') {
5857
+ if (el.tagName == 'svg') return el;
5858
+ el = el.parentNode;
5859
+ }
5860
+ return null;
5861
+ }
5952
5862
 
5953
- new SimpleButton(popup.findChild('.filter-btn')).on('click', function() {
5954
- var cmd = '-filter ids=' + getIdsOpt();
5955
- runCommand(cmd);
5956
- });
5863
+ // p: pixel coordinates of label anchor
5864
+ function autoUpdateTextAnchor(textNode, rec, p) {
5865
+ var svg = findSvgRoot(textNode);
5866
+ var rect = textNode.getBoundingClientRect();
5867
+ var labelCenterX = rect.left - svg.getBoundingClientRect().left + rect.width / 2;
5868
+ var xpct = (labelCenterX - p[0]) / rect.width; // offset of label center from anchor center
5869
+ var value = xpct < -0.25 && 'end' || xpct > 0.25 && 'start' || 'middle';
5870
+ updateTextAnchor(value, textNode, rec);
5871
+ }
5957
5872
 
5958
- new SimpleButton(popup.findChild('.split-btn')).on('click', function() {
5959
- var cmd = '-split ids=' + getIdsOpt();
5960
- runCommand(cmd);
5961
- });
5873
+ // @value: optional position to set; if missing, auto-set
5874
+ function updateTextAnchor(value, textNode, rec) {
5875
+ var rect = textNode.getBoundingClientRect();
5876
+ var width = rect.width;
5877
+ var curr = rec['text-anchor'] || 'middle';
5878
+ var xshift = 0;
5962
5879
 
5963
- new SimpleButton(popup.findChild('.cancel-btn')).on('click', function() {
5964
- hit.clearSelection();
5965
- });
5880
+ // console.log("anchor() curr:", curr, "xpct:", xpct, "left:", rect.left, "anchorX:", anchorX, "targ:", targ, "dx:", xshift)
5881
+ if (curr == 'middle' && value == 'end' || curr == 'start' && value == 'middle') {
5882
+ xshift = width / 2;
5883
+ } else if (curr == 'middle' && value == 'start' || curr == 'end' && value == 'middle') {
5884
+ xshift = -width / 2;
5885
+ } else if (curr == 'start' && value == 'end') {
5886
+ xshift = width;
5887
+ } else if (curr == 'end' && value == 'start') {
5888
+ xshift = -width;
5889
+ }
5890
+ if (xshift) {
5891
+ rec['text-anchor'] = value;
5892
+ applyDelta(rec, 'dx', Math.round(xshift));
5893
+ }
5894
+ }
5895
+
5896
+ // handle either numeric strings or numbers in fields
5897
+ function applyDelta(rec, key, delta) {
5898
+ var currVal = rec[key];
5899
+ var isString = utils.isString(currVal);
5900
+ var newVal = (+currVal + delta) || 0;
5901
+ rec[key] = isString ? String(newVal) : newVal;
5902
+ }
5966
5903
 
5967
- function runCommand(cmd) {
5968
- popup.hide();
5969
- if (gui.console) gui.console.runMapshaperCommands(cmd, function(err) {
5970
- reset();
5971
- });
5904
+ var snapVerticesToPoint = internal.snapVerticesToPoint;
5905
+
5906
+ function getDisplayCoordsById(id, layer, ext) {
5907
+ var coords = getPointCoordsById(id, layer);
5908
+ return ext.translateCoords(coords[0], coords[1]);
5909
+ }
5910
+
5911
+ function getPointCoordsById(id, layer) {
5912
+ var coords = layer && layer.geometry_type == 'point' && layer.shapes[id];
5913
+ if (!coords || coords.length != 1) {
5914
+ return null;
5972
5915
  }
5916
+ return coords[0];
5973
5917
  }
5974
5918
 
5975
- // toNext, toPrev: trigger functions for switching between multiple records
5976
- function Popup(gui, toNext, toPrev) {
5919
+ function translateDeltaDisplayCoords(dx, dy, ext) {
5920
+ var a = ext.translatePixelCoords(0, 0);
5921
+ var b = ext.translatePixelCoords(dx, dy);
5922
+ return [b[0] - a[0], b[1] - a[1]];
5923
+ }
5924
+
5925
+
5926
+ function InteractiveEditor(gui, ext, hit) {
5927
+ // var targetTextNode; // text node currently being dragged
5928
+ var dragging = false;
5929
+ var activeRecord;
5930
+ var activeId = -1;
5977
5931
  var self = new EventDispatcher();
5978
- var parent = gui.container.findChild('.mshp-main-map');
5979
- var el = El('div').addClass('popup').appendTo(parent).hide();
5980
- var content = El('div').addClass('popup-content').appendTo(el);
5981
- // multi-hit display and navigation
5982
- var tab = El('div').addClass('popup-tab').appendTo(el).hide();
5983
- var nav = El('div').addClass('popup-nav').appendTo(tab);
5984
- var prevLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('◀');
5985
- var navInfo = El('span').addClass('popup-nav-info').appendTo(nav);
5986
- var nextLink = El('span').addClass('popup-nav-arrow colored-text').appendTo(nav).text('▶');
5987
- var refresh = null;
5988
- var currId = -1;
5932
+ var activeVertexIds = null; // for vertex dragging
5989
5933
 
5990
- el.addClass('rollover'); // used as a sentinel for the hover function
5934
+ initDragging();
5991
5935
 
5992
- nextLink.on('click', toNext);
5993
- prevLink.on('click', toPrev);
5994
- gui.on('popup-needs-refresh', function() {
5995
- if (refresh) refresh();
5996
- });
5936
+ return self;
5997
5937
 
5998
- self.show = function(id, ids, lyr, pinned) {
5999
- var table = lyr.data; // table can be null (e.g. if layer has no attribute data)
6000
- var editable = pinned && gui.interaction.getMode() == 'data';
6001
- var maxHeight = parent.node().clientHeight - 36;
6002
- currId = id;
6003
- // stash a function for refreshing the current popup when data changes
6004
- // while the popup is being displayed (e.g. while dragging a label)
6005
- refresh = function() {
6006
- var rec = table && (editable ? table.getRecordAt(id) : table.getReadOnlyRecordAt(id)) || {};
6007
- render(content, rec, table, editable);
6008
- };
6009
- refresh();
6010
- if (ids && ids.length > 1) {
6011
- showNav(id, ids, pinned);
6012
- } else {
6013
- tab.hide();
6014
- }
6015
- el.show();
6016
- if (content.node().clientHeight > maxHeight) {
6017
- content.css('height:' + maxHeight + 'px');
6018
- }
6019
- };
5938
+ function labelEditingEnabled() {
5939
+ return gui.interaction && gui.interaction.getMode() == 'labels' ? true : false;
5940
+ }
6020
5941
 
6021
- self.hide = function() {
6022
- if (!isOpen()) return;
6023
- refresh = null;
6024
- currId = -1;
6025
- // make sure any pending edits are made before re-rendering popup
6026
- GUI.blurActiveElement(); // this should be more selective -- could cause a glitch if typing in console
6027
- content.empty();
6028
- content.node().removeAttribute('style'); // remove inline height
6029
- el.hide();
6030
- };
5942
+ function locationEditingEnabled() {
5943
+ return gui.interaction && gui.interaction.getMode() == 'location' ? true : false;
5944
+ }
6031
5945
 
6032
- return self;
5946
+ function vertexEditingEnabled() {
5947
+ return gui.interaction && gui.interaction.getMode() == 'vertices' ? true : false;
5948
+ }
6033
5949
 
6034
- function isOpen() {
6035
- return el.visible();
5950
+ // update symbol by setting attributes
5951
+ function updateSymbol(node, d) {
5952
+ var a = d['text-anchor'];
5953
+ if (a) node.setAttribute('text-anchor', a);
5954
+ setMultilineAttribute(node, 'dx', d.dx || 0);
5955
+ node.setAttribute('y', d.dy || 0);
6036
5956
  }
6037
5957
 
6038
- function showNav(id, ids, pinned) {
6039
- var num = ids.indexOf(id) + 1;
6040
- navInfo.text(' ' + num + ' / ' + ids.length + ' ');
6041
- nextLink.css('display', pinned ? 'inline-block' : 'none');
6042
- prevLink.css('display', pinned && ids.length > 2 ? 'inline-block' : 'none');
6043
- tab.show();
5958
+ // update symbol by re-rendering it
5959
+ function updateSymbol2(node, d, id) {
5960
+ var o = internal.svg.importStyledLabel(d); // TODO: symbol support
5961
+ var activeLayer = hit.getHitTarget().layer;
5962
+ var xy = activeLayer.shapes[id][0];
5963
+ var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
5964
+ var node2;
5965
+ o.properties.transform = getSvgSymbolTransform(xy, ext);
5966
+ o.properties['data-id'] = id;
5967
+ // o.properties['class'] = 'selected';
5968
+ g.innerHTML = internal.svg.stringify(o);
5969
+ node2 = g.firstChild;
5970
+ node.parentNode.replaceChild(node2, node);
5971
+ gui.dispatchEvent('popup-needs-refresh');
5972
+ return node2;
6044
5973
  }
6045
5974
 
6046
- function render(el, rec, table, editable) {
6047
- var tableEl = El('table').addClass('selectable'),
6048
- rows = 0;
6049
- // self.hide(); // clean up if panel is already open
6050
- el.empty(); // clean up if panel is already open
6051
- utils.forEachProperty(rec, function(v, k) {
6052
- var type;
6053
- // missing GeoJSON fields are set to undefined on import; skip these
6054
- if (v !== undefined) {
6055
- type = getFieldType(v, k, table);
6056
- renderRow(tableEl, rec, k, type, editable);
6057
- rows++;
5975
+ function initDragging() {
5976
+ var downEvt;
5977
+ var eventPriority = 1;
5978
+
5979
+ // inspector and label editing aren't fully synced - stop editing if inspector opens
5980
+ // gui.on('inspector_on', function() {
5981
+ // stopEditing();
5982
+ // });
5983
+
5984
+ gui.on('interaction_mode_change', function(e) {
5985
+ if (e.mode != 'labels') {
5986
+ stopDragging();
6058
5987
  }
5988
+ gui.undo.clear(); // TODO: put this elsewhere?
6059
5989
  });
6060
5990
 
6061
- if (rows > 0) {
6062
- tableEl.appendTo(el);
6063
-
6064
- tableEl.on('copy', function(e) {
6065
- // remove leading or trailing tabs that sometimes get copied when
6066
- // selecting from a table
6067
- var pasted = window.getSelection().toString();
6068
- var cleaned = pasted.replace(/^\t/, '').replace(/\t$/, '');
6069
- if (pasted != cleaned && !window.clipboardData) { // ignore ie
6070
- (e.clipboardData || e.originalEvent.clipboardData).setData("text", cleaned);
6071
- e.preventDefault(); // don't copy original string with tabs
6072
- }
6073
- });
5991
+ // down event on svg
5992
+ // a: off text
5993
+ // -> stop editing
5994
+ // b: on text
5995
+ // 1: not editing -> nop
5996
+ // 2: on selected text -> start dragging
5997
+ // 3: on other text -> stop dragging, select new text
6074
5998
 
6075
- } else {
6076
- // Some individual features can have undefined values for some or all of
6077
- // their data properties (properties are set to undefined when an input JSON file
6078
- // has inconsistent fields, or after force-merging layers with inconsistent fields).
6079
- el.html(utils.format('<div class="note">This %s is missing attribute data.</div>',
6080
- table && table.getFields().length > 0 ? 'feature': 'layer'));
6081
- }
6082
- }
5999
+ hit.on('dragstart', function(e) {
6000
+ if (e.id >= 0 === false) return;
6001
+ if (labelEditingEnabled() && onLabelDragStart(e)) {
6002
+ triggerGlobalEvent('label_dragstart', e);
6003
+ startDragging();
6004
+ } else if (locationEditingEnabled()) {
6005
+ triggerGlobalEvent('symbol_dragstart', e);
6006
+ startDragging();
6007
+ } else if (vertexEditingEnabled()) {
6008
+ onVertexDragStart(e);
6009
+ triggerGlobalEvent('vertex_dragstart', e);
6010
+ startDragging();
6011
+ }
6012
+ });
6083
6013
 
6084
- function renderRow(table, rec, key, type, editable) {
6085
- var rowHtml = '<td class="field-name">%s</td><td><span class="value">%s</span> </td>';
6086
- var val = rec[key];
6087
- var str = formatInspectorValue(val, type);
6088
- var cell = El('tr')
6089
- .appendTo(table)
6090
- .html(utils.format(rowHtml, key, utils.htmlEscape(str)))
6091
- .findChild('.value');
6092
- setFieldClass(cell, val, type);
6093
- if (editable) {
6094
- editItem(cell, rec, key, type);
6095
- }
6096
- }
6014
+ hit.on('drag', function(e) {
6015
+ if (labelEditingEnabled()) {
6016
+ onLabelDrag(e);
6017
+ } else if (locationEditingEnabled()) {
6018
+ onLocationDrag(e);
6019
+ } else if (vertexEditingEnabled()) {
6020
+ onVertexDrag(e);
6021
+ }
6022
+ });
6097
6023
 
6098
- function setFieldClass(el, val, type) {
6099
- var isNum = type ? type == 'number' : utils.isNumber(val);
6100
- var isNully = val === undefined || val === null || val !== val;
6101
- var isEmpty = val === '';
6102
- el.classed('num-field', isNum);
6103
- el.classed('object-field', type == 'object');
6104
- el.classed('null-value', isNully);
6105
- el.classed('empty', isEmpty);
6106
- }
6024
+ hit.on('dragend', function(e) {
6025
+ if (locationEditingEnabled()) {
6026
+ triggerGlobalEvent('symbol_dragend', e);
6027
+ stopDragging();
6028
+ } else if (labelEditingEnabled()) {
6029
+ triggerGlobalEvent('label_dragend', e);
6030
+ stopDragging();
6031
+ } else if (vertexEditingEnabled()) {
6032
+ // kludge to get dataset to recalculate internal bounding boxes
6033
+ hit.getHitTarget().arcs.transformPoints(function() {});
6034
+ triggerGlobalEvent('vertex_dragend', e);
6035
+ stopDragging();
6036
+ }
6037
+ });
6107
6038
 
6108
- function editItem(el, rec, key, type) {
6109
- var input = new ClickText2(el),
6110
- strval = formatInspectorValue(rec[key], type),
6111
- parser = getInputParser(type);
6112
- el.parent().addClass('editable-cell');
6113
- el.addClass('colored-text dot-underline');
6114
- input.on('change', function(e) {
6115
- var val2 = parser(input.value()),
6116
- strval2 = formatInspectorValue(val2, type);
6117
- if (strval == strval2) {
6118
- // contents unchanged
6119
- } else if (val2 === null && type != 'object') { // allow null objects
6120
- // invalid value; revert to previous value
6121
- input.value(strval);
6122
- } else {
6123
- // field content has changed
6124
- strval = strval2;
6125
- gui.dispatchEvent('data_preupdate', {FID: currId}); // for undo/redo
6126
- rec[key] = val2;
6127
- gui.dispatchEvent('data_postupdate', {FID: currId});
6128
- input.value(strval);
6129
- setFieldClass(el, val2, type);
6130
- self.dispatchEvent('update', {field: key, value: val2, id: currId});
6039
+ hit.on('click', function(e) {
6040
+ if (labelEditingEnabled()) {
6041
+ var target = hit.getHitTarget();
6042
+ onLabelClick(e);
6131
6043
  }
6132
6044
  });
6133
- }
6134
- }
6135
6045
 
6136
- function formatInspectorValue(val, type) {
6137
- var str;
6138
- if (type == 'date') {
6139
- str = utils.formatDateISO(val);
6140
- } else if (type == 'object') {
6141
- str = val ? JSON.stringify(val) : "";
6142
- } else {
6143
- str = String(val);
6144
- }
6145
- return str;
6146
- }
6046
+ // TODO: highlight hit vertex in path edit mode
6047
+ if (false) hit.on('hover', function(e) {
6048
+ if (vertexEditingEnabled() && !dragging) {
6049
+ onVertexHover(e);
6050
+ }
6051
+ }, null, 100);
6147
6052
 
6148
- var inputParsers = {
6149
- date: function(raw) {
6150
- var d = new Date(raw);
6151
- return isNaN(+d) ? null : d;
6152
- },
6153
- string: function(raw) {
6154
- return raw;
6155
- },
6156
- number: function(raw) {
6157
- var val = Number(raw);
6158
- if (raw == 'NaN') {
6159
- val = NaN;
6160
- } else if (isNaN(val)) {
6161
- val = null;
6053
+ function onVertexHover(e) {
6054
+ // hovering in vertex edit mode: find vertex insertion point
6055
+ var target = hit.getHitTarget();
6056
+ var shp = target.layer.shapes[e.id];
6057
+ var p = ext.translatePixelCoords(e.x, e.y);
6058
+ var o = internal.findInsertionPoint(p, shp, target.arcs, ext.getPixelSize());
6162
6059
  }
6163
- return val;
6164
- },
6165
- object: function(raw) {
6166
- var val = null;
6167
- try {
6168
- val = JSON.parse(raw);
6169
- } catch(e) {}
6170
- return val;
6171
- },
6172
- boolean: function(raw) {
6173
- var val = null;
6174
- if (raw == 'true') {
6175
- val = true;
6176
- } else if (raw == 'false') {
6177
- val = false;
6060
+
6061
+
6062
+ function getVertexEventData(e) {
6063
+ return {
6064
+ FID: activeId,
6065
+ vertexIds: activeVertexIds
6066
+ };
6178
6067
  }
6179
- return val;
6180
- },
6181
- multiple: function(raw) {
6182
- var val = Number(raw);
6183
- return isNaN(val) ? raw : val;
6184
- }
6185
- };
6186
6068
 
6187
- function getInputParser(type) {
6188
- return inputParsers[type || 'multiple'];
6189
- }
6069
+ function onLocationDrag(e) {
6070
+ var lyr = hit.getHitTarget().layer;
6071
+ var p = getPointCoordsById(e.id, lyr);
6072
+ if (!p) return;
6073
+ var diff = translateDeltaDisplayCoords(e.dx, e.dy, ext);
6074
+ p[0] += diff[0];
6075
+ p[1] += diff[1];
6076
+ triggerRedraw();
6077
+ triggerGlobalEvent('symbol_drag', e);
6078
+ }
6190
6079
 
6191
- function getFieldType(val, key, table) {
6192
- // if a field has a null value, look at entire column to identify type
6193
- return internal.getValueType(val) || internal.getColumnType(key, table.getRecords());
6194
- }
6080
+ function onVertexDragStart(e) {
6081
+ var target = hit.getHitTarget();
6082
+ var shp = target.layer.shapes[e.id];
6083
+ var p = ext.translatePixelCoords(e.x, e.y);
6084
+ activeVertexIds = internal.findNearestVertices(p, shp, target.arcs);
6085
+ activeId = e.id;
6086
+ }
6195
6087
 
6196
- function InspectionControl2(gui, hit) {
6197
- var _popup = new Popup(gui, hit.getSwitchTrigger(1), hit.getSwitchTrigger(-1));
6198
- var _self = new EventDispatcher();
6088
+ function onVertexDrag(e) {
6089
+ var target = hit.getHitTarget();
6090
+ if (!activeVertexIds) return; // ignore error condition
6091
+ var p = ext.translatePixelCoords(e.x, e.y);
6092
+ if (gui.keyboard.shiftIsPressed()) {
6093
+ internal.snapPointToArcEndpoint(p, activeVertexIds, target.arcs);
6094
+ }
6095
+ snapVerticesToPoint(activeVertexIds, p, target.arcs);
6096
+ triggerRedraw();
6097
+ }
6199
6098
 
6200
- gui.on('interaction_mode_change', function(e) {
6201
- if (e.mode == 'off') {
6202
- inspect(-1); // clear the popup
6099
+ function onLabelClick(e) {
6100
+ var textNode = getTextTarget3(e);
6101
+ var rec = getLabelRecordById(e.id);
6102
+ if (textNode && rec && isMultilineLabel(textNode)) {
6103
+ toggleTextAlign(textNode, rec);
6104
+ updateSymbol2(textNode, rec, e.id);
6105
+ // e.stopPropagation(); // prevent pin/unpin on popup
6106
+ }
6203
6107
  }
6204
- });
6205
6108
 
6206
- _popup.on('update', function(e) {
6207
- _self.dispatchEvent('data_change', e.data); // let map know which field has changed
6208
- });
6109
+ function triggerRedraw() {
6110
+ gui.dispatchEvent('map-needs-refresh');
6111
+ }
6209
6112
 
6210
- hit.on('change', function(e) {
6211
- var ids;
6212
- if (!inspecting()) return;
6213
- ids = e.mode == 'selection' ? null : e.ids;
6214
- inspect(e.id, e.pinned, ids);
6215
- });
6113
+ function triggerGlobalEvent(type, e) {
6114
+ if (e.id >= 0 === false) return;
6115
+ var o = {
6116
+ FID: e.id,
6117
+ layer_name: hit.getHitTarget().layer.name,
6118
+ vertex_ids: activeVertexIds
6119
+ };
6120
+ // fire event to signal external editor that symbol coords have changed
6121
+ gui.dispatchEvent(type, o);
6122
+ }
6216
6123
 
6217
- // id: Id of a feature in the active layer, or -1
6218
- function inspect(id, pin, ids) {
6219
- var target = hit.getHitTarget();
6220
- if (id > -1 && inspecting() && target && target.layer) {
6221
- _popup.show(id, ids, target.layer, pin);
6222
- } else {
6223
- _popup.hide();
6124
+ function getLabelRecordById(id) {
6125
+ var table = hit.getTargetDataTable();
6126
+ if (id >= 0 === false || !table) return null;
6127
+ // add dx and dy properties, if not available
6128
+ if (!table.fieldExists('dx')) {
6129
+ table.addField('dx', 0);
6130
+ }
6131
+ if (!table.fieldExists('dy')) {
6132
+ table.addField('dy', 0);
6133
+ }
6134
+ if (!table.fieldExists('text-anchor')) {
6135
+ table.addField('text-anchor', '');
6136
+ }
6137
+ return table.getRecordAt(id);
6224
6138
  }
6225
- }
6226
6139
 
6227
- // does the attribute inspector appear on rollover
6228
- function inspecting() {
6229
- return gui.interaction && gui.interaction.getMode() != 'off';
6230
- }
6140
+ function onLabelDragStart(e) {
6141
+ var textNode = getTextTarget3(e);
6142
+ var table = hit.getTargetDataTable();
6143
+ if (!textNode || !table) return false;
6144
+ activeId = e.id;
6145
+ activeRecord = getLabelRecordById(activeId);
6146
+ downEvt = e;
6147
+ return true;
6148
+ }
6231
6149
 
6232
- return _self;
6233
- }
6150
+ function onLabelDrag(e) {
6151
+ var scale = ext.getSymbolScale() || 1;
6152
+ var textNode;
6153
+ if (!dragging) return;
6154
+ if (e.id != activeId) {
6155
+ error("Mismatched hit ids:", e.id, activeId);
6156
+ }
6157
+ applyDelta(activeRecord, 'dx', e.dx / scale);
6158
+ applyDelta(activeRecord, 'dy', e.dy / scale);
6159
+ textNode = getTextTarget3(e);
6160
+ if (!isMultilineLabel(textNode)) {
6161
+ // update anchor position of single-line labels based on label position
6162
+ // relative to anchor point, for better placement when eventual display font is
6163
+ // different from mapshaper's font.
6164
+ autoUpdateTextAnchor(textNode, activeRecord, getDisplayCoordsById(activeId, hit.getHitTarget().layer, ext));
6165
+ }
6166
+ // updateSymbol(targetTextNode, activeRecord);
6167
+ updateSymbol2(textNode, activeRecord, activeId);
6168
+ }
6234
6169
 
6235
- // Test if map should be re-framed to show updated layer
6236
- function mapNeedsReset(newBounds, prevBounds, viewportBounds, flags) {
6237
- var viewportPct = getIntersectionPct(newBounds, viewportBounds);
6238
- var contentPct = getIntersectionPct(viewportBounds, newBounds);
6239
- var boundsChanged = !prevBounds.equals(newBounds);
6240
- var inView = newBounds.intersects(viewportBounds);
6241
- var areaChg = newBounds.area() / prevBounds.area();
6242
- var chgThreshold = flags.proj ? 1e3 : 1e8;
6243
- // don't reset if layer extent hasn't changed
6244
- if (!boundsChanged) return false;
6245
- // reset if layer is out-of-view
6246
- if (!inView) return true;
6247
- // reset if content is mostly offscreen
6248
- if (viewportPct < 0.3 && contentPct < 0.9) return true;
6249
- // reset if content bounds have changed a lot (e.g. after projection)
6250
- if (areaChg > chgThreshold || areaChg < 1/chgThreshold) return true;
6251
- return false;
6252
- }
6170
+ function getSymbolNodeById(id, parent) {
6171
+ // TODO: optimize selector
6172
+ var sel = '[data-id="' + id + '"]';
6173
+ return parent.querySelector(sel);
6174
+ }
6253
6175
 
6254
- // Returns proportion of bb2 occupied by bb1
6255
- function getIntersectionPct(bb1, bb2) {
6256
- return getBoundsIntersection(bb1, bb2).area() / bb2.area() || 0;
6257
- }
6176
+ function getTextTarget3(e) {
6177
+ if (e.id > -1 === false || !e.container) return null;
6178
+ return getSymbolNodeById(e.id, e.container);
6179
+ }
6258
6180
 
6259
- function getBoundsIntersection(a, b) {
6260
- var c = new Bounds();
6261
- if (a.intersects(b)) {
6262
- c.setBounds(Math.max(a.xmin, b.xmin), Math.max(a.ymin, b.ymin),
6263
- Math.min(a.xmax, b.xmax), Math.min(a.ymax, b.ymax));
6181
+ function getTextTarget2(e) {
6182
+ var el = e && e.targetSymbol || null;
6183
+ if (el && el.tagName == 'tspan') {
6184
+ el = el.parentNode;
6185
+ }
6186
+ return el && el.tagName == 'text' ? el : null;
6187
+ }
6188
+
6189
+ function getTextTarget(e) {
6190
+ var el = e.target;
6191
+ if (el.tagName == 'tspan') {
6192
+ el = el.parentNode;
6193
+ }
6194
+ return el.tagName == 'text' ? el : null;
6195
+ }
6264
6196
  }
6265
- return c;
6197
+
6198
+ function startDragging() {
6199
+ dragging = true;
6200
+ }
6201
+
6202
+ function stopDragging() {
6203
+ dragging = false;
6204
+ activeId = -1;
6205
+ activeRecord = null;
6206
+ activeVertexIds = null;
6207
+ }
6208
+
6209
+ function isClickEvent(up, down) {
6210
+ var elapsed = Math.abs(down.timeStamp - up.timeStamp);
6211
+ var dx = up.screenX - down.screenX;
6212
+ var dy = up.screenY - down.screenY;
6213
+ var dist = Math.sqrt(dx * dx + dy * dy);
6214
+ return dist <= 4 && elapsed < 300;
6215
+ }
6216
+
6266
6217
  }
6267
6218
 
6268
6219
  var darkStroke = "#334",
@@ -6620,6 +6571,7 @@
6620
6571
 
6621
6572
  this.maxScale = maxScale;
6622
6573
 
6574
+ // Display scale, e.g. meters per pixel or degrees per pixel
6623
6575
  this.getPixelSize = function() {
6624
6576
  return 1 / this.getTransform().mx;
6625
6577
  };
@@ -6904,7 +6856,7 @@
6904
6856
  _self.drawVertices = function(shapes, arcs, style, filter) {
6905
6857
  var iter = new internal.ShapeIter(arcs);
6906
6858
  var t = getScaledTransform(_ext);
6907
- var radius = (style.strokeWidth * 0.8 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext);
6859
+ var radius = (style.strokeWidth * 0.9 || 2.2) * GUI.getPixelRatio() * getScaledLineScale(_ext);
6908
6860
  var color = style.strokeColor || 'black';
6909
6861
  var shp;
6910
6862
  _ctx.beginPath();
@@ -7990,7 +7942,7 @@
7990
7942
  _mouse.disable();
7991
7943
  });
7992
7944
 
7993
- gui.on('undo_redo', function() {
7945
+ gui.on('map-needs-refresh', function() {
7994
7946
  drawLayers();
7995
7947
  });
7996
7948
 
@@ -8152,13 +8104,7 @@
8152
8104
  });
8153
8105
  }
8154
8106
 
8155
- if (true) { // TODO: add option to disable?
8156
- _editor = new SymbolDragging2(gui, _ext, _hit);
8157
- _editor.on('location_change', function(e) {
8158
- // TODO: look into optimizing, so only changed symbol is redrawn
8159
- drawLayers();
8160
- });
8161
- }
8107
+ _editor = new InteractiveEditor(gui, _ext, _hit);
8162
8108
 
8163
8109
  _ext.on('change', function(e) {
8164
8110
  if (e.reset) return; // don't need to redraw map here if extent has been reset